Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 59 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# logixia

<p align="center">
<strong>The async-first logging library that ships complete.</strong><br/>
TypeScript-first &middot; Non-blocking by design &middot; NestJS &middot; Database &middot; Cloud &middot; Tracing &middot; OTel &middot; Browser
<strong>The async-first TypeScript logger that ships complete.</strong><br/>
Wide events &middot; OpenTelemetry (OTLP) &middot; Runtime log levels &middot; Redaction &middot; Adaptive sampling<br/>
NestJS &middot; Express &middot; Fastify &middot; Database &middot; Cloud &middot; Tracing &middot; Prometheus &middot; Browser
</p>

<p align="center">
Expand Down Expand Up @@ -163,45 +164,70 @@ logixia takes a different approach: **everything ships built-in, and nothing blo

## Feature comparison

| Feature | **logixia** | pino | winston | bunyan |
| ------------------------------------ | :---------: | :---------: | :-----------------------: | :-----: |
| TypeScript-first | yes | partial | partial | partial |
| Async / non-blocking writes | yes | no | no | no |
| NestJS module (built-in) | yes | no | no | no |
| Database transports (built-in) | yes | no | no | no |
| Cloud transports (CW, GCP, Azure) | yes | no | no | no |
| File rotation (built-in) | yes | pino-roll | winston-daily-rotate-file | no |
| Multi-transport concurrent | yes | no | yes | no |
| Log search | yes | no | no | no |
| Field redaction (built-in) | yes | pino-redact | no | no |
| Request tracing (AsyncLocalStorage) | yes | no | no | no |
| Kafka + WebSocket trace interceptors | yes | no | no | no |
| Correlation ID propagation | yes | no | no | no |
| Browser / Edge / Bun / Deno support | yes | partial | no | no |
| OpenTelemetry / W3C headers | yes | no | no | no |
| Graceful shutdown / flush | yes | no | no | no |
| Custom log levels | yes | yes | yes | yes |
| Adaptive log level (NODE_ENV) | yes | no | no | no |
| Plugin / extension API | yes | no | no | no |
| Prometheus metrics extraction | yes | no | no | no |
| Visual TUI log explorer | yes | no | no | no |
| Actively maintained | yes | yes | yes | no |
| Feature | **logixia** | pino | winston | bunyan |
| ------------------------------------- | :---------: | :---------: | :-----------------------: | :-----: |
| TypeScript-first | yes | partial | partial | partial |
| Async / non-blocking writes | yes | no | no | no |
| NestJS module (built-in) | yes | no | no | no |
| Database transports (built-in) | yes | no | no | no |
| Cloud transports (CW, GCP, Azure) | yes | no | no | no |
| File rotation (built-in) | yes | pino-roll | winston-daily-rotate-file | no |
| Multi-transport concurrent | yes | no | yes | no |
| Log search | yes | no | no | no |
| Field + message redaction (built-in) | yes | pino-redact | no | no |
| Request tracing (AsyncLocalStorage) | yes | no | no | no |
| Kafka + WebSocket trace interceptors | yes | no | no | no |
| Correlation ID propagation | yes | no | no | no |
| Browser / Edge / Bun / Deno support | yes | partial | no | no |
| OpenTelemetry / W3C headers | yes | no | no | no |
| **OTLP logs export (OTel-native)** | **yes** | transport | no | no |
| **Wide events / canonical log lines** | **yes** | no | no | no |
| **Runtime log-level reconfig** | **yes** | external | no | no |
| **Adaptive (anomaly) sampling** | **yes** | no | no | no |
| Graceful shutdown / flush (no loss) | yes | partial | no | no |
| Custom log levels | yes | yes | yes | yes |
| Adaptive log level (NODE_ENV) | yes | no | no | no |
| Plugin / extension API | yes | no | no | no |
| Prometheus metrics extraction | yes | no | no | no |
| Visual TUI log explorer | yes | no | no | no |
| Actively maintained | yes | yes | yes | no |

---

## Performance

logixia uses `fast-json-stringify` (a pre-compiled serializer) for JSON output, which is ~59% faster than `JSON.stringify`. The hot path β€” level check, redaction decision, and format β€” is optimised with pre-built caches built once on construction, not on every log call.
logixia is async-first and built for the hot path: a synchronous fast path for in-process transports (no Promise allocated when the write completes synchronously), a millisecond-cached timestamp, lazy formatting (each transport formats once β€” no wasted pre-format), and per-call work (level check, namespace resolution, redaction decision) served off pre-built caches. The result: logixia **beats pino on 5 of 6 real-world scenarios**, beats winston and bunyan across the board, and keeps **p99 latency at 1–3Β΅s** with no tail spikes.

| Library | Simple log (ops/sec) | Structured log (ops/sec) | Error log (ops/sec) | p99 latency |
| ----------- | -------------------: | -----------------------: | ------------------: | -----------: |
| pino | 1,258,000 | 630,000 | 390,000 | 2.5–12Β΅s |
| **logixia** | **840,000** | **696,000** | **654,000** | **4.8–10Β΅s** |
| winston | 738,000 | 371,000 | 433,000 | 9–16Β΅s |
Benchmarked against **pino, winston, and bunyan** β€” all writing to `/dev/null` (pure serialization + framework overhead, no disk/terminal cost). Node 20, Apple M-series; numbers are ops/sec, higher is better. Reproduce with `npm run benchmark`.

logixia is **10% faster than pino on structured logging** and **68% faster on error serialization**. It beats winston across the board. Pino leads on simple string logs because it uses synchronous direct writes to `process.stdout` β€” a trade-off that blocks the event loop under heavy I/O and disappears as soon as you add real metadata.
| Scenario | pino | **logixia** | winston | bunyan |
| ------------------------------ | --------: | ------------: | --------: | ------: |
| Simple string log | 3,220,000 | 2,769,000 | 1,577,000 | 707,000 |
| **Structured log (5 fields)** | 1,319,000 | **1,536,000** | 699,000 | 536,000 |
| **Error object logging** | 907,000 | **1,940,000** | 1,062,000 | 573,000 |
| **Child / per-request logger** | 1,093,000 | **1,436,000** | 321,000 | 380,000 |
| **Deep nested object** | 891,000 | **1,040,000** | 435,000 | 442,000 |
| **High-cardinality (12 flds)** | 651,000 | **1,027,000** | 316,000 | 404,000 |

To reproduce: `node benchmarks/run.mjs`
**What this means:**

- βœ… **logixia is faster than pino on 5 of 6 scenarios** β€” including **+114% on error logging**, **+58% on high-cardinality**, **+31% on child loggers**, and **+16% on structured logs** β€” the shapes that dominate real production traffic.
- βœ… **logixia beats winston and bunyan in every scenario**, often by 2–3Γ—, and avoids their tail-latency spikes (winston hit **3,038Β΅s p99** on high-cardinality and **412Β΅s** on deep objects; logixia stays **1–3Β΅s p99** throughout).
- βš–οΈ **pino still wins the trivial simple-string case** (βˆ’14%) because it writes synchronously straight to `process.stdout` β€” fast in a microbenchmark, but it blocks the event loop under real I/O and is exactly the path behind pino's open [flush-on-exit log-loss bug](#graceful-shutdown). logixia stays non-blocking and guarantees delivery, and pulls ahead the moment you log anything structured.

**Distinctive-feature throughput** (no cross-library equivalent β€” `npm run benchmark:features`):

| Operation | ops/sec | p99 |
| ---------------------------------------- | --------: | ----: |
| Wide event (accumulate 6 fields + emit) | 742,000 | 3.7Β΅s |
| `safeStringify` (BigInt + circular) | 2,735,000 | 0.5Β΅s |
| `decycle` + `retrocycle` round-trip | 1,003,000 | 1.3Β΅s |
| Adaptive-sampling decision (hot path) | 1,950,000 | 0.9Β΅s |
| Namespace child logging (`db.*` β†’ debug) | 1,966,000 | 0.8Β΅s |

Sampling and namespace resolution add **negligible overhead** (~Β΅s), so you can keep them on in production.

To reproduce: `npm run benchmark` (core) and `npm run benchmark:features` (distinctive APIs).

---

Expand Down
143 changes: 143 additions & 0 deletions benchmarks/features.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/**
* Logixia feature benchmark suite.
*
* Measures the throughput of logixia's distinctive APIs (no cross-library
* equivalent), so the README can quote real ops/sec for them:
* - wide events (canonical log line accumulation + emit)
* - safeStringify (BigInt + circular safe) vs JSON.stringify baseline
* - decycle/retrocycle round-trip
* - adaptive sampling decision (shouldEmit hot path)
* - per-namespace runtime level resolution on a child logger
*
* Run: node benchmarks/features.mjs (build first: npm run build)
*/

