Telemetry
A shared OpenTelemetry layer any app or library can use to emit spans without pulling @opentelemetry/api as a peer dep — the same layer every AbsoluteJS package uses. 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 an instrumented package's tracerProvider option. With no provider set, the 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 an AbsoluteJS package:
5const provider = new NodeTracerProvider();
6provider.register();
7
8const engine = createSyncEngine({ tracerProvider: provider });
9
10// With no tracerProvider, the package uses a noop tracer. Spans are
11// emitted regardless — the noop drops them; a real provider records
12// them. No code path branches on "is OTel installed."#Why a separate package
A library that wants to emit OTel spans shouldn't force every consumer to install OTel. @absolutejs/telemetry is the shared layer that lets a package say "trace this if a provider is wired, otherwise no-op." Three properties make it work:
Type-replicated OTel surface
The Tracer / Span / TracerProvider types match @opentelemetry/api structurally — a NodeTracerProvider plugs in without an adapter, and @opentelemetry/api is never a peer dep.
Built-in noop tracer
Calling startSpan() without a provider is a no-allocation pass-through that returns no-op spans. tracerOrNoop(provider, name) is the single entry point.
ABS_ATTRS semantic conventions
Standard attribute names (abs.tenant, abs.engine.id, abs.collection, …) so spans from different packages correlate via consistent keys.
Without the shared layer, every package would either reinvent noop spans inline or take @opentelemetry/api as a hard dependency. Both lose.
#tracerOrNoop
The canonical entry point — every instrumented package's tracer line looks the same. Provider undefined returns the shared noop tracer (zero allocations, no-op spans); provider defined returns provider.getTracer(name, version). Any OTel-compatible provider works — NodeTracerProvider, the OpenTelemetry Collector SDK, or a custom one.
1import { ABS_ATTRS, tracerOrNoop } from '@absolutejs/telemetry';
2
3const tracer = tracerOrNoop(
4 options.tracerProvider, // user-supplied; may be undefined
5 '@absolutejs/<pkg-name>' // tracer name (becomes resource.name)
6);
7
8// Then in code:
9const span = tracer.startSpan('sync.runMutation', {
10 attributes: {
11 [ABS_ATTRS.tenant]: ctx.tenantId,
12 [ABS_ATTRS.mutation]: name,
13 },
14});
15try {
16 // …work…
17 span.setStatus({ code: 1 /* OK */ });
18} catch (e) {
19 span.recordException(e);
20 span.setStatus({ code: 2 /* ERROR */, message: String(e) });
21 throw e;
22} finally {
23 span.end();
24}#withSpan
Collapses the repeated try / finally / status pattern into one async wrapper: OK status on resolve, exception recorded + ERROR status + rethrow on reject, span.end() in finally — and it returns whatever the wrapped fn returned. A withSpanSync variant covers the same pattern in synchronous code paths.
1import { withSpan, ABS_ATTRS } from '@absolutejs/telemetry';
2
3const result = await withSpan(
4 tracer,
5 'sync.runMutation',
6 { attributes: { [ABS_ATTRS.mutation]: name } },
7 async (span) => {
8 span.setAttribute(ABS_ATTRS.mutationAttempt, attempt);
9 return await invoke(args);
10 }
11);#ABS_ATTRS
Shared semantic conventions so spans across packages use the same attribute keys and correlate without per-package translation. abs.tenant is universal; per-package keys cover sync, queue, runtime, router, secrets, and audit.
| Attribute | Package | Meaning |
|---|---|---|
abs.tenant | all | Tenant / shard key. Carried on every span when available. |
abs.shard.id | all | Cluster member id. |
abs.engine.id | sync | Sync engine instance id. |
abs.collection | sync | Collection the change or subscription targets. |
abs.mutation | sync | Mutation name being run. |
abs.mutation.attempt | sync | Retry attempt number for the mutation. |
abs.subscription.id | sync | Subscription the span belongs to. |
abs.batch.size | sync | Number of changes in an applyChangeBatch. |
abs.cluster.origin | sync | Cluster member a replicated change originated from. |
abs.job.id | queue | Id of the job being processed. |
abs.job.kind | queue | Job kind from the typed registry. |
abs.job.attempt | queue | Current attempt number. |
abs.job.max_attempts | queue | Attempt budget before dead-letter. |
abs.worker.id | queue | Worker that claimed the job. |
abs.runtime.key | runtime | Key of the managed process. |
abs.runtime.pid | runtime | Pid of the spawned process. |
abs.runtime.port | runtime | Port the process is bound to. |
abs.runtime.exit_reason | runtime | Why the process exited. |
abs.runtime.readiness_ms | runtime | Milliseconds from spawn to readiness. |
abs.route.shard | router | Shard the request was routed to. |
abs.route.decision | router | Routing decision taken. |
abs.secret.name | secrets | Name of the secret (never the value). |
abs.secret.fingerprint | secrets | Fingerprint of the secret value. |
abs.audit.kind | audit | Kind of the audit event. |
span.setAttribute(ABS_ATTRS.tenant, ctx.tenantId) — ABS_ATTRS.tenant is type-checked; a raw 'abs.tenant' string is a typo waiting to happen.#readActiveTraceId
Get the active trace id from a non-OTel surface (log line, audit event, error response). The module specifier is built at runtime so bundlers don't statically resolve @opentelemetry/api as a hard dep; it returns undefined when OTel isn't installed or no active context exists. The same trick powers tracerOrNoop's optional provider — telemetry stays peer-dep-free for consumers who don't run OTel.
1import { readActiveTraceId } from '@absolutejs/telemetry';
2
3const traceId = await readActiveTraceId();
4log.error({ traceId, err }, 'failed to process job');#Instrumented Packages
Every package below already calls tracerOrNoop and emits spans with ABS_ATTRS keys. Wire one TracerProvider on your app and every span lights up.
| Package | Spans emitted |
|---|---|
@absolutejs/sync | sync.runMutation, sync.subscribe, sync.applyChange, sync.cluster.publish |
@absolutejs/queue | queue.enqueue, queue.worker.process, queue.worker.retry |
@absolutejs/runtime | runtime.spawn, runtime.exit, runtime.health-check |
@absolutejs/router | router.route (decision attribute), router.shard-resolve |
@absolutejs/secrets | secrets.read, secrets.rotate |
@absolutejs/rate-limit | rate-limit.check, rate-limit.block |
@absolutejs/isolated-jsc | isolate.spawn, isolate.invoke, isolate.hibernate, isolate.resume |
Every span carries abs.tenant when available, so a single trace view can filter "show me everything for tenant-7."