# lognix

[![npm version](https://img.shields.io/npm/v/lognix.svg?style=flat-square&color=blue)](https://www.npmjs.com/package/lognix)
[![npm downloads](https://img.shields.io/npm/dm/lognix.svg?style=flat-square)](https://www.npmjs.com/package/lognix)
[![types](https://img.shields.io/badge/types-included-3178c6?style=flat-square&logo=typescript&logoColor=white)](#)
[![license](https://img.shields.io/npm/l/lognix.svg?style=flat-square&color=green)](#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.

```bash
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._

<p align="center">
<a href="https://cdn.jsdelivr.net/npm/lognix/docs/overview.png"><img src="https://cdn.jsdelivr.net/npm/lognix/docs/overview.png" alt="Overview — KPIs, charts, breakdowns" title="Overview — KPIs, charts, breakdowns" width="240" /></a>
<a href="https://cdn.jsdelivr.net/npm/lognix/docs/logs.png"><img src="https://cdn.jsdelivr.net/npm/lognix/docs/logs.png" alt="Logs — Kibana-style explorer" title="Logs — Kibana-style explorer" width="240" /></a>
<a href="https://cdn.jsdelivr.net/npm/lognix/docs/requests.png"><img src="https://cdn.jsdelivr.net/npm/lognix/docs/requests.png" alt="Requests — HTTP traffic, status-stacked" title="Requests — HTTP traffic, status-stacked" width="240" /></a>
<a href="https://cdn.jsdelivr.net/npm/lognix/docs/drawer.png"><img src="https://cdn.jsdelivr.net/npm/lognix/docs/drawer.png" alt="Drawer — Request detail with Copy as cURL" title="Drawer — Request detail with Copy as cURL" width="240" /></a>
<a href="https://cdn.jsdelivr.net/npm/lognix/docs/alerts.png"><img src="https://cdn.jsdelivr.net/npm/lognix/docs/alerts.png" alt="Alerts — Grouped by fingerprint" title="Alerts — Grouped by fingerprint" width="240" /></a>
<a href="https://cdn.jsdelivr.net/npm/lognix/docs/settings.png"><img src="https://cdn.jsdelivr.net/npm/lognix/docs/settings.png" alt="Settings — Token + sharing generator" title="Settings — Token + sharing generator" width="240" /></a>
<a href="https://cdn.jsdelivr.net/npm/lognix/docs/sharing-modal.png"><img src="https://cdn.jsdelivr.net/npm/lognix/docs/sharing-modal.png" alt="Sharing modal — One-click generate &amp; copy" title="Sharing modal — One-click generate &amp; copy" width="240" /></a>
<a href="https://cdn.jsdelivr.net/npm/lognix/docs/light-theme.png"><img src="https://cdn.jsdelivr.net/npm/lognix/docs/light-theme.png" alt="Light theme" title="Light theme" width="240" /></a>
</p>

---

## Quick start

### Console + file logging

```js
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

```js
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.

```js
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.

```js
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.dashboard` → `dashboardOptions.enabled`
> - `remoteOptions.dashboardToken` → `dashboardOptions.token`
> - `remoteOptions.serviceName` (when used for branding) → `dashboardOptions.serviceName`
> - `remoteOptions.logo` / `remoteOptions.logoSize` → `dashboardOptions.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:
> ```js
> 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) 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:

```bash
export DASHBOARD_TOKEN=your-stable-secret
```

```js
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:
```js
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:
```js
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`:
```js
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:
```json
{"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:
```js
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`](https://www.npmjs.com/package/https-proxy-agent) — `lognix` ships zero runtime dependencies, so proxy support is opt-in via your own agent.

### Process-exit behavior

```js
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.

```js
new Logger({
    sensitiveFields: ['password', 'token', 'apiKey', 'creditCard', 'ssn', 'pin']
});
```

---

## Programmatic API

```js
// 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

```bash
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.stringify` → `JSON.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 **[gondaliya.ronak100@gmail.com](mailto:gondaliya.ronak100@gmail.com)**.

---

## Author

Made by **Ronak Gondaliya** — [gondaliya.ronak100@gmail.com](mailto:gondaliya.ronak100@gmail.com)

## License

MIT
