API

The Telemetry token and the package's entry points

The package publishes one entry point per job rather than a single barrel, so an import says what it reaches for and nothing pulls in the SDK by accident.

ImportWhat it is for
@vercube/telemetryThe Telemetry token and TelemetryPlugin
@vercube/telemetry/apiThe OpenTelemetry API
@vercube/telemetry/attributesSpan and metric attribute keys
@vercube/telemetry/instrumentInstrumenting a library of your own
@vercube/telemetry/sdkTracer and meter provider wiring
@vercube/telemetry/otlpOTLP serializers and exporters
@vercube/telemetry/testingIn-memory providers for tests

This is also the only package in the framework that depends on @opentelemetry/*. @vercube/storage, @vercube/cache, @vercube/queue, @vercube/auth, @vercube/ws and @vercube/devtools all instrument themselves through the subpaths above, so the OpenTelemetry version in use is decided in one place.

Telemetry

The dependency-injection token, bound only when telemetry is enabled. Inject it with @InjectOptional from code that must also run without it.

import { Inject } from '@vercube/di';
import { Telemetry } from '@vercube/telemetry';

class ReportService {
  @Inject(Telemetry)
  private gTelemetry!: Telemetry;
}

span

span<T>(name: string, fn: (span: Span) => T, options?: SpanOptions): T

Runs fn inside a new span that ends when the work settles. The return value is passed through unchanged, so wrapping synchronous code does not make it asynchronous.

const total = this.gTelemetry.span('report.total', (span) => {
  span.setAttribute('report.rows', rows.length);
  return rows.reduce((sum, row) => sum + row.amount, 0);
});

activeSpan, traceId, spanId

activeSpan(): Span | undefined
get traceId(): string | undefined
get spanId(): string | undefined

The span active on the current async execution path, and its ids in lowercase hex. Useful for putting a trace id into an error response so a user can quote it in a bug report.

inject, extract

inject(carrier: Headers | Record<string, string>): void
extract(headers: Headers | Record<string, string>): Context

Write W3C trace context headers for the active span into a carrier, or read them back into a context usable as a span parent. extract is what lets a background worker continue the trace of the request that queued its job.

onFlush, flush

onFlush(flush: () => Promise<void>): void
flush(): Promise<void>

flush() pushes out everything buffered by batching exporters and drains. Call it before a process that may be frozen or killed goes away. onFlush registers additional work to run when it does.

tracer, meter

The underlying OpenTelemetry Tracer and Meter, for anything the wrapper does not cover, such as creating your own counters and histograms.

const refunds = this.gTelemetry.meter.createCounter('billing.refunds');
refunds.add(1, { 'billing.reason': reason });

@vercube/telemetry/api

The OpenTelemetry API, re-exported in full. Use it to annotate spans without adding a dependency of your own.

import { SpanKind, trace } from '@vercube/telemetry/api';

trace.getActiveSpan()?.setAttribute('invoice.id', id);

Anything @opentelemetry/api exports is available here, including the types: Span, Context, Tracer, Meter, Attributes, Counter, Histogram.

@vercube/telemetry/attributes

The attribute keys the framework writes. The HTTP names follow the stable OpenTelemetry semantic conventions, the vercube.* names are this framework's own.

import { HTTP_ROUTE, VERCUBE_CONTROLLER } from '@vercube/telemetry/attributes';

const route = span.attributes[HTTP_ROUTE];

Reading a span rather than producing one is what this is for, so it does not pull in the plugin.

@vercube/telemetry/instrument

The toolkit for instrumenting a library. See Instrumenting your own code.

@vercube/telemetry/sdk

Everything that needs the OpenTelemetry SDK lives behind this subpath, so the application entry stays free of it.

startNodeTelemetry

startNodeTelemetry(options?: NodeTelemetryOptions): Promise<NodeTelemetry>

Wires a Node tracer provider and registers it globally. Accepts serviceName, serviceVersion, environment, resourceAttributes, sampler, endpoint, headers, exporter and spanProcessors. Returns a handle with shutdown().

The context manager and the propagator are deliberately not replaced: TelemetryPlugin already installed ones backed by Vercube's request context, and letting the SDK swap them in would open a second AsyncLocalStorage frame per request.

addSpanProcessor

addSpanProcessor(processor: SpanProcessor): () => void

Adds a span processor whether or not a provider exists yet. This is how a package can observe spans without owning the SDK setup, and it works the same whether the application called startNodeTelemetry first, later, or never.

addMetricReader, ensureMeterProvider

addMetricReader(reader: IMetricReader): () => void
ensureMeterProvider(options?: NodeTelemetryOptions): MeterProvider

Metric readers have to be registered before the provider is built, and instruments have to be created after it exists. The OpenTelemetry metrics API has no proxy meter, so an instrument created before a provider is registered stays a no-op for the life of the process.

SDK re-exports

The SDK classes and types a consumer needs to build a pipeline of its own: NodeTracerProvider, BasicTracerProvider, MeterProvider, BatchSpanProcessor, SimpleSpanProcessor, InMemorySpanExporter, InMemoryMetricExporter, PeriodicExportingMetricReader, AggregationTemporality, and the types ReadableSpan, SpanProcessor, SpanExporter, IMetricReader, ResourceMetrics, Sampler.

The SDK's mutable span class is re-exported as the type alias SdkSpan, because it is a different thing from the API's Span interface and the two are not interchangeable. It is export type, so it is available to import type and has no runtime binding. SpanProcessor.onStart receives an SdkSpan, onEnd receives a ReadableSpan.

import { JsonTraceSerializer } from '@vercube/telemetry/otlp';
import type { ReadableSpan, SdkSpan, SpanProcessor } from '@vercube/telemetry/sdk';

class MyProcessor implements SpanProcessor {
  public onStart(_span: SdkSpan): void {}
  public onEnd(span: ReadableSpan): void {
    void JsonTraceSerializer.serializeRequest([span]);
  }
  public async forceFlush(): Promise<void> {}
  public async shutdown(): Promise<void> {}
}

@vercube/telemetry/otlp

JsonTraceSerializer and JsonMetricsSerializer turn collected signals into the OTLP/JSON wire format without sending them anywhere, which is what a consumer shipping its own transport needs. @vercube/devtools uses them to push spans over its own bus.

createOtlpTraceExporter(options: OtlpExporterOptions): Promise<SpanExporter>
createOtlpMetricExporter(options: OtlpExporterOptions): Promise<PushMetricExporter>

Both take { endpoint, headers? } and append the OTLP signal path for you. The exporter packages are optional peer dependencies loaded on demand, so a missing one produces a message telling you what to install rather than a module-resolution error at import time.

@vercube/telemetry/testing

createTestTelemetry

createTestTelemetry(): TestTelemetry

Registers in-memory tracer and meter providers plus a context manager, and hands back everything a test needs to assert on what was recorded.

import { createTestTelemetry } from '@vercube/telemetry/testing';

const telemetry = createTestTelemetry();

afterEach(() => telemetry.reset());
afterAll(() => telemetry.shutdown());

it('traces the request', async () => {
  await app.fetch(new Request('http://localhost/users/1'));

  expect(telemetry.span('GET /users/:id')).toBeDefined();
});

spans() returns finished spans, span(name) finds one, collect() collects the registered instruments, settle() waits for spans whose end was deferred (body capture defers it), and reset() clears everything between tests.

Previous

Instrumentation

Producing spans and metrics from a library of your own

Next