lognix
v1.3.0
Published
Zero-dependency Node.js logger with a built-in real-time observability dashboard, Express HTTP request/response capture, batched remote shipping with on-disk fallback, archive viewer, and time-limited sharing tokens.
Maintainers
Readme
lognix
A production-ready Node.js logger with a built-in real-time observability dashboard, HTTP request/response capture, batched remote shipping with on-disk fallback, archive viewer, and time-limited sharing tokens. Drop it into an Express app and get a live dashboard, structured logs, and remote ingestion in under ten lines of config.
npm install lognixWhy this logger
Most Node loggers stop at "write a line to a file." This one is a complete observability layer for small-to-mid teams that don't want to stand up Datadog or run an ELK stack:
- Zero runtime dependencies — single bundled
dist/index.js, built on Node's nativehttp/https/zlib/fs - Drop-in dashboard — set
dashboardOptions.enabled: trueand you get a real-time observability UI on the same Express app, no separate service, no log-shipping endpoint required - HTTP traffic capture — every request and response (headers, parsed bodies, durations) recorded automatically, with sensitive-field masking
- Remote shipping that doesn't lose logs — batched POSTs with retries; if the destination is down, batches persist as JSONL on disk and replay when it returns
- Production-grade defaults — sensible
responseBodyMaxBytes, sensitive-field masking on by default, self-traffic excluded from the Requests tab
74 tests (60 integration + 14 smoke) cover the auth model, batching, gzip, fallback, archive parsing, and refresh persistence.
Screenshots
Click any thumbnail to open full-size. Hover for the screen name.
Quick start
Console + file logging
const Logger = require('lognix').default;
const logger = new Logger({
level: 'DEBUG',
consolePrefix: 'my-app',
timezone: 'IST'
});
logger.info('Server starting');
logger.warn('Cache miss', { key: 'user:42' });
logger.error('DB connect failed', { source: 'postgres' });Logs print to the console (color-coded) and persist under ./logs/my-app/.
Express HTTP watch
const express = require('express');
const Logger = require('lognix').default;
const app = express();
const logger = new Logger({
level: 'INFO',
watch: true, // auto-log every request and response
expressApp: app
});
app.get('/users/:id', (req, res) => res.json({ id: req.params.id }));
app.listen(3000);Every request/response is captured (method, URL, headers, parsed body, status code, duration) and printed as a banner block. Sensitive fields are masked before they reach disk.
Dashboard only — no remote endpoint
If you just want the live dashboard on your own app and don't need to ship logs anywhere, skip remote and endpoint entirely. The dashboard's in-memory log ring is fed in-process via SSE — no HTTP self-loop, nothing extra to configure.
const express = require('express');
const Logger = require('lognix').default;
const app = express();
app.use(express.json());
const logger = new Logger({
level: 'INFO',
consolePrefix: 'my-api',
watch: true,
expressApp: app,
dashboardOptions: {
enabled: true,
token: process.env.DASHBOARD_TOKEN,
serviceName: 'my-api'
}
});
app.listen(2000, () => logger.info('listening'));Full setup — dashboard + remote + watch
Add remote: true and remoteOptions.endpoint when you also want batched shipping to an external sink (Loki, Datadog, your own ingestion service). The dashboard still works in-process; the remote transport runs alongside it.
const express = require('express');
const Logger = require('lognix').default;
const app = express();
app.use(express.json());
const logger = new Logger({
level: 'INFO',
consolePrefix: 'my-api',
watch: true,
expressApp: app,
remote: true,
sensitiveFields: ['password', 'token', 'authorization', 'creditCard', 'ssn'],
dashboardOptions: {
enabled: true,
token: process.env.DASHBOARD_TOKEN,
serviceName: 'my-api'
},
remoteOptions: {
endpoint: `http://localhost:2000/receive-logs`,
authToken: process.env.DASHBOARD_TOKEN
}
});
app.listen(2000, () => logger.info('listening'));Open the URL printed in the startup banner. You get a live dashboard with charts, log search, request introspection, and an archive viewer.
Migrating from <= 1.2.x? Dashboard keys used to live inside
remoteOptions. They still work but emit a one-time deprecation warning perLoggerconstruction. Move:
remoteOptions.dashboard→dashboardOptions.enabledremoteOptions.dashboardToken→dashboardOptions.tokenremoteOptions.serviceName(when used for branding) →dashboardOptions.serviceNameremoteOptions.logo/remoteOptions.logoSize→dashboardOptions.logo/dashboardOptions.logoSizeremoteOptions.expressApp→ top-levelexpressApp(shared withwatch, single source of truth)The deprecated path is scheduled for removal in v2.0.
💡 Tip — when the dashboard is enabled, set
format: 'json'. The dashboard's Archive viewer parses watch files line-by-line. JSON files give one structured record per request with explicit fields (level, timestamp, method, url, status, duration, headers, body, source, etc.), which the archive can index and filter directly. The defaultplainformat works too, but the dashboard has to fall back to a heuristic parser to reconstruct each request from multi-line banner blocks — slower, and field fidelity is lower (e.g. explicitsourcefrom the JSON record beats folder-name fallback). For the best dashboard experience, use:new Logger({ format: 'json', watch: true, dashboardOptions: { enabled: true, expressApp: app } });Console output stays human-readable either way — only the file format changes.
Features
Core logging
- 8 levels — DEBUG, INFO, SUCCESS, WARN, ERROR, FATAL, HTTP, CUSTOM
- Dual format —
plain(banner block) orjson(one structured record per line) for files; console always stays human-readable - File rotation — daily (
DD-MM-YYYY) or hourly (DD-MM-YYYY/HH), size-capped, auto-cleanup based onfileBackups - Timezone-aware timestamps — IST, UTC, any IANA zone
- 12 / 24-hour clock via
use24Hour - Plug-in transports — add your own with
addTransport(fn) - Hooks — register a callback for any level via
logger.hook('ERROR', fn)
HTTP watch
- Automatic — middleware auto-installed when you pass
expressApp - Captures — method, URL, request headers, parsed JSON body, response status, duration, response body (size-capped)
- Sensitive-field masking — recursive on bodies, hardcoded list for headers (
authorization,cookie,x-api-key,x-csrf-token, …) and URL query params (token,secret,password, …) - Safe defaults — binary content types skipped, oversized responses pre-flighted, truncated JSON replaced with placeholder (so masking can't be defeated)
- Self-traffic exclusion — dashboard's own polling never appears in the Requests tab
Web dashboard
- Overview — KPI cards (events/sec, error rate, p95/p99 latency), throughput by level, latency time-series with SLO threshold lines, top error sources, HTTP method/status distributions
- Logs — Kibana-style: clock-aligned date histogram (stacked by level), available-fields sidebar with click-to-filter, virtualized table; click any row → side drawer with full record detail
- Requests — HTTP-only table; click a row → side drawer with Payload / Response / Headers / Metadata / Raw tabs and Copy as cURL
- Archive — read historical log files from disk (source/file/lines pickers, multi-line banner aggregation)
- Alerts — only ERROR/FATAL records, grouped by fingerprint with sparklines and trend chips
- Settings — theme toggle, sign-out, master token, sharing-link generator
- Themes — dark (default) and light, theme-aware shadows and surfaces
- Brand customization — top-left brand text, browser tab title, and favicon all auto-derive from
serviceName+ optionallogo(URL, data URI, or inline SVG). Image logos get bitmap-friendly styling (object-fit: contain, sharper rendering) and analtattribute for screen readers - 304 effective-body display — when Express's freshness check downgrades a 200 to a 304, the dashboard surfaces the body the route handler intended to send (captured via
res.json/res.sendhook, normalized throughJSON.stringifyso Mongoose docs and othertoJSONclasses flatten cleanly). Falls back to a client-side lookup of the most recent prior 200 to the same URL. Shows a spec hint for empty 204/205/304/HEAD bodies - Server uptime indicator — the topbar's
Livepill shows adaptive uptime (5s/5m/2h 15m/3d 4h) sourced fromprocess.uptime(), survives dashboard refresh, and visibly drops to0son restart. Hover for exact start time + preciseHH:MM:SS - Restart-safe history — on boot,
setupDashboardhydrates the in-memory log ring from the tail of the freshest log file per source (capped at 500 lines / 1 MB per file). After a restart, the Logs and Requests tabs are immediately populated with recent history; older entries stay reachable via the Archive tab - Smart noise filters — browser-tooling probes (
/favicon.ico, Chrome DevTools'/.well-known/appspecific/com.chrome.devtools.json) are auto-excluded so they don't pollute the Requests tab
Remote logging
- Batched HTTP POST to any endpoint that accepts JSON arrays (Loki, Datadog HTTP, Splunk HEC, your custom service)
- Retry with exponential backoff before failing over to disk
- JSONL fallback queue — failed batches persist under
./logs/Fallback/, auto-replayed when the endpoint returns - Graceful shutdown — SIGINT/SIGTERM/beforeExit flush in-memory queue to disk
- Optional gzip compression
- Bearer token auth
Auth model
- Token-or-open — set
dashboardOptions.token(orDASHBOARD_TOKENenv) → restricted; leave it unset → open access - No surprise auto-generation — what you configure is what runs
- Sharing tokens — time-limited (15 min / 1h / 8h / 24h), master-only mint/revoke, in-memory only (reset on restart), 32 active max
- No privilege escalation — temp tokens cannot mint more temp tokens
Configuration
Top-level Logger options
Grouped by what they control. Three independent feature blocks — HTTP capture (watch), remote shipping (remote + remoteOptions), and dashboard (dashboardOptions) — can be turned on individually or together.
Core
| Option | Type | Default | Description |
|---|---|---|---|
| level | string | 'DEBUG' | Minimum level to record (DEBUG < INFO < WARN < ERROR < FATAL …) |
| format | 'plain' | 'json' | 'plain' | Saved file format. Console always plain regardless. Recommended: 'json' when dashboardOptions.enabled: true — the archive viewer parses JSON records natively |
| console | bool | true | Enable console transport |
| consolePrefix | string | 'Application' | Folder + prefix for app-level logs |
| sensitiveFields | string[] | ['password','token','secret','authorization','secret_key'] | Field names redacted in bodies (case-insensitive, recursive) |
| timezone | string | 'IST' | IANA timezone, e.g. 'UTC', 'America/New_York' |
| use24Hour | bool | false | Show timestamps in 24-hour format |
File rotation
| Option | Type | Default | Description |
|---|---|---|---|
| fileRotate | bool | true | Enable file rotation |
| fileFormat | string | 'DD-MM-YYYY' | 'DD-MM-YYYY' (daily) or 'DD-MM-YYYY/HH' (hourly) |
| fileBackups | number | 7 | Number of rotated files to keep per prefix |
| fileSize | number | 10 | Max file size in MB before rotation |
HTTP capture (Express)
| Option | Type | Default | Description |
|---|---|---|---|
| watch | bool | true | Enable Express request/response capture |
| watchPrefix | string | 'Global' | Folder for watched HTTP logs |
| expressApp | object | — | Required when watch: true or dashboardOptions.enabled: true. Single source of truth — both features take the app from here |
| responseBodyMaxBytes | number | 65536 | Per-request response body cap (larger → placeholder) |
Remote shipping (set remote: true to enable)
| Option | Type | Default | Description |
|---|---|---|---|
| remote | bool | false | Enable batch shipping to a remote endpoint. Requires remoteOptions.endpoint — without it, a warning is logged and shipping is skipped |
| remoteOptions | object | {} | See remoteOptions reference below |
Dashboard (set dashboardOptions.enabled: true to mount)
| Option | Type | Default | Description |
|---|---|---|---|
| dashboardOptions | object | {} | See dashboardOptions reference below. Independent of remote — works with or without remote shipping configured. The Express app comes from the top-level expressApp option (shared with watch) |
remoteOptions reference
Quick reference (full per-option workflow further down):
| Option | Type | Default | Purpose |
|---|---|---|---|
| endpoint | string | required when remote: true | URL to POST batches to. Optional in dashboard-only mode (dashboardOptions.enabled: true without remote: true) — the in-process SSE feed populates the dashboard's log ring directly |
| authToken | string | '' | Authorization: Bearer … header on every batch |
| serviceName | string | 'lognix' | Stamped on every record as service AND used as the dashboard's top-left brand text + browser tab title |
| logo | string | — | Custom dashboard logo. Accepts a same-origin URL (/assets/logo.png), a data: URI, or an inline SVG string (<svg>...</svg>). Same value also drives the favicon. CSP allows 'self' and data: only — external CDN URLs are blocked |
| logoSize | number | 18 | Logo size in pixels (clamped to 12–64). Out-of-range values are silently ignored |
| environment | string | process.env.NODE_ENV \|\| 'development' | Stamped on every record as environment |
| batchSize | number | 50 | Records per batch — triggers immediate flush when reached |
| batchIntervalMs | number | 3000 | Max time records sit in queue before flush |
| gzip | bool | true | Compress batch with gzip + send Content-Encoding: gzip |
| retryLimit | number | 5 | Retry attempts on POST failure before fallback to disk |
| retryBaseMs | number | 500 | Base ms for exponential backoff (base × 2^attempt) |
| fallback.enabled | bool | true | Persist failed batches to disk |
| fallback.baseDir | string | ./logs/Fallback | Folder for fallback .jsonl files |
| httpOptions | object | {} | Spread into the underlying http(s).request() options (custom agent for proxy / mTLS / keep-alive, extra default headers) |
| ~~dashboard~~ | ~~bool~~ | ~~false~~ | Deprecated — use dashboardOptions.enabled. Still works; emits one-time warning |
| ~~dashboardToken~~ | ~~string~~ | ~~''~~ | Deprecated — use dashboardOptions.token. Still works; emits one-time warning |
| ~~expressApp~~ | ~~object~~ | — | Deprecated — use dashboardOptions.expressApp (or pass expressApp at the top-level for watch). Still works; emits one-time warning |
| ~~logo~~ / ~~logoSize~~ | — | — | Deprecated — use dashboardOptions.logo / dashboardOptions.logoSize. Still work; emit one-time warning |
dashboardOptions reference
New in v1.3.0. Replaces the dashboard-related keys that used to live inside remoteOptions. The old shape still works for one minor cycle but emits a deprecation warning.
| Option | Type | Default | Purpose |
|---|---|---|---|
| enabled | bool | false | Mount the dashboard. Express app is taken from the top-level expressApp option (shared with watch) |
| token | string | '' | Master dashboard token (falls back to DASHBOARD_TOKEN env) |
| serviceName | string | 'lognix' | Top-left brand text + browser tab title. Independent of remoteOptions.serviceName, which still controls how records are stamped on the wire |
| logo | string | — | Custom logo. Same-origin URL (/assets/logo.png), data: URI, or inline SVG. Same value drives the favicon. CSP allows 'self' and data: only |
| logoSize | number | 18 | Logo size in pixels (clamped to 12–64) |
Auth & sharing
Restricted (configured token)
Set a stable token via env var or option:
export DASHBOARD_TOKEN=your-stable-secretdashboardOptions: { enabled: true, expressApp: app /* env picks up token */ }The startup banner prints:
┌──────────────────────────────────────────────────────────────────────
│ lognix Dashboard
│
│ URL http://localhost:2000/dashboard?token=your-stable-secret
│ Auth Restricted (configured token)
└──────────────────────────────────────────────────────────────────────Open the URL once — the token is saved to sessionStorage (kept until the tab closes) and stripped from the URL bar so it doesn't end up in shoulder-surfed screenshots or browser history.
Open access (no token)
Don't pass dashboardOptions.token, don't set DASHBOARD_TOKEN:
┌──────────────────────────────────────────────────────────────────────
│ lognix Dashboard
│
│ URL http://localhost:2000/dashboard
│ Auth Open access (no token configured)
│ Note Set DASHBOARD_TOKEN env or dashboardOptions.token to lock down.
└──────────────────────────────────────────────────────────────────────The dashboard accepts requests without any token. Sharing-token API is disabled in this mode (no master to mint from).
Use open access for local dev only. Anyone reaching the URL can read all logs.
Sharing links (restricted mode only)
From the dashboard's Settings tab → Dashboard token & sharing → pick a duration → optional note → Generate.
A modal appears with the URL and the token. Either copy the URL (one click → shareable in Slack/email) or just the token. The card auto-closes after the first successful copy.
- TTLs: 15 min, 1 hour, 8 hours, 24 hours
- Master-only mint / list / revoke
- Temp tokens cannot mint more temp tokens
- In-memory only — reset on server restart
- Hard cap: 32 active simultaneously
Token API
| Method | Path | Auth | Purpose |
|---|---|---|---|
| POST | /api/tokens | master | Mint a sharing token. Body: { ttlSeconds?, note? } |
| GET | /api/tokens | master | List active sharing tokens (preview only — full tokens never returned in bulk) |
| GET | /api/tokens/:id | master | Get a single token's full value (for the dashboard's Reveal/Copy buttons) |
| DELETE | /api/tokens/:id | master | Revoke a sharing token before expiry |
| GET | /api/me | any | Returns current identity — kind: 'master', 'temp', or 'open' |
Remote logging — full workflow
The big picture
logger.info(...)
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ transport(record) — adds id / service / environment / host / pid │
│ queue.push(record) │
└─────────────────┬───────────────────────────────────────────────────┘
│
queue.length >= batchSize ?
│
┌──────────┴──────────┐
YES NO
│ │
▼ ▼
flush() now scheduleFlush() — setTimeout(batchIntervalMs)
│ │
└──────────┬───────────┘
▼
sendBatch(queue.splice(0, batchSize))
│
▼
JSON.stringify(batch)
│
gzip:true ? ── yes ─▶ zlib.gzipSync + Content-Encoding: gzip
│
no
│
▼
POST endpoint (Authorization: Bearer authToken if set)
│
┌──────────┴───────────┐
│ 2xx │ failure
▼ ▼
done attempt < retryLimit ?
│
┌───────────┴───────────┐
YES NO
│ │
wait retryBaseMs * 2^attempt fallback.enabled ?
retry sendBatch │
┌─────────┴────────┐
YES NO
│ │
PersistentQueue.push(record) drop record
→ ./logs/Fallback/YYYY-MM-DD.jsonl
[separately, every max(5s, batchIntervalMs * 2):]
replayFallbackFiles() — when consecutiveFailures === 0:
read each .jsonl file
resend in batchSize chunks
delete file on successPer-option workflow
endpoint (required when remote: true; optional for dashboard-only)
URL to POST batches to. Sent via Node's native https.request() / http.request() on every batch send. The path part also tells setupDashboard where to register the /receive-logs route. Required when remote: true. Optional when you only want the dashboard — set dashboardOptions.enabled: true without remote: true/endpoint and the in-process SSE feed populates the log ring directly. If remote: true is set without endpoint, the remote transport is skipped and a warning is logged.
authToken
Sent as Authorization: Bearer <authToken> on every POST. Sanitized to strip CRLF (CRLF-injection guard) and trimmed. Omitted entirely if not set. Auto-mirrors dashboardOptions.token when only one is set, so /receive-logs accepts what the remote transport posts without you configuring two secrets.
serviceName / environment
Stamped on every record before queueing:
const record = {
id: uuidv4(),
service: serviceName,
environment: environment,
host: os.hostname(),
pid: process.pid,
...info
};Use them to slice logs by app / region / env in your downstream tool (Loki, Datadog).
batchSize (default: 50)
Triggers an immediate flush when the in-memory queue hits this size:
queue.push(record);
if (queue.length >= batchSize) flush(); // size-triggered
else scheduleFlush(); // time-triggeredInside flush(), the queue drains in batchSize chunks. Logging 247 records in one tick produces 5 POSTs (50, 50, 50, 50, 47).
batchIntervalMs (default: 3000)
Max time records sit in the queue before flushing, even when batchSize isn't reached. Used by setTimeout(scheduleFlush, batchIntervalMs). New logs don't reset the timer — first log starts the clock; either size hits first or the timer fires.
Rule of thumb: lower for faster dashboard updates (500 for local dev). Raise to reduce ratelimits on paid services.
gzip (default: true)
Compresses the batch JSON before POSTing and sets Content-Encoding: gzip:
if (gzipEnabled) {
bodyBuffer = zlib.gzipSync(Buffer.from(JSON.stringify(payload), 'utf8'));
} else {
bodyBuffer = Buffer.from(JSON.stringify(payload), 'utf8');
}Disable only if your receiver doesn't auto-decompress.
retryLimit (default: 5) + retryBaseMs (default: 500)
On a failed POST, the transport retries with exponential backoff: retryBaseMs × 2^attempt.
With defaults (retryLimit: 5, retryBaseMs: 500):
| Attempt | Wait before retry | |---|---| | 0 → 1 | 500 ms | | 1 → 2 | 1 s | | 2 → 3 | 2 s | | 3 → 4 | 4 s | | 4 → 5 | 8 s | | 5 → fallback | — |
Total ≈ 15.5 seconds of retrying before disk persist. Set retryLimit: 0 to fail straight to fallback (no retries).
fallback (default: { enabled: true, baseDir: './logs/Fallback' })
Where failed batches go after retries are exhausted. Each record is appended as a JSON line to a daily file:
logs/Fallback/
├── 2026-04-29.jsonl
├── 2026-04-30.jsonl
└── 2026-05-01.jsonlEach line is a complete log record:
{"id":"...","service":"my-api","level":"INFO","message":"event","timestamp":"...","metadata":{...}}Replay (background) — drains the fallback dir automatically
Not a config option, but the other half of fallback. A timer fires every max(5s, batchIntervalMs * 2):
- Skip if recent POSTs are still failing (
consecutiveFailures > 0) - Read every file under
fallback.baseDir - For each file: read all lines, send in
batchSizechunks - All chunks succeed → delete the file
- Any chunk fails →
sendBatchre-pushes records to disk, replay stops (try again next tick)
Practical effect: remote down at 14:00, comes back at 14:30 → next successful POST resets failure counter → replay timer fires → all 30 minutes of accumulated logs replay → file deleted. No log loss as long as the fallback file isn't manually deleted.
httpOptions
Spread into the underlying http.request() / https.request() options at send time:
const https = require('https');
const fs = require('fs');
remoteOptions: {
endpoint: 'https://logs.example.com/receive-logs',
httpOptions: {
agent: new https.Agent({
ca: fs.readFileSync('my-ca.pem'),
keepAlive: true
}),
headers: { 'X-Tenant-Id': 'acme' }
}
}For HTTP-CONNECT proxies pass an agent like https-proxy-agent — lognix ships zero runtime dependencies, so proxy support is opt-in via your own agent.
Process-exit behavior
process.once('SIGINT', shutdownHandler);
process.once('SIGTERM', shutdownHandler);
process.once('beforeExit', shutdownHandler);On Ctrl+C / kill / clean exit:
- Cancel flush + replay timers
- Drain in-memory queue to disk (every queued record persisted as a fallback line)
- Process exits naturally — no
process.exit()call (so other shutdown hooks still run)
Guarantees zero log loss across restarts.
What your endpoint needs to do
- Accept
POSTof a JSON array of log records - Validate
Authorization: Bearer <authToken>if you set it - Return any 2xx for success — anything else triggers retry → fallback
- If you set
gzip: true, decompressContent-Encoding: gzip(most frameworks do this automatically)
Works out of the box with: Loki HTTP receiver, Datadog HTTP intake, Splunk HEC, Vector / Fluent Bit HTTP input, your own ingestion service.
Decision matrix — when to tune what
| You want | Set |
|---|---|
| Faster dashboard updates (local dev) | batchIntervalMs: 500 |
| Lower bandwidth | gzip: true (default) + larger batchSize |
| Fewer outbound HTTP calls | larger batchSize (e.g. 200) and/or batchIntervalMs |
| Fail fast (no retries) | retryLimit: 0 |
| Custom fallback location | fallback.baseDir: '/var/log/myapp/queue' |
| Multi-tenant log routing | serviceName: 'tenant-A-api', environment: 'us-east-prod' |
| Auth header | authToken: process.env.LOG_INGEST_TOKEN |
| Custom HTTPS / proxy / mTLS | httpOptions: { agent: new https.Agent({ ... }) } |
Log levels
| Level | Color | Numeric value | Use for |
|---|---|---|---|
| DEBUG | cyan | 0 | Detailed traces |
| INFO | blue | 1 | Routine operational events |
| WARN | yellow | 2 | Recoverable issues |
| ERROR | red | 3 | Failures that need attention |
| FATAL | magenta | 4 | Critical, app may crash |
| SUCCESS | green | 5 | Operations completed correctly |
| HTTP | white | 6 | Request/response banners |
| CUSTOM | bright gray | 7 | Custom hooks |
Filtering is by numeric value: a record is kept when its level number is >= the configured level. So level: 'DEBUG' (0) records everything; level: 'INFO' (1) records INFO/WARN/ERROR/FATAL/SUCCESS/HTTP/CUSTOM; level: 'ERROR' (3) keeps only ERROR/FATAL/SUCCESS/HTTP/CUSTOM. The numeric ladder is intentional — SUCCESS, HTTP, and CUSTOM sit above FATAL so they're always emitted regardless of severity threshold.
File layout
When fileRotate: true and watch: true are both on:
logs/
├── Application/ # consolePrefix — app-level logs
│ ├── 15-04-2025.log
│ └── 14-04-2025.log
├── Global/ # watchPrefix — HTTP watch logs
│ ├── 15-04-2025.log
│ └── 14-04-2025.log
└── Fallback/ # remote-transport persistent queue
└── 2025-04-15.jsonlWith fileFormat: 'DD-MM-YYYY/HH' you get hourly folders:
logs/Application/15-04-2025/13.log
logs/Application/15-04-2025/14.logThe directory tree is created automatically. fileBackups applies independently per prefix.
Sensitive-field masking
Three layers, all on by default:
| Layer | What's masked | Configurable via |
|---|---|---|
| Body | Any field name in sensitiveFields (case-insensitive, recursive) | Logger({ sensitiveFields: [...] }) |
| Headers | authorization, proxy-authorization, cookie, set-cookie, x-api-key, x-auth-token, x-access-token, x-csrf-token | hardcoded |
| URL query | token, access_token, apikey, api_key, auth, authorization, secret, password | hardcoded |
Masked values are replaced with "xxxx" before any logging or shipping.
new Logger({
sensitiveFields: ['password', 'token', 'apiKey', 'creditCard', 'ssn', 'pin']
});Programmatic API
// Direct level methods
logger.debug(msg, metadata?);
logger.info(msg, metadata?);
logger.warn(msg, metadata?);
logger.error(msg, metadata?);
logger.fatal(msg, metadata?);
logger.success(msg, metadata?);
logger.http(msg, metadata?);
logger.custom(msg, metadata?);
// Generic
logger.log(level, msg, metadata?);
// Change level at runtime
logger.setLevel('WARN');
// Add your own transport
logger.addTransport((info) => {
// info: { level, message, timestamp, metadata }
sendToYourSink(info);
});
// Hooks (callback per level or all)
logger.hook('ERROR', (level, msg, metadata) => alertPagerDuty(msg, metadata));
logger.hook('*', (level, msg, metadata) => writeToYourPipeline(level, msg));
// Express middleware (if you didn't pass expressApp)
app.use(logger.middleware());
// Plain-http server attach
logger.attach(httpServer);
// Exclude a path from watch transport
logger.excludeFromWatch('/health', '/metrics');FAQ
Why does my Requests tab show /api/analytics or /receive-logs?
It shouldn't — the watch transport excludes those paths automatically. If you're seeing them, your server is running an older dist/. Rebuild + restart.
The dashboard URL changes on every restart.
You're in open-access mode (no token configured) or running an older build that auto-generated tokens. Set DASHBOARD_TOKEN env var or pass dashboardOptions.token to lock the URL.
Why is metadata.source blank in the Archive viewer?
You're running with format: 'plain', which doesn't persist metadata to disk. Switch to format: 'json' for full-fidelity archive search. Console output stays human-readable either way.
Where do failed remote shipments go?
./logs/Fallback/<YYYY-MM-DD>.jsonl. They're auto-replayed when the endpoint returns. Ctrl+C / SIGTERM also flushes pending in-memory batches to that folder.
Can I use this without Express?
Console + file + remote work standalone. The HTTP watch and dashboard need an Express app. For plain http servers, use logger.attach(server) to wire up the watch transport.
What if I want sharing tokens to survive restarts? Not yet — they're in-memory by design (short-lived secrets). Persistent sharing tokens are on the v1.5 roadmap.
Can I send logs to S3 / CloudWatch / GCS directly? Not directly — the remote transport speaks HTTP/JSON. Run a thin forwarder (or use Vector/Fluent Bit) that accepts HTTP and writes to your cloud target.
Testing
npm test # runs both suites: 14 smoke + 60 integration (Express)74 total. Covers: auth, body capture, sensitive masking, response body cap with placeholder, archive endpoints, HTTP banner aggregator, JSONL/plain format parsing, hourly date-folder layout, refresh persistence, self-traffic exclusion, /api/me, sharing tokens (mint/list/revoke/expire), filter-then-limit on /api/logs, env-var token fallback, open-access mode, remote batching, gzip compression, and on-disk fallback persistence.
Changelog
1.3.x — current line
Dashboard works standalone (no endpoint required)
- Set
dashboardOptions.enabled: truewithexpressAppand you get the live dashboard — noremote: true, noendpoint. Logs flow into the in-memory ring via the SSE hub directly, in-process, no HTTP self-loop remote: trueand dashboard mounting are now independent capabilities. Either, both, or neither can be enabled- Setting
remote: truewithoutendpointno longer throws — the remote transport is skipped and a[lognix]warning is logged - Each log record now has a stable
idstamped atLogger.log()time. Both feeders (SSE mirror +/receive-logsPOST) carry the same id sologStoragecan dedupe across them /receive-logsroute is only registered whenendpointis configured (smaller attack surface in dashboard-only mode)- Removed the inline row-expand panel from Logs and HTTP requests tables; click any row → side drawer with full record detail (the inline expand and the drawer were rendering the same data)
API: dashboard config moves to dashboardOptions (deprecation, not breaking)
- New top-level
dashboardOptionsobject:enabled,token,serviceName,logo,logoSize expressAppstays at the top level (shared withwatch) — single source of truth, no duplication. Top-levelexpressAppoverrides the deprecatedremoteOptions.expressAppwhen both are set- The old shape —
remoteOptions.dashboard,remoteOptions.dashboardToken,remoteOptions.expressApp,remoteOptions.logo,remoteOptions.logoSize, and dashboard-contextremoteOptions.serviceName— still works for one minor cycle and is mirrored into the new shape internally - Using any old key emits a one-time
[lognix] dashboard config inside remoteOptions is deprecatedwarning perLoggerconstruction, listing the affected keys with the exact path each should move to remoteOptions.serviceNamekeeps its dual role for record stamping; if you only want it on the wire (not as the dashboard brand), pass it inremoteOptionsand not indashboardOptions- The deprecated path is scheduled for removal in v2.0
1.2.x
Dashboard branding
serviceName(fromremoteOptions) now drives the dashboard top-left brand text and the browser tab title — was previously dropped at runtime- New
remoteOptions.logo(URL / data URI / inline SVG) andremoteOptions.logoSize(12–64 px) options - Favicon auto-derived from the same
logovalue — no separate config - Image logos get bitmap-friendly styling (
filter: none,object-fit: contain, rounded corners, sharper scaling) alttext on<img>logos derived fromserviceNamefor screen readers and broken-image fallback
304 response handling
- Effective body capture via
res.json/res.sendhook — surfaces the body the route handler computed before Express's freshness check rewrote the response to a304 Not Modifiedwith empty wire body - Mongoose internals no longer leak — captured body is normalized via
JSON.stringify→JSON.parsebefore masking, so any class with atoJSONflattens to its wire payload - Client-side fallback walks the in-memory ring for the most recent prior 200 to the same
method+urlwhen the server-side hook didn't fire - Spec-explanation hint for empty bodies on 204, 205, 304, and HEAD
Live status indicator
- Topbar
Livepill now shows server uptime (5s/5m/2h 15m/3d 4h) instead of "time since last event" — monotonic, survives dashboard refresh, drops to0son restart - Hover the pill for exact start time + precise
HH:MM:SSuptime serverStartedAt(epoch ms, derived fromprocess.uptime()) added to/api/analyticsresponse
Restart-safe history
logStorageis hydrated from disk on boot — the tail of the freshest log file per source is parsed and pushed into the in-memory ring so the Logs and Requests tabs show recent history immediately after a restart- Bounded reads (500 lines / 1 MB per file, capped at
logStorage.maxLogstotal) — boot stays fast on multi-GB log archives - Best-effort with try/catch; missing or unreadable files never block startup
UX polish
- Request volume chart no longer collapses to a full-width block at low traffic — minimum 60-second window and y-axis floor of 5
- Browser tooling noise (
/favicon.ico, Chrome DevTools'/.well-known/appspecific/com.chrome.devtools.json) is auto-excluded from the Requests tab
1.0.x — initial release
- Web Dashboard with live charts and Kibana-style Logs explorer
- HTTP request/response capture with sensitive-field masking
- Remote logging with retry + on-disk fallback
- Archive viewer
- Sharing tokens (TTL + revoke)
- Light & dark themes
- Token-or-open auth model
Roadmap
Planned for v1.5
- Persistent sharing tokens (survive restart)
- Read-only scope on temp tokens
- Email & Slack alerts on ERROR / FATAL
Backlog
- Encrypted log files for sensitive environments
- Async logging queue (zero I/O blocking on
logger.info) - CLI tool to tail and search logs from the terminal
- Native cloud transports (S3, CloudWatch direct upload)
Have a feature in mind? File an issue or email [email protected].
Author
Made by Ronak Gondaliya — [email protected]
License
MIT
