npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

express-loglens-ui

v1.0.0

Published

A drop-in log viewer UI for Node.js/Express apps with search, filter, date range, chart, CSV export and .env-based authentication

Readme

express-loglens-ui

A drop-in log viewer UI for Node.js (Express and NestJS) — search, filter, date range, time-series chart, CSV export, auth, and pluggable memory/file storage.

npm version license node downloads


Table of Contents

  1. Overview
  2. Installation
  3. Quick Start — Express
  4. Quick Start — NestJS
  5. Storage Modes
  6. Configuration Reference
  7. Environment Variables
  8. Authentication
  9. Public API
  10. REST Endpoints
  11. Dashboard Walkthrough
  12. Writing Logs (Best Practices)
  13. CSV Export
  14. Security
  15. Troubleshooting
  16. Contributing / Changelog / License

1. Overview

express-loglens-ui mounts a small Express router that serves a self-contained log dashboard plus a JSON API. It works in two distinct modes:

| Mode | Source of logs | Use when | |---|---|---| | Memory | Logs you write via logger.info/warn/error/debug (and optionally console.*) | You want a live, in-process viewer with zero infrastructure. | | File | A Winston-style JSON log file (or many files via glob) | You already write JSON logs to disk and want to browse them. |

Features

  • Time-series volume chart by level (Chart.js), clickable to drill into an hour.
  • Search, level filter, source filter, date range (presets + custom), removable filter chips.
  • Expandable rows for multi-line messages and structured metadata.
  • Quality insights: error/warn rate, top noisy sources, recurring errors, spike hours.
  • CSV export of the currently filtered set.
  • .env-driven auth (bcrypt + JWT cookie), brute-force lockout, multi-user roles.
  • Responsive UI for desktop / laptop / tablet.
  • TypeScript types shipped, zero hard framework dependency beyond Express.

2. Installation

npm install express-loglens-ui

Requirements:

  • Node.js >=16
  • express >=4.18 (peer dependency)
  • For NestJS users: the Express platform adapter (default in @nestjs/platform-express).

3. Quick Start — Express

import 'dotenv/config';
import express from 'express';
import { createLoggerUI } from 'express-loglens-ui';

const app = express();

const logger = createLoggerUI({
  path: '/logs',
  source: 'app',
  storageMode: 'memory',
  interceptConsole: true,
});

app.use(logger.middleware());

logger.info('Server started', { port: 3000 });
app.listen(3000, () => logger.info('Listening on http://localhost:3000/logs'));

Open http://localhost:3000/logs and sign in with the credentials configured in your .env file (see §8).


4. Quick Start — NestJS

express-loglens-ui is just an Express router, so it works inside any NestJS app that uses the default Express adapter (@nestjs/platform-express).

4.1 Bootstrap-time mount (simplest)

Mount the middleware directly on the underlying Express instance in main.ts:

import 'dotenv/config';
import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import { createLoggerUI } from 'express-loglens-ui';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule);

  const logger = createLoggerUI({
    path: '/logs',
    source: 'nest-app',
    storageMode: 'file',
    filePath: 'logs/app.log',
  });

  app.use(logger.middleware());
  (global as any).logger = logger;

  await app.listen(3000);
}
bootstrap();

4.2 As an injectable Nest provider (recommended)

Wrap the logger so any service can inject and use it.

// logger.module.ts
import { Module, Global } from '@nestjs/common';
import { createLoggerUI, LoggerUI } from 'express-loglens-ui';

export const LOGGER = Symbol('LOGGER');

@Global()
@Module({
  providers: [
    {
      provide: LOGGER,
      useFactory: (): LoggerUI =>
        createLoggerUI({
          path: '/logs',
          source: 'nest',
          storageMode: (process.env.LOG_STORAGE_MODE as 'memory' | 'file') ?? 'memory',
        }),
    },
  ],
  exports: [LOGGER],
})
export class LoggerModule {}
// main.ts
import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import { AppModule } from './app.module';
import { LOGGER } from './logger.module';
import type { LoggerUI } from 'express-loglens-ui';

