使用 instrumentation.ts 进行 OpenTelemetry 追踪
通过 instrumentation 钩子注册 OpenTelemetry,以追踪服务器请求和跨度。
使用 instrumentation.ts 进行 OpenTelemetry 追踪 是 CoddyKit 上的免费 Next.js 15 Fullstack (App Router + Server Actions) 课时。 这是第 1 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Next.js 15 Fullstack (App Router + Server Actions) 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Next.js 15 Fullstack (App Router + Server Actions) 课程共包含 4 节课。
本课时的部分内容尚未翻译,以英文显示。
What Is OpenTelemetry and Why Next.js Needs It
OpenTelemetry (OTel) is a vendor-neutral observability framework that produces traces, metrics, and logs from your application. A trace records the full journey of a single request — from the edge, through Server Components, Server Actions, and database calls — as a tree of spans.
Next.js 15 has built-in, first-class support for OTel via the instrumentation.ts hook. This means you get automatic span creation for:
- App Router page and layout renders
- Route Handler HTTP requests
- Server Actions invocations
- Fetch calls made inside server code
Without tracing you can only guess where latency hides. With tracing you see the exact span — and its duration — that is slow.
Enabling the Instrumentation Hook in next.config.ts
Before instrumentation.ts is picked up, you must opt in. In Next.js 15 the flag is stable, but the config key is still required for older 14-compat setups. Add it to your config:
The file next.config.ts is the TypeScript-native config introduced in Next.js 15. The experimental.instrumentationHook key tells the framework to import instrumentation.ts once when the Node.js server boots — before any request is handled.
After adding this flag, create instrumentation.ts in the project root (same level as app/, not inside it).
// next.config.ts
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
experimental: {
// Required in Next.js 14; stable & default-true in Next.js 15
// but explicit opt-in avoids version ambiguity
instrumentationHook: true,
},
};
export default nextConfig;Installing OpenTelemetry Packages
The OTel ecosystem is split into many small packages. For a Next.js setup you need:
@opentelemetry/sdk-node— the Node.js SDK that wires everything together@opentelemetry/auto-instrumentations-node— auto-instrumentshttp,fetch,dns, and popular libraries without manual code@opentelemetry/exporter-trace-otlp-http— exports spans over OTLP/HTTP to a collector (Jaeger, Grafana Tempo, Honeycomb, Datadog, etc.)@opentelemetry/resourcesand@opentelemetry/semantic-conventions— describe your service with standard attribute names
Install them all as production dependencies:
// Terminal — install once
// npm install @opentelemetry/sdk-node \
// @opentelemetry/auto-instrumentations-node \
// @opentelemetry/exporter-trace-otlp-http \
// @opentelemetry/resources \
// @opentelemetry/semantic-conventions
// No code to run — this is a shell command reference.
// After install, package.json will contain these entries:
const expectedDeps = [
'@opentelemetry/sdk-node',
'@opentelemetry/auto-instrumentations-node',
'@opentelemetry/exporter-trace-otlp-http',
'@opentelemetry/resources',
'@opentelemetry/semantic-conventions',
];
console.log('Required OTel packages:', expectedDeps);The register() Export — Entry Point for Instrumentation
instrumentation.ts must export a named async function called register(). Next.js calls it exactly once when the server process starts.
Key rules:
- The file runs in the Node.js runtime only — not in the Edge runtime or the browser.
- Use
process.env.NEXT_RUNTIME === 'nodejs'to guard Node-only imports, because Next.js may import the file in other runtimes during analysis. - SDK initialisation must complete synchronously or before the first request —
register()is awaited by the framework.
// instrumentation.ts (project root)
export async function register() {
// Guard: only run the heavy Node.js OTel SDK in the Node runtime.
// Edge runtime does not support 'async_hooks' which OTel depends on.
if (process.env.NEXT_RUNTIME === 'nodejs') {
// Dynamically import so the module is never bundled into Edge chunks.
await import('./instrumentation.node');
}
}Creating instrumentation.node.ts — SDK Initialisation
By convention, the heavy setup lives in a separate file instrumentation.node.ts that is only ever imported inside the Node runtime guard. This keeps your bundle clean.
The NodeSDK class from @opentelemetry/sdk-node is the main entry point. You pass it:
- A resource — metadata about your service (name, version, environment)
- A trace exporter — where spans are sent
- Optional instrumentations — auto-instrument libraries automatically
Call sdk.start() to activate. Register a SIGTERM handler to flush in-flight spans before the process exits.
// instrumentation.node.ts (project root)
import { NodeSDK } from '@opentelemetry/sdk-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { Resource } from '@opentelemetry/resources';
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
const sdk = new NodeSDK({
resource: new Resource({
[ATTR_SERVICE_NAME]: process.env.OTEL_SERVICE_NAME ?? 'my-nextjs-app',
[ATTR_SERVICE_VERSION]: process.env.npm_package_version ?? '0.0.0',
}),
traceExporter: new OTLPTraceExporter({
// Default: http://localhost:4318/v1/traces
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? 'http://localhost:4318/v1/traces',
}),
instrumentations: [
getNodeAutoInstrumentations({
// Disable noisy filesystem instrumentation in Next.js
'@opentelemetry/instrumentation-fs': { enabled: false },
}),
],
});
sdk.start();
// Flush spans before the process exits (e.g. during Vercel cold-start teardown)
process.on('SIGTERM', () => {
sdk.shutdown().finally(() => process.exit(0));
});What Spans Next.js Creates Automatically
Once the SDK is running, Next.js 15 emits spans automatically for every server-side operation. You do not write any tracing code in your components. The built-in spans include:
BaseServer.handleRequest— top-level span for every HTTP requestNextNodeServer.findPageComponents— span for resolving which page/layout to renderAppRender.getBodyResult— span covering React server renderingAppRouteRouteHandler.runHandler— span wrapping a Route Handler- Nested fetch spans — each
fetch()call in server code becomes a child span with URL, method, and status
These spans are automatically linked as a parent-child tree so you can see the full call graph in your tracing UI (Jaeger, Grafana Tempo, Honeycomb, etc.).
Adding Custom Spans with @opentelemetry/api
Auto-instrumentation covers HTTP and fetch. For your own business logic — such as a slow database query helper or a complex computation — you create custom spans using @opentelemetry/api.
The pattern is always:
- Get the global tracer:
trace.getTracer('your-scope-name') - Call
tracer.startActiveSpan('span-name', async (span) => { ... }) - Set attributes on the span for searchable metadata
- Always call
span.end()in afinallyblock
Custom spans automatically become children of the currently active span, so they nest correctly in the trace tree.
// lib/db.ts — wrapping a Postgres query with a custom span
import { trace, SpanStatusCode } from '@opentelemetry/api';
import { sql } from '@vercel/postgres'; // or any pg client
const tracer = trace.getTracer('my-nextjs-app/db');
export async function getUserById(id: string) {
return tracer.startActiveSpan('db.getUserById', async (span) => {
span.setAttributes({
'db.system': 'postgresql',
'db.operation': 'SELECT',
'app.user.id': id,
});
try {
const result = await sql`SELECT * FROM users WHERE id = ${id} LIMIT 1`;
span.setStatus({ code: SpanStatusCode.OK });
return result.rows[0] ?? null;
} catch (err) {
span.recordException(err as Error);
span.setStatus({ code: SpanStatusCode.ERROR, message: (err as Error).message });
throw err;
} finally {
span.end();
}
});
}Tracing Inside a Server Action
Server Actions run on the server and are wrapped by Next.js in their own HTTP lifecycle. You can add custom spans inside a Server Action just as you would in any server function — the active span context is propagated automatically.
This example creates a span around the database write so you can see separately how long validation took versus the actual INSERT:
// app/actions/create-post.ts
'use server';
import { trace, SpanStatusCode } from '@opentelemetry/api';
import { revalidatePath } from 'next/cache';
import { z } from 'zod';
const tracer = trace.getTracer('my-nextjs-app/actions');
const CreatePostSchema = z.object({
title: z.string().min(1).max(200),
body: z.string().min(1),
});
export async function createPost(formData: FormData) {
return tracer.startActiveSpan('action.createPost', async (span) => {
try {
// Validation span — nested child
const parsed = CreatePostSchema.safeParse({
title: formData.get('title'),
body: formData.get('body'),
});
if (!parsed.success) {
span.setStatus({ code: SpanStatusCode.ERROR, message: 'Validation failed' });
return { error: parsed.error.flatten() };
}
// Simulate DB insert (replace with real client)
// await db.posts.create({ data: parsed.data });
span.setAttributes({ 'post.title': parsed.data.title });
span.setStatus({ code: SpanStatusCode.OK });
revalidatePath('/posts');
return { success: true };
} finally {
span.end();
}
});
}Propagating Trace Context Across fetch() Calls
When your Next.js server calls an external microservice with fetch(), OTel should inject W3C Trace Context headers (traceparent, tracestate) so the downstream service's spans appear as children in the same trace.
With getNodeAutoInstrumentations() enabled, the @opentelemetry/instrumentation-undici or @opentelemetry/instrumentation-http package handles this automatically for fetch and http calls. You do not need to manually add headers.
To verify propagation is working, inspect the outgoing request headers — you should see:
traceparent: 00-<traceId>-<spanId>-01- The receiving service must also run OTel and read these headers (W3C standard).
// app/api/summary/route.ts — fetch with automatic context propagation
import { NextResponse } from 'next/server';
import { trace } from '@opentelemetry/api';
const tracer = trace.getTracer('my-nextjs-app/api');
export async function GET() {
return tracer.startActiveSpan('api.getSummary', async (span) => {
try {
// OTel auto-instrumentation injects 'traceparent' header automatically.
// The downstream service will see this request as a child span.
const response = await fetch('https://internal-api.example.com/data', {
next: { revalidate: 60 }, // Next.js cache hint
});
if (!response.ok) {
span.setStatus({ code: 2, message: `Upstream ${response.status}` });
return NextResponse.json({ error: 'upstream failed' }, { status: 502 });
}
const data = await response.json();
span.setStatus({ code: 1 });
return NextResponse.json(data);
} finally {
span.end();
}
});
}Environment Variables and Collector Configuration
The OTel SDK respects the standard OTel environment variables, which means you rarely need to hard-code endpoints in code. Set these in .env.local (development) and your hosting platform's env config (production):
OTEL_SERVICE_NAME— identifies your service in the tracing UIOTEL_EXPORTER_OTLP_ENDPOINT— the collector URL (e.g.http://localhost:4318for a local Jaeger all-in-one)OTEL_TRACES_SAMPLER— e.g.parentbased_traceidratioto sample only a fraction of traces in productionOTEL_TRACES_SAMPLER_ARG— e.g.0.1to sample 10% of traces
For Vercel, install the Vercel OTel integration or use @vercel/otel which wraps the SDK and reads these same env vars from the Vercel dashboard.
# .env.local — development with a local Jaeger all-in-one container
# docker run -d --name jaeger -p 16686:16686 -p 4318:4318 jaegertracing/all-in-one
OTEL_SERVICE_NAME=my-nextjs-app
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
# Sample every request in dev, 10% in production
# (set OTEL_TRACES_SAMPLER_ARG=0.1 in prod env)
OTEL_TRACES_SAMPLER=always_on
OTEL_TRACES_SAMPLER_ARG=1Using @vercel/otel for Simplified Setup
Vercel provides @vercel/otel, a thin wrapper around the OTel SDK optimised for Next.js deployments. It reduces boilerplate to a single registerOTel() call and handles Edge runtime compatibility automatically.
When deploying to Vercel, this is the recommended approach. For self-hosted or Docker deployments, the manual NodeSDK setup from earlier scenes gives you more control.
Both approaches produce identical spans — the difference is only in setup complexity and Edge support.
// instrumentation.ts — simplified with @vercel/otel
// npm install @vercel/otel
import { registerOTel } from '@vercel/otel';
export function register() {
// Works in both Node.js and Edge runtimes.
// Reads OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT from env automatically.
registerOTel({
serviceName: process.env.OTEL_SERVICE_NAME ?? 'my-nextjs-app',
// Optionally add custom attributes visible on every span:
attributes: {
'deployment.environment': process.env.VERCEL_ENV ?? 'development',
'deployment.region': process.env.VERCEL_REGION ?? 'local',
},
});
}Knowledge Check: Where Does SDK Initialisation Belong?
You are setting up OpenTelemetry tracing for a Next.js 15 App Router project deployed on a self-hosted Node.js server. Which approach correctly initialises the OTel NodeSDK?
Recap: OpenTelemetry Tracing with instrumentation.ts
In this lesson you learned how to bring production-grade distributed tracing to a Next.js 15 App Router application:
- Enable the hook — set
experimental.instrumentationHook: trueinnext.config.ts(stable default in Next.js 15). - Create instrumentation.ts — export a
register()function at the project root; use it as the single entry point for all server-side initialisation. - Guard the runtime — wrap Node.js-only imports with
process.env.NEXT_RUNTIME === 'nodejs'and use dynamicimport()to prevent Edge bundling. - Initialise NodeSDK in a separate
instrumentation.node.ts— provide aResource, anOTLPTraceExporter, and auto-instrumentations; callsdk.start()once. - Automatic spans — Next.js emits spans for page renders, Route Handlers, Server Actions, and fetch calls with no extra code.
- Custom spans — use
trace.getTracer()andstartActiveSpan()from@opentelemetry/apito instrument your own business logic. - Context propagation — W3C
traceparentheaders are injected automatically into outgoing fetch calls by auto-instrumentation. - Configuration — standard
OTEL_*env vars drive the SDK; no need to hard-code collector URLs in source code.
常见问题解答
「使用 instrumentation.ts 进行 OpenTelemetry 追踪」课时是免费的吗?
是的 — 「使用 instrumentation.ts 进行 OpenTelemetry 追踪」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Next.js 15 Fullstack (App Router + Server Actions) 课程的其余内容,请升级到 CoddyKit PRO。 Next.js 15 Fullstack (App Router + Server Actions) 课程共包含 4 节课。
「使用 instrumentation.ts 进行 OpenTelemetry 追踪」这节课中我会学到什么?
通过 instrumentation 钩子注册 OpenTelemetry,以追踪服务器请求和跨度。 你通过在浏览器中直接运行的动手代码来练习 Next.js 15 Fullstack (App Router + Server Actions),全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 Next.js 15 Fullstack (App Router + Server Actions) 需要有经验吗?
无需任何先前经验。CoddyKit 上的 Next.js 15 Fullstack (App Router + Server Actions) 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 1 节课,共 4 节。
「使用 instrumentation.ts 进行 OpenTelemetry 追踪」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 Next.js 15 Fullstack (App Router + Server Actions) 课中编写并运行代码吗?
能。每节 Next.js 15 Fullstack (App Router + Server Actions) 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。
此课程中的所有课时
- 使用 instrumentation.ts 进行 OpenTelemetry 追踪
- 细粒度 error.tsx 与全局错误边界
- 跨服务器与 Edge 的结构化日志记录
- 捕获服务器操作失败与遥测数据