Log Ingestion

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().

Structured data

Every log method accepts an optional second argument for structured metadata:

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

Reserved fields

The following top-level keys have special meaning and are handled separately from general 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>Explicit metadata bucket

All other keys are treated as metadata automatically:

// 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 - 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)
})

How encryption works

Every log entry is encrypted with AES-256-GCM before leaving your process:

  1. A 12-byte random nonce (IV) is generated per log entry
  2. The JSON payload (message, requestId, traceId, context, and metadata) is encrypted using your encryptionKey and the nonce
  3. The API receives the log level plus { nonce, payload, keyVersion }, where payload is the 16-byte GCM auth tag followed by the ciphertext
  4. Decryption happens on-device in the Lumber mobile app

The log level is the one field sent and stored unencrypted, so Lumber can filter by level and decide when to send push alerts. The Lumber API cannot read your log content at any point. Even if the API were compromised, your logs remain unreadable without your encryption key.

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,
  encryptionKey: tenantEncryptionKey,
})

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