
Мы соберем полноценную наблюдаемость (observability) на базе OpenTelemetry: будем видеть, что происходит внутри сервиса, как быстро отвечают эндпоинты, где теряются секунды, какие ошибки бьют по выручке. Дадим готовые настройки: docker-compose для Jaeger (трейсы), Prometheus + Grafana (метрики), а также рабочий пример FastAPI с метриками и трассировками и короткую инструкцию для Django.
Цель — сократить время на поиск проблем (MTTR) в 2–3 раза, держать SLA без «слепых зон» и быстрее находить узкие места, чтобы экономика сервиса сходилась.
Числа во времени: ошибки в минуту, задержки ответа, число заказов. Метрики хорошо агрегируются, дешево хранятся и подходят для алертов.
Подробные записи событий. Нужны для расследований и аудита. Должны быть структурированы (JSON), чтобы их можно было искать и аггрегировать.
«След» запроса через сервис: какие функции/запросы БД дергались и сколько заняли. Позволяют за минуты ответить «почему медленно именно тут». OpenTelemetry — открытый стандарт для трассировок, метрик и логов.
Минимальный набор для старта:
Альтернатива — платные APM. Плюсы: быстрее запуск и меньше возни. Минусы: стоимость на объёмах. Этот материал показывает, как собрать «своими руками» и контролировать расходы.
Создадим каталог проекта и папки для приложения и конфигов:
mkdir -p observability-demo/app
cd observability-demo
docker-compose.yml:
version: "3.9"
services:
jaeger:
image: jaegertracing/all-in-one:1.57
ports:
- "16686:16686" # UI Jaeger
- "4317:4317" # OTLP gRPC для трасс
prometheus:
image: prom/prometheus:v2.53.0
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
ports:
- "9090:9090"
grafana:
image: grafana/grafana:11.1.0
depends_on:
- prometheus
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
ports:
- "3000:3000"
app:
build: ./app
environment:
- OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4317
- OTEL_TRACES_EXPORTER=otlp
- OTEL_SERVICE_NAME=shop-api
ports:
- "8000:8000"
depends_on:
- jaeger
prometheus.yml:
global:
scrape_interval: 10s
scrape_configs:
- job_name: "app"
static_configs:
- targets: ["app:8000"]
labels:
service: "shop-api"
Dockerfile и приложение (FastAPI) поместим в ./app:
app/requirements.txt:
fastapi==0.115.2
uvicorn[standard]==0.32.0
prometheus-client==0.21.0
structlog==24.4.0
requests==2.32.3
opentelemetry-api==1.27.0
opentelemetry-sdk==1.27.0
opentelemetry-exporter-otlp==1.27.0
opentelemetry-instrumentation-fastapi==0.48b0
opentelemetry-instrumentation-requests==0.48b0
app/Dockerfile:
FROM python:3.12-slim
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY main.py .
CMD ["python", "main.py"]
app/main.py:
import time
import uuid
from typing import Dict
import requests
import structlog
from fastapi import FastAPI, Request
from prometheus_client import Counter, Histogram, make_asgi_app
from starlette.middleware.base import BaseHTTPMiddleware
# OpenTelemetry (трассировки)
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.instrumentation.requests import RequestsInstrumentor
# Настройка трассировок: отправляем в OTLP (Jaeger)
resource = Resource.create({"service.name": "shop-api"})
provider = TracerProvider(resource=resource)
span_exporter = OTLPSpanExporter()
processor = BatchSpanProcessor(span_exporter)
provider.add_span_processor(processor)
trace.set_tracer_provider(provider)
tracer = trace.get_tracer(__name__)
# Логи: добавляем trace_id/span_id
def _add_trace_context(_, __, event_dict: Dict):
span = trace.get_current_span()
ctx = span.get_span_context() if span else None
if ctx and ctx.is_valid:
event_dict["trace_id"] = format(ctx.trace_id, "032x")
event_dict["span_id"] = format(ctx.span_id, "016x")
return event_dict
structlog.configure(
processors=[
_add_trace_context,
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.JSONRenderer(),
]
)
log = structlog.get_logger()
# Метрики
REQUESTS = Counter(
"http_requests_total",
"Количество HTTP-запросов",
labelnames=("method", "path", "status_class"),
)
LATENCY = Histogram(
"http_request_duration_seconds",
"Задержка HTTP-запросов, секунды",
labelnames=("method", "path"),
buckets=(0.025, 0.05, 0.1, 0.25, 0.5, 1, 2, 5)
)
ORDERS_CREATED = Counter(
"orders_created_total", "Создано заказов", labelnames=("source",)
)
app = FastAPI(title="Shop API")
# Экспорт метрик для Prometheus
app.mount("/metrics", make_asgi_app())
# Middleware для метрик
class MetricsMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
start = time.perf_counter()
response = await call_next(request)
path = request.url.path
# Слишком шумные пути можно схлопывать
if path.startswith("/order/"):
path = "/order/{id}"
if path != "/metrics":
LATENCY.labels(request.method, path).observe(time.perf_counter() - start)
status_class = f"{response.status_code // 100}xx"
REQUESTS.labels(request.method, path, status_class).inc()
return response
app.add_middleware(MetricsMiddleware)
# Автоинструментирование FastAPI и requests
FastAPIInstrumentor.instrument_app(app)
RequestsInstrumentor().instrument()
@app.get("/health")
def health():
return {"status": "ok"}
@app.post("/order")
def create_order():
# Бизнес-метрика
ORDERS_CREATED.labels(source="web").inc()
# Имитируем обращение к внешнему сервису оплаты
with tracer.start_as_current_span("payment"):
r = requests.get("https://httpbin.org/delay/0.2", timeout=3)
r.raise_for_status()
order_id = str(uuid.uuid4())
log.info("order_created", order_id=order_id, amount=1990, currency="RUB")
return {"order_id": order_id}
@app.get("/order/{order_id}")
def get_order(order_id: str):
# Имитируем кэш/БД
time.sleep(0.03)
log.info("order_read", order_id=order_id)
return {"order_id": order_id, "status": "paid"}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
Запускаем:
docker compose up -d --build
Проверяем:
curl -X POST http://localhost:8000/order и curl http://localhost:8000/order/123.Если у вас Django, добавьте инструментирование трассировок и экспортер OTLP. Установите зависимости:
pip install opentelemetry-api opentelemetry-sdk opentelemetry-exporter-otlp opentelemetry-instrumentation-django
В wsgi.py подключите инструментатор (раньше, чем создается приложение):
# myproject/wsgi.py
import os
from django.core.wsgi import get_wsgi_application
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.instrumentation.django import DjangoInstrumentor
resource = Resource.create({"service.name": "django-app"})
provider = TracerProvider(resource=resource)
provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))
trace.set_tracer_provider(provider)
DjangoInstrumentor().instrument()
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings")
application = get_wsgi_application()
Метрики экспонируйте через prometheus-client или готовые пакеты (например, django-prometheus). Важно: не забывайте фильтровать path в метриках (слишком детальные пути раздувают кардинальность).
В примере выше мы добавили процессор structlog, который вытаскивает trace_id и span_id из текущего контекста. Это позволяет в любой системе логов быстро провалиться из записи лога в конкретный трейc в Jaeger (по trace_id). Если используете стандартный logging, включите opentelemetry-instrumentation-logging или добавьте фильтр, который подмешивает идентификаторы из opentelemetry.trace.get_current_span().
Правило гигиены: не пишите в логи персональные данные и платежные реквизиты. Идентификаторы пользователей и заказов достаточно.
Ключевые сервисные SLI:
Примеры PromQL:
sum(rate(http_requests_total{status_class="5xx"}[5m]))
/ ignoring(status_class) sum(rate(http_requests_total[5m]))
histogram_quantile(
0.95,
sum(rate(http_request_duration_seconds_bucket[5m])) by (le)
)
sum(rate(http_requests_total{path="/order"}[1m]))
Алерты (идеи):
Бизнес-дашборд в Grafana:
/order/{id}.Симптом: p95 /order вырос с 150 до 900 мс, конверсия упала. По логам всё «чисто». В Jaeger видно: внутри запроса есть спан payment длительностью ~700 мс. В таймлайне — всплески только в рабочие часы банка. Отключаем повторный DNS‑резолв, включаем Keep‑Alive, договариваемся с провайдером, добавляем таймауты и ретраи. Итог: p95 снова 170–200 мс, конверсия вернулась, поиски заняли 20 минут вместо «полдня». Без трассировок это был бы долгий перебор гипотез.
Наблюдаемость — это не «ещё один дашборд». Это системный способ экономить время инженеров, быстрее понимать, где теряются деньги, и держать SLA под контролем. Используя OpenTelemetry, Prometheus, Grafana и Jaeger, можно за 1–2 недели собрать базу, которую легко масштабировать. Начните с метрик HTTP и ключевых бизнес‑счетчиков, включите трассинг на критичных путях, добавьте логи с trace_id — и уже через пару спринтов ваша команда будет тратить на инциденты в разы меньше времени.