📑 Содержание
- 1. Observability vs Мониторинг
- 2. Три столпа: логи, метрики, трейсы
- 3. Что такое OpenTelemetry
- 4. OTel для Node.js (Express/Fastify)
- 5. OTel для Python (FastAPI/Django)
- 6. OTel для Go
- 7. Distributed Tracing на практике
- 8. Ключевые метрики API
- 9. Structured Logging
- 10. Бэкенды: Jaeger, Grafana, Prometheus
- 11. Алертинг: правила и каналы
- 12. Best practices
- FAQ
1. Observability vs Мониторинг
Мониторинг отвечает на вопрос «работает ли система?» — вы заранее определяете, что проверять (uptime, CPU, error rate). Observability отвечает на вопрос «почему система сломалась?» — вы можете исследовать любую проблему, даже ту, которую не предвидели.
| Аспект | Мониторинг | Observability |
|---|---|---|
| Вопрос | «Работает ли?» | «Почему не работает?» |
| Подход | Заранее определённые проверки | Исследование неизвестных проблем |
| Данные | Метрики, healthcheck | Логи + метрики + трейсы |
| Пример | «Error rate > 5% — алерт» | «Почему запрос от user-123 занял 12 секунд?» |
| Архитектура | Достаточно для монолита | Критично для микросервисов |
2. Три столпа: логи, метрики, трейсы
Логи
Дискретные события с временной меткой. Что произошло и когда.
Метрики
Числовые измерения. Сколько, как быстро, какой процент.
Трейсы
Путь запроса через систему. Где задержка, что вызвало ошибку.
📝 Логи (Logs)
LogsЛог — запись о событии в определённый момент времени. Structured logs (JSON) предпочтительнее plain text для автоматического анализа.
// Structured log (хорошо)
{
"timestamp": "2026-02-23T14:30:00Z",
"level": "error",
"message": "Failed to fetch user",
"service": "user-service",
"trace_id": "abc123def456",
"span_id": "789ghi",
"user_id": "user-42",
"http_status": 500,
"duration_ms": 1523,
"error": "Connection refused: postgres:5432"
}
📊 Метрики (Metrics)
MetricsМетрика — числовое значение, агрегированное за интервал. Четыре типа:
- Counter — монотонно растущий счётчик (
http_requests_total) - Gauge — текущее значение (
active_connections) - Histogram — распределение значений (
request_duration_seconds) - Summary — квантили (
request_duration_p99)
🔗 Трейсы (Traces)
TracesТрейс — полный путь запроса через все сервисы. Состоит из spans (отрезков), связанных trace_id:
Распределённый трейс: один запрос → 5 spans в 4 сервисах. PostgreSQL — узкое место (180ms).
3. Что такое OpenTelemetry
OpenTelemetry (OTel) — open-source проект CNCF, стандарт для сбора телеметрии. Объединяет OpenTracing + OpenCensus.
SDK + auto-instrumentation
приём, обработка
Jaeger / Grafana / Datadog
Ключевые компоненты
| Компонент | Описание |
|---|---|
| SDK | Библиотека для инструментирования кода (Node.js, Python, Go, Java, .NET, PHP, Rust) |
| Auto-instrumentation | Автоматический сбор телеметрии из популярных библиотек (Express, Prisma, pg, fetch) |
| OTLP | OpenTelemetry Protocol — единый протокол передачи данных (gRPC / HTTP) |
| Collector | Промежуточный агент: приём → обработка → экспорт в бэкенды |
| Exporters | Отправка данных в Jaeger, Prometheus, Grafana Tempo, Datadog, New Relic и др. |
4. OTel для Node.js (Express/Fastify)
Установка
npm install @opentelemetry/sdk-node \
@opentelemetry/api \
@opentelemetry/auto-instrumentations-node \
@opentelemetry/exporter-trace-otlp-http \
@opentelemetry/exporter-metrics-otlp-http \
@opentelemetry/resources \
@opentelemetry/semantic-conventions
Инициализация (загрузить до остального кода)
// tracing.ts — импортировать ПЕРВЫМ
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http';
import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
import { Resource } from '@opentelemetry/resources';
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions';
const sdk = new NodeSDK({
resource: new Resource({
[ATTR_SERVICE_NAME]: 'user-service',
[ATTR_SERVICE_VERSION]: '1.2.0',
environment: process.env.NODE_ENV ?? 'development',
}),
traceExporter: new OTLPTraceExporter({
url: 'http://otel-collector:4318/v1/traces',
}),
metricReader: new PeriodicExportingMetricReader({
exporter: new OTLPMetricExporter({
url: 'http://otel-collector:4318/v1/metrics',
}),
exportIntervalMillis: 15000,
}),
instrumentations: [
getNodeAutoInstrumentations({
'@opentelemetry/instrumentation-http': { enabled: true },
'@opentelemetry/instrumentation-express': { enabled: true },
'@opentelemetry/instrumentation-pg': { enabled: true },
'@opentelemetry/instrumentation-redis': { enabled: true },
}),
],
});
sdk.start();
console.log('OpenTelemetry initialized');
process.on('SIGTERM', () => sdk.shutdown());
Кастомные spans
import { trace, SpanStatusCode } from '@opentelemetry/api';
const tracer = trace.getTracer('user-service');
async function getUser(userId: string) {
return tracer.startActiveSpan('getUser', async (span) => {
try {
span.setAttribute('user.id', userId);
const user = await db.user.findUnique({ where: { id: userId } });
if (!user) {
span.setStatus({ code: SpanStatusCode.ERROR, message: 'User not found' });
span.setAttribute('error.type', 'NOT_FOUND');
throw new TRPCError({ code: 'NOT_FOUND' });
}
span.setAttribute('user.role', user.role);
span.setStatus({ code: SpanStatusCode.OK });
return user;
} catch (error) {
span.recordException(error as Error);
span.setStatus({ code: SpanStatusCode.ERROR });
throw error;
} finally {
span.end();
}
});
}
Кастомные метрики
import { metrics } from '@opentelemetry/api';
const meter = metrics.getMeter('user-service');
const requestCounter = meter.createCounter('api.requests.total', {
description: 'Total API requests',
});
const requestDuration = meter.createHistogram('api.request.duration', {
description: 'API request duration in ms',
unit: 'ms',
});
const activeUsers = meter.createUpDownCounter('api.active_users', {
description: 'Currently active users',
});
// Express middleware
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
const labels = {
method: req.method,
path: req.route?.path ?? req.path,
status: res.statusCode.toString(),
};
requestCounter.add(1, labels);
requestDuration.record(duration, labels);
});
next();
});
5. OTel для Python (FastAPI/Django)
pip install opentelemetry-api \
opentelemetry-sdk \
opentelemetry-instrumentation-fastapi \
opentelemetry-instrumentation-sqlalchemy \
opentelemetry-instrumentation-redis \
opentelemetry-exporter-otlp
# tracing.py
from opentelemetry import trace, metrics
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter
from opentelemetry.sdk.resources import Resource
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
resource = Resource.create({
"service.name": "order-service",
"service.version": "2.1.0",
})
# Traces
tracer_provider = TracerProvider(resource=resource)
tracer_provider.add_span_processor(
BatchSpanProcessor(OTLPSpanExporter(endpoint="otel-collector:4317"))
)
trace.set_tracer_provider(tracer_provider)
# Metrics
metric_reader = PeriodicExportingMetricReader(
OTLPMetricExporter(endpoint="otel-collector:4317"),
export_interval_millis=15000,
)
metrics.set_meter_provider(MeterProvider(resource=resource, metric_readers=[metric_reader]))
# Auto-instrumentation
FastAPIInstrumentor.instrument_app(app)
SQLAlchemyInstrumentor().instrument(engine=engine)
# Кастомные spans в FastAPI
from opentelemetry import trace
tracer = trace.get_tracer("order-service")
@app.post("/orders")
async def create_order(order: OrderCreate):
with tracer.start_as_current_span("create_order") as span:
span.set_attribute("order.items_count", len(order.items))
span.set_attribute("order.total", order.total)
with tracer.start_as_current_span("validate_inventory"):
await check_inventory(order.items)
with tracer.start_as_current_span("process_payment"):
payment = await charge_card(order.payment_method, order.total)
span.set_attribute("payment.id", payment.id)
with tracer.start_as_current_span("save_order"):
db_order = await save_to_db(order)
return db_order
6. OTel для Go
go get go.opentelemetry.io/otel \
go.opentelemetry.io/otel/sdk \
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp \
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp
package main
import (
"context"
"log"
"net/http"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)
func initTracer() (*sdktrace.TracerProvider, error) {
exporter, err := otlptracehttp.New(context.Background(),
otlptracehttp.WithEndpoint("otel-collector:4318"),
otlptracehttp.WithInsecure(),
)
if err != nil {
return nil, err
}
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter),
sdktrace.WithResource(resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceNameKey.String("payment-service"),
semconv.ServiceVersionKey.String("3.0.1"),
)),
)
otel.SetTracerProvider(tp)
return tp, nil
}
func main() {
tp, _ := initTracer()
defer tp.Shutdown(context.Background())
mux := http.NewServeMux()
mux.HandleFunc("/payments", handlePayment)
handler := otelhttp.NewHandler(mux, "payment-server")
log.Fatal(http.ListenAndServe(":8080", handler))
}
func handlePayment(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
tracer := otel.Tracer("payment-service")
ctx, span := tracer.Start(ctx, "processPayment")
defer span.End()
span.SetAttributes(
attribute.String("payment.method", "card"),
attribute.Float64("payment.amount", 99.99),
)
// Вложенный span для обращения к внешнему API
ctx, dbSpan := tracer.Start(ctx, "saveToDatabase")
// ... save ...
dbSpan.End()
}
7. Distributed Tracing на практике
Distributed tracing — главная суперсила observability. Один trace_id связывает все сервисы, через которые прошёл запрос.
Как работает propagation
создаёт trace_id
W3C traceparent
создаёт child span
продолжает trace
Контекст трейса передаётся через HTTP-заголовок traceparent (W3C Trace Context):
# W3C Trace Context заголовок
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
# ^^-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^-^^^^^^^^^^^^^^^^-^^
# version trace-id span-id flags
Пример: отладка медленного запроса
Пользователь жалуется: «Страница грузится 8 секунд». Открываем трейс в Jaeger:
Виновник найден за 30 секунд: ML Service отвечает 7.2 секунды. Без трейсинга поиск занял бы часы.
8. Ключевые метрики API
Четыре золотых сигнала (Google SRE)
Latency
Время ответа: p50, p95, p99
Traffic
Запросов в секунду (RPS)
Errors
% ошибок (4xx, 5xx)
RED метрики для API
| Метрика | Prometheus query | Описание | Целевое значение |
|---|---|---|---|
| Rate | rate(http_requests_total[5m]) |
Запросов в секунду | Зависит от нагрузки |
| Errors | rate(http_requests_total{status=~"5.."}[5m]) |
Ошибки в секунду | < 0.1% от Rate |
| Duration p50 | histogram_quantile(0.5, rate(http_request_duration_seconds_bucket[5m])) |
Медиана времени ответа | < 100ms |
| Duration p95 | histogram_quantile(0.95, ...) |
95-й перцентиль | < 500ms |
| Duration p99 | histogram_quantile(0.99, ...) |
99-й перцентиль | < 1s |
| Saturation | node_cpu_seconds_total |
Загрузка CPU/RAM | < 80% |
| Apdex | Кастомная формула | Удовлетворённость пользователей | > 0.95 |
SLI (Service Level Indicator) — измеряемая метрика (p99 latency = 230ms).
SLO (Service Level Objective) — цель (p99 latency < 500ms, 99.9% запросов).
SLA (Service Level Agreement) — договор с последствиями (если SLO нарушен → компенсация).
Подробнее о HTTP-статусах и их влиянии на error rate.
9. Structured Logging
Structured logging — логи в JSON-формате вместо plain text. Каждая запись содержит ключ-значение пары для автоматического поиска и анализа.
Node.js (pino + OpenTelemetry)
import pino from 'pino';
import { trace, context } from '@opentelemetry/api';
const logger = pino({
level: process.env.LOG_LEVEL ?? 'info',
formatters: {
log(object) {
const span = trace.getSpan(context.active());
if (span) {
const { traceId, spanId } = span.spanContext();
return { ...object, trace_id: traceId, span_id: spanId };
}
return object;
},
},
});
// Использование
app.get('/users/:id', async (req, res) => {
logger.info({ userId: req.params.id, method: req.method }, 'Fetching user');
try {
const user = await getUser(req.params.id);
logger.info({ userId: user.id, role: user.role }, 'User found');
res.json(user);
} catch (error) {
logger.error({ userId: req.params.id, err: error }, 'Failed to fetch user');
res.status(500).json({ error: 'Internal error' });
}
});
Python (structlog + OpenTelemetry)
import structlog
from opentelemetry import trace
structlog.configure(
processors=[
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.add_log_level,
add_trace_context, # кастомный процессор
structlog.processors.JSONRenderer(),
],
)
def add_trace_context(logger, method_name, event_dict):
span = trace.get_current_span()
if span.is_recording():
ctx = span.get_span_context()
event_dict["trace_id"] = format(ctx.trace_id, '032x')
event_dict["span_id"] = format(ctx.span_id, '016x')
return event_dict
log = structlog.get_logger()
@app.post("/orders")
async def create_order(order: OrderCreate):
log.info("Creating order", items_count=len(order.items), total=order.total)
# ...
log.info("Order created", order_id=db_order.id)
- Пароли, токены, API-ключи (аутентификационные данные)
- Персональные данные (email, телефон, адрес) — GDPR / ФЗ-152
- Номера кредитных карт — PCI DSS
- Полные тела запросов/ответов в production (объём!)
10. Бэкенды: Jaeger, Grafana, Prometheus
OTel Collector — центральный хаб
# otel-collector-config.yaml
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
batch:
timeout: 5s
send_batch_size: 1024
memory_limiter:
check_interval: 1s
limit_mib: 512
exporters:
# Трейсы → Jaeger
otlp/jaeger:
endpoint: jaeger:4317
tls:
insecure: true
# Метрики → Prometheus
prometheus:
endpoint: 0.0.0.0:8889
# Логи → Loki
loki:
endpoint: http://loki:3100/loki/api/v1/push
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [otlp/jaeger]
metrics:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [prometheus]
logs:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [loki]
Docker Compose — полный стек
# docker-compose.observability.yml
services:
otel-collector:
image: otel/opentelemetry-collector-contrib:latest
volumes:
- ./otel-collector-config.yaml:/etc/otelcol-contrib/config.yaml
ports:
- "4317:4317" # OTLP gRPC
- "4318:4318" # OTLP HTTP
- "8889:8889" # Prometheus metrics
jaeger:
image: jaegertracing/all-in-one:latest
ports:
- "16686:16686" # UI
- "4317" # OTLP gRPC
prometheus:
image: prom/prometheus:latest
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
ports:
- "9090:9090"
grafana:
image: grafana/grafana:latest
ports:
- "3000:3000"
environment:
- GF_AUTH_ANONYMOUS_ENABLED=true
loki:
image: grafana/loki:latest
ports:
- "3100:3100"
| Бэкенд | Сигнал | Тип | Когда использовать |
|---|---|---|---|
| Jaeger | Трейсы | Open-source | Distributed tracing, поиск узких мест |
| Grafana Tempo | Трейсы | Open-source | Трейсы + интеграция с Grafana dashboards |
| Prometheus | Метрики | Open-source | Метрики, алерты, PromQL запросы |
| Grafana | Визуализация | Open-source | Дашборды для метрик, логов, трейсов |
| Loki | Логи | Open-source | Логи с label-based поиском (как Prometheus) |
| Datadog | Всё | SaaS | All-in-one: трейсы + метрики + логи + APM |
| Grafana Cloud | Всё | SaaS | Managed Grafana + Tempo + Mimir + Loki |
11. Алертинг: правила и каналы
Observability без алертов — как пожарная сигнализация без звука. Настройте правила для критичных метрик.
Prometheus alerting rules
# alerts.yml
groups:
- name: api-alerts
rules:
- alert: HighErrorRate
expr: |
sum(rate(http_requests_total{status=~"5.."}[5m]))
/
sum(rate(http_requests_total[5m]))
> 0.05
for: 2m
labels:
severity: critical
annotations:
summary: "API error rate > 5%"
description: "{ $value | humanizePercentage } ошибок за последние 5 минут"
- alert: HighLatency
expr: |
histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m])) > 2
for: 5m
labels:
severity: warning
annotations:
summary: "API p99 latency > 2s"
- alert: ServiceDown
expr: up == 0
for: 1m
labels:
severity: critical
annotations:
summary: "Service { $labels.instance } is down"
1 - (sum(rate(http_requests_total{status!~"5.."}[30d])) / sum(rate(http_requests_total[30d]))) > 0.0005
12. Best practices
1. Инструментируйте на уровне middleware
// Один middleware = покрытие всех эндпоинтов
app.use((req, res, next) => {
const span = tracer.startSpan(`${req.method} ${req.path}`);
span.setAttribute('http.method', req.method);
span.setAttribute('http.url', req.originalUrl);
span.setAttribute('http.user_agent', req.get('user-agent'));
res.on('finish', () => {
span.setAttribute('http.status_code', res.statusCode);
if (res.statusCode >= 500) {
span.setStatus({ code: SpanStatusCode.ERROR });
}
span.end();
});
next();
});
2. Используйте Semantic Conventions
OpenTelemetry определяет стандартные имена атрибутов:
| Атрибут | Пример значения |
|---|---|
http.request.method | GET, POST, PUT |
http.response.status_code | 200, 404, 500 |
url.path | /api/users/123 |
server.address | api.example.com |
db.system | postgresql, redis |
db.statement | SELECT * FROM users WHERE id = $1 |
3. Sampling для production
// 100% трейсов в dev, 10% в production
const sampler = process.env.NODE_ENV === 'production'
? new TraceIdRatioBasedSampler(0.1) // 10%
: new AlwaysOnSampler();
// Или умный sampling: всегда трейсить ошибки
const sampler = new ParentBasedSampler({
root: new TraceIdRatioBasedSampler(0.1),
// Ошибки и медленные запросы — всегда
});
4. Корреляция логов и трейсов
trace_id, вы можете перейти из лога в Jaeger/Tempo и увидеть полный трейс. И наоборот — из трейса найти все логи запроса в Loki. Это называется корреляция сигналов.
5. Dashboards — стартовый набор
- API Overview — RPS, error rate, p50/p95/p99 latency
- Per-endpoint — разбивка по path + method
- Dependencies — время ответа DB, Redis, внешних API
- Infrastructure — CPU, RAM, disk, connections
- SLO Tracker — остаток error budget за период
FAQ
Чем observability отличается от мониторинга?
Мониторинг проверяет заранее известные метрики («работает ли?»). Observability позволяет исследовать неизвестные проблемы («почему не работает?»). Мониторинг — подмножество observability. Для микросервисов одного мониторинга недостаточно.
Что такое OpenTelemetry?
OpenTelemetry (OTel) — open-source стандарт CNCF для сбора телеметрии: трейсов, метрик и логов. Vendor-neutral: инструментируете код один раз, данные отправляете в любой бэкенд (Jaeger, Datadog, Grafana).
Какие метрики API отслеживать в первую очередь?
Четыре золотых сигнала Google SRE: Latency (p50/p95/p99), Traffic (RPS), Errors (% 5xx), Saturation (CPU/RAM). Дополнительно: apdex, SLO compliance, per-endpoint breakdown.
Нужен ли OpenTelemetry для одного сервиса?
Для монолита достаточно structured logging + Prometheus + Grafana. OpenTelemetry становится критичным при микросервисах, когда запрос проходит через несколько сервисов. Начните с логов и метрик, добавьте OTel при росте.
Сколько стоит observability?
Open-source стек (Jaeger + Prometheus + Grafana + Loki) — бесплатно, но нужны ресурсы на хостинг. SaaS (Datadog, Grafana Cloud) — от $0 (free tier) до $$$. Основная стоимость — объём данных: sampling на 10-20% критичен для production.
Как связать observability с дебагом API-ошибок?
Observability — превентивный инструмент. Когда приходит алерт о росте ошибок, вы: 1) Смотрите метрики (какой эндпоинт). 2) Открываете трейс ошибочного запроса. 3) Находите span с ошибкой. 4) Переходите к логам этого span по trace_id. Весь процесс — 2-5 минут вместо часов.
Полезные ссылки
- Чеклист отладки API — пошаговый дебаг ошибок
- HTTP-статусы — коды ответов для метрик error rate
- REST API — основы API, которые мониторим
- Инструменты тестирования API — тестирование до production
- Формат JSON — structured logging в JSON
- Аутентификация API — что не логировать (токены, секреты)
📊 Мониторьте свои API
LightBox API — создавайте и тестируйте API с полным логированием запросов. Видьте каждый запрос: метод, заголовки, тело, время ответа.
- ✓ Логирование всех входящих запросов
- ✓ Настраиваемые HTTP-статусы и задержки
- ✓ Swagger документация
- ✓ Webhook тестирование
- ✓ Бесплатный план
Статья опубликована: 23 февраля 2026
Автор: LightBox API Team