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

@arkv/logger

v0.13.0

Published

Node.js structured logger with async context, sanitization, and console, file, stream, HTTP and syslog transports

Readme

@arkv/logger

Structured logger for Node.js with async context, sanitization, colored output, and console + rotating file transports.

Framework-agnostic, but not runtime-agnostic: this package targets Node (>= 18) and uses node:fs and node:async_hooks. It is not built for the browser. ESM, CJS and types are all shipped, because Node's own dual module system needs them.

Install

bun add @arkv/logger
# or
npm install @arkv/logger

Usage

Basic Logging

import { Logger } from '@arkv/logger';

const logger = new Logger({
  name: 'my-app',
  version: '1.0.0',
  env: 'production',
});

logger.info('Server started');
logger.debug('Loading config', { path: '/etc/app' });
logger.warn('Disk usage high', { usage: 92 });
logger.error('Request failed', new Error('timeout'));

log() is a deprecated alias for info() and still works everywhere info does. It cannot be removed: NestJS's LoggerService interface mandates a log method. It is only a name — both emit level: 'info'.

Log Levels

Six levels in ascending severity:

import { Logger, LogLevel } from '@arkv/logger';

const logger = new Logger({ level: LogLevel.WARN });

logger.debug('skipped');  // below WARN, not logged
logger.warn('logged');    // WARN and above are logged
logger.error('logged');
logger.fatal('logged');

| Level | Value | |-------|-------| | LogLevel.VERBOSE | 'verbose' | | LogLevel.DEBUG | 'debug' | | LogLevel.INFO | 'info' | | LogLevel.WARN | 'warn' | | LogLevel.ERROR | 'error' | | LogLevel.FATAL | 'fatal' |

There is no LogLevel.LOG and nothing emits 'log'. NestJS names this level log; this package names it info, matching the method that emits it.

LogLevel is a frozen object plus a union of its values, so it works in both value and type position and the bare strings are accepted too:

const a: LogLevel = LogLevel.WARN;
const b: LogLevel = 'warn';       // also valid

Async Context

Track request-scoped data across async boundaries using ContextStore (backed by AsyncLocalStorage):

import { Logger, ContextStore } from '@arkv/logger';

const context = new ContextStore();
const logger = new Logger({ name: 'api' }, context);

function handleRequest(req) {
  context.runWithContext(
    { requestId: req.id, userId: req.user },
    () => {
      // requestId and userId are automatically
      // included in every log entry
      logger.info('Processing request');
      doWork();
    },
  );
}

The store is per-instance, not per-process. Every new ContextStore() owns its own AsyncLocalStorage, so two stores in one process hold two independent contexts and a Logger only ever reads the store it was constructed with. Two apps booted in the same process each get their own unless they are handed the same instance — construct one and share it.

Nested scopes merge. The inner scope inherits the outer's fields and overrides only the keys it names, so a job started inside a request keeps that request's requestId. The merged context is a fresh object, so an updateContext inside the inner scope does not leak back out.

context.runWithContext({ requestId: 'r-1' }, () => {
  context.runWithContext({ flow: 'job' }, () => {
    // { requestId: 'r-1', flow: 'job' }
  });

  // Opt out for a genuinely detached scope:
  context.runWithContext({ flow: 'cron' }, work, { inherit: false });
});

The context is a contract, not a class

ContextStore is the batteries-included implementation, not a requirement. The logger's second argument accepts anything that can answer "what are this request's fields":

import { Logger, RequestScopedContext } from '@arkv/logger';

// A ContextStore, backed by AsyncLocalStorage. What most services want.
new Logger(config, new ContextStore());

// A scope with no AsyncLocalStorage in it, for a logger built per request.
new Logger(config, new RequestScopedContext({ requestId: 'r-1' }));

// A plain object, read live, so fields added later still appear.
const fields = {};
new Logger(config, fields);

// A function, for a host that already has its own per-request lookup.
new Logger(config, () => myFramework.currentRequest()?.fields);

The contract is two interfaces, because the halves differ in what can implement them. ContextReader is the read side and the only half the logger depends on. ContextScope adds updateContext and runWithContext, which is what a request pipeline needs and what only AsyncLocalStorage can do across an await. A single interface would have obliged every consumer to implement async propagation it may not have.

