Skip to content

Spans

Spans are loggers with timing. Call .span() and it creates a child logger that tracks how long the block runs.

Basic Usage

typescript
{
  using span = log.span("import", { file: "data.csv" })
  span.info?.("parsing rows")
  const rows = await parseFile()
  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 or code:

bash
TRACE=1 bun run app              # All spans
TRACE=myapp:db bun run app       # Only db spans
TRACE=myapp,other bun run app    # Multiple namespaces
typescript
import { enableSpans, setTraceFilter } from "loggily"

enableSpans() // All spans
setTraceFilter(["myapp:db"]) // Only db spans

Nested Spans

Spans automatically track parent-child relationships and share trace IDs:

typescript
{
  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:

typescript
{
  using span = log.span("batch")
  span.spanData.total = items.length

  for (const item of items) {
    await process(item)
    span.spanData.processed = ((span.spanData.processed as number) ?? 0) + 1
  }

  span.spanData.status = "complete"
}

Read-only Properties

PropertyTypeDescription
idstringUnique span ID (sp_1, sp_2, ...)
traceIdstringShared across nested spans
parentIdstring | nullParent span ID
startTimenumberStart timestamp (ms since epoch)
endTimenumber | nullEnd timestamp (null while running)
durationnumberLive duration (computed on access)

Manual End

For environments without using support:

typescript
const span = log.span("operation")
try {
  await doWork()
  span.spanData.result = "success"
} finally {
  span.end()
}

Logging Within Spans

Spans are full loggers -- you can call .info?.(), .debug?.(), etc:

typescript
{
  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:

bash
TRACE=1 LOG_FORMAT=json bun run app
json
{
  "time": "2026-01-15T14:32:16.456Z",
  "level": "span",
  "name": "myapp:import",
  "msg": "(1234ms)",
  "duration": 1234,
  "rowCount": 500
}

Released under the MIT License.