Пошук уроків, статей та іншого контенту
Інтегруйте OpenTelemetry для трасування запитів, метрик і зв’язків між сервісами Next.js.
OpenTelemetry — це стандарт і набір SDK для збору телеметрії застосунків:
трас — послідовностей операцій під час обробки запиту;
спанів — окремих операцій усередині траси;
метрик — лічильників, гістограм і вимірювань;
контексту — інформації, яка передається між сервісами разом із запитом.
У Next.js OpenTelemetry допомагає відповісти на запитання:
скільки часу займає обробка HTTP-запиту;
який серверний компонент або Route Handler працює повільно;
де виникає помилка в ланцюжку запитів;
який зовнішній сервіс збільшує час відповіді;
скільки запитів обробляє застосунок і з якою частотою виникають помилки.
OpenTelemetry не є системою зберігання даних. Застосунок генерує телеметрію та передає її до бекенда спостережуваності, наприклад до OpenTelemetry Collector або іншої сумісної системи.
Траса складається зі спанів:
HTTP GET /api/orders
└── orders.load
├── database.query
└── fetch catalog service
└── catalog.getProductКожен спан має:
назву;
час початку та завершення;
атрибути;
статус;
інформацію про помилки;
ідентифікатор трасування та батьківського спана.
Коли Next.js виконує серверний fetch, OpenTelemetry може створити дочірній спан і передати заголовок traceparent. Якщо інший сервіс також використовує OpenTelemetry, він продовжить ту саму трасу.
У результаті запит між сервісами можна побачити як одну пов’язану структуру, а не як набір незалежних логів.
Встановіть OpenTelemetry SDK для Next.js і API для створення власних спанів та метрик:
npm install @vercel/otel @opentelemetry/api@vercel/otel спрощує інтеграцію OpenTelemetry із Next.js: він налаштовує SDK і базові інструментації для серверного застосунку.
Створіть файл instrumentation.ts у корені проєкту:
import { registerOTel } from '@vercel/otel';
export function register() {
registerOTel({
serviceName: 'orders-next-app',
});
}Якщо застосунок використовує каталог src, файл можна розмістити як src/instrumentation.ts.
Next.js викликає register під час запуску серверного середовища. Реєстрація відбувається до обробки запитів, тому автоматично створені спани можуть охоплювати життєвий цикл серверного застосунку.
Назва сервісу повинна бути стабільною. Не використовуйте як serviceName випадкове значення або ідентифікатор конкретного запиту.
Для передавання телеметрії задайте змінні середовища. Наприклад:
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
OTEL_SERVICE_NAME=orders-next-appЗначення OTEL_EXPORTER_OTLP_ENDPOINT залежить від вашого OpenTelemetry Collector або іншого OTLP-сумісного бекенда.
Можна налаштовувати окремі кінцеві точки:
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://localhost:4318/v1/traces
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT=http://localhost:4318/v1/metricsСекрети, токени та адреси production-систем не слід зберігати у файлі .env у репозиторії.
Автоматична інструментація охоплює стандартні операції, але бізнес-логіку зазвичай потрібно позначати власними спанами.
Створіть файл lib/telemetry.ts:
import { metrics, SpanStatusCode, trace } from '@opentelemetry/api';
const tracer = trace.getTracer('orders-app');
const meter = metrics.getMeter('orders-app');
export const ordersCounter = meter.createCounter('orders.processed', {
description: 'Кількість оброблених замовлень',
});
export const orderDuration = meter.createHistogram('orders.processing.duration', {
description: 'Тривалість обробки замовлення в мілісекундах',
unit: 'ms',
});
export async function processOrder(orderId: string) {
return tracer.startActiveSpan('orders.process', async (span) => {
const startedAt = performance.now();
span.setAttribute('order.id', orderId);
try {
// Імітація операції бізнес-логіки.
await new Promise((resolve) => setTimeout(resolve, 40));
const order = {
id: orderId,
status: 'processed',
};
ordersCounter.add(1, {
'order.status': order.status,
});
return order;
} catch (error) {
span.recordException(error as Error);
span.setStatus({
code: SpanStatusCode.ERROR,
message: error instanceof Error ? error.message : 'Невідома помилка',
});
throw error;
} finally {
orderDuration.record(performance.now() - startedAt);
span.end();
}
});
}startActiveSpan робить створений спан активним на час виконання callback. Це важливо: дочірні операції, зокрема інструментований fetch, зможуть отримати поточний контекст і приєднатися до правильної траси.
Атрибути повинні описувати операцію, але не містити секретів:
span.setAttribute('order.id', orderId);Не додавайте до атрибутів:
паролі;
токени;
повні тіла запитів;
персональні дані без потреби;
великі або необмежені за розміром значення.
Створіть файл app/api/orders/[id]/route.ts:
import { NextResponse } from 'next/server';
import { processOrder } from '@/lib/telemetry';
export const runtime = 'nodejs';
type RouteContext = {
params: Promise<{
id: string;
}>;
};
export async function POST(
_request: Request,
context: RouteContext,
) {
const { id } = await context.params;
if (!id) {
return NextResponse.json(
{ error: 'Ідентифікатор замовлення є обов’язковим' },
{ status: 400 },
);
}
try {
const order = await processOrder(id);
return NextResponse.json(order);
} catch {
return NextResponse.json(
{ error: 'Не вдалося обробити замовлення' },
{ status: 500 },
);
}
}Запустіть застосунок:
npm run devВиконайте запит:
curl -X POST http://localhost:3000/api/orders/42У результаті будуть доступні:
автоматичний спан HTTP-запиту;
власний спан orders.process;
метрика orders.processed;
гістограма orders.processing.duration.
Фактичне відображення телеметрії залежить від налаштованого OTLP-приймача.
Розглянемо серверну функцію, яка звертається до сервісу каталогу:
import { SpanStatusCode, trace } from '@opentelemetry/api';
const tracer = trace.getTracer('orders-app');
export async function loadProduct(productId: string) {
return tracer.startActiveSpan('catalog.getProduct', async (span) => {
span.setAttribute('product.id', productId);
try {
const catalogUrl = process.env.CATALOG_URL;
if (!catalogUrl) {
throw new Error('Змінна CATALOG_URL не налаштована');
}
const response = await fetch(
`${catalogUrl}/products/${encodeURIComponent(productId)}`,
{
headers: {
accept: 'application/json',
},
cache: 'no-store',
},
);
span.setAttribute('http.response.status_code', response.status);
if (!response.ok) {
throw new Error(`Catalog service повернув ${response.status}`);
}
return await response.json();
} catch (error) {
span.recordException(error as Error);
span.setStatus({
code: SpanStatusCode.ERROR,
message: error instanceof Error ? error.message : 'Помилка запиту',
});
throw error;
} finally {
span.end();
}
});
}Якщо інструментація fetch активна, під час запиту OpenTelemetry може:
створити спан вихідного HTTP-запиту;
визначити поточний активний контекст;
додати до запиту заголовок traceparent;
передати ідентифікатор поточної траси сервісу каталогу.
Сервіс каталогу повинен окремо використовувати OpenTelemetry та витягувати контекст із вхідних заголовків. Тоді його спани будуть дочірніми спанами тієї самої розподіленої траси.
Передавання контексту не означає передавання бізнес-даних. У запиті передається технічна інформація для зв’язування спанів.
Метрики відповідають на питання про поведінку системи за певний проміжок часу:
скільки замовлень оброблено;
скільки операцій завершилися помилкою;
яка тривалість операцій;
як змінюється навантаження.
Основні типи метрик:
Counter — значення лише збільшується, наприклад кількість запитів;
Histogram — розподіл значень, наприклад тривалість запитів;
UpDownCounter — значення може збільшуватися та зменшуватися.
Для тривалості операцій використовуйте Histogram, а не Counter.
Атрибути метрик називають мітками або dimensions. Їхня кількість повинна бути контрольованою:
ordersCounter.add(1, {
'order.status': 'processed',
});Поганим прикладом буде використання ідентифікатора користувача як атрибута метрики для мільйонів користувачів. Це створює високу cardinality — надто велику кількість унікальних часових рядів.
Для метрик зазвичай підходять обмежені набори значень:
status: success | error;
method: GET | POST;
operation: create | update | delete.
Якщо операція завершується помилкою, зафіксуйте її у спані:
try {
await operation();
} catch (error) {
span.recordException(error as Error);
span.setStatus({
code: SpanStatusCode.ERROR,
});
throw error;
} finally {
span.end();
}recordException додає інформацію про виняток, а setStatus позначає спан як помилковий.
Не ковтайте помилки лише для того, щоб трасування виглядало успішним. Якщо бізнес-логіка повинна повернути помилку клієнту, зафіксуйте її у спані та поверніть коректний HTTP-статус.
Траси та метрики доповнюють одна одну:
метрики показують, що проблема існує;
траси допомагають знайти конкретну операцію, яка її спричинила.
Наприклад, гістограма може показати зростання тривалості orders.processing.duration. За допомогою трас можна визначити, чи сповільнилася база даних, зовнішній каталог або внутрішня бізнес-логіка.
Не потрібно створювати окремий спан для кожного рядка коду. Спани повинні відповідати логічним операціям:
обробка замовлення;
запит до бази даних;
звернення до зовнішнього сервісу;
виконання суттєвої бізнес-операції.
OpenTelemetry SDK у цьому прикладі налаштовується для серверного середовища Next.js.
Власні спани та метрики з @opentelemetry/api не слід додавати у звичайні клієнтські компоненти без окремої конфігурації браузерного SDK. Серверна телеметрія та браузерна телеметрія мають різні вимоги до експортерів, безпеки й обсягу даних.
Для серверних Route Handler явно вказуйте:
export const runtime = 'nodejs';Це особливо важливо, якщо код використовує Node.js-специфічні інструментації або бібліотеки.
У production-середовищі кількість спанів може бути значною. Враховуйте:
не створюйте спани всередині великих циклів без потреби;
не записуйте великі тіла запитів і відповідей;
обмежуйте атрибути з високою cardinality;
застосовуйте sampling на рівні SDK або бекенда;
не додавайте секрети та персональні дані;
розділяйте назви сервісів для різних застосунків і середовищ.
Назви спанів повинні бути стабільними:
catalog.getProduct
orders.process
database.queryНе використовуйте значення з ідентифікатором ресурсу в самій назві:
catalog.getProduct.12345
catalog.getProduct.98765Ідентифікатор потрібно зберігати в атрибуті, якщо це дозволено політикою безпеки:
span.setAttribute('product.id', productId);instrumentation.ts повинен реєструвати SDK на етапі запуску застосунку. Не відкладайте реєстрацію до першого HTTP-запиту.
Застосунок може працювати без видимого результату, якщо експортер налаштований на адресу, де немає Collector або іншого приймача телеметрії.
Перевірте:
адресу OTLP endpoint;
порт;
протокол;
змінні середовища;
заголовки авторизації;
доступність endpoint із середовища, де запущено Next.js.
span.end()Спан потрібно завершувати навіть у разі помилки. Для цього використовуйте finally.
Якщо власний спан створено, але callback виконується поза його активним контекстом, дочірні операції можуть не пов’язатися з очікуваним спаном. Для асинхронної логіки використовуйте startActiveSpan.
Додавання до метрик унікальних ідентифікаторів користувачів, замовлень або URL створює надмірну cardinality. Такі дані краще залишати атрибутами спанів, якщо вони взагалі потрібні.
recordException автоматично перехопить помилкуOpenTelemetry не завжди знає про виняток у вашій бізнес-логіці. У власному catch потрібно явно викликати recordException і встановити статус помилки.
OpenTelemetry додає до Next.js трасування, метрики та контекст розподілених запитів.
instrumentation.ts — точка реєстрації OpenTelemetry під час запуску Next.js.
@vercel/otel спрощує базове налаштування SDK та експорту телеметрії.
@opentelemetry/api використовується для власних спанів і метрик.
startActiveSpan зберігає зв’язок між поточною операцією та дочірніми асинхронними запитами.
Заголовок traceparent дає змогу об’єднувати спани різних сервісів в одну трасу.
Counter підходить для кількості подій, а Histogram — для тривалості.
Помилки потрібно записувати через recordException, позначати статусом ERROR і завершувати спан у finally.
Атрибути мають бути корисними, обмеженими та не містити секретних даних.