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.
Table of Contents
- Overview
- Installation
- Quick Start — Express
- Quick Start — NestJS
- Storage Modes
- Configuration Reference
- Environment Variables
- Authentication
- Public API
- REST Endpoints
- Dashboard Walkthrough
- Writing Logs (Best Practices)
- CSV Export
- Security
- Troubleshooting
- 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-uiRequirements:
- 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/logsroute 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=trueRules:
- Filenames must contain a
YYYY-MM-DDdate — that's how the day window is computed. - Only files within the last
LOG_FILE_DAYSdays are loaded. LOG_FILE_GLOBtakes precedence overLOG_FILE_PATH(unless you passfilePathexplicitly tocreateLoggerUI).- 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=50000Startup fails fast if
LOG_AUTH_ENABLED=trueand any ofLOG_JWT_SECRET/LOG_PASSWORD_HASHare 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 anlgr_sessionJWT cookie. - Cookie flags:
httpOnly,sameSite: 'strict',securein production. - TTL controlled by
LOG_SESSION_TTL. - Logout
POST <mountPath>/auth/logoutclears 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=falseNever 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
sourcesfield of theGET /logs/statsresponse — there is no standalone/sourcesroute.
11. Dashboard Walkthrough
Open the mount path in a browser. The page is laid out top-down for fast triage.
- Metric cards — Total / Errors / Warnings / Info for the active filter set.
- Log Volume by Hour — stacked bar chart by level. Click a bar to filter to that hour; click again to clear.
- 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.
- Filter toolbar — search (debounced, matches message + source), level dropdown, source dropdown, date range (presets
1h/24h/7d/CustomwithApply/Cancel/Clearand inline validation),Clear Filters,Export CSV. Active filters appear as removable chips. - 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
metapayload.
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/logsthe full URL is/logs/logs/export/csv). - Columns:
id, timestamp, level, source, message, meta. metais 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', andsecurein production. - Startup validation refuses to boot with a missing/short/placeholder
LOG_JWT_SECRETwhen 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
- Contributing: CONTRIBUTING.md
- Code of Conduct: CODE_OF_CONDUCT.md
- Changelog: CHANGELOG.md
- Security policy: SECURITY.md
- License: MIT — see LICENSE.
