Observability
Request tracing
Instrument your application with request-scoped spans. Every trace carries a correlation id, and spans created inside a traced request inherit it automatically, so you can drill from a log line into a waterfall trace in your APM.
How tracing works
The tracing layer is a lightweight wrapper around OpenTelemetry-compatible tracers. X never initializes an OTel SDK — your application provides a tracer whose interface matches the small TracerLike surface (the real OTel startSpan method works). When no tracer is configured, every tracing call is a synchronous no-op with zero overhead.
Setting up a tracer
Call setTracer once at application startup with your OTel tracer. Then wrap your request handler with withRequestTracing to create an x.http root span for every request:
import { setTracer, withRequestTracing } from "@thexjs/core";import { trace } from "@opentelemetry/api";// Your OTel SDK setup — initialise the provider firstconst otelTracer = trace.getTracer("my-app");setTracer(otelTracer);// Wrap the framework's own request handlerconst app = createApp({ ... });Bun.serve({ fetch: withRequestTracing((req) => app.fetch(req)), port: 3000,});Every incoming request now gets an x.http root span with attributes route, method, x.requestId, and (once the response is written) http.response.status_code.
Instrumenting loaders and actions
Use tracePhase to create child spans inside your loaders and server functions. The span inherits the current request's correlation id automatically:
import { tracePhase } from "@thexjs/core";export async function loader({}: LoaderArgs) { const users = await tracePhase("fetch.users", { "db.table": "users" }, async () => { return db.query("SELECT * FROM users ORDER BY created_at DESC").all(); }); return { users };}If the request is not being traced (e.g. a build-time render or a background job), tracePhase runs the function with zero overhead — no span is created. For synchronous work, use tracePhaseSync with the same signature.
Database trace attributes
The dbTraceAttributes helper builds a standard set of DB-span attributes — system, operation, and a redacted statement — for use with any database driver:
import { tracePhase, dbTraceAttributes } from "@thexjs/core";function queryUsers() { return tracePhase("db.query", dbTraceAttributes("sqlite", "SELECT * FROM users WHERE id = ?"), async () => { return db.query("SELECT * FROM users WHERE id = ?").all(); });}The statement has its quoted literals masked (so bound parameters never appear in traces) and is truncated to 512 characters. The redaction layer also strips bearer/authorization-shaped patterns before the attribute is set.
Error status codes
When an instrumented phase throws, the span is marked with error status and the exception is recorded. The OTEL_ERROR_STATUS_CODE constant matches OpenTelemetry's SpanStatusCode.ERROR (2) for when you need to set error status yourself:
import { OTEL_ERROR_STATUS_CODE, getTracer } from "@thexjs/core";const span = getTracer().startSpan("custom.work");try { const result = doWork(); span.end(); return result;} catch (error) { span.recordException(error); span.setStatus({ code: OTEL_ERROR_STATUS_CODE, message: String(error) }); span.end(); throw error;}Low-level APIs
For advanced use cases — writing a custom server, running background jobs, or instrumenting non-request code — the underlying primitives are exported directly:
- runWithRequestSpan(requestId, attributes, work) runs work inside a root span and an AsyncLocalStorage context, returning the work's value.
- traceRequestId(req) returns the request id a given Request is traced under, minting one if none exists.
- setTracer(tracer) and getTracer() manage the active tracer globally.