AbsoluteJS
AbsoluteJS

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."

TS
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.

MARKDOWN
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).

TS
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.

TS
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.

MARKDOWN
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.

TS
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.

MARKDOWN
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."