В распределенных fullstack-приложениях изолированный мониторинг отдельных сервисов (только бэкенд или только клиентский APM) перестает работать. Когда пользователь инициирует действие в веб-интерфейсе, запрос проходит многослойный путь: клиентский браузер, CDN / Edge Proxy, серверный Middleware, маршрутизатор App Router, дерево React Server Components (RSC), сериализацию потока данных Flight, Server Actions и внешние микросервисы баз данных.
Потеря контекста между клиентом и сервером делает невозможной локализацию деградации задержек (TTFB, High P99 Latency). Архитектура OpenTelemetry (OTel) в Next.js 16 решает задачу сквозной наблюдаемости (End-to-End Distributed Tracing).
┌─────────────────────────────────────────────────────────────────────────────┐
│ DISTRIBUTED TRACE CONTEXT PROPAGATION │
│ │
│ [Browser: Click Event] ──► Span: "client-click" (traceparent header) │
│ │ │
│ ▼ │
│ [Next.js Server: Node.js] ─► Span: "middleware" │
│ │ │
│ ▼ │
│ Span: "rsc-render /dashboard" │
│ │ │
│ ▼ │
│ Span: "flight-serialization" │
│ │ │
│ ▼ │
│ [Downstream: Postgres / API] Span: "db-query: SELECT * FROM orders" │
└─────────────────────────────────────────────────────────────────────────────┘Next.js 16 предоставляет нативную точку расширения среды исполнения — файл instrumentation.ts с хуком register() для серверных сред (Node.js и Edge), а также клиентскую точку instrumentation-client.ts. Это позволяет связать контекст W3C Trace Context (traceparent и tracestate) воедино от клика пользователя до выполнения SQL-запроса в базе данных.
Ментальная модель сквозной трассировки строится на дереве спанов (Trace Tree), объединенных единым TraceId. Каждый спан (Span) фиксирует конкретную операцию, её продолжительность, атрибуты и родительский спан (ParentSpanId).
TraceId и связывает его с клиентским спаном.traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01БРАУЗЕР (Client Tracing)
└── [Span: "user-navigate-to-analytics"] (trace_id: 4bf92f35...)
│
├── Внедрение HTTP-заголовка traceparent
│
▼
СЕРВЕР (Server Tracing: instrumentation.ts)
└── [Span: "next.request: GET /analytics"] (parent_id: user-navigate...)
├── [Span: "middleware.execute"]
├── [Span: "rsc.render: Page"]
│ └── [Span: "db.query: aggregate_stats"]
└── [Span: "rsc.flight_payload_stream"]
└── (Потоковая отправка чанков Flight в сеть)Хук register() в instrumentation.ts конфигурирует TracerProvider, батч-процессоры и OTLP-экспортеры.
instrumentation-client.ts начинает корневой спан и прокидывает заголовок traceparent в fetch-запрос.
Next.js генерирует спаны рендеринга серверных компонентов и этапов сериализации виртуального дерева.
При падении компонента хук логирует ошибку, сопоставляя Error Digest со спаном трассировки.
Пакетная передача спанов по протоколу gRPC / HTTP (Protobuf) в OpenTelemetry Collector, Jaeger или Datadog.
Браузер отправляет запрос к Server Action. В Jaeger отображаются две независимые разрозненные трассы: одна для клика в браузере, вторая — для серверного исполнения Server Action. В чем фундаментальная причина разрыва графа наблюдаемости?
В моменты пиковых нагрузок (распродажи, Black Friday) время ответа чекаута может вырасти с 80 мс до 4 секунд. Сквозная трассировка позволяет инженеру открыть конкретный трейс и моментально увидеть: задержка вызвана не рендерингом React, а блокировкой пула соединений в базе данных на спане pg.pool.connect или холодным стартом серверной функции.
В продакшене Next.js скрывает детали серверных исключений от пользователя, генерируя Error: Digest 283910283. Благодаря хуку onRequestError этот digest автоматически прикрепляется как атрибут next.error.digest к текущему спану в Jaeger / Datadog. Дежурный инженер по номеру digest из тикета пользователя моментально находит стек вызовов, параметры запроса и ID сессии.
При потоковом рендеринге (Streaming SSR) компоненты, завернутые в <Suspense>, разрешаются асинхронно. Трассировка спанов RSC позволяет визуализировать сетевые водопады: например, когда компонент Reviews начинает обращение к API только после полного завершения запроса компонента UserProfile.
Финансовые и банковские платформы обязаны логировать путь каждого платежного запроса через границы микросервисов. Экспорт OTLP в централизованный OpenTelemetry Collector гарантирует вендор-независимость: телеметрия одновременно дублируется в защищенное хранилище аудита (ClickHouse) и в оперативный мониторинг Datadog без изменения бизнес-кода.
Файл instrumentation.ts располагается в корне проекта (или внутри каталога src/). Хук register() выполняется один раз при старте инстанса Next.js в среде Node.js или Edge.
import { registerOTel } from '@vercel/otel';
import { diag, DiagConsoleLogger, DiagLogLevel } from '@opentelemetry/api';
export async function register() {
// Настройка внутреннего логирования OTel для отладки
if (process.env.NODE_ENV === 'development') {
diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.INFO);
}
// Регистрация нативной обвязки OpenTelemetry для Next.js
registerOTel({
serviceName: 'enterprise-nextjs-app',
attributes: {
'deployment.environment': process.env.NODE_ENV || 'production',
'service.version': process.env.APP_VERSION || '16.0.0',
},
});
}
// Нативный перехватчик серверных ошибок Next.js 16
export async function onRequestError(
err: { digest: string } & Error,
request: {
path: string;
method: string;
headers: { [key: string]: string };
},
context: {
routerKind: 'App Router' | 'Pages Router';
routeType: 'render' | 'route' | 'action' | 'middleware';
renderSource: 'server' | 'edge';
}
) {
const { trace, context: otelContext } = await import('@opentelemetry/api');
const activeSpan = trace.getSpan(otelContext.active());
const errorPayload = {
message: err.message,
name: err.name,
digest: err.digest,
stack: err.stack,
path: request.path,
method: request.method,
routeType: context.routeType,
renderSource: context.renderSource,
};
if (activeSpan) {
activeSpan.recordException(err);
activeSpan.setAttribute('error', true);
activeSpan.setAttribute('next.error.digest', err.digest || 'unknown');
activeSpan.setAttribute('http.target', request.path);
activeSpan.setAttribute('http.method', request.method);
activeSpan.setAttribute('next.route_type', context.routeType);
}
// Централизованный структурированный вывод в stdout для log-агентов
console.error('[CRITICAL_SERVER_ERROR]', JSON.stringify(errorPayload));
}Для связывания браузерных действий с серверными трейсами в Next.js 16 настраивается клиентская инструментация, автоматически перехватывающая вызовы fetch и прокидывающая контекст.
import { WebTracerProvider } from '@opentelemetry/sdk-trace-web';
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { ZoneContextManager } from '@opentelemetry/context-zone';
import { registerInstrumentations } from '@opentelemetry/instrumentation';
import { FetchInstrumentation } from '@opentelemetry/instrumentation-fetch';
import { B3Propagator } from '@opentelemetry/propagator-b3';
import { W3CTraceContextPropagator } from '@opentelemetry/core';
import { CompositePropagator } from '@opentelemetry/core';
// Инициализация клиентского TracerProvider
const exporter = new OTLPTraceExporter({
url: process.env.NEXT_PUBLIC_OTEL_EXPORTER_OTLP_ENDPOINT || '/api/otlp/v1/traces',
headers: {},
});
const provider = new WebTracerProvider({
spanProcessors: [
new BatchSpanProcessor(exporter, {
maxQueueSize: 500,
scheduledDelayMillis: 1000,
}),
],
});
provider.register({
contextManager: new ZoneContextManager(),
propagator: new CompositePropagator({
propagators: [new W3CTraceContextPropagator(), new B3Propagator()],
}),
});
// Автоматический перехват исходящих браузерных HTTP-запросов
registerInstrumentations({
instrumentations: [
new FetchInstrumentation({
propagateTraceHeaderCorsUrls: [
new RegExp(window.location.host),
/api\.internal\.enterprise\.com/,
],
clearTimingResources: true,
}),
],
});Пользователь активирует форму, instrumentation-client генерирует traceparent заголовок с флагом sampled=01.
Связывает клик мыши со сетевым вызовом.
Серверный рантайм парсит HTTP-заголовки, привязывает AsyncLocalStorage Node.js к родительскому TraceId.
Все вложенные вызовы баз данных становятся дочерними спанами.
Генерация специализированных спанов next.rsc.render и next.action с замером времени передачи первого чанка.
Позволяет отслеживать время задержки сериализации.
Батч-процессор асинхронно отправляет структурированные спаны в коллектор без блокировки потоков рендера.
Поддерживает экспорт в Datadog, Jaeger, Grafana Tempo.
В серверном компоненте выполняется параллельный запрос к двум микросервисам через Promise.all. В интерфейсе трейсинга Jaeger один из запросов отображается внутри спана страницы, а второй неожиданно отделился в самостоятельный спан без родителя. В чем причина потери связи контекста?
Попытка импортировать @opentelemetry/sdk-node или плагины авто-инструментации Node.js (@opentelemetry/auto-instrumentations-node) в клиентский файл instrumentation-client.ts приводит к фатальному сбою сборки Webpack/Turbopack из-за обращения к модулям fs, net, child_process. На клиенте разрешено использовать только легковесные пакеты @opentelemetry/sdk-trace-web и @opentelemetry/instrumentation-fetch.
Хук onRequestError выполняется в критическом пути обработки серверного сбоя. Попытка синхронно отправлять сетевые HTTP-запросы в сторонние API внутри этого хука без асинхронного таймаута замораживает поток обработки запроса и многократно увеличивает задержку ответа клиенту. Экспорт данных телеметрии должен выполняться строго асинхронно через буферизированный BatchSpanProcessor.
При использовании клиентской авто-инструментации FetchInstrumentation браузер по умолчанию не добавляет заголовок traceparent к кросс-доменным запросам из соображений безопасности. Если микросервисы находятся на других поддоменах (api.domain.com), необходимо явно объявлять их в массиве propagateTraceHeaderCorsUrls, иначе серверные спаны не смогут объединиться с клиентской сессией.
Запись необработанных заголовков запроса (request.headers) или тел ответов в атрибуты спанов OTel приводит к попаданию токенов авторизации (Bearer JWT), Cookie сессий и персональных данных (PII) в открытые хранилища Jaeger/Datadog. Всегда фильтруйте чувствительные ключи перед вызовом span.setAttribute().