Telemetry
The shared OpenTelemetry layer every AbsoluteJS substrate package uses to emit spans without pulling @opentelemetry/api as a peer dep. A type-replicated OTel surface, a built-in noop tracer for "no provider wired," ABS_ATTRS semantic conventions, and tracerOrNoop(provider, name) as the canonical entry point.
#Quick Start
Wire any standard OTel TracerProvider to a substrate package's tracerProvider option. With no provider set, the substrate package uses a noop tracer — spans are emitted regardless; the noop just drops them. No code path branches on "is OTel installed."
1import { createSyncEngine } from '@absolutejs/sync';
2import { trace, NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
3
4// Wire any standard OTel provider to your substrate package:
5const provider = new NodeTracerProvider();
6provider.register();
7
8const engine = createSyncEngine({ tracerProvider: provider });
9
10// With no tracerProvider, every substrate package uses a noop tracer.
11// Spans are emitted regardless — the noop drops them; a real provider
12// records them. No code path branches on "is OTel installed."#Why a separate package
The substrate needs to emit OTel spans without forcing every consumer to install OTel. This package is the shared layer — type-replicated structurally compatible types, a noop tracer that costs nothing when there's no provider, and shared semantic attribute names so spans across packages correlate.
1# Why a separate telemetry package
2
3 Every substrate library wants to emit OTel spans without forcing
4 consumers to install @opentelemetry/api. @absolutejs/telemetry is
5 the shared layer that lets every package say "trace this if a
6 provider is wired, otherwise no-op."
7
8 Three properties make it work:
9
10 1. Type-replicated OTel surface. The Tracer / Span / TracerProvider
11 types match @opentelemetry/api's structural shape — a NodeTracer
12 provider plugs in without an adapter. We do NOT take
13 @opentelemetry/api as a peer-dep; the types are duplicated and
14 structurally compatible.
15
16 2. A noop tracer that returns no-op spans. Calling startSpan()
17 without a provider is a no-allocation pass-through.
18 tracerOrNoop(provider, name) is the single entry point every
19 substrate package uses.
20
21 3. ABS_ATTRS semantic conventions. Standard attribute names
22 (abs.tenant, abs.engine.id, abs.collection, …) so spans from
23 different substrate packages correlate via consistent keys.
24
25 Pre-G2 every substrate package would have either (a) reinvented
26 noop spans inline, or (b) forced @opentelemetry/api as a hard dep.
27 Both lose.#tracerOrNoop
The canonical entry point — every substrate package starts with tracerOrNoop(options.tracerProvider, '@absolutejs/<pkg>'). Provider undefined returns the shared noop; provider defined returns provider.getTracer(name, version).
1# tracerOrNoop — the canonical entry point
2
3 Every substrate package's tracer line looks the same:
4
5 import { tracerOrNoop } from '@absolutejs/telemetry';
6
7 const tracer = tracerOrNoop(
8 options.tracerProvider, // user-supplied; may be undefined
9 '@absolutejs/<pkg-name>' // tracer name (becomes resource.name)
10 );
11
12 - Provider undefined → returns the shared noop tracer. Zero
13 allocations, no-op spans.
14 - Provider defined → returns provider.getTracer(name, version).
15 Any OTel-compatible provider works (NodeTracerProvider, the
16 OpenTelemetry Collector SDK, a custom one).
17
18 Then in code:
19
20 const span = tracer.startSpan('sync.runMutation', {
21 attributes: {
22 [ABS_ATTRS.tenant]: ctx.tenantId,
23 [ABS_ATTRS.mutation]: name,
24 },
25 });
26 try {
27 // …work…
28 span.setStatus({ code: 1 /* OK */ });
29 } catch (e) {
30 span.recordException(e);
31 span.setStatus({ code: 2 /* ERROR */, message: String(e) });
32 throw e;
33 } finally {
34 span.end();
35 }#withSpan
Collapses the repeated try / finally / status pattern into one async wrapper. Records exceptions, sets the ERROR status, calls span.end() in finally — and returns whatever the wrapped fn returned.
1# withSpan — the common pattern, wrapped
2
3 Most call sites repeat the same try/finally pattern. withSpan
4 collapses it into a single async wrapper that captures success /
5 error status automatically:
6
7 import { withSpan, ABS_ATTRS } from '@absolutejs/telemetry';
8
9 const result = await withSpan(
10 tracer,
11 'sync.runMutation',
12 { attributes: { [ABS_ATTRS.mutation]: name } },
13 async (span) => {
14 span.setAttribute(ABS_ATTRS.mutationAttempt, attempt);
15 return await invoke(args);
16 }
17 );
18
19 - OK status set on resolve.
20 - exception recorded + ERROR status + rethrow on reject.
21 - span.end() called in finally.
22
23 A withSpanSync variant exists for the same pattern in synchronous
24 code paths.#ABS_ATTRS
Shared semantic conventions so spans across substrate packages use the same attribute keys. abs.tenant is universal; per-package keys cover sync, queue, runtime, router, secrets, audit.
1# ABS_ATTRS — shared semantic conventions
2
3 Every substrate package uses the SAME attribute keys so spans across
4 packages correlate without per-package translation:
5
6 abs.tenant → tenant / shard key (universal)
7 abs.shard.id → cluster member id
8
9 // sync
10 abs.engine.id, abs.collection, abs.mutation,
11 abs.mutation.attempt, abs.subscription.id, abs.batch.size,
12 abs.cluster.origin
13
14 // queue
15 abs.job.id, abs.job.kind, abs.job.attempt,
16 abs.job.max_attempts, abs.worker.id
17
18 // runtime
19 abs.runtime.key, abs.runtime.pid, abs.runtime.port,
20 abs.runtime.exit_reason, abs.runtime.readiness_ms
21
22 // router
23 abs.route.shard, abs.route.decision
24
25 // secrets
26 abs.secret.name, abs.secret.fingerprint
27
28 // audit
29 abs.audit.kind
30
31 Use them as TypeScript keys, not raw strings — ABS_ATTRS.tenant is
32 type-checked; 'abs.tenant' is a typo waiting to happen:
33
34 span.setAttribute(ABS_ATTRS.tenant, ctx.tenantId);#readActiveTraceId
Get the active trace id from a non-OTel surface (log line, audit event, error response). Dynamic import of @opentelemetry/api keeps the package peer-dep-free; returns undefined when no provider is wired.
1# readActiveTraceId — correlate logs to traces
2
3 When you want the active trace id in a non-OTel surface (a log
4 line, an audit event, an error response), readActiveTraceId() does
5 the dynamic import dance for you:
6
7 import { readActiveTraceId } from '@absolutejs/telemetry';
8
9 const traceId = await readActiveTraceId();
10 log.error({ traceId, err }, 'failed to process job');
11
12 Implementation detail: the module specifier is built at runtime
13 (['@opentelemetry', 'api'].join('/')) so bundlers don't statically
14 resolve @opentelemetry/api as a hard dep. Returns undefined when
15 @opentelemetry/api isn't installed or no active context exists.
16
17 The same trick powers tracerOrNoop's optional provider — telemetry
18 remains peer-dep-free for consumers who don't run OTel.#Substrate Coverage
G2 closed across the substrate — every package below already calls tracerOrNoop and emits spans with ABS_ATTRS keys. Wire oneTracerProvider on your app and every span lights up.
1# What's already instrumented
2
3 G2 closed across the substrate: every package below already calls
4 tracerOrNoop and emits spans with ABS_ATTRS keys. You wire ONE
5 TracerProvider on your app and every span lights up.
6
7 @absolutejs/sync → sync.runMutation, sync.subscribe,
8 sync.applyChange, sync.cluster.publish
9 @absolutejs/queue → queue.enqueue, queue.worker.process,
10 queue.worker.retry
11 @absolutejs/runtime → runtime.spawn, runtime.exit,
12 runtime.health-check
13 @absolutejs/router → router.route (decision attr),
14 router.shard-resolve
15 @absolutejs/secrets → secrets.read, secrets.rotate
16 @absolutejs/rate-limit → rate-limit.check, rate-limit.block
17 @absolutejs/isolated-jsc → isolate.spawn, isolate.invoke,
18 isolate.hibernate, isolate.resume
19
20 Every span carries abs.tenant when available, so a single trace
21 view can filter "show me everything for tenant-7."