async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule);
  const logger = app.get<LoggerUI>(LOGGER);
  app.use(logger.middleware());
  await app.listen(3000);
}
bootstrap();
// any.service.ts
import { Inject, Injectable } from '@nestjs/common';
import type { LoggerUI } from 'express-loglens-ui';
import { LOGGER } from './logger.module';

@Injectable()
export class PaymentsService {
  constructor(@Inject(LOGGER) private readonly logger: LoggerUI) {}

  charge(orderId: string) {
    this.logger.info('Charge started', { orderId });
  }
}

4.3 Global Nest exception filter

Forward all unhandled exceptions into the viewer:

import { ArgumentsHost, Catch, ExceptionFilter, HttpException } from '@nestjs/common';
import type { LoggerUI } from 'express-loglens-ui';

@Catch()
export class LogLensFilter implements ExceptionFilter {
  constructor(private readonly logger: LoggerUI) {}

  catch(exception: any, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const req = ctx.getRequest();
    const status = exception instanceof HttpException ? exception.getStatus() : 500;

    this.logger.error(
      `${exception?.name ?? 'Error'}: ${exception?.message ?? 'Unknown'}\n${exception?.stack ?? ''}`,
      { method: req.method, url: req.url, statusCode: status },
    );

    throw exception;
  }
}

Fastify users: This package targets the Express adapter. To use it under @nestjs/platform-fastify, either expose an Express bridge for the /logs route or migrate that route to Express middleware.


5. Storage Modes

You pick the mode once at bootstrap. The dashboard, API, filters, chart, and CSV export are identical across modes — only the data source changes.

5.1 Memory mode

In-process ring buffer capped by maxEntries. The oldest entries are evicted when the cap is reached.

const logger = createLoggerUI({
  storageMode: 'memory',
  maxEntries: 10_000,
  interceptConsole: true,
});

| Characteristic | Behaviour | |---|---| | Persistence | None — logs are lost on process restart. | | Ingestion | logger.info/warn/error/debug(...) and (optionally) console.* interception. | | Cap | maxEntries (default 10000 or LOG_MAX_ENTRIES). | | Multi-process | Each process has its own buffer. Use file mode (or a shared store) for clusters. | | Best for | Local dev, single-process apps, demos, integration tests. |

Memory mode is the default and requires no config beyond auth.

5.2 File mode — single file

Reads a Winston-style JSON log file (one JSON object per line). The viewer parses the file on demand and re-parses it when the file is modified (if live tail is on).

const logger = createLoggerUI({
  storageMode: 'file',
  filePath: 'logs/app.log',
  fileLiveTail: true,
  maxEntries: 50_000,
});

Expected line format

{"timestamp":"2026-05-12T07:00:00.000Z","level":"info","message":"Request complete","source":"api","meta":{"status":200,"durationMs":42}}

Rules:

  • Supported keys: timestamp, level, message, source, meta.
  • Any extra keys are folded into meta.
  • Malformed lines are skipped (the rest still load).
  • Accepted timestamps: ISO strings, Winston YYYY-MM-DD HH:mm:ss, epoch seconds or milliseconds (number or string).
  • If a timestamp can't be parsed, the entry is still ingested with “now” and tagged meta._timestampParseFailed=true.

Live tail

With fileLiveTail: true (default), the file is re-read whenever its mtime changes — no socket, no polling daemon. Set to false if you want a static snapshot.

5.3 File mode — daily rotate (glob)

For setups where each day is a separate file (logs/2026-05-11.log, logs/2026-05-12.log, …):

