- 发布日期
第 34 讲:Instrumentation、OpenTelemetry、日志与可观测性
instrumentation.ts 钩子、OpenTelemetry 集成与生产可观测性实践
第 33 讲讲了如何处理错误。本讲讲生产里真的能用的可观测性:让你不用 SSH 进容器看日志,而是在 Datadog / Honeycomb / Grafana 里直接看到 "/products/iphone P99=200ms,其中 30% 来自 db.query"。Next.js 自带 OpenTelemetry tracing,本讲会讲清楚它的内部 span 名约定、
instrumentation.ts的 register 生命周期、以及如何对接外部 collector。
学习目标
- 看懂
NextTracer(packages/next/src/server/lib/trace/tracer.ts)的实现:基于 OpenTelemetry API。 - 列出 Next.js 内置 span 命名约定,会读 trace 找瓶颈。
- 在
instrumentation.ts里初始化 OTel SDK,把 trace 发到 Jaeger/Honeycomb/Datadog。 - 区分 4 类信号:traces / metrics / logs / events,知道分别什么时候用。
- 用 trace 排查 4 类常见问题:慢 page / 频繁 cache miss / Server Action 慢 / 上游 fetch 慢。
一、4 类可观测性信号
| 信号 | 数据模型 | Next.js 关注点 |
|---|---|---|
| Traces | 树形 span,记录请求经过哪些操作 | 一条请求的延迟分布 |
| Metrics | 时序数值(counter/gauge/histogram) | QPS、P99 latency、错误率、内存 |
| Logs | 结构化文本事件 | 业务日志、stack trace |
| Events | 一次性结构化记录 | deploy、feature flag 改 |
Next.js 内置 trace 能力(OpenTelemetry),其它信号需要自己接(pino、prom-client、Datadog SDK 等)。
二、Next.js 的 OTel 实现
class NextTracerImpl implements NextTracer {
private getTracerInstance(): Tracer {
return trace.getTracer('next.js', '0.0.1')
}
// ...
}
Next.js 使用标准 @opentelemetry/api 接口,意味着:
- 它不绑定到任何具体的 OTel SDK
- 你在
instrumentation.ts里 init 哪个 SDK,next.js 的 span 就发到哪里 - 不 init SDK 的话,trace 是 no-op,零开销
内置 span 命名
export enum BaseServerSpan {
handleRequest = 'BaseServer.handleRequest',
run = 'BaseServer.run',
pipe = 'BaseServer.pipe',
render = 'BaseServer.render',
renderToResponseWithComponents = 'BaseServer.renderToResponseWithComponents',
renderToHTML = 'BaseServer.renderToHTML',
// ...
}
export enum NextNodeServerSpan {
createComponentTree = 'NextNodeServer.createComponentTree',
clientComponentLoading = 'NextNodeServer.clientComponentLoading',
// ...
}
每个请求会生成一棵 span 树,大致结构:
BaseServer.handleRequest 总耗时
├── BaseServer.run 路由匹配
└── BaseServer.renderToResponseWithComponents
└── BaseServer.renderToHTML
├── NextNodeServer.createComponentTree
│ ├── NextNodeServer.getLayoutOrPageModule
│ └── NextNodeServer.clientComponentLoading
├── AppRender.renderToStream
├── AppRender.fetch (× N 次)
├── AppRender.getDynamicHTML
└── AppRender.useCache (× N 次)
记住这棵树的形状,能让你打开 Jaeger UI 一眼定位瓶颈。
启用方式
next.config.js 不需要任何配置。只要:
- 安装
@vercel/otel或手动 init OTel SDK - 在
instrumentation.ts的register()里启动 SDK
// instrumentation.ts
import { registerOTel } from "@vercel/otel";
export function register() {
registerOTel({
serviceName: "my-next-app",
// 默认走 OTLP exporter,可以发到 collector
});
}
或手写:
// instrumentation-node.ts (Node runtime only)
import { NodeSDK } from "@opentelemetry/sdk-node";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { Resource } from "@opentelemetry/resources";
import { SemanticResourceAttributes } from "@opentelemetry/semantic-conventions";
const sdk = new NodeSDK({
resource: new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: "my-next-app",
[SemanticResourceAttributes.SERVICE_VERSION]: process.env.GIT_SHA,
}),
traceExporter: new OTLPTraceExporter({
url: process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT,
}),
});
sdk.start();
三、instrumentation.ts 生命周期
// instrumentation.ts
export async function register() {
// 在 server 启动时执行一次(每个 worker 各一次)
if (process.env.NEXT_RUNTIME === 'nodejs') {
await import('./instrumentation-node')
}
if (process.env.NEXT_RUNTIME === 'edge') {
await import('./instrumentation-edge')
}
}
export const onRequestError = (...) => { /* 第 33 讲 */ }
register 执行时机:
next dev:dev server 启动时next start:每个 render worker / router server 启动时next build --debug-prerender:build 时也跑(用于 prerender 阶段抓 trace)
注意:
- 必须按 runtime 分支加载:edge runtime 没法 require
@opentelemetry/sdk-node,会 import 报错 - 用动态 import 而非 top-level import,避免边缘 bundle 被污染
四、自定义 span
业务代码可以自己加 span:
import { context, trace } from "@opentelemetry/api";
async function getProducts() {
const tracer = trace.getTracer("my-app");
return tracer.startActiveSpan("db.products.findMany", async (span) => {
try {
span.setAttribute("db.system", "postgresql");
const result = await db.products.findMany();
span.setAttribute("result.count", result.length);
return result;
} catch (err) {
span.recordException(err as Error);
span.setStatus({ code: 2, message: (err as Error).message });
throw err;
} finally {
span.end();
}
});
}
或用便捷库:
import { traced } from "@/lib/tracing";
const result = await traced(
"db.products.findMany",
{ "db.system": "postgresql" },
async () => {
return db.products.findMany();
},
);
业务 span 自动嵌套在 Next.js 的 request span 下,trace UI 一棵完整树。
五、Trace 排障实战
案例 1:某 page P99 200ms 但 P50 30ms
打开慢 trace 看 span 树。可能看到:
BaseServer.handleRequest [200ms]
├── BaseServer.run [5ms]
└── AppRender.renderToStream [195ms]
├── AppRender.fetch /api/products [180ms] ← 元凶
└── ...
定位到上游 /api/products 慢,去查上游服务。
案例 2:cache miss 多
Next.js fetch / use cache 有专门的 span:
AppRender.fetch /api/products
attributes:
http.url: ...
http.method: GET
next.span_name: AppRender.fetch
next.fetch_cache: MISS
按 next.fetch_cache=MISS 过滤,看是哪些 page / 哪些 fetch 没命中。
案例 3:Server Action 慢
HandleAction
├── HandleAction.deserialize [2ms]
├── action invocation [500ms]
│ └── db.users.update [480ms]
└── HandleAction.serialize [3ms]
定位到 db 慢 → 加索引。
案例 4:上游 fetch 慢
OTel HTTP instrumentation 自动给所有 fetch 加 span。结合 @opentelemetry/instrumentation-http,能看到完整的 outbound 请求树。
六、Trace 后端选择
| 后端 | 特点 | exporter |
|---|---|---|
| Jaeger | 开源,自托管 | OTLP / Jaeger native |
| Honeycomb | SaaS,高基数 query | OTLP |
| Datadog APM | SaaS,与 logs/metrics 整合好 | DD agent |
| Tempo + Grafana | 开源,Loki/Prometheus 整合 | OTLP |
| Vercel | Vercel 项目内置 | 自动 |
| New Relic | SaaS | OTLP |
| AWS X-Ray | AWS 原生 | X-Ray exporter |
OTLP exporter 是事实标准,写一份配置可以切换后端。
七、Logs:结构化日志
Next.js 自身用 console.log,没有内置日志库。生产建议:
// lib/logger.ts
import pino from "pino";
export const logger = pino({
level: process.env.LOG_LEVEL ?? "info",
base: {
service: "my-next-app",
env: process.env.NODE_ENV,
region: process.env.AWS_REGION,
},
redact: {
paths: [
"req.headers.cookie",
"req.headers.authorization",
"password",
"*.token",
],
censor: "[REDACTED]",
},
});
业务代码:
logger.info({ userId, orderId }, "order created");
输出(JSON Lines):
{
"level": "info",
"time": 1700000000,
"service": "my-next-app",
"userId": "u1",
"orderId": "o1",
"msg": "order created"
}
Fluentd / Vector 直接收 JSON Lines 进 ES / Loki / Datadog。
关联 trace 与 log
在每条 log 里带上 trace ID 和 span ID:
import { trace } from "@opentelemetry/api";
const span = trace.getActiveSpan();
const traceId = span?.spanContext().traceId;
const spanId = span?.spanContext().spanId;
logger.info({ traceId, spanId, userId }, "something happened");
Datadog / Honeycomb 会把这条日志关联到对应 trace,点 log 一秒跳到 trace 上下文。
八、Metrics:业务关键指标
虽然 Next.js 默认不出 metrics,但你能自己加:
import { metrics } from '@opentelemetry/api'
const meter = metrics.getMeter('my-app')
const ordersCounter = meter.createCounter('orders_total', {
description: 'Total orders placed',
})
const orderLatency = meter.createHistogram('order_latency_ms', {
description: 'Order processing latency',
unit: 'ms',
})
async function placeOrder(...) {
const t0 = Date.now()
try {
const result = await ...
ordersCounter.add(1, { status: 'success', region: 'us-east' })
orderLatency.record(Date.now() - t0, { status: 'success' })
return result
} catch (err) {
ordersCounter.add(1, { status: 'error' })
throw err
}
}
OTel SDK 会定期推到 Prometheus / Datadog / OTLP collector。
Web Vitals 上报
useReportWebVitals 收集前端性能数据:
"use client";
import { useReportWebVitals } from "next/web-vitals";
export function WebVitalsReporter() {
useReportWebVitals((metric) => {
fetch("/api/web-vitals", {
method: "POST",
body: JSON.stringify(metric),
keepalive: true,
});
});
return null;
}
后端 route handler 接收:
export async function POST(req: NextRequest) {
const metric = await req.json();
// 上报 metric 到 Prometheus / DD
return new Response(null, { status: 204 });
}
九、对接 Datadog / Vercel 的样板
Datadog
// instrumentation.ts
export async function register() {
if (process.env.NEXT_RUNTIME === "nodejs") {
// 用 dd-trace 一站式
const { tracer } = await import("dd-trace");
tracer.init({
service: "my-next-app",
env: process.env.DD_ENV,
version: process.env.GIT_SHA,
logInjection: true, // 自动给 pino log 注 traceId
runtimeMetrics: true, // CPU / heap 自动 metrics
});
}
}
Vercel
import { registerOTel } from "@vercel/otel";
export function register() {
registerOTel({ serviceName: "my-next-app" });
}
Vercel 自动把 trace 接到 Observability 面板,零配置可看。
十、Performance 监控的金字塔
┌──────────┐
│ Synthetic │ 用户体验真实模拟(Datadog Synthetic)
├──────────┤
│ RUM │ 真实用户监控(前端 web-vitals + 错误)
├──────────┤
│ APM │ 后端 traces + spans
├──────────┤
│ Logs │ 原始事件
└──────────┘
- 最上层最贵但最贴近用户
- 最下层最全但最难查
- 任何一层缺都会让排障变成"猜"
十一、配套 fixture
fixtures/lecture-34/ 提供:
instrumentation.ts:自定义 console-only tracer(无需 OTel 后端)app/slow/page.tsx:用业务 span 包了一个 800ms 慢操作app/api/web-vitals/route.ts:接收前端 web-vitals 数据lib/logger.ts:pino 风格的结构化日志(实现成 console,避免增加依赖)
启动后访问 /slow,终端打 fake trace 树,能看到每个 span 的耗时。
十二、本讲小结
- Next.js 内置 OTel tracer,无需配置就能产出 span;只需在
instrumentation.tsinit SDK 就能看 trace。 - span 命名约定记熟,能让 trace UI 一目了然。
- 4 类信号:traces / metrics / logs / events,互补使用。
- 关联 traceId 到 log,能在 log 一键跳 trace。
- 生产可观测性的金字塔:synthetic / RUM / APM / logs,缺一不可。
阶段五完结
至此阶段五"运行时与部署"全部完成。下个阶段六(第 35-38 讲)进入"性能优化与生产问题排查":
- 35 讲:Next.js 应用的性能优化清单(Bundle Size、TTFB、CLS、INP)
- 36 讲:高并发与冷启动的优化
- 37 讲:调试技巧大全(dev / Node inspector / source map / production debug)
- 38 讲:5 个真实生产案例复盘