Пошук уроків, статей та іншого контенту
Збиратимете технічні й бізнес-метрики та підготуєте їх для спостереження в production.
Метрики — це числові значення, які застосунок регулярно збирає для подальшого аналізу. Вони допомагають відповідати на запитання:
скільки запитів обробляє застосунок;
скільки часу займає обробка запитів;
як часто виникають помилки;
скільки замовлень створюють користувачі;
чи змінюється бізнес-активність після релізу.
Метрики особливо корисні в production, де перегляд логів кожного запиту не дає повної картини. Замість цього можна спостерігати за часовими рядами та налаштовувати сповіщення.
У NestJS метрики зазвичай публікують через HTTP-ендпойнт, наприклад GET /metrics. Система моніторингу періодично виконує цей запит і зберігає отримані значення.
Для Prometheus та сумісних систем найчастіше використовують такі типи.
Counter — лічильник, який тільки збільшується.
Приклади:
кількість HTTP-запитів;
кількість помилок;
кількість створених замовлень;
кількість відправлених повідомлень.
Значення лічильника може початися з нуля після перезапуску процесу. Це нормально: під час аналізу використовують швидкість зміни значення, а не саме абсолютне число.
Gauge може як збільшуватися, так і зменшуватися.
Приклади:
кількість активних з’єднань;
розмір черги;
кількість процесів, що виконуються;
використання пам’яті.
Histogram розподіляє значення за діапазонами. Найчастіше його використовують для вимірювання тривалості операцій або розміру відповіді.
Приклади:
тривалість HTTP-запитів;
час виконання SQL-запиту;
сума замовлення;
розмір HTTP-відповіді.
Histogram дає змогу обчислювати перцентилі, наприклад p95: час, швидшим за який виконуються 95% запитів.
Для роботи з метриками використаємо пакет prom-client:
npm install prom-clientПакет надає:
готовий реєстр метрик;
стандартні метрики Node.js;
класи Counter, Gauge і Histogram;
формат експорту, сумісний із Prometheus.
Створимо окремий сервіс, який володіє реєстром метрик. Реєстр потрібен для централізованого зберігання та експорту всіх метрик застосунку.
// src/metrics/metrics.service.ts
import { Injectable } from '@nestjs/common';
import {
Counter,
Histogram,
Registry,
collectDefaultMetrics,
} from 'prom-client';
@Injectable()
export class MetricsService {
readonly registry = new Registry();
readonly httpRequestsTotal = new Counter({
name: 'shop_http_requests_total',
help: 'Загальна кількість HTTP-запитів',
labelNames: ['method', 'route', 'status_code'] as const,
registers: [this.registry],
});
readonly httpRequestDuration = new Histogram({
name: 'shop_http_request_duration_seconds',
help: 'Тривалість обробки HTTP-запитів у секундах',
labelNames: ['method', 'route', 'status_code'] as const,
buckets: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2, 5],
registers: [this.registry],
});
readonly ordersCreatedTotal = new Counter({
name: 'shop_orders_created_total',
help: 'Загальна кількість створених замовлень',
registers: [this.registry],
});
readonly orderValueUah = new Histogram({
name: 'shop_order_value_uah',
help: 'Розподіл сум створених замовлень у гривнях',
buckets: [100, 250, 500, 1000, 2500, 5000, 10000],
registers: [this.registry],
});
constructor() {
// Додаємо стандартні метрики процесу Node.js до цього реєстру.
collectDefaultMetrics({
register: this.registry,
prefix: 'shop_',
});
}
get contentType(): string {
return this.registry.contentType;
}
getMetrics(): Promise<string> {
return this.registry.metrics();
}
}Важливо створювати метрики один раз у singleton-сервісі. Якщо створювати новий Counter для кожного запиту, застосунок швидко отримає дублікати метрик або помилки реєстрації.
// src/metrics/metrics.module.ts
import { Module } from '@nestjs/common';
import { MetricsService } from './metrics.service';
@Module({
providers: [MetricsService],
exports: [MetricsService],
})
export class MetricsModule {}Сервіс експортується, щоб його могли використовувати інтерсептор і бізнес-сервіси.
Створимо ендпойнт GET /metrics. Він повертає весь реєстр у текстовому форматі.
// src/metrics/metrics.controller.ts
import { Controller, Get, Res } from '@nestjs/common';
import { Response } from 'express';
import { MetricsService } from './metrics.service';
@Controller('metrics')
export class MetricsController {
constructor(private readonly metricsService: MetricsService) {}
@Get()
async getMetrics(@Res() response: Response): Promise<void> {
response.setHeader(
'Content-Type',
this.metricsService.contentType,
);
response.end(await this.metricsService.getMetrics());
}
}Додамо контролер до модуля:
// src/metrics/metrics.module.ts
import { Module } from '@nestjs/common';
import { MetricsController } from './metrics.controller';
import { MetricsService } from './metrics.service';
@Module({
controllers: [MetricsController],
providers: [MetricsService],
exports: [MetricsService],
})
export class MetricsModule {}Після запуску застосунку запит:
curl http://localhost:3000/metricsповерне стандартні метрики процесу Node.js та створені нами метрики.
Метрики HTTP-запитів зручно збирати в інтерсепторі. Він виконується для різних контролерів централізовано, тому не потрібно вручну додавати код у кожен метод.
// src/metrics/http-metrics.interceptor.ts
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Request, Response } from 'express';
import { Observable } from 'rxjs';
import { finalize } from 'rxjs/operators';
import { MetricsService } from './metrics.service';
@Injectable()
export class HttpMetricsInterceptor implements NestInterceptor {
constructor(private readonly metricsService: MetricsService) {}
intercept(
context: ExecutionContext,
next: CallHandler,
): Observable<unknown> {
const request = context.switchToHttp().getRequest<Request>();
const response = context.switchToHttp().getResponse<Response>();
const startedAt = process.hrtime.bigint();
const method = request.method;
// Шаблон маршруту не містить конкретних ідентифікаторів ресурсів.
const route = request.route?.path ?? 'unknown';
return next.handle().pipe(
finalize(() => {
const durationSeconds =
Number(process.hrtime.bigint() - startedAt) / 1_000_000_000;
const statusCode = String(response.statusCode);
this.metricsService.httpRequestsTotal.inc({
method,
route,
status_code: statusCode,
});
this.metricsService.httpRequestDuration.observe(
{
method,
route,
status_code: statusCode,
},
durationSeconds,
);
}),
);
}
}finalize виконується після завершення Observable як для успішної, так і для неуспішної обробки запиту. Тому інтерсептор вимірює також запити, які завершилися помилкою.
Підключимо інтерсептор глобально:
// src/app.module.ts
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { MetricsModule } from './metrics/metrics.module';
import { HttpMetricsInterceptor } from './metrics/http-metrics.interceptor';
import { OrdersController } from './orders/orders.controller';
import { OrdersService } from './orders/orders.service';
@Module({
imports: [MetricsModule],
controllers: [OrdersController],
providers: [
OrdersService,
{
provide: APP_INTERCEPTOR,
useClass: HttpMetricsInterceptor,
},
],
})
export class AppModule {}Після цього для кожного HTTP-запиту з’являться значення на кшталт:
shop_http_requests_total{method="GET",route="/orders",status_code="200"} 12та histogram із суфіксами _bucket, _sum і _count.
Технічних метрик недостатньо. Застосунок може працювати без помилок, але бізнес-показники можуть погіршуватися. Наприклад, HTTP-запити успішні, але користувачі перестали створювати замовлення.
Бізнес-метрику потрібно оновлювати в тому місці, де справді відбулася бізнес-операція. Для створення замовлення це має бути сервіс домену, а не загальний HTTP-інтерсептор.
// src/orders/orders.service.ts
import { Injectable } from '@nestjs/common';
import { randomUUID } from 'node:crypto';
import { MetricsService } from '../metrics/metrics.service';
@Injectable()
export class OrdersService {
constructor(private readonly metricsService: MetricsService) {}
create(totalUah: number) {
const order = {
id: randomUUID(),
totalUah,
createdAt: new Date().toISOString(),
};
// Операція вважається успішною після створення замовлення.
this.metricsService.ordersCreatedTotal.inc();
this.metricsService.orderValueUah.observe(totalUah);
return order;
}
}Для прикладу додамо контролер:
// src/orders/orders.controller.ts
import { Body, Controller, Post } from '@nestjs/common';
import { OrdersService } from './orders.service';
interface CreateOrderBody {
totalUah: number;
}
@Controller('orders')
export class OrdersController {
constructor(private readonly ordersService: OrdersService) {}
@Post()
create(@Body() body: CreateOrderBody) {
return this.ordersService.create(body.totalUah);
}
}Тепер запит:
curl -X POST http://localhost:3000/orders \
-H "Content-Type: application/json" \
-d '{"totalUah":1250}'збільшить shop_orders_created_total і додасть суму замовлення до shop_order_value_uah.
У реальному застосунку перед оновленням бізнес-метрики потрібно переконатися, що операція справді завершилася успішно. Якщо замовлення зберігається в базі даних, лічильник слід збільшувати після успішного збереження, а не перед ним.
Нижче наведено мінімальний набір файлів для NestJS-застосунку з HTTP- і бізнес-метриками.
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();// src/app.module.ts
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { MetricsModule } from './metrics/metrics.module';
import { HttpMetricsInterceptor } from './metrics/http-metrics.interceptor';
import { OrdersController } from './orders/orders.controller';
import { OrdersService } from './orders/orders.service';
@Module({
imports: [MetricsModule],
controllers: [OrdersController],
providers: [
OrdersService,
{
provide: APP_INTERCEPTOR,
useClass: HttpMetricsInterceptor,
},
],
})
export class AppModule {}Після запуску можна перевірити результат:
npm run start:devcurl http://localhost:3000/metricscurl -X POST http://localhost:3000/orders \
-H "Content-Type: application/json" \
-d '{"totalUah":1250}'Мітки дають змогу розділяти одну метрику на групи. У HTTP-метриці ми використовуємо:
method;
route;
status_code.
Завдяки цьому можна окремо аналізувати GET /orders і POST /orders, а також успішні та невдалі відповіді.
Мітки повинні мати обмежену кількість можливих значень. Не варто використовувати як мітку:
user_id;
order_id;
повний URL із query-параметрами;
текст помилки;
IP-адресу користувача.
Наприклад, такі маршрути створять зайву кількість часових рядів:
/orders/1
/orders/2
/orders/3Замість цього маршрут потрібно нормалізувати до шаблону:
/orders/:idВисока кардинальність збільшує споживання пам’яті та навантаження на систему моніторингу. Ідентифікатори окремих користувачів або замовлень краще шукати в логах чи трасуванні, а не в мітках метрик.
Ендпойнт /metrics зазвичай не призначений для публічних клієнтів. У production потрібно:
дозволити доступ до нього лише системі моніторингу;
не додавати секрети та персональні дані до значень метрик;
перевірити, що ендпойнт не доступний безпосередньо з Інтернету;
контролювати кількість міток;
визначити однакові імена метрик для всіх інстансів застосунку.
Приклад конфігурації збору для Prometheus:
scrape_configs:
- job_name: shop-api
metrics_path: /metrics
static_configs:
- targets:
- api-1:3000
- api-2:3000Якщо застосунок запущений у кількох процесах або контейнерах, кожен процес має власний лічильник. Це очікувана поведінка. Під час аналізу дані потрібно агрегувати між інстансами.
Приклади запитів для аналізу:
sum(rate(shop_http_requests_total[5m]))Загальна кількість HTTP-запитів за секунду.
sum(rate(shop_http_requests_total{status_code=~"5.."}[5m]))
/
sum(rate(shop_http_requests_total[5m]))Частка відповідей із кодами 5xx.
histogram_quantile(
0.95,
sum by (le) (
rate(shop_http_request_duration_seconds_bucket[5m])
)
)95-й перцентиль тривалості HTTP-запитів.
sum(rate(shop_orders_created_total[5m]))Швидкість створення замовлень за останні п’ять хвилин.
Якщо збільшити orders_created_total перед записом у базу даних, а запис завершиться помилкою, метрика покаже замовлення, якого фактично не було.
Оновлюйте бізнес-лічильник після успішного завершення операції.
URL може містити ідентифікатори та довільні query-параметри. Це призводить до великої кількості унікальних часових рядів.
Використовуйте шаблон маршруту контролера.
Метрики потрібно створювати під час ініціалізації застосунку. Створення нового Counter або Histogram для кожного запиту спричинить дублювання та витік ресурсів.
Якщо збирати метрики лише в успішній гілці Observable, помилкові запити не потраплять до лічильника. Для вимірювання і успішних, і невдалих запитів використовуйте finalize.
Не кожне внутрішнє значення потрібно експортувати. Метрика має допомагати відповісти на конкретне питання моніторингу. Варто почати з:
кількості запитів;
тривалості запитів;
частки помилок;
ключових бізнес-операцій.
Низька затримка та відсутність помилок не гарантують, що продукт працює успішно. Додавайте метрики завершених бізнес-операцій: створених замовлень, успішних оплат або інших дій, важливих для конкретного застосунку.
Метрики — це числові часові ряди для спостереження за застосунком.
Counter використовується для операцій, які накопичуються.
Gauge підходить для значень, що збільшуються та зменшуються.
Histogram допомагає аналізувати розподіл тривалості або числових значень.
У NestJS метрики можна централізовано збирати через інтерсептор.
Ендпойнт /metrics повертає реєстр у форматі, який може зчитувати Prometheus.
Бізнес-метрики потрібно оновлювати в доменних сервісах після успішного виконання операцій.
Мітки не повинні містити ідентифікатори з необмеженою кількістю значень.
У production потрібно захистити ендпойнт метрик і враховувати роботу кількох інстансів застосунку.