RequestScopedContext.runWithContext saves and restores rather than propagating. That is correct for a synchronous scope and for one instance per request. It is not correct for two overlapping async flows through one instance: the restore runs when the callback returns, and an await inside it hands control to another flow that then reads these fields. Use ContextStore for that.

ContextReader has an optional peekContext, which returns the live fields without copying and which the logger prefers when present. ContextStore does not implement it on purpose: that class is public and subclassable, and a subclass overriding getContext to add or redact a field would be silently bypassed if the logger read an inherited peekContext instead.

Sensitive Field Masking

Fields matching known sensitive names are automatically replaced with [MASKED]:

logger.info('User login', {
  username: 'alice',
  password: 'secret',    // → [MASKED]
  token: 'jwt-abc',      // → [MASKED]
  apiKey: 'key-123',     // → [MASKED]
});

Default masked fields: password, secret, token, authorization, cookie, apiKey, apiSecret, apiPass. Add custom fields via config:

const logger = new Logger({
  maskFields: ['ssn', 'creditCard'],
});

Error Handling

Errors are automatically extracted and serialized from multiple patterns:

// Direct Error object
logger.error('Failed', new Error('timeout'));

// Error as message
logger.error(new Error('crash'));

// Wrapped in object
logger.error('Op failed', { err: new Error('db') });
logger.error('Op failed', { error: new Error('db') });

// String shorthand at error/warn/fatal level
logger.error('Op failed', { error: 'connection refused' });

// Deeply nested errors are found automatically
logger.error('Op failed', {
  metadata: { nested: { err: new Error('deep') } },
});

Reserved Fields

level, timestamp, pid, message, appId and error are written by the logger. A caller field of the same name no longer replaces one — the entry's value wins and the caller's is kept under reservedFieldConflicts, so nothing is lost and the clash is visible to whatever reads the log:

logger.debug('msg', { level: 'x' });
// → { level: 'debug', message: 'msg', …, reservedFieldConflicts: { level: 'x' } }

The same applies to fields arriving from the async context or from bindings. RESERVED_ENTRY_KEYS and RESERVED_CONFLICTS_KEY are exported.

null vs missing

null is preserved; undefined is dropped. null is a real JSON value and is the difference between "this field was empty" and "this field was never logged". undefined has no JSON representation — JSON.stringify erases the key anyway, so keeping it would make the colored and plain renderings disagree.

logger.info('user', { deletedAt: null, nickname: undefined });
// → { …, deletedAt: null }   // nickname is absent

Child Loggers

const dbLogger = logger.child({ component: 'db' });
dbLogger.info('query', { ms: 12 });
// → { …, component: 'db', ms: 12 }

A child shares the parent's transports and context store. Per-call fields and async context both take precedence over bindings. Because the transports are shared, close() on a child closes them for the parent too — close the root.

Transports

Console today, file optionally, both with independent levels:

import { ConsoleTransport, FileTransport, Logger, LogLevel } from '@arkv/logger';

const logger = new Logger({
  level: LogLevel.DEBUG,
  transports: [
    new ConsoleTransport({ level: LogLevel.DEBUG }),
    new FileTransport({
      path: '/var/log/app/app.log',
      level: LogLevel.WARN,
      maxSize: 10 * 1024 * 1024,
      maxFiles: 5,
      interval: 'daily',
    }),
  ],
});

Supplying transports replaces the default console transport, so

new Logger({ transports: [new FileTransport({ path: '/var/log/app.log' })] });

is how stdout/stderr output is turned off. An empty array logs nowhere.

A transport is one required method and five optional ones; implement it for anything else:

interface Transport {
  readonly level?: LogLevel;
  write(entry: LogEntry, level: LogLevel): void;
  flush?(): void;
  close?(): void;
  // Awaited by `logger.flushAsync()` / `closeAsync()`, for a sink that cannot
  // answer synchronously. Implement whichever pair you can honour.
  flushAsync?(): Promise<void>;
  closeAsync?(): Promise<void>;
  // What you are holding and what you have lost, for `logger.stats()`.
  stats?(): TransportStats;
}

Each transport formats for itself, so the console can be colored while the file stays plain JSON. jsonFormat and prettyFormat are exported.

Batching the console

