Instrumentation.ts trong Next.js 16: Thiết Lập OpenTelemetry và Observability Toàn Diện (2026)
Hướng dẫn setup instrumentation.ts trong Next.js 16 để tích hợp OpenTelemetry, tracing và error tracking. Bao gồm @vercel/otel, sdk-node, sampling production và onRequestError với Sentry.
Instrumentation.ts trong Next.js 16 là file entry-point ở gốc dự án cho phép bạn khởi tạo OpenTelemetry, error tracking và các công cụ observability khác trước khi bất kỳ module ứng dụng nào được load. Bạn chỉ cần export hàm register(), và Next.js sẽ tự động phát hiện và gọi nó đúng một lần cho mỗi server instance. Không cần bật flag experimental.instrumentationHook nào cả; kể từ Next.js 15 hook này đã stable, và trong Next.js 16 nó là con đường chính thức để tích hợp OpenTelemetry vào cả Node runtime lẫn Edge runtime.
instrumentation.ts nằm ở gốc dự án (hoặc trong src/ nếu bạn dùng nó), export hai hàm chính: register() và onRequestError().
Với Next.js 15 trở lên, hook này stable. Không còn cần experimental.instrumentationHook: true trong next.config.ts.
Setup nhanh dùng @vercel/otel; setup production-grade dùng @opentelemetry/sdk-node với sampling và OTLP exporter tự chọn (Datadog, Grafana, SigNoz, Uptrace, Axiom).
onRequestError bắt error từ Server Components, Route Handlers, Server Actions và proxy.ts, là nơi lý tưởng gửi context về Sentry hoặc bất kỳ APM nào.
Luôn kiểm tra process.env.NEXT_RUNTIME === 'nodejs' và dùng dynamic import() để tránh Node-only module bị bundle vào Edge, làm hỏng build.
Trong Next.js 16 với Turbopack build production, bundling cho instrumentation nhanh hơn nhưng cách viết code không đổi.
Instrumentation.ts là gì và tại sao cần nó?
Trong hai năm làm việc sâu với App Router, tôi thấy đội nào cũng đến một điểm mà console.log không còn đủ. Bạn có một Server Component render chậm ở p95, một Server Action đôi khi timeout, một fetch call bị cache sai lớp. Bạn cần trace, không phải log riêng lẻ, mà là timeline hoàn chỉnh của mỗi request qua middleware, layout, page, và các API bên trong. Đó là bài toán của observability, và instrumentation.ts chính là entry-point mà Next.js dành riêng cho nó.
Điểm khác biệt so với việc gọi registerOTel trong app/layout.tsx hay một API route: instrumentation.ts chạy trước khi bất kỳ code ứng dụng nào được đánh giá. Điều này quan trọng vì OpenTelemetry cần patch các module built-in (như http, fetch, driver database) tại thời điểm require/import. Nếu bạn khởi tạo muộn, các module đã được load rồi thì auto-instrumentation không còn cách nào chèn hook vào nữa. Đó là lý do file này phải nằm ở gốc dự án và Next.js đảm bảo register() hoàn thành trước khi server sẵn sàng nhận request đầu tiên.
Thú thật, khi tôi migrate observability stack ở dự án hiện tại, hook này thay thế hoàn toàn workaround kiểu node -r ./tracing.js hoặc file preload.ts mà nhiều team viết trước đây. Bạn không còn phải sửa package.json script hay Dockerfile để nạp trace agent. Next.js xử lý runtime bootstrapping cho bạn, cả khi chạy next start, khi deploy lên Vercel, hay khi self-host với Docker.
Cài đặt và cấu trúc file instrumentation.ts
Tạo file instrumentation.ts ở gốc project, cùng cấp với app/, package.json, next.config.ts. Nếu dự án dùng src/, file cũng phải nằm trong src/, cạnh src/app/. Đặt sai vị trí là lỗi phổ biến nhất, vì Next.js sẽ không báo lỗi, chỉ đơn giản là hàm register() không bao giờ chạy.
// instrumentation.ts (ở gốc dự án, hoặc trong src/)
export async function register() {
// Chỉ chạy trên Node runtime, KHÔNG chạy trong Edge
if (process.env.NEXT_RUNTIME === 'nodejs') {
// Dynamic import — Node-only module không bị bundle vào Edge
await import('./instrumentation.node')
}
if (process.env.NEXT_RUNTIME === 'edge') {
// Nếu bạn dùng Edge runtime cho một số route
await import('./instrumentation.edge')
}
}
// Optional: hook error tracking, thêm sau
export async function onRequestError(
err: unknown,
request: {
path: string
method: string
headers: { [key: string]: string }
},
context: {
routerKind: 'Pages Router' | 'App Router'
routePath: string
routeType: 'render' | 'route' | 'action' | 'middleware'
renderSource: 'react-server-components' | 'react-server-components-payload' | 'server-rendering'
revalidateReason: 'on-demand' | 'stale' | undefined
renderType: 'dynamic' | 'static'
}
) {
// Gửi lỗi kèm context đến APM
}
Không cần chỉnh next.config.ts. Nếu codebase của bạn đang có experimental.instrumentationHook: true (di sản từ Next.js 14 trở về trước), hãy xóa dòng đó, vì nó bị deprecated và sẽ warning ở build time. Chi tiết đầy đủ nằm trong Next.js Instrumentation guide.
Setup OpenTelemetry nhanh với @vercel/otel
Với 80% dự án, gói @vercel/otel là điểm khởi đầu đúng. Nó bọc @opentelemetry/sdk-node với default hợp lý, tự động detect resource attributes trên Vercel, và tương thích với cả self-hosted. Cài đặt:
Rồi tạo instrumentation.node.ts ở cùng thư mục với instrumentation.ts:
// instrumentation.node.ts
import { registerOTel } from '@vercel/otel'
registerOTel({
serviceName: 'logistics-web',
// Attributes gắn vào MỌI span — hữu ích để filter theo env, region
attributes: {
'deployment.environment': process.env.NODE_ENV ?? 'development',
'service.version': process.env.APP_VERSION ?? 'dev',
},
})
Nếu bạn deploy lên Vercel, chỉ cần cài Vercel OpenTelemetry Collector integration là các trace tự động hiển thị ở dashboard. Với self-host, bạn phải set biến môi trường OTEL_EXPORTER_OTLP_ENDPOINT và optional OTEL_EXPORTER_OTLP_HEADERS (ví dụ authorization=Bearer <token> cho Grafana Cloud).
Sau khi restart dev server, mở một request thử. Bạn sẽ thấy trace với các span như GET /dashboard (root span), lồng bên trong là render route, generateMetadata, và mỗi fetch call đến API bên ngoài. Đây là điểm mạnh của instrumentation-first approach: bạn không cần thêm dòng code nào trong Server Components để có trace HTTP, vì auto-instrumentation lo phần đó. Nếu bạn đang tinh chỉnh streaming với Suspense và loading.tsx, mỗi Suspense boundary sẽ hiện thành một span riêng, giúp bạn nhìn thấy đúng phần nào của tree đang là bottleneck TTFB.
Production-grade setup với @opentelemetry/sdk-node
Khi cần kiểm soát chi tiết (chọn exporter cụ thể, batch span, sampling, tùy chỉnh resource), hãy dùng thẳng @opentelemetry/sdk-node. Đây là setup tôi chạy production ở startup logistics:
// instrumentation.node.ts
import { NodeSDK } from '@opentelemetry/sdk-node'
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-node'
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
import { resourceFromAttributes } from '@opentelemetry/resources'
import {
ATTR_SERVICE_NAME,
ATTR_SERVICE_VERSION,
} from '@opentelemetry/semantic-conventions'
import { TraceIdRatioBasedSampler } from '@opentelemetry/sdk-trace-node'
const sdk = new NodeSDK({
resource: resourceFromAttributes({
[ATTR_SERVICE_NAME]: 'logistics-web',
[ATTR_SERVICE_VERSION]: process.env.APP_VERSION ?? 'dev',
'deployment.environment': process.env.NODE_ENV ?? 'development',
}),
// Sampling: giữ 100% trong dev, 10% trong production
sampler:
process.env.NODE_ENV === 'production'
? new TraceIdRatioBasedSampler(0.1)
: new TraceIdRatioBasedSampler(1.0),
spanProcessors: [
new BatchSpanProcessor(
new OTLPTraceExporter({
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT + '/v1/traces',
headers: {
authorization: `Bearer ${process.env.OTEL_API_KEY}`,
},
}),
{ maxExportBatchSize: 512, scheduledDelayMillis: 5000 }
),
],
instrumentations: [
getNodeAutoInstrumentations({
// Tắt fs instrumentation — quá nhiều noise, hiếm khi hữu ích
'@opentelemetry/instrumentation-fs': { enabled: false },
// Bật undici (Node fetch) rõ ràng
'@opentelemetry/instrumentation-undici': { enabled: true },
}),
],
})
sdk.start()
// Shutdown gracefully khi process nhận SIGTERM
process.on('SIGTERM', () => {
sdk.shutdown().finally(() => process.exit(0))
})
Ba điểm cần chú ý ở setup này. Thứ nhất, BatchSpanProcessor chứ không phải SimpleSpanProcessor, vì batch mode gom span và gửi định kỳ, tránh block critical path bằng một HTTP call cho mỗi span. Thứ hai, tắt instrumentation-fs vì mỗi lần Next.js đọc bundle chunk từ disk sẽ sinh ra span, làm trace lộn xộn. Thứ ba, đăng ký handler SIGTERM để flush span cuối cùng trước khi container bị kill. Nếu không, bạn sẽ mất trace của những request cuối trong lúc deploy.
onRequestError hook và error tracking
Từ Next.js 15, file instrumentation.ts có thể export thêm hàm onRequestError. Hàm này fire khi có exception ném từ Server Component, Route Handler, Server Action, hoặc proxy.ts (file thay thế middleware.ts trong Next.js 16; nếu bạn chưa migrate, đọc bài chuyển đổi middleware.ts sang proxy.ts). Đây là chỗ lý tưởng để gửi context đến Sentry, Datadog, hay tự log:
Điểm quan trọng: onRequestError fire cho lỗi server-side, không bao gồm lỗi từ Client Components (những lỗi đó bạn xử lý bằng error.tsx và Sentry.init phía client). Ngoài ra, hàm này chạy sau khi response đã được gửi, nghĩa là nó không thể thay đổi response cho user, chỉ để observability. Nếu bạn cần bắt lỗi và render fallback, dùng error.tsx trong route segment tương ứng.
Gửi trace đến Datadog, Grafana Cloud, SigNoz và Uptrace
OpenTelemetry là chuẩn mở, nên bất kỳ backend nào hỗ trợ OTLP đều nhận trace từ Next.js. Bảng so sánh nhanh các option tôi đã setup trong 12 tháng qua:
Backend
OTLP endpoint mẫu
Free tier
Điểm mạnh
Grafana Cloud
https://otlp-gateway-prod-*.grafana.net/otlp
50 GB traces/tháng
Kết hợp trace + metric + log trong một UI
Datadog
https://trace.agent.datadoghq.com
14 ngày trial
APM trưởng thành, nhiều integration
SigNoz
https://ingest.<region>.signoz.cloud:443
30 ngày, 100 GB
Open-source, có thể self-host
Uptrace
https://otlp.uptrace.dev
50k span/ngày
Rẻ, UI clean, tốt cho team nhỏ
Axiom
https://api.axiom.co
500 GB/tháng
Query engine cực nhanh, kèm log
Với Grafana Cloud, bạn set OTEL_EXPORTER_OTLP_ENDPOINT và OTEL_EXPORTER_OTLP_HEADERS=authorization=Basic%20<base64(instance-id:token)>. Với Datadog, dùng gói @opentelemetry/exporter-trace-otlp-proto vì Datadog Agent nhận protobuf nhanh hơn HTTP JSON. Với SigNoz self-host, chỉ cần trỏ endpoint đến cluster của bạn (không cần header auth). Tài liệu chính thức của OpenTelemetry hỗ trợ Next.js nằm ở Next.js OpenTelemetry guide, và spec OTLP đầy đủ có tại OpenTelemetry Protocol Specification.
Sampling và hiệu suất production
Trace không miễn phí. Mỗi span cost một chút CPU để tạo, memory để giữ, và bandwidth để export. Với traffic > 100 req/s, bạn cần sampling. Nếu không, chi phí APM sẽ vượt chi phí compute.
Ba chiến lược sampling phổ biến:
Head-based ratio sampling: quyết định giữ/bỏ trace ngay khi request đến, dựa trên trace ID. Đây là TraceIdRatioBasedSampler ở ví dụ trên. Đơn giản, rẻ, nhưng bạn có thể miss error nếu ratio thấp.
Parent-based sampling: nếu upstream service đã sample trace này, giữ nó; ngược lại thì áp ratio. Phù hợp khi Next.js nằm sau API Gateway.
Tail-based sampling: giữ 100% error và slow request, sample ratio thấp cho request khỏe. Cần OpenTelemetry Collector ở giữa vì phải chờ full trace trước khi quyết định.
Với dự án hiện tại của tôi (~500 req/s peak), setup đang chạy là: ParentBased(root=TraceIdRatioBasedSampler(0.1)) ở app, kèm tail sampling ở collector giữ 100% span có status = ERROR hoặc duration > 1s. Kết quả: ingestion volume giảm 80%, mà vẫn giữ được mọi trace có ý nghĩa debug.
Tương thích Edge runtime và các lỗi thường gặp
Edge runtime của Next.js chạy trên V8 isolate, không có Node built-in như fs, perf_hooks, async_hooks. Vì hầu hết SDK OpenTelemetry phụ thuộc async_hooks cho context propagation, chúng không chạy được trong Edge. Đây là lý do check process.env.NEXT_RUNTIME === 'nodejs' là bắt buộc.
Nếu bạn có route dùng export const runtime = 'edge' và muốn trace chúng, có hai lựa chọn:
Dùng @vercel/otel phiên bản mới nhất, vì nó có Edge-compatible exporter riêng, tự động detect runtime.
Ghi trace thủ công bằng @opentelemetry/api (chỉ types, không có runtime), rồi gửi qua fetch() đến OTLP endpoint. Không có auto-instrumentation, nhưng bạn kiểm soát mọi span.
Ba lỗi tôi hay gặp khi review PR:
Module not found: 'perf_hooks' khi build: bạn đang import package OpenTelemetry ở top-level. Chuyển sang dynamic import trong nhánh Node.
Trace không xuất hiện dù server chạy: kiểm tra OTEL_EXPORTER_OTLP_ENDPOINT đã set chưa, và endpoint có suffix /v1/traces. Nhiều backend yêu cầu path đầy đủ.
Trace bị mất khi dev: dev mode fork process, và next dev có thể khiến register() chạy nhiều lần. Dùng globalThis guard để tránh double-registration.
Nếu observability là bước tiếp theo trong journey Next.js 16 của bạn, hãy đảm bảo đã nắm chắc kiến trúc Route Handlers vs Server Actions, vì mỗi loại xuất hiện dưới dạng span kind khác nhau (server span cho route handler, internal span cho server action), và biết phân biệt giúp bạn đọc trace nhanh hơn nhiều.
Câu hỏi thường gặp
Instrumentation.ts có cần bật flag experimental không?
Không. Kể từ Next.js 15, instrumentation.ts ổn định và được auto-detect. Nếu next.config.ts của bạn vẫn có experimental.instrumentationHook: true, hãy xóa dòng đó, vì Next.js 16 sẽ warning khi thấy flag deprecated này.
Instrumentation.ts có chạy trên Edge runtime không?
Có, hàm register() được gọi trong mọi runtime, nhưng bạn phải check process.env.NEXT_RUNTIME để load module tương ứng. Hầu hết SDK OpenTelemetry Node không chạy được trong Edge vì thiếu async_hooks. Hãy dùng @vercel/otel nếu cần trace cả Edge routes.
Sự khác biệt giữa @vercel/otel và @opentelemetry/sdk-node là gì?
@vercel/otel là wrapper opinionated quanh sdk-node với default hợp lý cho platform Vercel, setup 3 dòng code. @opentelemetry/sdk-node là SDK gốc, cho bạn kiểm soát đầy đủ sampler, exporter, span processor, và instrumentation. Dùng vercel/otel cho prototype, sdk-node cho production tuning.
Làm sao debug khi trace không xuất hiện trong backend?
Thêm ConsoleSpanExporter song song với OTLP exporter để in span ra terminal. Nếu console có span nhưng backend không, vấn đề là network hoặc auth. Kiểm tra OTEL_EXPORTER_OTLP_ENDPOINT có suffix /v1/traces, header authorization đúng format, và tường lửa không block egress đến endpoint.
onRequestError có bắt được lỗi từ Client Components không?
Không. Hook onRequestError chỉ fire cho lỗi server-side, tức từ Server Components, Route Handlers, Server Actions và proxy.ts. Lỗi phía client cần xử lý riêng bằng error.tsx và tích hợp SDK client của APM (ví dụ Sentry.init() trong client component root).
Marcus picked up React in 2017 and has been writing it professionally ever since. Five years at Stripe on the Dashboard team, where he led the migration of the billing UI off a legacy webpack 4 setup onto Next.js with module federation for the embedded merchant components. He shipped the first internal RSC prototype there in late 2023, which mostly worked.
Now he's a staff engineer at a logistics startup in Austin, owning the customer-facing Next.js app and the design system that backs it. He spends a disproportionate amount of time thinking about route group conventions, parallel routes for modal UX, and why next/image keeps surprising people on Cloudflare's image resizing.
His writing favors annotated code over prose. Seven years of React, two of those deep in App Router land.
Migration từ Webpack sang Turbopack production build trong Next.js 16 với config chi tiết, custom loaders (SVGR, MDX, GraphQL), benchmark thực tế trên 3 codebase, và checklist migration từng bước.
Migration Pages Router sang App Router trong Next.js 16 là quy trình incremental 2-6 tuần với dự án cỡ vừa. Codemod tự động 80-90% thay đổi cơ học, còn lại là refactor data fetching. Playbook đầy đủ từ 5 dự án thực tế.
Hướng dẫn dùng Suspense và loading.tsx trong Next.js 16 để stream HTML theo chunks, giảm TTFB xuống dưới 200ms, và tối ưu Core Web Vitals với React 19 use() hook cùng preload pattern.