OpenTelemetry
Vendor-neutral instrumentation for traces, metrics, and logs.
OpenTelemetry (OTel) is the open standard for collecting telemetry from applications. It provides a single instrumentation layer that exports to Sentry, Axiom, or any OTLP-compatible backend.
What OpenTelemetry provides
| Signal | Purpose | Example |
|---|---|---|
| Traces | Follow a request across services | Login → API → Database |
| Metrics | Numeric measurements over time | Request rate, latency p95, error count |
| Logs | Structured event records | { level: "error", requestId: "..." } |
Architecture in our stack
Node.js backend setup
pnpm add @opentelemetry/sdk-node \
@opentelemetry/auto-instrumentations-node \
@opentelemetry/exporter-trace-otlp-http// instrumentation.ts — load before app starts
import { NodeSDK } from "@opentelemetry/sdk-node";
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
const sdk = new NodeSDK({
traceExporter: new OTLPTraceExporter({
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
}),
instrumentations: [getNodeAutoInstrumentations()],
serviceName: process.env.OTEL_SERVICE_NAME ?? "backend-api",
});
sdk.start();Start the app with the instrumentation hook:
node --import ./instrumentation.ts dist/main.jsNext.js setup
Use the official OpenTelemetry package for Next.js:
pnpm add @vercel/otel// instrumentation.ts (project root)
import { registerOTel } from "@vercel/otel";
export function register() {
registerOTel({ serviceName: "web-portal" });
}Enable instrumentation in next.config.ts:
const nextConfig = {
experimental: {
instrumentationHook: true,
},
};
export default nextConfig;Manual spans
Add custom spans for business-critical operations:
import { trace } from "@opentelemetry/api";
const tracer = trace.getTracer("checkout-service");
async function processCheckout(orderId: string) {
return tracer.startActiveSpan("processCheckout", async (span) => {
span.setAttribute("order.id", orderId);
try {
const result = await chargePayment(orderId);
span.setStatus({ code: SpanStatusCode.OK });
return result;
} catch (err) {
span.recordException(err as Error);
span.setStatus({ code: SpanStatusCode.ERROR });
throw err;
} finally {
span.end();
}
});
}Environment variables
OTEL_SERVICE_NAME=backend-api
OTEL_EXPORTER_OTLP_ENDPOINT=https://otel-collector.company.com/v1/traces
OTEL_TRACES_SAMPLER=parentbased_traceidratio
OTEL_TRACES_SAMPLER_ARG=0.1| Variable | Description |
|---|---|
OTEL_SERVICE_NAME | Identifies the service in traces |
OTEL_EXPORTER_OTLP_ENDPOINT | Where to send telemetry |
OTEL_TRACES_SAMPLER_ARG | Sample rate (0.1 = 10% in prod) |
Propagation
OpenTelemetry propagates trace context via HTTP headers (traceparent). When the frontend calls the backend, the same trace ID links both spans — essential for debugging cross-service requests.