OpenTelemetry provides standardized observability for distributed systems. Here is how to add tracing to your Next.js application.
Step 1: Install Dependencies
pnpm add @opentelemetry/sdk-node @opentelemetry/api \
@opentelemetry/auto-instrumentations-node \
@opentelemetry/exporter-trace-otlp-http \
@opentelemetry/resources @opentelemetry/semantic-conventions
Step 2: Instrumentation File
Next.js supports OpenTelemetry via the instrumentation hook.
// instrumentation.ts
export async function register() {
if (process.env.NEXT_RUNTIME === "nodejs") {
const { NodeSDK } = await import("@opentelemetry/sdk-node");
const { OTLPTraceExporter } = await import(
"@opentelemetry/exporter-trace-otlp-http"
);
const { Resource } = await import("@opentelemetry/resources");
const {
ATTR_SERVICE_NAME,
ATTR_SERVICE_VERSION,
} = await import("@opentelemetry/semantic-conventions");
const { getNodeAutoInstrumentations } = await import(
"@opentelemetry/auto-instrumentations-node"
);
const sdk = new NodeSDK({
resource: new Resource({
[ATTR_SERVICE_NAME]: "rcb-software-web",
[ATTR_SERVICE_VERSION]: "1.0.0",
environment: process.env.NODE_ENV,
}),
traceExporter: new OTLPTraceExporter({
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? "http://localhost:4318/v1/traces",
}),
instrumentations: [
getNodeAutoInstrumentations({
"@opentelemetry/instrumentation-http": { enabled: true },
"@opentelemetry/instrumentation-fetch": { enabled: true },
}),
],
});
sdk.start();
process.on("SIGTERM", () => {
sdk.shutdown().catch(console.error);
});
}
}
Step 3: Enable in Next.js Config
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
experimental: {
instrumentationHook: true,
},
};
export default nextConfig;
Step 4: Custom Spans
Add custom spans to track specific operations.
// lib/tracing.ts
import { trace, SpanStatusCode, type Span } from "@opentelemetry/api";
const tracer = trace.getTracer("rcb-software-web");
export function createSpan(name: string) {
return tracer.startSpan(name);
}
export async function withSpan<T>(
name: string,
fn: (span: Span) => Promise<T>,
attributes?: Record<string, string | number | boolean>
): Promise<T> {
return tracer.startActiveSpan(name, async (span) => {
if (attributes) {
span.setAttributes(attributes);
}
try {
const result = await fn(span);
span.setStatus({ code: SpanStatusCode.OK });
return result;
} catch (error) {
span.setStatus({
code: SpanStatusCode.ERROR,
message: error instanceof Error ? error.message : "Unknown error",
});
span.recordException(error as Error);
throw error;
} finally {
span.end();
}
});
}
Step 5: Trace Database Queries
// lib/db.ts
import { withSpan } from "./tracing";
export async function findUserById(id: string) {
return withSpan(
"db.findUserById",
async (span) => {
span.setAttribute("db.system", "postgresql");
span.setAttribute("db.operation", "SELECT");
span.setAttribute("db.table", "users");
span.setAttribute("user.id", id);
// Your actual query
const user = await db.select().from(users).where(eq(users.id, id));
span.setAttribute("db.rows_returned", user.length);
return user[0] ?? null;
}
);
}
Step 6: Trace API Routes
// app/api/users/[id]/route.ts
import { NextRequest, NextResponse } from "next/server";
import { withSpan } from "@/lib/tracing";
import { findUserById } from "@/lib/db";
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
return withSpan(
"api.getUser",
async (span) => {
span.setAttribute("http.method", "GET");
span.setAttribute("user.id", id);
const user = await findUserById(id);
if (!user) {
span.setAttribute("http.status_code", 404);
return NextResponse.json({ error: "Not found" }, { status: 404 });
}
span.setAttribute("http.status_code", 200);
return NextResponse.json({ user });
}
);
}
Step 7: Trace External API Calls
// lib/external.ts
import { withSpan } from "./tracing";
export async function fetchFromExternalAPI(endpoint: string) {
return withSpan(
"external.api.request",
async (span) => {
span.setAttribute("http.url", endpoint);
span.setAttribute("http.method", "GET");
const start = Date.now();
const response = await fetch(endpoint);
const duration = Date.now() - start;
span.setAttribute("http.status_code", response.status);
span.setAttribute("http.duration_ms", duration);
if (!response.ok) {
throw new Error(`External API error: ${response.status}`);
}
return response.json();
}
);
}
Step 8: Trace Server Components
// app/dashboard/page.tsx
import { withSpan } from "@/lib/tracing";
async function loadDashboardData() {
return withSpan("dashboard.loadData", async (span) => {
const [stats, recentActivity, notifications] = await Promise.all([
withSpan("dashboard.getStats", async () => getStats()),
withSpan("dashboard.getActivity", async () => getRecentActivity()),
withSpan("dashboard.getNotifications", async () => getNotifications()),
]);
span.setAttribute("stats.count", Object.keys(stats).length);
span.setAttribute("activity.count", recentActivity.length);
span.setAttribute("notifications.count", notifications.length);
return { stats, recentActivity, notifications };
});
}
export default async function DashboardPage() {
const data = await loadDashboardData();
return (
<div>
{/* Render dashboard with traced data */}
</div>
);
}
Step 9: Local Development with Jaeger
# docker-compose.yml
services:
jaeger:
image: jaegertracing/all-in-one:latest
ports:
- "16686:16686" # UI
- "4318:4318" # OTLP HTTP
environment:
COLLECTOR_OTLP_ENABLED: true
docker compose up -d
# Open http://localhost:16686 for the Jaeger UI
Set your environment variable:
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
Step 10: Production with Honeycomb
For production, export to Honeycomb or a similar service:
OTEL_EXPORTER_OTLP_ENDPOINT=https://api.honeycomb.io
OTEL_EXPORTER_OTLP_HEADERS=x-honeycomb-team=YOUR_API_KEY
Best Practices
- Name spans descriptively:
db.findUserById,api.createOrder - Add meaningful attributes for filtering and grouping
- Record exceptions with
span.recordException() - Use
withSpanfor clean, consistent instrumentation - Avoid tracing trivial operations that add noise
- Set sampling rates in production to control costs
Need Observability for Your App?
We implement monitoring, tracing, and alerting to keep your applications healthy. Contact us for production-grade observability.