ConsoleTransport calls console.log once per entry. Under load that is one write(2) per line, and against a consumer that reads slowly it is one encounter with kernel backpressure per line. batch coalesces everything at info and below into one write per timer tick:

new Logger({ batchConsole: true });

// or, when you are supplying transports yourself
new Logger({ transports: [new ConsoleTransport({ batch: true })] });

Measure before reaching for this. No throughput win was reproducible for it: in @dunx/http's request path on Bun 1.4.0, round-robin over five rounds, it was 10267 ns unbatched against 11106 ns batched with stdout on /dev/null, and 22.9 us against 23.8 us with stdout in a pipe nobody drains. Slower in both. A write to /dev/null costs about 100 ns, so there is little to save, and against a blocked consumer the limit is bytes written rather than calls made. Batching removes syscalls, so it pays only where syscall count is the bottleneck.

It is also off by default because it changes when output appears, not what appears. A logger.info() no longer reaches stdout before the next statement runs, so a test asserting on console.log straight after a log call sees nothing until the tick. Three things bound what that can cost:

  • warn and above are never buffered, and flush whatever is queued behind them, so an error still arrives immediately and in order.
  • The window is one timer tick.
  • flush() is public, close() flushes, and a process.on('exit') hook flushes unless you pass flushOnExit: false.

A process killed with SIGKILL still loses what was queued. That is the trade; leave it off where you cannot take it.

A batched write is reached from a timer and from process.on('exit'), so unlike an unbatched one it has no caller for the logger's transport isolation to be. A stdout that throws, an EPIPE from a closed pipe being the usual way, is counted rather than raised: transport.stats() reports it under errors, with the entries that went with it under dropped.

Writing to a stream

StreamTransport takes any Writable: a socket, a pipe to a log collector, an open file handle, or process.stdout.

import { Logger, StreamTransport } from '@arkv/logger';

const logger = new Logger({
  transports: [new StreamTransport(process.stdout, { bufferBytes: 65536 })],
});

That is also the batched console: process.stdout is a Writable, so one write per 64 KiB instead of one per line. It takes the same bufferBytes, flushIntervalMs and flushOnExit options as FileTransport, with the same defaults, so the two cannot disagree about when they flush.

Batching buys syscalls, not throughput. Measured on Bun 1.3.14 and Node v24.18.0: batching 100 entries per turn cuts write(2) from 1.000 per entry to 0.010, worth 5.4x on the write path. End to end through Logger it is 1.00x, since the write is 4 to 9 percent of a log call and entry assembly plus sanitization is 73 to 93 percent. Turn it on for the syscall economy and to bound what a full pipe does, not for faster logging.

The stream belongs to the caller. close() flushes and drops the timer and exit hook; it does not end the stream, because process.stdout must not be ended.

Flushing, exit, and durability

write, flush and close are synchronous, and FileTransport writes with fs.writeSync on an append-mode fd. That is the whole design: process.on('exit') cannot await, so a transport whose only flush is asynchronous cannot guarantee its buffer reaches disk when the process ends.

