AbsoluteJS

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

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

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

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

AttributePackageMeaning
abs.tenantallTenant / shard key. Carried on every span when available.
abs.shard.idallCluster member id.
abs.engine.idsyncSync engine instance id.
abs.collectionsyncCollection the change or subscription targets.
abs.mutationsyncMutation name being run.
abs.mutation.attemptsyncRetry attempt number for the mutation.
abs.subscription.idsyncSubscription the span belongs to.
abs.batch.sizesyncNumber of changes in an applyChangeBatch.
abs.cluster.originsyncCluster member a replicated change originated from.
abs.job.idqueueId of the job being processed.
abs.job.kindqueueJob kind from the typed registry.
abs.job.attemptqueueCurrent attempt number.
abs.job.max_attemptsqueueAttempt budget before dead-letter.
abs.worker.idqueueWorker that claimed the job.
abs.runtime.keyruntimeKey of the managed process.
abs.runtime.pidruntimePid of the spawned process.
abs.runtime.portruntimePort the process is bound to.
abs.runtime.exit_reasonruntimeWhy the process exited.
abs.runtime.readiness_msruntimeMilliseconds from spawn to readiness.
abs.route.shardrouterShard the request was routed to.
abs.route.decisionrouterRouting decision taken.
abs.secret.namesecretsName of the secret (never the value).
abs.secret.fingerprintsecretsFingerprint of the secret value.
abs.audit.kindauditKind of the audit event.
Use the TypeScript keys
Set attributes via 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.

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

PackageSpans emitted
@absolutejs/syncsync.runMutation, sync.subscribe, sync.applyChange, sync.cluster.publish
@absolutejs/queuequeue.enqueue, queue.worker.process, queue.worker.retry
@absolutejs/runtimeruntime.spawn, runtime.exit, runtime.health-check
@absolutejs/routerrouter.route (decision attribute), router.shard-resolve
@absolutejs/secretssecrets.read, secrets.rotate
@absolutejs/rate-limitrate-limit.check, rate-limit.block
@absolutejs/isolated-jscisolate.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."