Logging

Log levels

The SDK exposes five log levels, each with a corresponding method:

MethodLevelUse for
lumber.debug()debugVerbose diagnostic information
lumber.info()infoNormal operational events
lumber.warn()warnUnexpected but recoverable situations
lumber.error()errorFailures that need attention
lumber.fatal()fatalCritical failures, app may not continue

lumber.log() is an alias for lumber.info().

The data argument

Every log method takes an optional second argument, an object of data to attach to the log:

lumber.info('User signed in', {
  userId: user.id,
  email: user.email,
  ip: req.ip,
})

Keys with their own place

Four top-level keys of the data argument aren't metadata. The SDK sends them as fields of their own (see the payload), and the Lumber app shows them apart from the metadata:

FieldTypeDescription
requestIdstringTies a log to a specific HTTP request
traceIdstringDistributed trace identifier
contextLogContextStructured context, merged over the automatic context (pid, hostname, env, and caller code/line/column when enabled)
metadataRecord<string, unknown>The metadata itself, for keys that would otherwise clash with the three above

Every other key goes into the log's metadata, and nothing inside metadata is special:

// These are equivalent
lumber.error('DB query failed', { duration: 1200, table: 'users' })

lumber.error('DB query failed', {
  metadata: { duration: 1200, table: 'users' },
})

Async queue

Logging is non-blocking. When you call any lumber.*() method:

  1. The payload is encrypted synchronously (in-process, zero network)
  2. The encrypted entry is pushed onto an internal async queue
  3. The queue drains in the background, sending one POST /logs request per log, one at a time
  4. Your code continues immediately

This means log calls never add latency to your request path.

Queue behavior

  • Logs are sent one at a time, in the order they were enqueued
  • A network error or any non-2xx response counts as a failed attempt. Each log gets up to 5 attempts, waiting 1s, 2s, 4s, then 8s between them. After the fifth failure the log is dropped and a warning is printed to the console
  • While a log is being retried, the logs behind it wait their turn
  • The queue is in memory, so logs are lost if the process exits before they drain

Always call lumber.flush() during graceful shutdown:

process.on('SIGTERM', async () => {
  await lumber.flush()
  process.exit(0)
})

Encryption

The SDK encrypts each log with AES-256-GCM (authenticated encryption, one of the ciphers HTTPS uses) before it's queued, using the encryption key in your API key, and sends Lumber only the token. The level is the one field Lumber can read, so it can filter by level and decide when to send push alerts. Encryption covers what Lumber can and can't see, and the HTTP API the exact format, for sending logs without the SDK.

Context capture

By default, the SDK automatically attaches environment context to every log:

// Captured once at lumber.config() time
{
  pid: 12345,
  hostname: 'my-server',
  env: 'production',
}

To capture the caller file and line number per log:

lumber.config({
  // ...
  context: { caller: true },
})

Note: Caller capture uses Error.stack parsing and adds minor per-call overhead. Use it in development or low-throughput scenarios.

To disable context entirely:

lumber.config({
  // ...
  context: false,
})

Using the class directly

For multiple isolated clients (e.g., per-tenant logging), use LumberClient instead of the singleton:

import { LumberClient } from '@infinitetoken/lumber'

const logger = new LumberClient({
  apiKey: tenantApiKey,
})

logger.info('Tenant action', { tenantId: '123' })
await logger.flush()