← назад к разделу

Представьте: сервис лежит в два часа ночи, телефон звонит. Что случилось? Если у сервиса нет логов, метрик и трейсов — вы будете гадать. Наблюдаемость — это способность ответить на вопрос «что сейчас происходит с сервисом?» без угадывания.

Три опоры наблюдаемости: логи — что произошло, трейсы — где и сколько ушло времени, метрики — сводная картина в числах. Плюс 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.