Skip to content

prsmjs/trace

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

@prsm/trace

@prsm/trace

test npm

Distributed trace propagation for Node.js. W3C TraceContext-compatible identifiers, automatic context propagation through AsyncLocalStorage, framework adapters for Express, Koa, Hono, and Fastify, and a span emitter that any collector or observability layer can consume.

Install

npm install @prsm/trace

The pattern

A tracer creates trace IDs at the edge of a request and propagates them through every async boundary without requiring you to thread context manually.

import { createTracer } from '@prsm/trace'

const tracer = createTracer({ service: 'order-api' })

app.use(tracer.express())

app.post('/order', async (req, res) => {
  // tracer.current() returns { traceId, spanId, parentSpanId, sampled }
  // available everywhere in this async chain
  await queue.push(job)
  await cache.fetch(key, loader)
  await workflow.start('process-order', input)
  res.json({ ok: true })
})

tracer.onSpan((span) => {
  // ship to your collector, log it, store it, broadcast it
})

When a downstream service receives a request from this server with a traceparent header, its own tracer can pick up where this one left off — the trace continues across process and network boundaries.

Why this exists

Tracing is the difference between "an order failed somewhere" and "the order failed because the cache stampede on user:42 blocked the workflow's lookup step for 11 seconds." It is the cross-cutting concern that turns a distributed system from a black box into something you can reason about.

The hard part is propagation. You can sprinkle traceId parameters through every function signature in your codebase, or you can use AsyncLocalStorage and have it just work — including across setTimeout, promise chains, and any package that opts into the convention. This package gives you the latter, with a tiny surface area and zero ceremony.

API

createTracer(options)

const tracer = createTracer({
  service: 'order-api',
  sampler: true,           // true | false | 0..1 | ({ name, attributes }) => boolean
})

tracer.span(name, attributes?, fn, opts?)

The most common way to instrument code. Wraps a function in a span that automatically captures duration, errors, and context.

const value = await tracer.span('compute-total', { orderId }, async () => {
  return await computeTotal()
})

Inside the callback, tracer.current() returns the active context. Errors thrown by fn are recorded on the span and rethrown.

tracer.startSpan(name, attributes?, opts?)

Lower-level: returns a span handle with setAttribute, setError, and end. Useful when the lifetime doesn't fit a single await.

const handle = tracer.startSpan('long-running-thing')
await tracer.run(handle.context, async () => {
  // ... work that can read tracer.current() ...
})
handle.end()

tracer.current()

Returns the active trace context or null. The context shape is { traceId, spanId, parentSpanId, sampled }.

tracer.run(ctx, fn)

Runs fn with ctx as the active context. Useful when you have a context from another source (e.g. a persisted record) and want to restore it before doing work.

tracer.inject(headers) / tracer.extract(headers)

Read and write the traceparent header on outbound and inbound requests.

const headers = {}
tracer.inject(headers)
await fetch(url, { headers })

// On the other end:
const parent = tracer.extract(req.headers)

tracer.fromTraceparent(header)

Parses a traceparent header string into { traceId, parentSpanId, sampled } for use as a span parent.

Framework adapters

app.use(tracer.express())               // Express
app.use(tracer.koa())                   // Koa
app.use(tracer.hono())                  // Hono
fastify.register(tracer.fastify())      // Fastify

Each adapter:

  • Reads traceparent from the incoming request to continue an upstream trace.
  • Starts a server span around the handler.
  • Captures http.method, http.path, http.status as attributes.
  • Marks 5xx responses as errors.
  • Puts the context in AsyncLocalStorage so everything inside the handler sees it.

The adapters can also be imported by subpath if you prefer:

import { expressMiddleware } from '@prsm/trace/express'

tracer.onSpan(callback)

Registers a span-end listener. The callback receives a fully-formed span:

{
  traceId, spanId, parentSpanId,
  service, name, kind,        // 'server' | 'client' | 'internal' | 'producer' | 'consumer'
  startedAt, endedAt, durationMs,
  attributes,
  status,                     // 'ok' | 'error'
  error,                      // { name, message, stack } | null
}

Hook this into your collector, log it, push it to @prsm/devtools, or ship it to OpenTelemetry.

Sampling

createTracer({ sampler: true })             // sample everything (default)
createTracer({ sampler: false })            // sample nothing
createTracer({ sampler: 0.1 })              // sample 10%
createTracer({ sampler: ({ name }) => name.startsWith('http.') })

The sampling decision is made when a root span starts (no parent). Child spans inherit their parent's decision, so a sampled trace stays fully sampled all the way down.

W3C TraceContext compatibility

Trace and span IDs use the W3C TraceContext format:

  • traceId: 16 random bytes, encoded as 32 hex characters.
  • spanId: 8 random bytes, encoded as 16 hex characters.
  • traceparent: 00-{traceId}-{spanId}-{flags} where flags 01 means sampled, 00 means not.

You can talk to any service that speaks the same format — OpenTelemetry collectors, other languages' OTel SDKs, browser-based instrumentation.

Dev

npm install
npm test

No infrastructure required.

About

Distributed trace propagation with W3C TraceContext, AsyncLocalStorage, and framework adapters

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors