Logging
Log levels
The SDK exposes five log levels, each with a corresponding method:
| Method | Level | Use for |
|---|---|---|
lumber.debug() | debug | Verbose diagnostic information |
lumber.info() | info | Normal operational events |
lumber.warn() | warn | Unexpected but recoverable situations |
lumber.error() | error | Failures that need attention |
lumber.fatal() | fatal | Critical 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:
| Field | Type | Description |
|---|---|---|
requestId | string | Ties a log to a specific HTTP request |
traceId | string | Distributed trace identifier |
context | LogContext | Structured context, merged over the automatic context (pid, hostname, env, and caller code/line/column when enabled) |
metadata | Record<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:
- The payload is encrypted synchronously (in-process, zero network)
- The encrypted entry is pushed onto an internal async queue
- The queue drains in the background, sending one
POST /logsrequest per log, one at a time - 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.stackparsing 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()