Spans
Spans are loggers with timing. Call .span?.() and it creates a child logger that tracks how long the block runs. When spans are disabled, .span is undefined, so optional chaining skips the call and its arguments.
Basic Usage
{
using span = log.span?.("import", { file: "data.csv" })
span?.info?.("parsing rows")
const rows = await parseFile()
if (span) span.spanData.rowCount = rows.length
}
// SPAN myapp:import (1234ms) {rowCount: 500, file: "data.csv"}The using keyword (TC39 Explicit Resource Management) calls span[Symbol.dispose]() when the block exits, which records the end time and emits the span event.
Enabling Spans
Span output is off by default. Enable via environment variables:
TRACE=1 bun run app # All spans
TRACE=myapp:db bun run app # Only db spans
TRACE=myapp,other bun run app # Multiple namespacesWith spans disabled, log.span is undefined:
using span = log.span?.("import", expensiveProps())
// expensiveProps() is not called unless TRACE enables this loggerSpan availability is checked when the logger is created. Set TRACE before creating module-level loggers. For a narrow filter, create the matching child logger first:
const db = createLogger("myapp").child("db")
using span = db.span?.("query") // enabled by TRACE=myapp:dbNested Spans
Spans automatically track parent-child relationships and share trace IDs:
{
using request = log.span?.("request", { path: "/api/users" })
{
using auth = request?.span("auth")
await verifyToken()
}
{
using db = request?.span("db:query")
// db?.spanData.parentId === request.spanData.id
// db?.spanData.traceId === request.spanData.traceId
await fetchUsers()
}
}Output:
SPAN myapp:auth (12ms) {}
SPAN myapp:db:query (45ms) {}
SPAN myapp:request (62ms) {path: "/api/users"}Span Data
Set custom attributes via span.spanData:
{
using span = log.span?.("batch")
if (span) span.spanData.total = items.length
for (const item of items) {
await process(item)
if (span) {
span.spanData.processed = ((span.spanData.processed as number) ?? 0) + 1
}
}
if (span) span.spanData.status = "complete"
}Read-only Properties
| Property | Type | Description |
|---|---|---|
id | string | Unique span ID (sp_1, sp_2, ...) |
traceId | string | Shared across nested spans |
parentId | string | null | Parent span ID |
startTime | number | Start timestamp (ms since epoch) |
endTime | number | null | End timestamp (null while running) |
duration | number | Live duration (computed on access) |
Manual End
For environments without using support:
const span = log.span?.("operation")
try {
await doWork()
if (span) span.spanData.result = "success"
} finally {
span?.end()
}Logging Within Spans
Spans are full loggers -- you can call .info?.(), .debug?.(), etc:
{
using span = log.span?.("import")
span?.info?.("starting import")
span?.debug?.("reading file")
span?.warn?.("skipping malformed row", { row: 42 })
}JSON Output
Spans respect the output format. Set it via environment variable or config array:
TRACE=1 LOG_FORMAT=json bun run appconst log = createLogger("myapp", [{ format: "json" }, console]){
"time": "2026-01-15T14:32:16.456Z",
"level": "span",
"name": "myapp:import",
"msg": "(1234ms)",
"duration": 1234,
"rowCount": 500
}