Legan Studio
Все статьи
~ 6 мин чтения

Observability и OpenTelemetry на сайте

Логи, метрики, трейсы для веб-приложения: OpenTelemetry, Grafana, Loki, Prometheus, Jaeger, как собрать и не утонуть в данных.

  • веб
  • devops
  • архитектура

«У нас всё работает» — слова, которые произносит разработчик, у которого нет observability. Без логов, метрик и трейсов вы узнаёте о проблемах от пользователей и чините их вслепую. Разберём, как собрать стек, который даст ответы на вопросы «почему страница тормозит», «куда делся запрос» и «что случилось в 3 ночи».

Три столпа observability

СтолпЧто отвечаетИнструменты
Логичто произошло конкретноLoki, Elastic, ClickHouse
Метрикисколько/как часто/с какой задержкойPrometheus, VictoriaMetrics
Трейсыгде время потерялось внутри запросаJaeger, Tempo, Honeycomb

Все три нужны. Только логи — придётся гадать о паттернах. Только метрики — видишь «плохо», но не знаешь почему. Только трейсы — без счётчиков непонятно, насколько массово.

OpenTelemetry — стандарт

OTel — open standard, который заменяет proprietary-агентов всех вендоров. Один SDK в коде, дальше отправляете куда хотите: Datadog, Grafana Cloud, Jaeger, Tempo, Honeycomb. Не привязываетесь к вендору.

npm install @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-node \
  @opentelemetry/exporter-trace-otlp-http
// instrumentation.ts (Next.js 15)
import { NodeSDK } from "@opentelemetry/sdk-node";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
import { Resource } from "@opentelemetry/resources";

const sdk = new NodeSDK({
  resource: new Resource({ "service.name": "site-api" }),
  traceExporter: new OTLPTraceExporter({
    url: "http://otel-collector:4318/v1/traces",
  }),
  instrumentations: [getNodeAutoInstrumentations()],
});

sdk.start();

Авто-инструментация подхватит fetch, http, postgres, redis — без явного wrap каждого вызова. На уровне Next.js — нужно подключить через файл instrumentation.ts в корне проекта.

Структурированные логи

Не console.log("user 42 created"), а JSON с полями:

import pino from "pino";
const log = pino();

log.info({
  event: "user.created",
  userId: 42,
  email: user.email,
  trace_id: getCurrentTraceId(),
}, "user created");

Loki/Elastic умеют фильтровать и агрегировать по полям. level=error AND service=api AND user_id=42 находит за секунду все логи конкретного пользователя за последний час.

trace_id — связка между логом и трейсом: видите ошибку в логах → переходите в Jaeger по trace_id → видите весь путь запроса.

Loki + Promtail

Дешёвая замена ELK. Хранит логи в объектном хранилище (S3-compatible), индексирует только метаданные.

# docker-compose
loki:
  image: grafana/loki:3.0
  ports: ["3100:3100"]

promtail:
  image: grafana/promtail:3.0
  volumes:
    - /var/log:/var/log
    - ./promtail.yaml:/etc/promtail/config.yml

Затраты на хранение: 10-100 раз дешевле Elasticsearch при той же глубине истории. Минус — нет полнотекста, поиск только по labels.

Prometheus для метрик

import { register, Counter, Histogram } from "prom-client";

const httpRequests = new Counter({
  name: "http_requests_total",
  help: "Total HTTP requests",
  labelNames: ["method", "route", "status"],
});

const httpDuration = new Histogram({
  name: "http_request_duration_seconds",
  help: "HTTP request duration",
  labelNames: ["method", "route"],
  buckets: [0.01, 0.05, 0.1, 0.5, 1, 2, 5],
});

// в middleware
httpRequests.inc({ method: req.method, route, status });
httpDuration.observe({ method: req.method, route }, durationSec);
// app/api/metrics/route.ts
export async function GET() {
  return new Response(await register.metrics(), {
    headers: { "Content-Type": register.contentType },
  });
}

Prometheus раз в 15 секунд опрашивает /api/metrics. Grafana строит графики.

Jaeger / Tempo для трейсов

Видите водопад: запрос пришёл в Next.js → вызвал API-роут → роут пошёл в Postgres → потом в Redis → потом в внешний API. Сразу понятно, что 80% времени съел внешний вызов.

jaeger:
  image: jaegertracing/all-in-one:1.60
  ports: ["16686:16686"]
  environment:
    COLLECTOR_OTLP_ENABLED: "true"

Tempo от Grafana — лучше для большого объёма (хранит в S3), но сложнее в установке. Jaeger проще для старта.

Что измерять — RED и USE

RED для сервисов:

  • Rate — запросов в секунду
  • Errors — % ошибок
  • Duration — латенция (p50, p95, p99)

USE для ресурсов:

  • Utilization — загрузка CPU, RAM, диска
  • Saturation — длина очередей
  • Errors — отказы (timeouts, OOM)

Эти 6 метрик покрывают 90% «почему упало». Дальше уже бизнес-метрики (конверсия, оборот, активные пользователи).

Алерты