const logger = createLoggerUI({ storageMode: 'file' });
LOG_STORAGE_MODE=file
LOG_FILE_GLOB=logs/*.log
LOG_FILE_DAYS=30
LOG_FILE_LIVE_TAIL=true

Rules:

  • Filenames must contain a YYYY-MM-DD date — that's how the day window is computed.
  • Only files within the last LOG_FILE_DAYS days are loaded.
  • LOG_FILE_GLOB takes precedence over LOG_FILE_PATH (unless you pass filePath explicitly to createLoggerUI).
  • The combined entry count is capped by LOG_MAX_ENTRIES (newest kept).

5.4 Custom store

Inject your own implementation of the LogStore interface (e.g. Redis, SQLite, S3-backed):

import { createLoggerUI, LogStore } from 'express-loglens-ui';

const myStore: LogStore = {
  append(entry) { /* ... */ return { ...entry, id: 'uuid' } as any; },
  query(opts)   { /* ... */ return { data: [], total: 0 }; },
  stats()       { /* ... */ return /* StatsResult */ as any; },
  clear()       { /* ... */ },
  getSources()  { return []; },
};

const logger = createLoggerUI({ store: myStore });

When store is provided, storageMode / filePath / maxEntries are ignored.


6. Configuration Reference

All options accepted by createLoggerUI(options):

| Option | Type | Default | Description | |---|---|---|---| | path | string | "/logs" (or LOG_VIEWER_PATH) | Mount path for the UI and API. | | source | string | "app" | Default source label for logger.info/warn/error/debug. | | interceptConsole | boolean | false | Capture console.log/info/warn/error/debug into the store. | | storageMode | "memory" \| "file" | "memory" (or LOG_STORAGE_MODE) | Storage backend. | | maxEntries | number | 10000 (or LOG_MAX_ENTRIES) | Cap for memory/file stores (oldest entries evicted). | | filePath | string | logs/app.log (or LOG_FILE_PATH) | Single-file path when storageMode: 'file'. | | fileLiveTail | boolean | true (or LOG_FILE_LIVE_TAIL) | Reload file(s) when modified. | | store | LogStore | undefined | Inject a custom store (overrides all storage options). |

Resolution order for each setting: explicit option → env var → built-in default.


7. Environment Variables

All vars are read from process.env (load via dotenv or your platform).

Authentication

| Key | Default | Description | |---|---|---| | LOG_AUTH_ENABLED | true | Master switch. When true, all UI and API routes require login. | | LOG_USERNAME | admin | Single-user username. | | LOG_PASSWORD_HASH | required when auth on | Bcrypt hash of the password (use the CLI below). | | LOG_JWT_SECRET | required, ≥32 chars | Signing secret for the session JWT. | | LOG_SESSION_TTL | 3600 | Session length in seconds. | | LOG_MAX_ATTEMPTS | 5 | Failed logins allowed before lockout. | | LOG_LOCKOUT_MINS | 15 | Lockout duration after LOG_MAX_ATTEMPTS. | | LOG_USERS | empty | Optional JSON array for multi-user mode (overrides LOG_USERNAME/LOG_PASSWORD_HASH). |

Viewer & storage

