instrumentation.tsによるOpenTelemetryトレーシング
instrumentationフックでOpenTelemetryを登録し、サーバーリクエストとスパンをトレースします。
「instrumentation.tsによるOpenTelemetryトレーシング」はCoddyKit上の無料Next.js 15 Fullstack (App Router + Server Actions)レッスンです。 これはレッスン1/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応の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.
AI チューターと学ぶ TypeScript — 無料
ブラウザでリアルコードを書いて実行し、24/7 の AI チューターから瞬時にサポートを受け、ウェブまたはアプリで続きから学習できます。
- コース
- 22
- レッスン
- 88
よくある質問
「instrumentation.tsによるOpenTelemetryトレーシング」レッスンは無料ですか?
はい。「instrumentation.tsによるOpenTelemetryトレーシング」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応の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)を演習し、24時間対応の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とglobal-errorの境界
- ServerとEdgeにまたがる構造化ロギング
- Server Actionの失敗とテレメトリの収集