mrkeyoor.com_
Thu 06 Aug 01:03 UTC
npmInfraupdated 05 Aug 2026

@opentelemetry/api

@opentelemetry/api is the interface half of OpenTelemetry for JavaScript: the types, enums, and global accessors for traces, metrics, context, and baggage, with no implementation behind them. Every method is a no-op until an SDK registers itself on a global, so a library can call trace.getTracer() and start spans without forcing any telemetry cost or vendor choice on the applications that install it. When an application does register an SDK, those same calls start producing real spans and metrics that flow to whatever exporter is configured. It has zero dependencies, ships its own TypeScript types, and gzips to under 5 KB, which is the whole point: instrument once against this, and the application decides where the data goes.

Verdict

The right dependency for library authors and for hand-written spans inside an app that already runs an SDK, and it costs almost nothing to add. Just be clear that this package alone produces no telemetry, and that assembling the SDK side of OpenTelemetry in Node is still meaningfully more work than installing a vendor agent.

API stability5/51.0 shipped in 2021 and the README's upgrade guide has had nothing to add since, because the 1.x line has made no breaking changes; metrics were added as new namespaces rather than by reshaping tracing.
Docs3/5The generated API reference is complete and the package README has a working tracing quick start, but the conceptual guides live on opentelemetry.io and are spread across API, SDK, and contrib packages, so working out which package owns a given feature takes longer than reading about the feature itself.
Maintenance5/5The monorepo was pushed the same day this was written, with 190 open issues (252 counting PRs) across the whole repo and releases every few weeks; it is a CNCF project with maintainers from several companies rather than a single owner.
Ecosystem5/5About 74 million weekly downloads and the interface that Datadog, Honeycomb, Grafana, New Relic, and the OTLP collector all accept, so instrumentation written against it is portable across vendors.

Use it if

  • You maintain a library or framework and want optional instrumentation: your users who run an SDK get spans, and your users who do not pay nothing but a few no-op function calls
  • You want manual spans inside an application that is already running an OpenTelemetry SDK, so your business logic shows up between the auto-instrumented HTTP and database spans
  • You want to stay vendor-neutral: the same instrumentation code exports to Jaeger, Tempo, Honeycomb, Datadog, or an OTLP collector by changing SDK configuration rather than application code
  • You need to propagate trace context across a service boundary yourself, for example over a message queue or a protocol the auto-instrumentations do not cover
Skip it if

Setup reality

npm install @opentelemetry/api is trivial: zero dependencies, types included, works in CommonJS and ESM. Everything after that is the work. Applications additionally install an SDK plus exporters, and the SDK must be initialized before any instrumented module is required, which in practice means node --require ./tracing.js or, for ESM, node --import with the register loader. Library authors should declare the API as a peer dependency with a caret range and never as a bundled dependency, because two copies in one node_modules can leave one of them holding a no-op. Attribute names are not provided here either: the semantic conventions live in @opentelemetry/semantic-conventions, which versions on its own schedule. Expect to read release notes: the API is 1.9.1 and stable, while the SDK and instrumentation packages around it move fast and still carry 0.x version numbers.

Patterns

Wrap work in an active spanstart-active-span

import { trace } from '@opentelemetry/api';

const tracer = trace.getTracer('checkout-service', '1.4.0');

async function charge(orderId) {
  return tracer.startActiveSpan('charge', async (span) => {
    try {
      const result = await gateway.charge(orderId);
      return result;
    } finally {
      span.end();
    }
  });
}

startActiveSpan does not end the span for you, and a span that is never ended is never exported. The finally block matters: an early return or a thrown error otherwise leaks the span.

Mark a span as failed and attach the exceptionrecord-error-status

import { SpanStatusCode } from '@opentelemetry/api';

try {
  await gateway.charge(orderId);
} catch (err) {
  span.recordException(err);
  span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
  throw err;
} finally {
  span.end();
}

recordException adds an event with the stack trace but does not change the span status, so you need both calls. Leave successful spans at the default UNSET rather than setting OK; backends treat an explicit OK as a signal that no further judgement should be applied.

Add attributes and events to a spanspan-attributes

span.setAttribute('order.id', orderId);
span.setAttributes({
  'payment.provider': 'stripe',
  'payment.amount_cents': 4999,
  'payment.retry': false,
});
span.addEvent('gateway.retry', { attempt: 2 });

Attribute values must be a string, number, boolean, or a homogeneous array of those. Passing a nested object drops the attribute and logs a diag warning that you will not see unless you set up a diag logger.

Add data to whatever span is currently activeenrich-active-span

import { trace } from '@opentelemetry/api';

