import { NodeSDK } from '@opentelemetry/sdk-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { Resource } from '@opentelemetry/resources';
import {
SEMRESATTRS_SERVICE_NAME,
SEMRESATTRS_SERVICE_VERSION,
} from '@opentelemetry/semantic-conventions';
const exporter = new OTLPTraceExporter({
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? 'http://localhost:4318/v1/traces',
});
const sdk = new NodeSDK({
resource: new Resource({
[SEMRESATTRS_SERVICE_NAME]: 'checkout-api',
[SEMRESATTRS_SERVICE_VERSION]: process.env.APP_VERSION ?? 'dev',
}),
spanProcessors: [new BatchSpanProcessor(exporter)],
instrumentations: [
getNodeAutoInstrumentations({
'@opentelemetry/instrumentation-fs': { enabled: false },
}),
],
});
sdk.start();
process.on('SIGTERM', () => {
sdk.shutdown().finally(() => process.exit(0));
});
import { trace, SpanStatusCode, Span, Attributes } from '@opentelemetry/api';
const tracer = trace.getTracer('checkout-api');
export async function withSpan<T>(
name: string,
attributes: Attributes,
fn: (span: Span) => Promise<T>,
): Promise<T> {
return tracer.startActiveSpan(name, { attributes }, async (span) => {
try {
const result = await fn(span);
span.setStatus({ code: SpanStatusCode.OK });
return result;
} catch (err) {
span.recordException(err as Error);
span.setStatus({
code: SpanStatusCode.ERROR,
message: err instanceof Error ? err.message : String(err),
});
throw err;
} finally {
span.end();
}
});
}
import './tracing';
import { createServer } from 'node:http';
import { withSpan } from './withSpan';
const PAYMENTS_URL = process.env.PAYMENTS_URL ?? 'http://payments:8080/charge';
async function handleCheckout(orderId: string, amount: number) {
return withSpan('checkout', { 'order.id': orderId, 'order.amount': amount }, async (span) => {
// Runs inside the active span, so traceparent is injected on this fetch.
const res = await fetch(PAYMENTS_URL, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ orderId, amount }),
});
span.setAttribute('payment.status_code', res.status);
if (!res.ok) {
throw new Error(`payment failed: ${res.status}`);
}
return (await res.json()) as { chargeId: string };
});
}
const server = createServer(async (req, res) => {
if (req.method !== 'POST' || req.url !== '/checkout') {
res.writeHead(404).end();
return;
}
try {
const { chargeId } = await handleCheckout('ord_123', 4200);
res.writeHead(200, { 'content-type': 'application/json' });
res.end(JSON.stringify({ chargeId }));
} catch (err) {
res.writeHead(502, { 'content-type': 'application/json' });
res.end(JSON.stringify({ error: (err as Error).message }));
}
});
server.listen(3000);
This snippet shows how a Node HTTP service is wired for distributed tracing with OpenTelemetry, from SDK bootstrap to per-request spans and downstream context propagation. The central idea is that a trace is a tree of Spans stitched together by a shared SpanContext that travels between processes as W3C traceparent headers, so the same request can be followed across service boundaries.
In tracing.ts the SDK is initialized before the app loads. A Resource tags every span with service.name and service.version, which is what backends use to group traces by service. The OTLPTraceExporter ships spans over OTLP/HTTP to a collector, and BatchSpanProcessor buffers and flushes them in batches so exporting never blocks the request path. getNodeAutoInstrumentations patches the built-in http module and common libraries automatically, meaning most spans appear without touching handler code. Registering start() at module load — and it must be imported first via -r ./tracing or an early import — ensures the monkey-patching happens before those modules are required.
The withSpan helper in withSpan.ts wraps manual work in a Span. It calls tracer.startActiveSpan, which both creates the span and makes it the active span in the current async context, so any child spans and auto-instrumented calls nest correctly. The try/catch/finally shape is the important part: errors are recorded with recordException and the status is set to ERROR, while span.end() in finally guarantees the span is always closed even on throw. Leaking unended spans is a common pitfall that leads to missing or infinitely-open traces.
In server.ts an incoming request handler uses withSpan to create a checkout span, sets domain attributes like order.id, and then makes an outbound fetch. Because the fetch runs inside the active span's context, the auto-instrumentation injects the current traceparent into the outgoing headers, propagating the trace to the payment service. Attributes should stay low-cardinality where they become metrics, and heavy payloads are best avoided as attribute values. Together these files give end-to-end visibility with minimal handler-level noise.
Related snips
package api
import (
"net/http"
"runtime/debug"
)
Expose build metadata for debugging deploys
export interface RetryOptions {
retries: number;
baseMs: number;
maxMs: number;
signal?: AbortSignal;
onRetry?: (attempt: number, delay: number, err: unknown) => void;
Exponential backoff with jitter for retries
use tracing::{info, instrument};
#[instrument]
fn process_request(user_id: u64) {
info!(user_id, "Processing request");
// Work happens here
tracing for structured logging and distributed tracing
package deps
import (
"net"
"net/http"
"time"
HTTP client tuned for production: timeouts, transport, and connection reuse
# Grafana provisioning: datasources
# /etc/grafana/provisioning/datasources/prometheus.yml
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
Grafana dashboards as code with JSON provisioning
import React from "react";
type FallbackProps = {
error: Error;
reset: () => void;
};
React Error Boundary + error reporting hook
Share this code
Here's the card — post it anywhere.