Представьте: сервис лежит в два часа ночи, телефон звонит. Что случилось? Если у сервиса нет логов, метрик и трейсов — вы будете гадать. Наблюдаемость — это способность ответить на вопрос «что сейчас происходит с сервисом?» без угадывания.
Три опоры наблюдаемости: логи — что произошло, трейсы — где и сколько ушло времени, метрики — сводная картина в числах. Плюс health-checks — машинный сигнал «жив / готов к трафику» для оркестратора (Kubernetes, Docker Swarm).
Зачем вообще это нужно
Пока сервис один и живёт на одном сервере, console.log в терминале кажется достаточным. Стоит сервису оказаться в контейнере под Kubernetes с несколькими репликами — и всё ломается:
- логи из трёх реплик вперемешку: непонятно, какой запрос к какой реплике попал;
- нет способа ответить «сколько у нас сейчас ошибок в секунду?»;
- сбой в середине цепочки вызовов не видно без трейсов.
Наблюдаемость — это инфраструктура диагностики, которую закладывают заранее.
Логи: от console.log к структурным событиям
NestJS поставляется с встроенным Logger. Он пишет в консоль с уровнями (log, warn, error, debug) и прокидывает контекст (обычно — имя класса):
import { Injectable, Logger } from '@nestjs/common';
@Injectable()
export class OrderService {
private readonly logger = new Logger(OrderService.name);
async create(dto: CreateOrderDto) {
const order = await this.repo.save(dto);
this.logger.log(`order created: ${order.id}`);
return order;
}
}
Проблема этого кода: строка order created: 123 — свободный текст. Системы сбора логов (Loki, Elasticsearch) не умеют с ним работать: нельзя отфильтровать все события по orderId, нельзя построить агрегацию.
Структурные логи — это события в виде JSON-объектов, где каждый факт — отдельное поле:
this.logger.log({ message: 'order_created', orderId: order.id, userId: dto.userId });
На практике стандартный логгер заменяют на nestjs-pino — он пишет JSON с нужными полями и сохраняет тот же интерфейс Logger. Ключевое поле — идентификатор запроса (requestId), который проставляется в interceptor: по нему строки одного запроса склеиваются в единую цепочку, даже если реплик десять.
Health-checks: два разных сигнала
Kubernetes следит за каждым подом через два зонда — liveness и readiness. Их часто путают, а это дорогая ошибка.
- liveness — «жив ли процесс?». Провал → Kubernetes убивает под и перезапускает. Должен быть простым: если процесс отвечает — значит жив.
- readiness — «готов ли принимать трафик?». Провал → Kubernetes временно убирает под из балансировки, но не перезапускает. Сюда добавляют проверку базы данных, кеша, внешних сервисов.
Почему важно не путать: если liveness проверяет базу данных и база упала — Kubernetes начнёт бесконечно перезапускать поды. Это не помогает: перезапуск пода чужую базу не починит, зато создаст шторм рестартов.
Официальный модуль @nestjs/terminus делает это просто:
import { Controller, Get } from '@nestjs/common';
import { HealthCheck, HealthCheckService, TypeOrmHealthIndicator } from '@nestjs/terminus';
@Controller('health')
export class HealthController {
constructor(
private readonly health: HealthCheckService,
private readonly db: TypeOrmHealthIndicator,
) {}
@Get('ready')
@HealthCheck()
ready() {
return this.health.check([() => this.db.pingCheck('database')]);
}
@Get('live')
@HealthCheck()
live() {
return this.health.check([]);
}
}
/health/ready проверяет базу, /health/live — только то, что процесс отвечает. В Kubernetes-манифесте каждый зонд указывает на свой путь.
Трейсинг: OpenTelemetry
Лог говорит «что-то пошло не так». Трейс показывает где именно и сколько времени ушло на каждый шаг.
Представьте запрос: клиент → NestJS → PostgreSQL → Redis → внешнее API. Трейс — это дерево «span»-ов, каждый из которых записывает промежуток времени одного шага. Видно, что запрос занял 800 мс и из них 780 мс — ожидание внешнего API.
Стандарт — OpenTelemetry. Его Node SDK умеет авто-инструментировать HTTP-сервер, TypeORM, HTTP-клиентов — span-ы добавляются автоматически без правки кода приложения. Куда отправлять трейсы (Jaeger, Tempo, Zipkin) — настраивается через экспортёр, это конфигурация, а не код.
Связь с логами: если в каждую строку лога добавить traceId текущего span-а — можно прыгнуть из лога прямо в трейс и увидеть полную картину конкретного запроса.
Метрики: Prometheus
Метрики — числа, которые меняются со временем: количество запросов в секунду, доля ошибок, время ответа (P50, P95, P99). Трейс отвечает на «что случилось с этим конкретным запросом», метрики — на «как сервис ведёт себя в целом».
Стандарт — Prometheus: он сам периодически приходит на эндпоинт /metrics и забирает данные. В Node-экосистеме за это отвечает prom-client. Для NestJS есть готовые интеграции, которые поднимают базовые метрики (число запросов, HTTP-коды, задержки) и отдают их автоматически.
Бизнес-метрики — «сколько заказов создано», «сколько платежей провалилось» — добавляют поверх стандартных:
import { Injectable } from '@nestjs/common';
import { Counter } from 'prom-client';
@Injectable()
export class OrderMetrics {
readonly created = new Counter({
name: 'orders_created_total',
help: 'Total number of created orders',
});
}
Данные с /metrics визуализируют в Grafana — там же настраивают алерты.
Коротко
- Наблюдаемость — три опоры: логи (что произошло), трейсы (где и сколько времени), метрики (сводная картина в числах).
- Логи в продакшне — структурные (JSON с полями), не свободный текст.
nestjs-pinoзаменяет стандартныйLogger, сохраняя интерфейс. - Health-checks: liveness — простой «жив ли процесс?»; readiness — «готов ли к трафику?» (здесь проверяют базу). Не смешивать: liveness зависимый от базы устраивает шторм рестартов.
@nestjs/terminus— официальный модуль для health-checks в NestJS.- OpenTelemetry авто-инструментирует HTTP, TypeORM, клиентов. Трейсы уходят в коллектор (Jaeger, Tempo) без правки кода.
- Prometheus + prom-client — стандарт для метрик. Базовые метрики подключаются готовым модулем, бизнес-метрики добавляются поверх.
Что почитать дальше
- Middleware и interceptors — как добавить
requestIdко всем логам. - Наблюдаемость в Go — те же три опоры, инструментами Go.
- Spring Actuator и observability — аналогичный слой в Spring Boot.