| Key | Default | Description | |---|---|---| | LOG_VIEWER_PATH | /logs | Default mount path (overridable via path option). | | LOG_MAX_ENTRIES | 10000 | Entry cap for memory/file stores. | | LOG_STORAGE_MODE | memory | memory or file. | | LOG_FILE_PATH | logs/app.log | Single JSON log file. | | LOG_FILE_GLOB | empty | Glob for daily-rotate mode (e.g. logs/*.log). | | LOG_FILE_DAYS | 30 | Days included when using LOG_FILE_GLOB. | | LOG_FILE_LIVE_TAIL | true | Reload file(s) when their mtime changes. |

Minimal .env example

LOG_AUTH_ENABLED=true
LOG_USERNAME=admin
LOG_PASSWORD_HASH=$2a$10$replace_with_real_hash
LOG_JWT_SECRET=a-long-random-string-of-32-chars-or-more
LOG_SESSION_TTL=3600

LOG_STORAGE_MODE=file
LOG_FILE_PATH=logs/app.log
LOG_FILE_LIVE_TAIL=true
LOG_MAX_ENTRIES=50000

Startup fails fast if LOG_AUTH_ENABLED=true and any of LOG_JWT_SECRET / LOG_PASSWORD_HASH are missing, if the secret is the placeholder value, or if it's shorter than 32 characters.


8. Authentication

Generating a password hash

npx express-loglens-ui hash-password "your-password"

Copy the output into LOG_PASSWORD_HASH.

Single user

LOG_USERNAME=admin
LOG_PASSWORD_HASH=$2a$10$...

Multiple users / roles

Set LOG_USERS to a JSON array (this overrides LOG_USERNAME / LOG_PASSWORD_HASH):

[
  { "username": "admin",  "passwordHash": "$2a$10$...", "role": "admin"  },
  { "username": "viewer", "passwordHash": "$2a$10$...", "role": "viewer" }
]

Session model

  • Login POST <mountPath>/auth/login (e.g. POST /logs/auth/login) issues an lgr_session JWT cookie.
  • Cookie flags: httpOnly, sameSite: 'strict', secure in production.
  • TTL controlled by LOG_SESSION_TTL.
  • Logout POST <mountPath>/auth/logout clears the cookie.

Brute-force protection

The login route is rate-limited. After LOG_MAX_ATTEMPTS failures, the client is locked out for LOG_LOCKOUT_MINS minutes.

Disabling auth (local dev only)

LOG_AUTH_ENABLED=false

Never disable auth on a network-reachable instance.


9. Public API

import { createLoggerUI, LoggerUI, LogStore, LogEntry } from 'express-loglens-ui';

createLoggerUI(options?: CreateLoggerUIOptions): LoggerUI

Returns:

interface LoggerUI {
  middleware(): Router;                                         // mount with app.use(...)
  info(message: string, meta?: object): void;
  warn(message: string, meta?: object): void;
  error(message: string, meta?: object): void;
  debug(message: string, meta?: object): void;
}

Special meta keys

| Key | Effect | |---|---| | _timestamp | ISO string override for the entry's timestamp (useful for replays/backfills). |

Exported types

LogEntry, LogLevel, LogStore, QueryOptions, QueryResult, StatsResult, StorageMode, CreateLogStoreOptions, HourBucket, SourceInsight, ErrorMessageInsight, SpikeHourInsight.


10. REST Endpoints

All paths below are relative to the mount path (default /logs). With the default mount path, prepend /logs to get the full URL — e.g. GET /logs/logs, POST /logs/auth/login.

All routes (except /login and the static assets) require auth when LOG_AUTH_ENABLED=true.

| Method | Path | Description | |---|---|---| | GET | / | HTML dashboard. | | GET | /login | Login HTML page. | | GET | /dashboard.css | Dashboard stylesheet (static asset). | | GET | /dashboard.js | Dashboard script (static asset). | | POST | /auth/login | { username, password } → sets lgr_session JWT cookie. | | POST | /auth/logout | Clears the lgr_session cookie. | | GET | /logs | Paginated log list. Query params: level, source, search, startDate, endDate, limit, offset. | | GET | /logs/stats | Aggregates for the active filter set: totals by level, hour buckets, error/warn rate, top noisy sources, recurring errors, spike hours. Also returns the sources array used to populate the filter dropdown. | | GET | /logs/export/csv | CSV download of the filtered set (same query params as GET /logs). |

No separate sources endpoint. Source labels are returned in the sources field of the GET /logs/stats response — there is no standalone /sources route.


11. Dashboard Walkthrough

Open the mount path in a browser. The page is laid out top-down for fast triage.

  1. Metric cards — Total / Errors / Warnings / Info for the active filter set.
  2. Log Volume by Hour — stacked bar chart by level. Click a bar to filter to that hour; click again to clear.
  3. Quality Insights
    • Error Rate / Warn Rate — health signal scoped to current filters.
    • Top Noisy Sources — highest-volume sources with their own error rate.
    • Recurring Error Messages — most repeated error texts.
    • Spike Hours — hours with unusually high activity or error concentration.
  4. Filter toolbar — search (debounced, matches message + source), level dropdown, source dropdown, date range (presets 1h / 24h / 7d / Custom with Apply / Cancel / Clear and inline validation), Clear Filters, Export CSV. Active filters appear as removable chips.
  5. Log table — Timestamp, Level (coloured badge), Source, Message. Only the first line of the message is shown; a caret marks rows with more content. Click a row to expand:
    • Full message — complete multi-line content, monospaced and scrollable.
    • Metadata — pretty-printed JSON of the meta payload.

The UI is responsive (desktop / laptop / tablet) and re-flows toolbar, insights, and pagination at smaller widths.


12. Writing Logs (Best Practices)

The viewer renders only the first line of the message in the table and tucks the rest into the expandable detail panel. Structure your logs around that.

Shape

Keep the first line short and scannable. Put context in meta.

logger.info('Request complete', { method: 'GET', path: '/users', status: 200, durationMs: 42 });
logger.warn('Slow query detected', { sqlHash: 'ab12', durationMs: 1800, rows: 3200 });
logger.error('Failed to charge customer', { customerId: 'cus_123', provider: 'stripe', code: 'card_declined' });

Errors with stack traces

try {
  await userService.load(id);
} catch (error) {
  logger.error(
    `${error.name}: ${error.message}\n${error.stack}`,
    { requestId: req.id, userId: id },
  );
}

Express error middleware

app.use((err, req, _res, next) => {
  logger.error(
    `${err.name ?? 'Error'}: ${err.message}\n${err.stack ?? ''}`,
    { method: req.method, url: req.originalUrl, ip: req.ip, statusCode: err.status ?? 500 },
  );
  next(err);
});

Choosing a level

| Level | Use for | |---|---| | info | Normal lifecycle events, successful requests, state transitions. | | warn | Recoverable issues, slow operations, deprecation hits. | | error | Thrown exceptions, failed jobs, 5xx responses. | | debug | Verbose diagnostics for non-production. |

Capturing legacy console.*

Set interceptConsole: true. Any console.log/info/warn/error/debug from third-party or legacy code will appear in the viewer with no refactor.

Replaying historical events

Pass meta._timestamp (ISO string) to override the ingestion time. Useful when importing past data.


13. CSV Export

  • Endpoint: GET /logs/export/csv (relative to the mount path; with default mount /logs the full URL is /logs/logs/export/csv).
  • Columns: id, timestamp, level, source, message, meta.
  • meta is JSON-encoded.
  • Filename: logs-YYYY-MM-DD.csv.
  • Respects the same query parameters as GET /logs (so the toolbar's filters carry over to the download).

14. Security

  • Auth routes are rate-limited.
  • JWT cookies are httpOnly, sameSite: 'strict', and secure in production.
  • Startup validation refuses to boot with a missing/short/placeholder LOG_JWT_SECRET when auth is enabled.
  • The package never logs the password or JWT.
  • File mode reads only the paths you configure — globs are resolved relative to the process CWD, not user input.
  • Responsible disclosure: see SECURITY.md.

15. Troubleshooting

| Symptom | Likely cause / fix | |---|---| | Missing required environment variables at startup | Set LOG_JWT_SECRET and LOG_PASSWORD_HASH, or set LOG_AUTH_ENABLED=false for local dev. | | LOG_JWT_SECRET must be at least 32 characters long. | Generate a longer random secret. | | Logout button visible when LOG_AUTH_ENABLED=false | Upgrade to the latest version — older builds showed auth UI unconditionally; it is now hidden when auth is disabled. | | Logging in succeeds but the page redirects to login | Cookie blocked. In production, ensure HTTPS so the secure cookie flag works; behind a proxy, set app.set('trust proxy', 1). | | Memory mode shows no logs after restart | Expected — memory mode is non-persistent. Switch to file mode. | | File mode shows no logs | Confirm the file exists, is readable, and contains one JSON object per line. Check meta._timestampParseFailed on rows. | | Daily glob shows no logs | Ensure filenames contain YYYY-MM-DD and fall within LOG_FILE_DAYS. | | 401 on every request after deploy | Clock skew can invalidate JWTs — sync server time (NTP). | | NestJS + Fastify: UI doesn't load | Use @nestjs/platform-express or bridge the route via Express. | | npm audit reports vulnerabilities after install | Run npm install with the latest published version — the package pins safe dependency ranges via overrides in package.json. |


16. Contributing / Changelog / License