Node.js custom OpenTelemetry instrumentation
The auto-instrumentations capture framework and library activity, but your business logic is invisible until you instrument it. Use the OpenTelemetry API (@opentelemetry/api) to add spans around your own operations, attach attributes for filtering, and record custom metrics. The API is already installed as a dependency of @opentelemetry/sdk-node.
Prerequisites
Section titled “Prerequisites”- A service already initialized with the OpenTelemetry SDK — follow one of the framework guides first.
- The SDK must be started before this code runs, so spans and metrics have a provider to record against.
Manual spans
Section titled “Manual spans”Get a tracer once per module, then wrap an operation in startActiveSpan. The active span becomes the parent of any spans created inside the callback — including those from auto-instrumented libraries — so downstream HTTP and database calls nest correctly.
const { trace, context, SpanStatusCode } = require('@opentelemetry/api');
const tracer = trace.getTracer('checkout-api');
function processOrder(order) { return tracer.startActiveSpan('processOrder', (span) => { try { span.setAttribute('order.id', order.id); span.setAttribute('order.item_count', order.items.length); span.addEvent('order.validated'); const result = chargeCard(order); span.setStatus({ code: SpanStatusCode.OK }); return result; } catch (err) { span.recordException(err); span.setStatus({ code: SpanStatusCode.ERROR, message: err.message }); throw err; } finally { span.end(); } });}- Attributes describe the operation and let you filter traces in Osuite. Prefer namespaced keys such as
order.id. - Events (
addEvent) mark points in time within a span. - Status — set
SpanStatusCode.ERRORand callrecordExceptionon failure so the span is flagged in error tracking. span.end()must always run. Thefinallyblock guarantees it even when the operation throws.
To create a child span without making it the active context, use startSpan and attach it manually:
function chargeCard(order) { const span = tracer.startSpan('chargeCard'); return context.with(trace.setSpan(context.active(), span), () => { try { return { charged: order.total }; } finally { span.end(); } });}Custom metrics
Section titled “Custom metrics”Get a meter, then create instruments once at module scope and record to them from your code. Attributes passed at record time become metric dimensions you can group by in Osuite.
const { metrics } = require('@opentelemetry/api');
const meter = metrics.getMeter('checkout-api');
const ordersProcessed = meter.createCounter('orders.processed', { description: 'Number of orders processed', unit: '{order}',});
const orderValue = meter.createHistogram('order.value', { description: 'Monetary value of each processed order', unit: 'USD',});
const ordersInFlight = meter.createUpDownCounter('orders.in_flight', { description: 'Orders currently being processed', unit: '{order}',});
const queueDepth = meter.createObservableGauge('order.queue.depth', { description: 'Current depth of the order queue', unit: '{order}',});
let currentQueueDepth = 0;queueDepth.addCallback((result) => { result.observe(currentQueueDepth, { queue: 'orders' });});
function recordOrder(order) { ordersInFlight.add(1, { 'order.type': order.type }); ordersProcessed.add(1, { 'order.type': order.type }); orderValue.record(order.total, { 'order.type': order.type }); ordersInFlight.add(-1, { 'order.type': order.type });}Choose the instrument by what you are measuring:
- Counter — a value that only increases (requests, orders, bytes). Use
add()with a positive delta. - UpDownCounter — a value that rises and falls (queue length, active connections, in-flight requests).
- Histogram — a distribution you want percentiles over (request duration, payload size, order value).
- Observable gauge — a point-in-time value read on each collection through a callback (pool size, temperature, current queue depth).
Verify in Osuite
Section titled “Verify in Osuite”What you should see
Exercise the instrumented code, then open a trace in APM — your manual spans appear as named children with the attributes you set. Open Dashboards → Explore metrics and search for the instrument name (for example orders.processed); the series appears with your attribute keys available as group-by dimensions.
Troubleshooting
Section titled “Troubleshooting”No data in Osuite after a few minutes? Work through these checks.
- Endpoint — confirm the exporter targets exactly
ingest.<region>.osuite.io:443for your region, over TLS. - Token — confirm the ingest header carries a valid
<your-ingest-token>and has not been rotated. - Pipeline — confirm the signal you expect (traces, logs, or metrics) is wired into an active pipeline with the
otlp/osuiteexporter attached. - Export errors — check the application or Collector logs for OTLP export failures (auth, DNS, TLS, connection refused).
- Network — confirm the host has outbound access to the ingest endpoint on port
443. - Timing — allow up to a minute for the first data to appear before assuming a failure.
Still stuck? Ask the Investigation Agent or contact support.
If custom spans or metrics never appear, confirm the SDK started before this module ran — see Node.js troubleshooting.
Next steps
Section titled “Next steps”- Query your custom metrics with PromQL.
- Build a dashboard from your spans and metrics.