Log Ingestion
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().
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:
| 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> | 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:
- 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 - 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:
- A 12-byte random nonce (IV) is generated per log entry
- The JSON payload (message,
requestId,traceId, context, and metadata) is encrypted using yourencryptionKeyand the nonce - The API receives the log level plus
{ nonce, payload, keyVersion }, wherepayloadis the 16-byte GCM auth tag followed by the ciphertext - 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.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,
encryptionKey: tenantEncryptionKey,
})
logger.info('Tenant action', { tenantId: '123' })
await logger.flush()