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

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.

Readme

lognix

npm version npm downloads types license

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 lognix

Why 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 native http / https / zlib / fs
  • Drop-in dashboard — set dashboardOptions.enabled: true and 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 per Logger construction. Move:

  • remoteOptions.dashboarddashboardOptions.enabled
  • remoteOptions.dashboardTokendashboardOptions.token
  • remoteOptions.serviceName (when used for branding) → dashboardOptions.serviceName
  • remoteOptions.logo / remoteOptions.logoSizedashboardOptions.logo / dashboardOptions.logoSize
  • remoteOptions.expressApp → top-level expressApp (shared with watch, 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 default plain format 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. explicit source from 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 formatplain (banner block) or json (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 on fileBackups
  • 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 + optional logo (URL, data URI, or inline SVG). Image logos get bitmap-friendly styling (object-fit: contain, sharper rendering) and an alt attribute 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.send hook, normalized through JSON.stringify so Mongoose docs and other toJSON classes 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 Live pill shows adaptive uptime (5s / 5m / 2h 15m / 3d 4h) sourced from process.uptime(), survives dashboard refresh, and visibly drops to 0s on restart. Hover for exact start time + precise HH:MM:SS
  • Restart-safe history — on boot, setupDashboard hydrates 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 (or DASHBOARD_TOKEN env) → 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-secret
dashboardOptions: { 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 success

Per-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-triggered

Inside 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.jsonl

Each 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):

  1. Skip if recent POSTs are still failing (consecutiveFailures > 0)
  2. Read every file under fallback.baseDir
  3. For each file: read all lines, send in batchSize chunks
  4. All chunks succeed → delete the file
  5. Any chunk fails → sendBatch re-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-agentlognix 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:

  1. Cancel flush + replay timers
  2. Drain in-memory queue to disk (every queued record persisted as a fallback line)
  3. 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 POST of 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, decompress Content-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.jsonl

With fileFormat: 'DD-MM-YYYY/HH' you get hourly folders:

logs/Application/15-04-2025/13.log
logs/Application/15-04-2025/14.log

The 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: true with expressApp and you get the live dashboard — no remote: true, no endpoint. Logs flow into the in-memory ring via the SSE hub directly, in-process, no HTTP self-loop
  • remote: true and dashboard mounting are now independent capabilities. Either, both, or neither can be enabled
  • Setting remote: true without endpoint no longer throws — the remote transport is skipped and a [lognix] warning is logged
  • Each log record now has a stable id stamped at Logger.log() time. Both feeders (SSE mirror + /receive-logs POST) carry the same id so logStorage can dedupe across them
  • /receive-logs route is only registered when endpoint is 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 dashboardOptions object: enabled, token, serviceName, logo, logoSize
  • expressApp stays at the top level (shared with watch) — single source of truth, no duplication. Top-level expressApp overrides the deprecated remoteOptions.expressApp when both are set
  • The old shape — remoteOptions.dashboard, remoteOptions.dashboardToken, remoteOptions.expressApp, remoteOptions.logo, remoteOptions.logoSize, and dashboard-context remoteOptions.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 deprecated warning per Logger construction, listing the affected keys with the exact path each should move to
  • remoteOptions.serviceName keeps its dual role for record stamping; if you only want it on the wire (not as the dashboard brand), pass it in remoteOptions and not in dashboardOptions
  • The deprecated path is scheduled for removal in v2.0

1.2.x

Dashboard branding

  • serviceName (from remoteOptions) 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) and remoteOptions.logoSize (12–64 px) options
  • Favicon auto-derived from the same logo value — no separate config
  • Image logos get bitmap-friendly styling (filter: none, object-fit: contain, rounded corners, sharper scaling)
  • alt text on <img> logos derived from serviceName for screen readers and broken-image fallback

304 response handling

  • Effective body capture via res.json / res.send hook — surfaces the body the route handler computed before Express's freshness check rewrote the response to a 304 Not Modified with empty wire body
  • Mongoose internals no longer leak — captured body is normalized via JSON.stringifyJSON.parse before masking, so any class with a toJSON flattens to its wire payload
  • Client-side fallback walks the in-memory ring for the most recent prior 200 to the same method+url when the server-side hook didn't fire
  • Spec-explanation hint for empty bodies on 204, 205, 304, and HEAD

Live status indicator

  • Topbar Live pill now shows server uptime (5s / 5m / 2h 15m / 3d 4h) instead of "time since last event" — monotonic, survives dashboard refresh, drops to 0s on restart
  • Hover the pill for exact start time + precise HH:MM:SS uptime
  • serverStartedAt (epoch ms, derived from process.uptime()) added to /api/analytics response

Restart-safe history

  • logStorage is 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.maxLogs total) — 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