initStrada() to get spans for HTTP and database calls. The SDK never patches modules and needs no preload:import { initStrada } from "@strada.sh/sdk" import { fetchSpans, httpServerSpans, mysql2Spans } from "@strada.sh/sdk/instrument" initStrada({ projectId: "01JTHG5M7XPQR8KNCZ0W4D", service: "api", integrations: [fetchSpans(), httpServerSpans(), mysql2Spans()], })
node:diagnostics_channel. Node, undici, and a growing list of libraries publish an event for every operation. Channels are process-global, so this works with any import order and inside bundled apps (Vite, Next).your code: fetch() / pool.query() / redis.get() │ ▼ library publishes an event (zero cost when nobody listens) node:diagnostics_channel ──────────► integration.setup() subscriber │ start span, end span, record errors ▼ OTLP export to Strada
| Integration | Spans | Needs |
fetchSpans() | outgoing fetch(), adds traceparent and baggage headers | Node |
httpClientSpans() | outgoing http.request / https.request, adds headers | Node 22.12+ |
httpServerSpans() | incoming http.createServer requests, active for the whole handler | Node |
mysql2Spans() | query() and execute() | mysql2 3.20+ |
redisSpans() | every command, plus MULTI / PIPELINE on node-redis | redis 5.12+ or ioredis 5.11+ |
mongooseSpans() | queries, aggregations, save(), insertMany(), bulkWrite(), cursors | mongoose 9.7+ |
graphqlSpans() | parse, validate, execute, subscribe (no resolver spans) | graphql 17+ |
aiSpans() | generateText / streamText, model calls, tool calls, embeddings, with token usage | ai 7+ |
h3Spans() | routes and middleware, with http.route | h3 2.0.1-rc.14+, with tracingPlugin() |
pinoLogs() | every pino log line as a log record (not spans) | pino 9.10+ |
httpServerSpans() when your framework already creates request spans (Spiceflow tracer, Next.js), or each request gets two spans.fetch. fetchSpans() skips requests to the ingest endpoint, otherwise every export would trace itself.startSpan():import { startSpan } from "@strada.sh/sdk" const rows = await startSpan( { name: "SELECT users", attributes: { "db.system.name": "postgresql" } }, () => pool.query("SELECT * FROM users WHERE id = $1", [id]), )
@opentelemetry/api themselves (Vercel AI SDK, Prisma) need registerOpenTelemetry() from @strada.sh/sdk/otel instead. See the SDK docs.setup() runs once at the end of initStrada(). The function it returns runs on shutdown():import type { StradaIntegration } from "@strada.sh/sdk" const integration: StradaIntegration = { name: "myIntegration", setup() { // subscribe here return () => { // unsubscribe here } }, }
TracingChannel and wraps each operation. The name convention is {npm-package}:{operation}:// @acme/queue import dc from "node:diagnostics_channel" type JobMessage = { queue: string; job: string; error?: unknown } const jobChannel = dc.tracingChannel<unknown, JobMessage>("acme-queue:job") export async function runJob(queue: string, job: string, handler: () => Promise<void>) { // Skip the message object when nobody listens: zero cost in production without APM. if (!jobChannel.hasSubscribers) return handler() return jobChannel.tracePromise(handler, { queue, job }) }
tracePromise publishes five events on the same message object:| Event | When |
start | synchronously, in the caller's context, before handler runs |
end | synchronously, after handler returned its promise |
error | the promise rejected, or handler threw synchronously |
asyncStart, asyncEnd | the promise settled |
traceCallback for callback APIs and traceSync for synchronous work.start and end it on asyncEnd. A synchronous throw publishes error then end with no asyncEnd, so end closes the span only when error is already set.diagnostics_channel turns a throwing subscriber into an uncaught exception, so every handler goes through guard():import dc from "node:diagnostics_channel" import { SpanKind, SpanStatusCode, trace, type Span, type StradaIntegration } from "@strada.sh/sdk" type JobMessage = { queue: string; job: string; error?: unknown } /** A throwing subscriber would crash the app: log and drop instead. */ function guard<T>(handler: (message: T) => void) { return (message: T) => { try { handler(message) } catch (error) { console.error("[acmeQueueSpans]", error) } } } export function acmeQueueSpans(): StradaIntegration { return { name: "acmeQueueSpans", setup() { const tracer = trace.getTracer("acme-queue") const channel = dc.tracingChannel<unknown, JobMessage>("acme-queue:job") const spans = new WeakMap<JobMessage, Span>() const end = (message: JobMessage) => { spans.get(message)?.end() spans.delete(message) } const handlers = { start: guard<JobMessage>((message) => { const span = tracer.startSpan(`process ${message.queue}`, { kind: SpanKind.CONSUMER, attributes: { "messaging.destination.name": message.queue, "acme.job": message.job }, }) spans.set(message, span) }), end: guard<JobMessage>((message) => { if ("error" in message) end(message) }), asyncStart() {}, asyncEnd: guard(end), error: guard<JobMessage>((message) => { const span = spans.get(message) if (!span) return span.recordException(message.error instanceof Error ? message.error : String(message.error)) span.setStatus({ code: SpanStatusCode.ERROR }) }), } channel.subscribe(handlers) return () => channel.unsubscribe(handlers) }, } }
initStrada({ projectId: "01JTHG5M7XPQR8KNCZ0W4D", service: "worker", integrations: [acmeQueueSpans()] })
runJob() was called, because start runs synchronously in the caller's context.guard() does above.WeakMap. It is the same object for all five events of one call.handler (the built-in integrations do, with bindStore on the SDK's internal AsyncLocalStorage). Spans, logs, and errors created inside the job attach to the caller's span, not to the process emails span.startSpan() inside the job instead:await runJob("emails", "welcome", () => startSpan({ name: "send welcome" }, () => sendWelcomeEmail()))
| Runtime | Built-in HTTP integrations | Custom and library integrations |
| Node | yes | yes |
| Cloudflare Workers | no (workerd publishes no HTTP channels) | yes, with nodejs_compat |
| Browser | no | setup() runs, but there is no diagnostics_channel |