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
121 changes: 121 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,11 @@ await logger.info('Server started', { port: 3000 });
- [Transport filter predicate](#transport-filter-predicate)
- [Log search](#log-search)
- [OpenTelemetry](#opentelemetry)
- [OTLP logs export](#otlp-logs-export-opentelemetry-native)
- [Wide events / canonical log lines](#wide-events--canonical-log-lines)
- [Dynamic runtime reconfiguration](#dynamic-runtime-reconfiguration)
- [Adaptive sampling](#adaptive-sampling)
- [Robust serialization](#robust-serialization)
- [Graceful shutdown](#graceful-shutdown)
- [Plugin / extension API](#plugin--extension-api)
- [Writing a plugin](#writing-a-plugin)
Expand Down Expand Up @@ -1640,10 +1645,126 @@ app.post('/checkout', async (req, res) => {

---

## OTLP logs export (OpenTelemetry-native)

logixia doesn't just _read_ the active OTel span (above) β€” it can _emit_ logs in the **OTLP/HTTP** format so they land in any OpenTelemetry backend (Grafana Loki, OpenObserve, Better Stack, Axiom, Datadog, SigNoz…) already correlated with traces. No `@opentelemetry/*` packages required (that JS API is still alpha) β€” the OTLP JSON is built directly, with proper `SeverityNumber` mapping (DEBUG=5, INFO=9, WARN=13, ERROR=17) and resource attributes.

```typescript
import { OtlpLogTransport } from 'logixia';

const logger = createLogger({
appName: 'api',
transports: {
custom: [
new OtlpLogTransport({
url: 'http://localhost:4318/v1/logs',
serviceName: 'api',
serviceVersion: '1.4.0',
environment: 'production',
headers: { 'x-api-key': process.env.OTLP_KEY! },
}),
],
},
});
// Every log is exported as an OTel LogRecord with traceId/spanId for native
// trace↔log correlation. Buffers drain on close() β€” no loss on shutdown.
```

---

## Wide events / canonical log lines

Instead of scattering a request's story across many narrow log lines that you have to JOIN during an incident, emit **one dense, structured event per request** β€” the "canonical log line" (Stripe) / "wide event" (Honeycomb, _Observability 2.0_) pattern. Fields accumulate as the request flows through middleware and business logic via `AsyncLocalStorage`, then the whole event is emitted **once** β€” in a teardown path so it fires even on errors.

```typescript
import { wideEventMiddleware, addEventFields } from 'logixia';

// One canonical line per request, auto-emitted on response finish/close:
app.use(wideEventMiddleware(logger)); // pre-fills method, url, ip, status, duration

app.get('/checkout', (req, res) => {
addEventFields({ userId: req.user.id, planTier: 'pro' }); // from anywhere
addEventFields({ dbQueries: 4, cacheHit: true });
res.json({ ok: true });
// β†’ ONE log line: { method, url, statusCode, durationMs, userId, planTier,
// dbQueries, cacheHit, traceId } β€” no JOINs at query time
});
```

Or wrap any unit of work manually β€” the event is emitted once, even if the callback throws:

```typescript
import { withWideEvent, addEventFields } from 'logixia';

await withWideEvent(logger, { job: 'reindex' }, async () => {
addEventFields({ shard: 3 });
await doWork(); // throws? β†’ event still emitted with { error: true, errorMessage }
});
```

---

## Dynamic runtime reconfiguration

Change log levels in a **running** process β€” no restart β€” to chase a bug without raising global volume. This is the single most-requested feature across the Winston ([#1107](https://github.com/winstonjs/winston/issues/1107)) and Pino ([#206](https://github.com/pinojs/pino/issues/206)) trackers; logixia ships it first-class.

```typescript
import { registerLevelSignal, createLevelControlHandler } from 'logixia';

// 1. Per-namespace level, live:
logger.setNamespaceLevels({ 'db.*': 'debug', '*': 'info' }); // db.* β†’ debug now

// 2. Cycle the global level with one signal (kill -USR2 <pid>):
registerLevelSignal(logger); // info β†’ debug β†’ trace β†’ … β†’ info

// 3. Ops endpoint (mount behind your auth):
app.all('/admin/log-level', createLevelControlHandler(logger));
// GET β†’ { level, namespaceLevels }
// POST { "level": "debug", "namespaceLevels": { "db.*": "trace" } }
```

---

## Adaptive sampling

On top of static / per-level / trace-consistent sampling + a token-bucket rate cap, logixia can **boost the sample rate automatically during an incident** β€” so high-volume cost control never costs you the logs that matter most. When the windowed error rate crosses a threshold, sampling lifts toward 1.0; in steady state it relaxes back to the base rate.

```typescript
const logger = createLogger({
appName: 'api',
sampling: {
rate: 0.1, // keep 10% in steady state
adaptive: {
errorRateThreshold: 0.05, // β‰₯5% errors in the window…
boostRate: 1.0, // …keep everything until it subsides
windowMs: 10_000,
},
},
});
```

---

## Robust serialization

logixia never throws while serializing a log payload. Circular references become `[Circular]`, and β€” going beyond what Winston/Pino do β€” **BigInt** is handled (raw `JSON.stringify` throws on it) and you can opt into **true round-trippable decycling** for shared/circular graphs:

```typescript
import { safeStringify, decycleValue, retrocycle } from 'logixia';

safeStringify({ id: 9007199254740993n, self: obj }); // BigInt + cycle safe
const json = safeStringify(graph, { decycle: true }); // $ref pointers, not [Circular]
const restored = retrocycle(JSON.parse(json)); // shared refs reconstructed
```

---

## Graceful shutdown

Ensures all buffered log entries are flushed to every transport before the process exits. Critical for database and analytics transports that batch writes.

> **Reliability guarantee β€” no log loss on shutdown.** The most painful, still-open bug in the most popular Node logger is exactly this: Pino's [#1705](https://github.com/pinojs/pino/issues/1705) ("Logs are not flushed, missing log entries after `process.exit()`") has been open since 2023, with its maintainer noting a race condition in the transport flush path that "I won't be able to fix it anytime soon." It recurs across [#542](https://github.com/pinojs/pino/issues/542), [#1774](https://github.com/pinojs/pino/issues/1774), [#1889](https://github.com/pinojs/pino/issues/1889), and [#2054](https://github.com/pinojs/pino/issues/2054). logixia is built the other way around: **every** batching/async transport (database, analytics, CloudWatch/GCP/Azure, worker-thread, browser, OTLP) drains its buffer synchronously on `close()` with bounded retry, and the SIGTERM/SIGINT handler is guarded against concurrent signals so a second Ctrl+C can't truncate the flush. Each guarantee is covered by a regression test.

The simplest approach is to set `gracefulShutdown: true` in config β€” logixia registers SIGTERM and SIGINT handlers automatically:

```typescript
Expand Down
Loading