Tracing — Architecture & API Reference
SkillMonitoring & opsArchitecture and API reference for @dxos/tracing. Use when working with TracingBackend, TRACE_PROCESSOR, @trace.resource(), @trace.span(), @trace.info(), metrics counters, diagnostics, ContextRpcCodec, tracing-types.ts, trace-processor.ts, or api.ts.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Tracing — Architecture & API Reference skill
What this skill tells your AI
The instructions your AI receives, as published by dxos/dxos in .agents/skills/tracing/SKILL.md and read by ahel’s review.
The @dxos/tracing package provides resource tracking, span creation, metrics, and diagnostics for DXOS. It defines a backend-agnostic TracingBackend interface — the @dxos/observability package provides the concrete OTEL implementation.
For rules on how to pass ctx: Context through methods to get connected traces, see the context-propagation skill.
import { trace, TRACE_PROCESSOR } from '@dxos/tracing';
Architecture
┌─────────────────────────────────────────────────────────┐
│ @dxos/context │
│ │
│ TraceContextData TRACE_SPAN_ATTRIBUTE │
│ ContextRpcCodec (encode/decode for RPC) │
└──────────────────────────┬──────────────────────────────┘
│ used by
┌──────────────────────────▼──────────────────────────────┐
│ @dxos/tracing (no OTEL dependency) │
│ │
│ trace.resource() trace.span() trace.info() │
│ trace.metricsCounter() trace.diagnostic() │
│ trace.spanStart() / trace.spanEnd() │
│ TRACE_PROCESSOR ─── tracingBackend?: TracingBackend │
└──────────────────────────┬──────────────────────────────┘
│ registers at startup
┌──────────────────────────▼──────────────────────────────┐
│ @dxos/observability │
│ │
│ OtelTraces implements TracingBackend │
│ Sets TRACE_PROCESSOR.tracingBackend at startup │
└─────────────────────────────────────────────────────────┘
TRACE_PROCESSOR is a global singleton (globalThis.TRACE_PROCESSOR). It holds resources, logs, metrics, diagnostics, and the optional tracingBackend.
Key design: TraceContextData is serializable strings
The TRACE_SPAN_ATTRIBUTE on DXOS Context stores TraceContextData — W3C traceparent/tracestate strings. Because these are plain strings (not live OTEL runtime objects):
- They remain valid after the originating span ends, so long-lived
this._ctxcan serve as parents. - They cross RPC boundaries without inject/extract —
ContextRpcCodecjust reads/writes them directly. - They cross WebSocket/HTTP boundaries without OTEL API imports — edge clients read the strings directly.
The OTEL backend performs propagation.extract/inject internally in startSpan.
Browser timeline
When showInBrowserTimeline = true, the @trace.span() decorator calls performance.measure() in the finally block. No custom span object is needed — just timestamps.
TracingBackend Interface
Defined in tracing-types.ts. Implemented by @dxos/observability.
interface TracingBackend {
startSpan: (options: StartSpanOptions) => RemoteSpan;
}
The backend receives and returns TraceContextData (W3C strings) — no opaque runtime objects cross the interface boundary. The OTEL backend performs propagation.extract/inject internally.
RemoteSpan
Returned by TracingBackend.startSpan().
type RemoteSpan = {
end: () => void;
setError?: (err: unknown) => void;
spanContext?: TraceContextData;
};
end()— must be called exactly once to signal span completion.setError(err)— records an error on the span (OTEL:recordException+setStatus(ERROR)).spanContext— W3C trace context strings stored on the DXOSContextviaTRACE_SPAN_ATTRIBUTE. Child spans read it and pass it back asStartSpanOptions.parentContext.
How the @trace.span() Decorator Works
- Checks if
args[0]is aContext— if so, readsTraceContextDatafrom itsTRACE_SPAN_ATTRIBUTE. - Calls
TRACE_PROCESSOR.tracingBackend?.startSpan({ name, parentContext, ... }). - Derives a child
Contextwith the new span'sTraceContextDataonTRACE_SPAN_ATTRIBUTE. - Replaces
args[0]with the child context before calling the method body. - On error: calls
remoteSpan.setError(err)before rethrowing. - Calls
remoteSpan.end()in afinallyblock. - If
showInBrowserTimeline, also callsperformance.measure()in the finally block.
If args[0] is not a Context, no parent linking occurs and no context replacement happens.
When showInRemoteTracing = false, the decorator skips steps 2-3 entirely. The child context has no TRACE_SPAN_ATTRIBUTE, so grandchild spans reconnect to the grandparent (trace continuity is preserved).
RPC Trace Context (ContextRpcCodec)
ContextRpcCodec (in @dxos/context) is hardcoded in RpcPeer. No configuration needed.
Outgoing RPC:
ctx.getAttribute(TRACE_SPAN_ATTRIBUTE) → TraceContextData on proto wire
Incoming RPC:
TraceContextData from proto wire → new Context({ TRACE_SPAN_ATTRIBUTE: traceContext })
Because TRACE_SPAN_ATTRIBUTE stores TraceContextData strings directly, the codec is a trivial read/write — no backend-specific inject/extract is needed.
API Reference
@trace.resource()
Class decorator. Registers every instance as a tracked resource in TRACE_PROCESSOR.resources. Required for @trace.info() and @trace.metricsCounter() to work.
@trace.resource()
class DataSpace {
// ...
}
With an annotation symbol for programmatic lookup:
const DataSpaceResource = Symbol.for('DataSpace');
@trace.resource({ annotation: DataSpaceResource })
class DataSpace {
// ...
}
// Later: find all DataSpace instances.
TRACE_PROCESSOR.findResourcesByAnnotation(DataSpaceResource);
Lifecycle span (lifecycle: true)
For Resource subclasses that set up background work (subscriptions, timers) in _open, enable lifecycle: true to get a long-lived span that starts on open() and ends on close(). this._ctx carries the lifecycle span's trace context, so background callbacks are properly parented.
@trace.resource({ lifecycle: true })
class AutomergeHost extends Resource {
@trace.span()
protected override async _open(ctx: Context): Promise<void> {
// Direct calls use ctx → children of _open span.
await this._collectionSynchronizer.open(ctx);
// Subscriptions use this._ctx → children of lifecycle span.
this._networkAdapter.on(this._ctx, () => this._handleUpdate(this._ctx));
}
}
Trace hierarchy produced:
caller
└─ AutomergeHost.lifecycle [════ open ════════════════════ close ════]
├─ AutomergeHost._open [==]
│ └─ CollectionSynchronizer.lifecycle (nested, child of _open)
├─ subscription callback 1 (child of lifecycle)
└─ subscription callback 2 (child of lifecycle)
Rules:
- Requires the class to
extend Resource. Throws at decoration time otherwise. - When
_openthrows, the lifecycle span records the error and ends immediately. - Double
open()calls do not start a second lifecycle span. - Works gracefully when no
TracingBackendis registered (no-op).
@trace.span()
Method decorator. Creates a span for the method's execution duration.
No orphaned internal spans. Every @trace.span() on an internal method must have a parent — either from an incoming ctx parameter, from this._ctx on a lifecycle: true resource, or from options.ctx in an RPC handler. If the method has no way to receive a parent trace context, don't add @trace.span() to it. See the context-propagation skill for the full rule.
@trace.resource()
class DataSpace {
@trace.span()
async open(ctx: Context): Promise<void> {
await this._initPipeline(ctx);
}
}
Options
@trace.span({
// Show in browser Performance tab (calls performance.measure()).
showInBrowserTimeline: true,
// When false, span is NOT sent to OTLP collector. Defaults to true.
// Grandchild spans reconnect to the grandparent.
showInRemoteTracing: false,
// Span category.
op: 'db.query',
// Static attributes attached to the span.
attributes: { 'ctx.space': 'my-space' },
})
async query(ctx: Context): Promise<void> { ... }
@trace.info()
Property/method decorator. Exposes a value in the resource's info section (visible in devtools diagnostics).
@trace.resource()
class DataSpace {
@trace.info()
get spaceId(): string {
return this._spaceId;
}
// Enum values are converted to their string representation.
@trace.info({ enum: SpaceState })
get state(): SpaceState {
return this._state;
}
// Control serialization depth (default: 0 = toString, null = unlimited up to 8).
@trace.info({ depth: 2 })
get config(): object {
return this._config;
}
}
@trace.metricsCounter()
Property decorator. Attaches a metrics counter to the resource. The property must be initialized with a counter instance.
import { MapCounter, UnaryCounter, TimeSeriesCounter, TimeUsageCounter } from '@dxos/tracing';
@trace.resource()
class RpcServer {
@trace.metricsCounter()
private readonly _requestCount = new UnaryCounter();
@trace.metricsCounter()
private readonly _callMetrics = new MapCounter();
handleRequest(method: string): void {
this._requestCount.inc();
this._callMetrics.inc(`${method} request`);
}
}
Available counters:
UnaryCounter— single incrementing value.inc(by?: number).MapCounter— keyed counters.inc(key: string, by?: number).TimeSeriesCounter— time-bucketed values.inc(by?: number).TimeUsageCounter— tracks active time.start()/stop().
trace.diagnostic()
Registers a named diagnostic that can be queried via the diagnostics channel.
trace.diagnostic({
id: 'space-status',
name: 'Space Status',
fetch: async () => ({
spaces: this._spaces.length,
openConnections: this._connections.size,
}),
});
trace.mark()
Emits a performance.mark() for the browser timeline.
trace.mark('space-ready');
trace.addLink()
Declares a parent-child relationship between two traced resource instances.
@trace.span()
async openFeed(ctx: Context): Promise<Feed> {
const feed = new Feed();
trace.addLink(this, feed, {});
return feed;
}
trace.spanStart() / trace.spanEnd()
Manual span API for cases where decorator-based spans don't fit (e.g., spans that cross method boundaries). Supports showInBrowserTimeline independently of showInRemoteTracing.
spanStart returns a derived Context carrying the new span's TRACE_SPAN_ATTRIBUTE. Callers MUST reassign the local ctx to the returned value so that downstream @trace.span() methods, RPC calls, and edge-client requests see this span as their parent. If the returned derived Context is not reassigned, downstream spans read the OLD TRACE_SPAN_ATTRIBUTE from the original ctx and attach to whatever span that was — becoming siblings of the manual span rather than children. When the original ctx had no parent span, they become a new root trace and the manual span is left as a single-span disconnected trace.
const spanId = `invitation-guest-${invitation.invitationId}`;
// Reassign ctx — downstream calls (@trace.span, RPC, edge-http-client) now nest under this span.
ctx = trace.spanStart({
id: spanId,
instance: this,
methodName: 'acceptInvitation',
parentCtx: ctx,
op: 'invitation.guest',
}) ?? ctx;
ctx.onDispose(() => trace.spanEnd(spanId));
// ... work that should nest under the manual span uses the reassigned ctx ...
await this._handleGuestFlow(ctx, ...);
spanStart returns the original parentCtx unchanged (not a derived ctx) when the span cannot be created — duplicate id, showInRemoteTracing: false, or no tracing backend. The ?? ctx fallback handles a null return if parentCtx was null.
Antipattern — ignoring the return value:
// ❌ WRONG — downstream spans see the OLD ctx, so they attach to ctx's parent span (becoming siblings of the manual span, or a new root if ctx had no parent).
trace.spanStart({ id: spanId, instance: this, methodName: 'acceptInvitation', parentCtx: ctx, ... });
await this._handleGuestFlow(ctx, ...); // ctx unchanged; handleGuestFlow's @trace.span is a sibling of the manual span, not a child.
This was the root cause of the historical pattern where InvitationsHandler.acceptInvitation appeared as a 1-span disconnected trace while EdgeInvitationHandler._handleSpaceInvitationFlow started its own parallel root. Fixed by reassigning ctx to the return value of spanStart.
trace.metrics
Access to RemoteMetrics for publishing OTEL-compatible metrics.
TRACE_PROCESSOR
Global singleton. Key fields:
| Field | Type | Purpose |
|---|---|---|
tracingBackend | TracingBackend? | Set by observability package at startup. |
resources | Map<number, ResourceEntry> | All @trace.resource() instances. |
resourceInstanceIndex | WeakMap<any, ResourceEntry> | Instance → resource lookup. |
logs | LogEntry[] | Captured ERROR/WARN/TRACE log entries. |
diagnostics | DiagnosticsManager | Registered diagnostics. |
remoteMetrics | RemoteMetrics | OTEL-compatible metrics publishing. |
Key methods:
getDiagnostics()— returns{ resources, logs }. Callsrefresh()on-demand.findResourcesByClassName(name)— find resources by class name.findResourcesByAnnotation(symbol)— find resources by annotation.refresh()— updates all resource info and metrics. Called on-demand bygetDiagnostics().
Complete Example
import { Context } from '@dxos/context';
import { MapCounter, trace } from '@dxos/tracing';
const SpaceResource = Symbol.for('Space');
@trace.resource({ annotation: SpaceResource })
class Space {
@trace.info()
get id(): string {
return this._id;
}
@trace.info({ enum: SpaceState })
get state(): SpaceState {
return this._state;
}
@trace.metricsCounter()
private readonly _mutations = new MapCounter();
@trace.span({ showInBrowserTimeline: true })
async open(ctx: Context): Promise<void> {
await this._loadPipeline(ctx);
await this._startReplication(ctx);
trace.mark('space-open');
}
@trace.span()
async close(ctx: Context): Promise<void> {
await this._stopReplication(ctx);
}
@trace.span({ op: 'db.write' })
async mutate(ctx: Context, objectId: string, data: any): Promise<void> {
this._mutations.inc(objectId);
await this._applyMutation(ctx, objectId, data);
}
private async _loadPipeline(ctx: Context): Promise<void> {
/* ... */
}
private async _startReplication(ctx: Context): Promise<void> {
/* ... */
}
private async _stopReplication(ctx: Context): Promise<void> {
/* ... */
}
private async _applyMutation(ctx: Context, id: string, data: any): Promise<void> {
/* ... */
}
}
File Map
| File | Purpose |
|---|---|
@dxos/context: trace-context.ts | TraceContextData, TRACE_SPAN_ATTRIBUTE, ContextRpcCodec. |
@dxos/tracing: api.ts | Public trace object with all decorators and functions. |
@dxos/tracing: tracing-types.ts | RemoteSpan, StartSpanOptions, TracingBackend. |
@dxos/tracing: trace-processor.ts | TraceProcessor singleton, ResourceEntry, TRACE_PROCESSOR. |
@dxos/tracing: symbols.ts | TracingContext, getTracingContext. |
@dxos/tracing: metrics/ | Counter implementations (UnaryCounter, MapCounter, etc.). |
@dxos/tracing: diagnostic.ts | DiagnosticsManager for queryable diagnostics. |
@dxos/tracing: diagnostics-channel.ts | Node.js diagnostics channel integration. |
Signals
- GitHub stars
- 520
- Forks
- 49
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
tracing- Source
- github.com/dxos/dxos