Node.js

@infinitetoken/lumber is Lumber's Node.js SDK. It encrypts every log with AES-256-GCM before it leaves your process, and sends it from an internal queue, so logging never adds latency to your request path. Encryption and delivery use Node's built-in crypto and fetch, and its one dependency is Lumber's own shared types package, which has none of its own.

Install

npm install @infinitetoken/lumber

Node.js 18 or later, for its built-in fetch.

Quick start

import lumber from '@infinitetoken/lumber'

lumber.config({ apiKey: process.env.LUMBER_API_KEY! })

lumber.info('Server started', { port: 3000 })
lumber.warn('Rate limit approaching', { requestId: 'req-123', userId: 456 })
lumber.error('Payment failed', { traceId: 'trace-abc', orderId: 789 })

// On graceful shutdown, send what's still queued
process.on('SIGTERM', async () => {
  await lumber.flush()
  process.exit(0)
})

Call lumber.config() once at startup, before any logging; calling a log method before it throws. Every module that imports @infinitetoken/lumber then logs with the same client.

The API key comes from the Lumber app (see Getting Started). Keep it in an environment variable or your secrets manager, never in source control, and never in an app or game you ship to people (why).

Log methods

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.flush(timeout?)Waits for queued logs to send; with a timeout in milliseconds, resolves false if logs were still queued then

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

Data

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

Four top-level keys 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 (see the context option under Options)
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' },
})

Explicit metadata keys win over colliding keys. Everything, the message included, is encrypted.

Options

lumber.config(options) takes these. Calling it again replaces the client: logs still queued in the previous one keep sending in the background, but lumber.flush() only waits for the new client's queue, so call await lumber.flush() before reconfiguring if you need to be sure they're delivered.

apiKey · string · required

The API key from your app's Secrets screen in the Lumber app: a 32-character hexadecimal token, a dot, then the app's base64-encoded encryption key. The SDK sends only the token to the API, as a Bearer token, and encrypts all log content with the encryption key before it leaves your process. lumber.config() (and new LumberClient()) throws a RangeError if the encryption key doesn't decode to exactly 32 bytes. See API Keys.

apiKey: process.env.LUMBER_API_KEY!

endpoint · string · optional

Override the Lumber API base URL (no trailing path). Defaults to the official Lumber API, https://api.lumber.infinitetoken.com. Useful for routing through a proxy. The SDK posts each log to {endpoint}/logs.

endpoint: 'https://your-proxy.example.com'

timeout · number · optional

How long to wait, in milliseconds, for Lumber to answer one log before counting the attempt as failed and trying again (see Delivery). Defaults to 10000.

timeout: 5000

console · boolean | ConsoleOptions · optional

Echo logs to stdout and stderr, handy during development. Off by default.

true: all levels, message only, colored prefix when TTY.

Object form:

console: {
  levels?: Partial<Record<'debug' | 'info' | 'warn' | 'error' | 'fatal', boolean>>
  prefix?: boolean    // show [DEBUG]/[INFO]/etc label, default true
  color?: boolean     // ANSI colors when TTY, default true
  data?: boolean      // append metadata after message, default false
  pretty?: boolean    // JSON.stringify extras with indentation, default false
  timestamp?: boolean | 'time' | 'iso' | 'relative'
}

timestamp values:

ValueOutput
true or 'time'14:32:01.452
'iso'2024-01-15T14:32:01.452Z
'relative'+0.452s (since process start)

Example development config:

lumber.config({
  apiKey: process.env.LUMBER_API_KEY!,
  console: {
    timestamp: 'time',
    color: true,
    data: true,
    pretty: true,
    levels: { debug: process.env.NODE_ENV !== 'production' },
  },
})

context · boolean | ContextOptions · optional

The environment context attached to every log. Defaults to true: the pid, hostname and NODE_ENV (as env), captured once at lumber.config() time.

{
  pid: 12345,
  hostname: 'my-server',
  env: 'production',
}

false turns context capture off entirely.

Object form:

context: {
  env?: boolean        // capture pid, hostname, NODE_ENV, default true
  caller?: boolean     // capture file/line/column per log, default false
  callerDepth?: number // extra stack frames to skip for wrapper layers, default 0
}

caller reads Error.stack on every log, which adds a little overhead per call: use it in development or at low volume. callerDepth is for when you wrap the Lumber client:

// Your wrapper adds one frame to the stack
function log(msg: string) {
  lumber.info(msg)
}

// Without callerDepth, context.code points to this wrapper, not your caller
lumber.config({
  context: { caller: true, callerDepth: 1 },
})

Delivery

Logging is non-blocking. Each lumber.*() call encrypts its log in-process and pushes it onto an internal queue, which sends one POST /logs per log in the background, and your code carries on at once.

  • Logs are sent one at a time, in the order they were logged. While a log is being retried, the logs behind it wait their turn.
  • A network error, a 5xx response or no answer within timeout counts as a failed attempt. Each log gets up to 5 attempts, waiting 1, 2, 4, then 8 seconds between them, and is then dropped with a warning on the console.
  • A log Lumber refuses (a 4xx, such as an invalid API key) is dropped at once with a warning, since sending it again won't help.
  • When your team has sent its plan's logs for the month, Lumber answers 429: the SDK warns once and drops logs until the month turns, rather than queueing them.
  • The queue is in memory, so logs still queued when the process exits are lost. Call lumber.flush() during graceful shutdown, as in the quick start; lumber.flush(5000) waits 5 seconds at most.

Serverless

A serverless platform can freeze or stop a function as soon as it responds, with logs still queued. Keep the function running until they're sent with the platform's own hook, giving flush() a time limit so a slow answer can't hold the function to its maximum duration. On Vercel, with waitUntil from @vercel/functions:

import { waitUntil } from '@vercel/functions'

export default async function handler(req, res) {
  lumber.info('Handled request', { path: req.url })
  res.send('ok')
  waitUntil(lumber.flush(10_000))
}

Where there's no such hook (AWS Lambda), await lumber.flush(10_000) before returning.

More than one client

For isolated clients (per-tenant logging, say), use LumberClient instead of the module's own:

import { LumberClient } from '@infinitetoken/lumber'

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

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

It takes the same options as lumber.config().

Source

The SDK is open source: Lumber-Node on GitHub, published as @infinitetoken/lumber on npm. It's tested against the same known answers for encryption and the wire format as Lumber's other clients.