A network sink cannot honour that, so it implements flushAsync / closeAsync instead, and a graceful shutdown awaits logger.closeAsync(). The synchronous pair stays what exit calls, best-effort. Nothing about the file and stream guarantees below changes.

  • Default (bufferBytes: 0) writes through on every entry — nothing can be lost, at one syscall per line.
  • bufferBytes > 0 batches. The buffer is flushed the moment it reaches that size, plus every flushIntervalMs (default 1000, on an unref'd timer), plus from a process.on('exit') hook.
  • logger.flush() and logger.close() are explicit and synchronous.

No signal handlers are installed. Registering a SIGINT/SIGTERM listener suppresses the default termination, which is the caller's decision, not a logger's — call logger.close() from your own shutdown hook.

Backpressure: blocks, does not drop

A synchronous batched write applies backpressure to the caller by blocking for the duration of one writeSync of at most bufferBytes. There is no unbounded in-memory queue to overflow, so under a slow disk the process slows down rather than growing a backlog that would eventually take it out of memory.

Entries are discarded only when the write itself fails or an open failure disabled the transport. Those are counted on droppedCount and announced in-band on the next successful write:

{"level":"warn","message":"file transport dropped 3 log entries","droppedEntries":3,"path":"/var/log/app.log"}

A failed payload is discarded rather than retained on purpose: holding a growing backlog against a disk that is not accepting writes is how a logger takes down the service it was meant to observe.

Transport failure is isolated

A transport that throws from write, flush or close never propagates into the caller. onTransportError receives it; without that hook the first failure per logger is reported on console.error and the rest are suppressed.

new Logger({
  transports: [new FileTransport({ path: '/var/log/app.log' })],
  onTransportError: (error, transport) => metrics.increment('log.transport.error'),
});

Output formats

Four formatters ship, and every transport takes one as format:

| Formatter | Emits | ns/entry | | --- | --- | ---: | | jsonFormat | {"level":"info","message":"order placed",…} | 272 | | prettyFormat | the same JSON, ANSI-coloured for a terminal | 238 | | textFormat | 09:00:15.123 INFO order placed requestId=r-1 | 661 | | logfmtFormat | level=info time=… msg="order placed" order.id=ord_1 | 826 |

import { ConsoleTransport, Logger, textFormat, logfmtFormat } from '@arkv/logger';

new Logger({
  transports: [
    new ConsoleTransport({
      format: process.env.NODE_ENV === 'production' ? logfmtFormat : textFormat,
    }),
  ],
});

textFormat is for a terminal. Reserved fields take their own positions, the rest trail as key=value, and an error follows on its own lines with its frames and its Caused by: chain, which is the shape a stack trace has everywhere else.

logfmtFormat is for a reader: Heroku, Grafana Loki and Splunk all parse it without being told a schema. logfmt is flat, so a nested object becomes dotted keys, order.id=ord_1 rather than an encoded blob, down to four levels. Values are quoted when they hold whitespace, a quote or an equals sign. It is never coloured.

No caller data can start a new line. Newlines and carriage returns are escaped in keys and values alike, in both formats. A raw one ends the physical line, so a message of ok\nlevel=error msg=forged, or a field named that, would produce a second record parsing as genuine, and any string a caller logs can carry one.

logfmtFormat is therefore always exactly one line, which is what a parser needs. textFormat is one line plus the error block it renders deliberately: the frames and the Caused by: chain are its own structure, not caller data, and a stack on one line is not a thing anybody reads.

util.inspect is not one of the options, and that is a measurement rather than a preference. It renders objects beautifully and costs 7.4 microseconds against 155 nanoseconds for JSON.stringify of the same entry, where a whole Logger.info call is roughly 1.5 microseconds. A formatter runs once per entry, so inspecting every one makes the formatter several times the cost of everything else. That trade is why pino pretty-prints in a separate process reading its JSON, and why textFormat renders nested values as JSON instead. If you want inspected output, pipe the JSON through something that inspects it; do not pay for it per entry in the process serving requests.

Shipping logs somewhere else

ConsoleTransport, StreamTransport and FileTransport write synchronously, which is what makes flush-on-exit possible. A collector reached over a network cannot honour that, and pretending otherwise is worse than saying so. The network transports implement flushAsync and closeAsync, and a graceful shutdown awaits logger.closeAsync() rather than calling close().

import { HttpTransport, Logger } from '@arkv/logger';

const logger = new Logger({
  transports: [
    new HttpTransport({
      url: 'https://logs.example.com/ingest',
      headers: { authorization: `Bearer ${token}` },
      batchSize: 200,
      flushIntervalMs: 2000,
    }),
  ],
});

process.on('SIGTERM', async () => {
  await server.close();
  await logger.closeAsync(); // the batch has actually left the process
});

encode is the seam every collector differs at, and the reason there is one HTTP transport rather than four:

// Datadog
new HttpTransport({
  url: 'https://http-intake.logs.datadoghq.com/api/v2/logs',
  headers: { 'DD-API-KEY': key },
  contentType: 'application/json',
  encode: (batch) => JSON.stringify(batch.map((each) => each.entry)),
});

// Splunk HEC
new HttpTransport({
  url: 'https://splunk.example.com:8088/services/collector',
  headers: { authorization: `Splunk ${token}` },
  contentType: 'application/json',
  encode: (batch) =>
    batch.map((each) => JSON.stringify({ event: each.entry })).join(''),
});

A 4xx that is not 408 or 429 is not retried, because the same bytes will be rejected the same way. Retry-After is obeyed in both its forms.

SyslogTransport speaks RFC 5424 over UDP (one datagram per entry) or TCP (octet-counted per RFC 6587, reconnecting when the daemon restarts).

Anything else that batches is a subclass of BatchTransport with a deliver method. The bounded queue, the batching, the single in-flight send, the backoff and the drop accounting all come from the base.

Thinning what gets through

import { SamplingTransport, ConsoleTransport } from '@arkv/logger';

new Logger({
  transports: [
    new SamplingTransport(new ConsoleTransport(), {
      rate: 0.1,          // one in ten of the routine traffic
      maxPerInterval: 50, // and at most fifty per second per event
      intervalMs: 1000,
    }),
  ],
});

Warnings and worse are never sampled, whatever the rate: sampling an error is how an incident becomes invisible. The per-event budget is what stops one hot loop spending the whole allowance. When a window closes having discarded anything, one line naming the count goes through, so a gap in the logs always carries its reason.

Correlating with a trace

There is no OpenTelemetry dependency here and there does not need to be. Logger's second argument accepts a plain function, so the active span is read per entry without this package knowing what a span is:

import { trace } from '@opentelemetry/api';

const logger = new Logger(config, () => {
  const span = trace.getActiveSpan()?.spanContext();
  return span ? { traceId: span.traceId, spanId: span.spanId } : {};
});

The same shape works for any propagation you already have. A ContextStore is for fields you set yourself; a function is for fields something else is already tracking.

Knowing whether logs are being lost

logger.stats();
// [{ name: 'FileTransport', dropped: 0, queued: 0, errors: 0 }]

Anything above zero on dropped is worth an alert. A transport that keeps no counters is omitted rather than reported as zero, which would read as measured and fine.

Changing the level on a running process

process.on('SIGUSR2', () => logger.setLevel(LogLevel.DEBUG));

A logger and every child it made share one level, so this reaches all of them. A transport that named its own level keeps it.

Uncaught Errors

import { captureGlobalErrors } from '@arkv/logger';

const stop = captureGlobalErrors(logger);

Logs uncaughtException at fatal and unhandledRejection at error, flushes, and then exits with code 1 — Node's own behaviour, which installing a listener otherwise suppresses. Pass { exitOnUncaught: false } if something else arranges the shutdown. Returns a disposer.

Testing

import { MemoryTransport, parseLogEntry } from '@arkv/logger/testing';

const memory = new MemoryTransport();
const logger = new Logger({ transports: [memory] });

logger.info('hello', { orderId: 1 });
expect(memory.last?.orderId).toBe(1);

MemoryTransport collects sanitized entries as objects — no console spy, no formatting to parse, no global to restore. parseLogEntry is there for the cases where you do spy on console.log: it strips ANSI and parses, so it works against the colored and plain renderings alike.

This is a real subpath export, unlike the old src/test-utils.ts, which was excluded from dist precisely because it was an accidental leak rather than a supported entry point.

Development vs Production Output

In development (NODE_ENV !== 'production'), output is colored JSON for readability. In production, output is plain JSON for log aggregators:

// Colored dev output (default)
const dev = new Logger({ isDevelopment: true });

// Plain JSON for production
const prod = new Logger({ isDevelopment: false });

Event Filtering

Suppress logs for specific events (e.g. health checks):

const logger = new Logger(
  { filterEvents: ['/health', '/ready'] },
  contextStore,
);

When the context's event field matches a filtered event, the log is silently dropped.

API

Logger

new Logger(config?: LoggerConfig, context?: ContextStore)

| Property | Type | Description | |----------|------|-------------| | logLevel | LogLevel | Current log level. Set it with setLevel | | appId | string \| undefined | name-version-env or undefined | | transports | readonly Transport[] | The configured transports |

| Method | Description | |--------|-------------| | info(message, ...params) | Log at info level | | log(message, ...params) | Deprecated alias for info | | debug(message, ...params) | Log at debug level | | verbose(message, ...params) | Log at verbose level | | warn(message, ...params) | Log at warn level | | error(message, ...params) | Log at error level | | fatal(message, ...params) | Log at fatal level | | child(bindings) | Logger with bound fields, sharing transports | | setLevel(level) | Change the level of this logger and its children | | stats() | What each transport is holding and has dropped | | flush() | Push every transport's buffer. Synchronous | | close() | Flush, then release transport resources | | flushAsync() | Drain every transport, awaiting the async ones | | closeAsync() | Drain, then release. What a graceful shutdown awaits |

Each method accepts string, Record<string, unknown>, or Error as the message, plus optional extra params in every case. warn(err, { attempt: 3 }) and info({ action: 'x' }, { ms: 4 }) both type-check.

A plain object param is merged into the entry as extra fields. Anything else object-shaped — an array, Map, Set, Date, typed array or class instance — cannot be merged without losing its contents, so it is collected under a params array instead and rendered by the sanitizer:

logger.info('Cache state', { hits: 1 });        // → { hits: 1 }
logger.info('Cache state', new Map([['a', 1]])); // → { params: [{ '[Map]': [['a', 1]] }] }

A message that is not a string, an Error or a plain object is reported with an invalidMessageWarning, and the value itself is kept under originalMessage.

LoggerConfig

| Field | Type | Default | Description | |-------|------|---------|-------------| | name | string | — | Application name | | version | string | — | Application version | | env | string | — | Environment name | | level | LogLevel | DEBUG | Minimum log level | | isDevelopment | boolean | NODE_ENV !== 'production' | Colored vs plain JSON output | | maskFields | string[] | [] | Additional fields to mask (merged with defaults) | | filterEvents | string[] | [] | Context events to suppress | | maxArrayLength | number | 100 | Max array items before truncation | | maxDepth | number | 32 | Max nesting depth before truncation | | transports | Transport[] | one ConsoleTransport | Replaces the default — see Transports | | bindings | Record<string, unknown> | — | Static fields merged into every entry | | onTransportError | (error, transport) => void | — | Called instead of the one-shot console.error |

FileTransport

| Option | Type | Default | Description | |-------|------|---------|-------------| | path | string | — | Log file. Parent directories are created | | level | LogLevel | logger's level | Minimum level for this transport | | format | LogFormatter | jsonFormat | Per-transport formatting | | maxSize | number | 0 (off) | Rotate once the file reaches this many bytes | | interval | 'hourly' \| 'daily' | — | Rotate when the UTC hour/day changes | | maxFiles | number | 5 | Rotated files to keep (app.log.1app.log.N) | | bufferBytes | number | 0 | 0 writes through; above that, batches | | flushIntervalMs | number | 1000 | Partial-buffer flush. Unref'd. Buffering only | | flushOnExit | boolean | true | process.on('exit') hook. Buffering only | | onError | (error) => void | — | Every write/rotate/open failure |

Readable state: filePath, droppedCount, errorCount, pendingBytes. Rotation is checked before each write, so a file overshoots maxSize by at most one batch.

Rotated files are named app.log.1, app.log.2 by default. naming: 'date' names them for the period they hold instead, app.log.2026-08-29, so a rotated file's name never changes again and a shipper globbing by day finds it. utc: false makes interval follow the host clock rather than UTC.

ContextStore

new ContextStore()

| Method | Description | |--------|-------------| | getContext() | Get current async context | | updateContext(partial) | Merge partial update into current context | | runWithContext(ctx, callback, options?) | Execute callback within a context. Nested calls merge unless { inherit: false } |

Deliberately not included

  • Signal handlers. Installing one changes how the process terminates. The SIGUSR2 and SIGTERM examples above are yours to install.
  • Compression of rotated files. Doing it synchronously stalls the event loop for as long as gzip takes on a file that may be hundreds of megabytes; doing it asynchronously races the next rotation. logrotate, pointed at path.*, does it properly and is already on the host.
  • A vendor transport per collector. They differ only in body shape, which is HttpTransport's encode. Four classes would be four buffering stories and three of them a bug.
  • Browser support. This is a Node package. node:fs, node:net, node:dgram and node:async_hooks are imported directly from the main entry, and process.pid / process.env are read without a guard.

Two things that used to be on this list and no longer are. Sampling is here as SamplingTransport, a decorator rather than a config field, so nothing is thinned unless it is wrapped and a discard is always announced. Async transports are here as the flushAsync / closeAsync half of the contract; the synchronous half stays exactly what it was, because process.on('exit') still cannot await.

License

MIT