export function tagTenant(tenantId) {
  trace.getActiveSpan()?.setAttribute('tenant.id', tenantId);
}

// pull the trace id for a log line
const traceId = trace.getActiveSpan()?.spanContext().traceId;

getActiveSpan returns undefined when no SDK is registered or when the context manager lost the async chain, so always use optional chaining here. Putting traceId into your logs is the cheapest way to join logs to traces.

Run code inside a specific span's contextmanual-context

import { context, trace } from '@opentelemetry/api';

const span = tracer.startSpan('background-job');
const ctx = trace.setSpan(context.active(), span);

context.with(ctx, () => {
  doWork();          // spans started here become children of span
});
span.end();

Use this when startActiveSpan does not fit, such as a span whose lifetime crosses callbacks. In Node the async chain only survives if the SDK registered AsyncLocalStorageContextManager, which sdk-node does by default.

Inject and extract trace context over your own transportpropagate-across-services

import { context, propagation, trace } from '@opentelemetry/api';

// producer
const headers = {};
propagation.inject(context.active(), headers);
await queue.publish({ body, headers });

// consumer
const parentCtx = propagation.extract(context.active(), message.headers);
context.with(parentCtx, () => {
  tracer.startActiveSpan('handle-message', (span) => { /* ... */ span.end(); });
});

inject writes the W3C traceparent header by default. It is a no-op if no propagator is configured on the SDK side, so an empty carrier object usually means the SDK, not your code.

Set span kind and link to related tracesspan-kind-and-links

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

const span = tracer.startSpan('consume batch', {
  kind: SpanKind.CONSUMER,
  links: messages.map((m) => ({
    context: trace.getSpanContext(propagation.extract(context.active(), m.headers)),
  })).filter((l) => l.context),
});

Links are how you model a batch consumer that serves many upstream traces, since a span can only have one parent. Kind drives service-map and latency views in most backends, so SERVER, CLIENT, PRODUCER, and CONSUMER are worth setting correctly.

Record a countercounter-metric

import { metrics } from '@opentelemetry/api';

const meter = metrics.getMeter('checkout-service', '1.4.0');
const charges = meter.createCounter('checkout.charges', {
  description: 'Completed charge attempts',
  unit: '{charge}',
});

charges.add(1, { provider: 'stripe', outcome: 'success' });

Create instruments once at module scope, not per request: creating a counter on every call churns the SDK's instrument registry. Keep attribute values low-cardinality, because each distinct combination becomes its own time series.

Record a latency histogramhistogram-metric

const latency = meter.createHistogram('checkout.duration', {
  unit: 'ms',
  description: 'Time to complete a checkout',
});

const started = performance.now();
try {
  await checkout();
} finally {
  latency.record(performance.now() - started, { route: '/checkout' });
}

Bucket boundaries are an SDK view concern, not an API one, so the defaults apply until someone configures a view. Never put a user id or order id in the attributes here.

Report a value that is read on collectionobservable-gauge

const gauge = meter.createObservableGauge('queue.depth');
gauge.addCallback((result) => {
  result.observe(queue.size(), { queue: 'emails' });
});

The callback runs on the SDK's collection interval, so keep it synchronous and cheap. Anything slow here delays the whole metric export cycle.

Carry a value across services with baggagebaggage

import { context, propagation } from '@opentelemetry/api';

const bag = propagation.createBaggage({ 'tenant.id': { value: tenantId } });
context.with(propagation.setBaggage(context.active(), bag), () => handle());

// downstream, after propagation.extract
const tenant = propagation.getBaggage(context.active())?.getEntry('tenant.id')?.value;

Baggage travels in a plain HTTP header to every downstream service, including third parties you call. Never put anything sensitive in it, and it does not become a span attribute unless the SDK is configured with a baggage span processor.

Find out why no spans are appearingdebug-noop-api

import { diag, DiagConsoleLogger, DiagLogLevel, trace } from '@opentelemetry/api';

diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.DEBUG);

const tracer = trace.getTracer('debug');
console.log(tracer.constructor.name); // NoopTracer means no SDK registered

A NoopTracer means either no SDK was registered, the SDK was registered after your module was imported, or a duplicate incompatible @opentelemetry/api copy is in node_modules. Check with npm ls @opentelemetry/api before blaming your code.

Alternatives

PackageRegistryPick it when
@opentelemetry/sdk-nodenpmYou are an application rather than a library, and need the piece that actually records and exports what the API describes.
dd-tracenpmYou are a Datadog shop and want auto-instrumentation, profiling, and runtime metrics from one package without assembling an SDK yourself.
@sentry/nodenpmErrors matter more to you than traces, and you want performance data as a side effect of the error tooling you were installing anyway.