sdxc

Type to search, or start from one of these:

@sdxc/trace-context

W3C Trace Context: traceparent and tracestate, one trace per invocation, propagated to jobs and outbound requests

npm add @sdxc/trace-context
pnpm add @sdxc/trace-context
yarn add @sdxc/trace-context
bun add @sdxc/trace-context
Depends on
remix
Used by
uptime, auth-saas, reader, blog, books

W3C Trace Context: traceparent and tracestate, one trace per invocation, propagated to jobs and outbound requests.

Installation

npm add @sdxc/trace-context

The middleware runs on the remix router and stamps the trace on the @sdxc/logger wide event; parsers return @sdxc/result values. All three install alongside this package.

W3C Trace Context defines two headers every hop of a distributed request agrees on: traceparent carries the trace id, the id of the calling span and a flags byte; tracestate carries up to 32 vendor-owned entries beside it. This package reads and writes both to the letter of the specification (Level 1, plus the Level 2 random-trace-id flag) and binds one trace to each invocation through AsyncLocalStorage. @sdxc/jobs carries the trace in its envelope and @sdxc/api-client injects it into every request.

Usage

Give Every Request A Trace

import { createLogger } from "@sdxc/logger";
import { log } from "@sdxc/logger/middleware";
import { trace } from "@sdxc/trace-context/middleware";
import { createRouter } from "remix/router";

let logger = createLogger({ service: "api" });
let router = createRouter({ middleware: [log(logger), trace()] });

router.get("/", (ctx) => Response.json({ traceId: ctx.trace.traceId }));

The request's log now carries trace_id, span_id, trace_flags, and parent_span_id when the caller sent a valid traceparent.

Propagate By Hand

import { inject, withTrace } from "@sdxc/trace-context";

let headers = new Headers();
inject(headers); // writes the current trace; nothing outside one
await fetch("https://api.example.com/items", { headers });

let response = await stub.fetch(withTrace(request)); // forwarding a request

Read And Write The Headers

import { isSuccess, unwrap } from "@sdxc/result";
import { parse, stringify } from "@sdxc/trace-context/traceparent";
import * as TraceStateHeader from "@sdxc/trace-context/tracestate";

let parent = parse("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01");
if (isSuccess(parent)) parent.data.sampled; // true

let state = unwrap(TraceStateHeader.parse("rojo=00f067aa0ba902b7,congo=t61rcWkgMzE"));
let updated = unwrap(state.set("congo", "new")); // "congo=new,rojo=00f067aa0ba902b7"
TraceStateHeader.stringify(updated, { maxLength: 512 });

API

@sdxc/trace-context

TraceContext

traceId (32 hex digits), spanId (this invocation's span, 16 hex digits), parentSpanId (the caller's span, null for a root), sampled, random, and state (a TraceState). TraceContext.StartOptions and TraceContext.Propagation type the options below.

startTrace(options?)

A new root with fresh random ids from crypto.getRandomValues, flagged random. sampled defaults to true; state to the empty state.

continueTrace(parent, state?)

The caller's trace id and flags, a new span id, and the caller's span as parentSpanId.

extract(headers)

Continues the trace headers carry. A missing or invalid traceparent starts a new trace and discards tracestate unread; an invalid tracestate beside a valid parent is dropped. It always returns a trace.

inject(headers, trace?, propagation?)

Writes traceparent naming trace.spanId as parent, and tracestate when the state has entries. trace defaults to currentTrace(), and nothing is written without one. A header already present is kept, and a caller-set traceparent gets no tracestate beside it. propagation is "all" (default), "traceparent" or "none".

withTrace(request, trace?)

A copy of request whose trace headers name the current span as parent; the body moves to the copy. Outside a trace, the request itself is returned.

toTraceParent(trace)

The traceparent value naming trace.spanId as parent, for carrying a trace as an RPC argument.

currentTrace() and runWithTrace(trace, fn)

The trace bound to the running invocation, and the binding itself. A nested binding restores the outer trace when it returns.

traceFields(trace)

{ trace_id, span_id, parent_span_id, trace_flags }, named as OpenTelemetry names them for logs, ready for log.set(). parent_span_id is undefined for a root. TraceFields is its type.

@sdxc/trace-context/traceparent

parse(value)

Reads version-traceid-parentid-flags into a Result<TraceParent.Value, TraceParentParseError>. A version above 00 is read by the 00 rules and may carry more after a -. TraceParent.Value exposes flags as the raw byte, and sampled (bit 0) and random (bit 1) as booleans.

stringify(value)

Writes version 00 with only the sampled and random bits.

TraceParentParseError

Its code (TraceParent.ErrorCode) is "malformed" (shape, length, uppercase hex), "invalid-version" (ff), "invalid-trace-id" or "invalid-parent-id" (all zero).

@sdxc/trace-context/tracestate

TraceState

An immutable, ordered list; leftmost is the most recently updated. TraceState.EMPTY, size, get(key), entries(), delete(key), and set(key, value), which validates the entry, refuses a 33rd key, and returns a new state with key first.

parse(value)

Reads a comma-separated list into a Result<TraceState, TraceStateParseError>, trimming optional whitespace and skipping empty members.

stringify(state, options?)

Writes the list, truncated to maxLength (default 512) by dropping entries over 128 characters first, then the rightmost entries. The empty state writes "".

TraceStateParseError

Its code (TraceState.ErrorCode) is "malformed", "invalid-key", "invalid-value", "duplicate-key" or "too-many" (over 32).

@sdxc/trace-context/middleware

trace(options?)

Continues or starts the request's trace, binds it for currentTrace(), publishes it as ctx.trace, and stamps traceFields() on the current log. A trace already current is joined. options.accept(request) returning false starts a new trace and notes the dropped header as trace.rejected on the log. TraceMiddleware.Options is its options type.

CurrentTrace

The context key the middleware sets, for reading the trace where the middleware chain's types are unknown: ctx.get(CurrentTrace).

Pattern: Join A Callee Reached Over RPC

An RPC call carries no headers, so the trace travels as an argument and the callee binds it:

import { isSuccess } from "@sdxc/result";
import { continueTrace, runWithTrace, startTrace, toTraceParent } from "@sdxc/trace-context";
import { parse } from "@sdxc/trace-context/traceparent";

await stub.subscribe(subject, toTraceParent(ctx.trace));

export class Subscriptions {
	async subscribe(subject: string, traceparent: string) {
		let parent = parse(traceparent);
		let trace = isSuccess(parent) ? continueTrace(parent.data) : startTrace();
		return runWithTrace(trace, () => this.store(subject));
	}
}

Pattern: Refuse Caller-Chosen Traces On A Public Endpoint

import { trace } from "@sdxc/trace-context/middleware";

let internal = (request: Request) => request.headers.has("X-Internal-Caller");

let router = createRouter({ middleware: [log(logger), trace({ accept: internal })] });

Mount trace() directly after log(logger), so the log it stamps is the request's.