Не на всё подряд. Хорошие алерты:

  • p95 латенции эндпоинта > 1 сек 5 минут подряд.
  • Error rate > 1% 2 минуты.
  • БД использует > 90% CPU 10 минут.
  • Очередь Redis длиннее 1000 сообщений.
  • Сертификат истекает через 7 дней.

Плохие алерты — на каждый чих. Пара ложных срабатываний — и команда начинает их игнорировать («это опять оно»).

В Grafana Alerting или Alertmanager — отправка в Telegram/Slack. На дежурный канал — критика, в чат разработки — предупреждения.

Sampling

Трейсы дорогие в хранении. На 10 000 RPS вы быстро забьёте диск. Решения:

  • Head-based sampling: рандомно 1-10% запросов трейсятся полностью. Просто, но «интересные» события (медленные, с ошибками) теряются.
  • Tail-based sampling: трейсятся все, но сохраняются только «интересные» — медленные, с ошибками. Сложнее, нужен OTel collector с processor.
# otel-collector tail_sampling
processors:
  tail_sampling:
    decision_wait: 10s
    policies:
      - name: errors
        type: status_code
        status_code: { status_codes: [ERROR] }
      - name: slow
        type: latency
        latency: { threshold_ms: 1000 }
      - name: random
        type: probabilistic
        probabilistic: { sampling_percentage: 1 }

Российские реалии

Datadog, New Relic, Honeycomb — недоступны для оплаты из РФ. Self-hosted — единственный реальный вариант:

  • Grafana + Loki + Prometheus + Tempo на одном VPS — стартовый стек, $20-50/мес.
  • VictoriaMetrics вместо Prometheus — российский, эффективнее по RAM, проще кластеризация.
  • ClickHouse для логов — если много, и нужен SQL-поиск.
  • Yandex Cloud Monitoring — managed-аналог Cloud Monitoring, оплата рублями.

С чего начать

  1. Подключить OpenTelemetry в код, отправлять в локальный collector.
  2. Поставить Grafana + Loki + Prometheus + Jaeger в docker-compose.
  3. Вывести минимум: 4 RED-метрики, логи приложения, трейсы критичных эндпоинтов.
  4. Настроить 5-10 базовых алертов.
  5. Через месяц — пересобрать алерты, выкинуть шумные, добавить недостающие.

Один день работы — и у вас совсем другой уровень understanding своего прода.

Итого

Observability — не роскошь, а инструмент команды. Без него инциденты длятся часами, постмортемы — гадание. С ним инцидент: «вижу пик 5xx → проверяю dashboard → вижу всплеск latency БД → проверяю slow queries → нахожу запрос → правлю». 15 минут вместо 5 часов. OpenTelemetry стандартизирует это и убирает вендор-лок.

Частые вопросы

Datadog или self-hosted?

Datadog удобнее, но дорогой и недоступен для российских юрлиц. Self-hosted (Grafana + Loki + Prometheus + Tempo) — почти то же самое за стоимость VPS. Команда из 3-5 человек поднимет за 1-2 дня. Если уже на Datadog в зарубежной юрисдикции — оставайтесь.

OpenTelemetry — это сложно?

Базовая настройка с авто-инструментацией — час работы (одна команда npm install, файл instrumentation.ts, переменные окружения). Кастомные spans для бизнес-операций — отдельные дни. Самое сложное — collector конфигурация и tail-sampling, но без них на старте можно жить.

Loki или Elasticsearch для логов?

Loki — для большинства проектов. Дешевле в 10-50 раз по диску, оптимизирован под высокий throughput. Elasticsearch — когда нужен полнотекст и сложные агрегации, обычно избыточно для логов веб-приложения. ClickHouse — компромисс: дешёвое хранение + SQL для аналитики.

Сколько хранить логи и трейсы?

Логи: 7-30 дней горячие (Loki/Elastic), 1-3 месяца архив (S3). Трейсы: 7 дней горячие, sampled — 30 дней. Метрики — 1-3 года, агрегированные. Дольше — обычно бесполезно, если только нет регуляторных требований.

trace_id в каждом логе — сложно настроить?

Нет, OpenTelemetry автоматически прокидывает context. В pino:pino-opentelemetry-transport подхватывает activeSpan и добавляет trace_id/span_id в JSON-лог. Дальше в Loki/Grafana — клик по логу → переход в Jaeger по trace_id. Это самая полезная фича connected observability.

Метрики на стороне клиента (frontend) — как собирать?

Web Vitals (LCP, INP, CLS) — через web-vitals npm и POST на свой /api/metrics. Дальше в Prometheus или ClickHouse. Sentry умеет это сам в Performance, GoatCounter и Plausible — частично. Для серьёзной аналитики front-perf берите OpenTelemetry browser SDK или специализированные инструменты типа SpeedCurve.

Сколько ресурсов жрёт OpenTelemetry в продакшене?

Auto-instrumentation добавляет 1-5% CPU и 50-200 МБ RAM на процесс приложения. Collector — отдельный процесс, 200-500 МБ RAM на средний поток. Не критично для типичного сервиса. Если упираетесь — sampling: 1-10% запросов трейсится, остальное считается метрикой.