Пошук уроків, статей та іншого контенту
Налаштуєте трасування, контекст запитів і експорт телеметрії для розподіленого моніторингу.
OpenTelemetry — це стандарт для збору телеметрії з застосунків. У контексті NestJS найчастіше використовують трасування:
кожен HTTP-запит отримує кореневий span;
виклики бази даних, HTTP-клієнтів та інших інструментованих бібліотек стають дочірніми span;
контекст trace автоматично передається між асинхронними операціями;
готові traces експортуються до колектора або системи спостереження.
Trace складається з одного або кількох span. Кожен span має:
traceId — ідентифікатор усього розподіленого запиту;
spanId — ідентифікатор конкретної операції;
parentSpanId — батьківський span;
назву операції;
час початку та завершення;
атрибути, події та статус.
Наприклад, один запит до NestJS може мати таку структуру:
HTTP GET /orders/42
└── OrderService.findOne
├── HTTP GET inventory-service/items/42
└── PostgreSQL SELECTДля автоматичного трасування Node.js-застосунку встановіть OpenTelemetry SDK та інструментації:
npm install \
@opentelemetry/api \
@opentelemetry/sdk-node \
@opentelemetry/auto-instrumentations-node \
@opentelemetry/exporter-trace-otlp-http \
@opentelemetry/resourcesПакети OpenTelemetry мають активно розвиватися, тому їх варто оновлювати узгоджено. Не змішуйте випадкові мажорні версії @opentelemetry/* в одному застосунку.
OpenTelemetry потрібно ініціалізувати до імпорту NestJS та бібліотек, які потрібно автоматично інструментувати. Інакше HTTP-інструментація може підключитися надто пізно.
Створіть файл src/telemetry.ts:
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { resourceFromAttributes } from '@opentelemetry/resources';
const traceExporter = new OTLPTraceExporter({
url:
process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ??
'http://localhost:4318/v1/traces',
});
const sdk = new NodeSDK({
resource: resourceFromAttributes({
'service.name': process.env.OTEL_SERVICE_NAME ?? 'orders-api',
'service.version': process.env.npm_package_version ?? '1.0.0',
'deployment.environment.name': process.env.NODE_ENV ?? 'development',
}),
traceExporter,
instrumentations: [
getNodeAutoInstrumentations({
// HTTP та Express потрібні для автоматичного трасування NestJS на Express.
'@opentelemetry/instrumentation-http': {
enabled: true,
},
'@opentelemetry/instrumentation-express': {
enabled: true,
},
}),
],
});
sdk.start();
const shutdownTelemetry = async (): Promise<void> => {
await sdk.shutdown();
};
process.once('SIGTERM', () => {
void shutdownTelemetry().finally(() => process.exit(0));
});
process.once('SIGINT', () => {
void shutdownTelemetry().finally(() => process.exit(0));
});service.name є особливо важливим атрибутом. За ним система спостереження відрізняє один сервіс від іншого.
Запускайте NestJS так, щоб файл телеметрії виконувався першим. Для TypeScript-проєкту з компіляцією у dist це можна зробити через окремий файл src/main.ts:
import './telemetry';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap(): Promise<void> {
const app = await NestFactory.create(AppModule);
await app.listen(process.env.PORT ?? 3000);
}
void bootstrap();Імпорт ./telemetry має бути перед імпортом AppModule, щоб SDK встиг підключити інструментацію до створення NestJS-застосунку.
У прикладі використано OTLPTraceExporter через HTTP. За замовчуванням він надсилає traces на:
http://localhost:4318/v1/tracesЦе endpoint OpenTelemetry Collector або іншої системи, яка підтримує OTLP/HTTP.
Адресу можна змінити змінною середовища:
OTEL_SERVICE_NAME=orders-api \
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://otel-collector:4318/v1/traces \
node dist/main.jsСам NestJS не є системою зберігання телеметрії. Його процес створює spans та передає їх експортеру. Колектор або backend відповідає за приймання, зберігання, пошук і візуалізацію traces.
Під час виклику sdk.shutdown() SDK намагається передати накопичені spans експортеру. Якщо процес завершити негайно, останні traces можуть не дійти до колектора.
Тому обробники SIGTERM і SIGINT потрібні особливо в контейнеризованих середовищах.
getNodeAutoInstrumentations() підключає інструментацію популярних Node.js-модулів. Для NestJS на Express важливими є:
http — створює server span для вхідного HTTP-запиту;
express — додає інформацію про middleware та маршрути;
HTTP-клієнти — створюють client span для вихідних запитів.
Після запуску застосунку запит:
curl http://localhost:3000/orders/42може створити server span із назвою на кшталт:
GETабо маршрутом, визначеним Express-інструментацією.
Точний формат назви залежить від версій інструментації та адаптера NestJS. Не слід покладатися на назву span як на стабільний контракт. Для власних операцій використовуйте явні назви.
Щоб пов’язати spans різних сервісів, OpenTelemetry використовує propagation. Для HTTP зазвичай застосовується W3C Trace Context із заголовками:
traceparent
tracestateКоли сервіс отримує HTTP-запит із traceparent, HTTP-інструментація:
зчитує trace-контекст із заголовків;
створює server span;
робить його активним у поточному асинхронному контексті;
дозволяє дочірнім spans успадкувати той самий traceId.
Під час вихідного HTTP-запиту інструментація додає контекст до заголовків клієнтського запиту. Інший сервіс може продовжити той самий trace.
Таким чином, контекст не потрібно вручну передавати через кожен метод:
await this.inventoryClient.getItem(id);Якщо HTTP-клієнт підтримується автоматичною інструментацією, дочірній span отримає правильного батька.
Контекст OpenTelemetry — це не об’єкт NestJS
Request. Він зберігається в асинхронному контексті Node.js і доступний через API OpenTelemetry.
Автоматична інструментація показує мережеві операції, але для бізнес-операцій часто потрібні власні spans. Наприклад, можна окремо виміряти виконання OrderService.findOne.
Створіть interceptor:
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import {
context as otelContext,
SpanStatusCode,
trace,
} from '@opentelemetry/api';
import { defer, Observable } from 'rxjs';
import { finalize, tap } from 'rxjs/operators';
@Injectable()
export class TelemetryInterceptor implements NestInterceptor {
private readonly tracer = trace.getTracer('orders-api');
intercept(
executionContext: ExecutionContext,
next: CallHandler,
): Observable<unknown> {
const className = executionContext.getClass().name;
const handlerName = executionContext.getHandler().name;
const spanName = `${className}.${handlerName}`;
return defer(() => {
const activeContext = otelContext.active();
return this.tracer.startActiveSpan(
spanName,
{},
activeContext,
(span) => {
span.setAttribute('nest.class', className);
span.setAttribute('nest.handler', handlerName);
const spanContext = trace.setSpan(activeContext, span);
return otelContext
.with(spanContext, () => next.handle())
.pipe(
tap({
error: (error: unknown) => {
const exception =
error instanceof Error
? error
: new Error(String(error));
span.recordException(exception);
span.setStatus({
code: SpanStatusCode.ERROR,
message: exception.message,
});
},
}),
finalize(() => {
span.end();
}),
);
},
);
});
}
}У цьому interceptor важливі кілька деталей:
startActiveSpan() створює span і робить його активним у callback;
defer() відкладає створення span до моменту підписки на Observable;
context.with() явно встановлює контекст для обробки запиту;
recordException() записує помилку в span;
finalize() викликається і для успішного завершення, і для помилки, тому span не залишиться відкритим.
Щоб застосувати interceptor до всіх контролерів, зареєструйте його в AppModule:
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { TelemetryInterceptor } from './telemetry.interceptor';
import { OrdersController } from './orders.controller';
import { OrdersService } from './orders.service';
@Module({
controllers: [OrdersController],
providers: [
OrdersService,
{
provide: APP_INTERCEPTOR,
useClass: TelemetryInterceptor,
},
],
})
export class AppModule {}Приклад контролера та сервісу:
import { Controller, Get, Param } from '@nestjs/common';
import { Injectable } from '@nestjs/common';
@Injectable()
export class OrdersService {
async findOne(id: string): Promise<{ id: string; status: string }> {
// У реальному застосунку тут може бути запит до бази даних.
return {
id,
status: 'created',
};
}
}
@Controller('orders')
export class OrdersController {
constructor(private readonly ordersService: OrdersService) {}
@Get(':id')
findOne(@Param('id') id: string): Promise<{ id: string; status: string }> {
return this.ordersService.findOne(id);
}
}Для запиту GET /orders/42 буде створено:
автоматичний HTTP server span;
власний span OrdersController.findOne;
дочірні spans для інструментованих зовнішніх операцій, якщо вони виконуються під час обробки запиту.
Іноді сервісу потрібно додати до поточного span бізнес-атрибути. Для цього можна отримати активний span:
import { Injectable } from '@nestjs/common';
import { trace } from '@opentelemetry/api';
@Injectable()
export class PaymentService {
private readonly tracer = trace.getTracer('orders-api');
async charge(orderId: string, amount: number): Promise<void> {
const span = trace.getActiveSpan();
span?.setAttribute('order.id', orderId);
span?.setAttribute('payment.amount', amount);
await this.processPayment(orderId, amount);
}
private async processPayment(
orderId: string,
amount: number,
): Promise<void> {
const span = this.tracer.startSpan('PaymentService.processPayment');
try {
span.setAttribute('order.id', orderId);
span.setAttribute('payment.amount', amount);
// Тут виконується операція оплати.
await Promise.resolve();
} finally {
span.end();
}
}
}trace.getActiveSpan() повертає span, який активний у поточному асинхронному контексті. Якщо метод викликається поза HTTP-запитом, активного span може не бути, тому результат потрібно перевіряти.
Для створення дочірнього span у звичайному асинхронному коді краще використовувати startActiveSpan():
import { Injectable } from '@nestjs/common';
import { SpanStatusCode, trace } from '@opentelemetry/api';
@Injectable()
export class InventoryService {
private readonly tracer = trace.getTracer('orders-api');
async reserve(itemId: string): Promise<void> {
return this.tracer.startActiveSpan(
'InventoryService.reserve',
async (span) => {
try {
span.setAttribute('inventory.item_id', itemId);
// Виконання операції резервування.
await Promise.resolve();
} catch (error) {
const exception =
error instanceof Error ? error : new Error(String(error));
span.recordException(exception);
span.setStatus({
code: SpanStatusCode.ERROR,
message: exception.message,
});
throw error;
} finally {
span.end();
}
},
);
}
}Не створюйте span для кожного незначного рядка коду. Span має описувати вимірювану операцію: звернення до зовнішнього сервісу, важливу бізнес-операцію або повільну ділянку виконання.
Атрибути допомагають фільтрувати та аналізувати traces:
span.setAttribute('order.id', orderId);
span.setAttribute('order.item_count', itemCount);
span.setAttribute('order.payment_method', 'card');Не записуйте до span:
паролі та токени;
повні HTTP-заголовки;
номери банківських карток;
персональні дані без необхідності;
великі JSON-документи;
повний текст SQL-запитів із конфіденційними значеннями.
Для ідентифікаторів користувачів використовуйте внутрішні або хешовані значення, якщо це відповідає вимогам безпеки та приватності.
У production-середовищі не завжди потрібно експортувати кожен trace. Високий обсяг трафіку може призвести до:
зайвого навантаження на застосунок;
великих витрат на зберігання;
перевантаження колектора.
Для цього використовують sampling. Конкретну стратегію можна налаштувати через OpenTelemetry SDK та змінні середовища, сумісні з вашим стеком. Під час налагодження зручно зберігати більшу частку traces, а в production — застосовувати контрольовану вибірку.
Важливі або помилкові traces часто обробляють окремо на рівні колектора або backend-системи.
Після запуску застосунку виконайте запит:
curl -i http://localhost:3000/orders/42Перевірте:
чи працює endpoint;
чи немає помилок експорту в логах;
чи приймає колектор запити на OTLP endpoint;
чи з’явився сервіс orders-api у системі спостереження;
чи містить trace HTTP span і власний NestJS span.
Якщо колектора немає, експортер не зможе доставити traces. У такому випадку для локальної перевірки можна тимчасово використати консольний експортер замість OTLP:
import { ConsoleSpanExporter } from '@opentelemetry/sdk-trace-node';
const traceExporter = new ConsoleSpanExporter();Цей варіант призначений для локального налагодження, оскільки виводить spans у stdout і не підходить для production.
Якщо SDK запускається після імпорту AppModule, частина бібліотек уже завантажена без інструментації. Внаслідок цього HTTP spans можуть не створюватися.
Ініціалізуйте телеметрію на початку entrypoint-файлу.
span.end()Span, який не завершено, не буде коректно експортовано. Використовуйте finally або finalize():
try {
await operation();
} finally {
span.end();
}Надмірна деталізація збільшує кількість даних і погіршує продуктивність. Створюйте spans для операцій, які допомагають пояснити затримки або помилки.
traceId через аргументиЗазвичай не потрібно додавати traceId до кожного методу. Використовуйте активний контекст OpenTelemetry. Ручне передавання часто призводить до розсинхронізації та зайвого зв’язування бізнес-коду з телеметрією.
Для OTLP через HTTP потрібен endpoint, сумісний із OTLP/HTTP, зазвичай із шляхом:
/v1/tracesEndpoint для OTLP/gRPC і endpoint для OTLP/HTTP — це різні налаштування.
Не додавайте в атрибути секрети, токени або повні об’єкти запитів. Телеметрія часто доступна ширшому колу інженерів, ніж production-база даних.
OpenTelemetry додає NestJS трасування, сумісне з розподіленими системами моніторингу.
NodeSDK потрібно запускати до імпорту NestJS та інструментованих бібліотек.
Автоматичні інструментації створюють HTTP spans і підтримують передавання контексту між сервісами.
startActiveSpan() використовується для власних бізнес-операцій.
trace.getActiveSpan() дає доступ до поточного span у межах асинхронного контексту.
finalize() або finally гарантує завершення span навіть у разі помилки.
OTLP exporter передає traces до колектора або backend-системи.
Атрибути мають бути корисними для діагностики, але не повинні містити конфіденційні дані.