import { Bench } from 'tinybench';

import {
addEventFields,
createLogger,
retrocycle,
safeStringify,
withWideEvent,
} from '../dist/index.js';

// Swallow console output so transport writes don't pollute timing.
const _realOut = process.stdout.write.bind(process.stdout);
const _realErr = process.stderr.write.bind(process.stderr);
const silence = () => {
process.stdout.write = () => true;
process.stderr.write = () => true;
};
const restore = () => {
process.stdout.write = _realOut;
process.stderr.write = _realErr;
};

const logger = createLogger({ appName: 'bench', environment: 'production', silent: true });

// Sampling logger with adaptive config β€” exercise the shouldEmit hot path.
const sampledLogger = createLogger({
appName: 'bench',
environment: 'production',
silent: true,
sampling: { rate: 0.5, adaptive: { errorRateThreshold: 0.05, boostRate: 1.0 } },
});

// Child logger in a namespace with a runtime-resolved level.
logger.setNamespaceLevels({ 'db.*': 'debug', '*': 'info' });
const dbChild = logger.child('db.queries');

// Payloads.
const META = { requestId: 'abc-123', userId: 42, action: 'login', ip: '127.0.0.1', latency: 14 };
const circular = { id: 7n, name: 'node' };
circular.self = circular;
const wide = { method: 'GET', url: '/checkout', userId: 'u1', planTier: 'pro', dbQueries: 4 };

function rows(bench) {
return bench.tasks
.filter((t) => t.result?.state === 'completed')
.map((t) => ({
name: t.name,
ops: Math.round(t.result.throughput.mean),
p99: (t.result.latency.p99 * 1000).toFixed(1),
}))
.sort((a, b) => b.ops - a.ops);
}

const wideBench = new Bench({ name: 'Wide event (accumulate + emit)', time: 2500 });
wideBench
.add('withWideEvent (5 fields)', async () => {
await withWideEvent(logger, wide, () => {
addEventFields({ cacheHit: true });
});
})
.add('plain structured log', async () => {
await logger.info('request', { ...wide, cacheHit: true });
});

const serializeBench = new Bench({ name: 'Serialization', time: 2500 });
serializeBench
.add('JSON.stringify (no cycles)', () => {
JSON.stringify(META);
})
.add('safeStringify (no cycles)', () => {
safeStringify(META);
})
.add('safeStringify (BigInt + circular)', () => {
safeStringify(circular);
})
.add('decycle + retrocycle round-trip', () => {
retrocycle(JSON.parse(safeStringify(circular, { decycle: true })));
});

const samplingBench = new Bench({ name: 'Adaptive sampling decision', time: 2500 });
samplingBench
.add('logixia.info (sampling on)', async () => {
await sampledLogger.info('hot path', META);
})
.add('logixia.info (no sampling)', async () => {
await logger.info('hot path', META);
});

const nsBench = new Bench({ name: 'Namespace child logging', time: 2500 });
nsBench.add('child(db.queries).debug', async () => {
await dbChild.debug('query', META);
});

async function run() {
_realOut('\nLogixia Feature Benchmarks\n');
_realOut('Node.js ' + process.version + ' | ' + process.platform + '-' + process.arch + '\n');
_realOut('='.repeat(60) + '\n');

const all = {};
silence();
try {
for (const bench of [wideBench, serializeBench, samplingBench, nsBench]) {
_realOut('\nRunning: "' + bench.name + '" ...');
await bench.run();
_realOut(' done\n');
all[bench.name] = rows(bench);
}
} finally {
restore();
}

console.log('\n' + '='.repeat(70));
console.log('Feature results β€” higher ops/sec is better\n');
for (const [name, list] of Object.entries(all)) {
console.log(name + ':');
for (const r of list) {
console.log(
' ' + r.name.padEnd(34) + r.ops.toLocaleString().padStart(14) + ' ops/sec p99: ' + r.p99 + 'Β΅s'
);
}
console.log('');
}
}

run().catch((e) => {
restore();
console.error(e);
process.exit(1);
});
Loading
Loading