lenec ru

← все посты

Observability в FastAPI: structured logs, метрики, tracing

11K

Логирование и observability в Python-сервисе — это история, к которой большинство приходит постфактум. Сначала пишешь print, потом logging.info, потом ставишь Sentry, потом упирается во что-то в проде, и ты осознаёшь, что у тебя нет ни корреляции запросов, ни структурированных полей, ни цельной картины. И начинается чистка.

Расскажу про то, как у нас построено наблюдение в FastAPI-сервисах. Не про «выберите лучший APM», а про конкретный набор практик, который реально работает: structured logs, request_id, OpenTelemetry, метрики Prometheus.

Структурированные логи как базовая привычка

Первый шаг — отучить логи от строкового формата. Запись типа "User 123 created order 456" в продакшене бесполезна: ты не отфильтруешь её по user_id, не сгруппируешь, не построишь графики. Структурированный лог — это JSON-объект с полями.

В Python для этого хорошо подходит structlog. Базовая настройка:

import logging
import structlog

logging.basicConfig(level=logging.INFO, format="%(message)s")

structlog.configure(
    processors=[
        structlog.contextvars.merge_contextvars,
        structlog.processors.add_log_level,
        structlog.processors.TimeStamper(fmt="iso"),
        structlog.processors.StackInfoRenderer(),
        structlog.processors.format_exc_info,
        structlog.processors.JSONRenderer(),
    ],
    wrapper_class=structlog.make_filtering_bound_logger(logging.INFO),
    cache_logger_on_first_use=True,
)

log = structlog.get_logger()

Теперь все логи в проде — это JSON. Используешь так:

log.info("order.created", user_id=user.id, order_id=order.id, amount=order.total)

В Kibana/Loki можно фильтровать по конкретным полям, считать метрики, строить дашборды. Никакого парсинга строк регулярками.

Request ID и контекст запроса

Один запрос пользователя обычно порождает 5–15 записей лога: на эндпоинте, в сервисе, в репозитории, во внешнем клиенте. Без идентификатора запроса найти их связано — мучение.

Ставим middleware, который генерирует/принимает X-Request-ID и кладёт его в context-vars:

import uuid
import structlog
from fastapi import Request

@app.middleware("http")
async def request_context(request: Request, call_next):
    request_id = request.headers.get("x-request-id") or uuid.uuid4().hex
    structlog.contextvars.bind_contextvars(request_id=request_id)
    try:
        response = await call_next(request)
        response.headers["X-Request-ID"] = request_id
        return response
    finally:
        structlog.contextvars.clear_contextvars()

Теперь любая запись через structlog в рамках запроса автоматически получит request_id. Прицепить туда же user_id, как только знаем пользователя:

async def get_current_user(...):
    user = ...
    structlog.contextvars.bind_contextvars(user_id=user.id)
    return user

В логах сразу видно, кто пытался что-то сделать, и можно построить трассировку конкретного пользовательского пути.

Access log: один источник правды

uvicorn по умолчанию пишет свой access log в текстовом виде. В проде он скорее мешает, чем помогает: формат свой, структуры нет.

Лучше выключить uvicorn-овский access и написать свой через middleware:

import time

@app.middleware("http")
async def access_log(request: Request, call_next):
    start = time.perf_counter()
    response = await call_next(request)
    duration = (time.perf_counter() - start) * 1000
    log.info(
        "request.handled",
        method=request.method,
        path=request.url.path,
        status=response.status_code,
        duration_ms=round(duration, 2),
    )
    return response

Запускаешь uvicorn с --no-access-log. Все логи теперь структурированные и проходят через единый pipeline.

Метрики Prometheus

Метрики и логи закрывают разные потребности. Логи — про «что произошло конкретно с этим запросом». Метрики — про «как ведёт себя система в целом».

Для FastAPI хороший пакет — prometheus-fastapi-instrumentator:

from prometheus_fastapi_instrumentator import Instrumentator

Instrumentator().instrument(app).expose(app, endpoint="/metrics")

Из коробки получаешь:

  • http_requests_total — количество запросов по методу, пути, статусу.
  • http_request_duration_seconds — гистограмма времени ответа.
  • Метрики Python-runtime (если включить).

Дальше добавляешь свои бизнес-метрики:

from prometheus_client import Counter, Histogram

orders_created = Counter("orders_created_total", "Created orders", ["country"])
payment_duration = Histogram("payment_duration_seconds", "Payment latency")

async def create_order(...):
    orders_created.labels(country=user.country).inc()
    with payment_duration.time():
        await payment_client.charge(...)

Главное правило: считай то, что важно для бизнеса и для дебага. Не плоди метрики «на всякий случай» — каждая лишняя метрика стоит памяти и в Prometheus, и в приложении.

Tracing через OpenTelemetry

Когда сервисов больше двух и они ходят друг к другу, без трасс становится плохо. OpenTelemetry — стандарт, который всё больше замещает специфичные APM-агенты.

Базовая инструментация FastAPI:

from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor

resource = Resource(attributes={"service.name": "orders-api"})
provider = TracerProvider(resource=resource)
provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter(endpoint="otel-collector:4317")))
trace.set_tracer_provider(provider)

FastAPIInstrumentor.instrument_app(app)

Дополняешь автоматическую инструментацию SQLAlchemy, httpx, Redis — для каждой есть свой пакет opentelemetry-instrumentation-*. После этого в Jaeger или Tempo видишь полное дерево вызовов: запрос → эндпоинт → сервис → SQL-запросы → внешние HTTP-запросы.

Внутри кода добавляешь свои спаны там, где это имеет смысл:

tracer = trace.get_tracer(__name__)

async def calculate_total(order: Order):
    with tracer.start_as_current_span("calculate_total") as span:
        span.set_attribute("order.id", order.id)
        ...

Корреляция логов и трасс

Когда есть trace_id, его удобно прокидывать в логи: тогда из конкретной строки лога можно одним кликом перейти в трассу того же запроса.

from opentelemetry import trace as ot_trace

def add_trace_context(logger, method_name, event_dict):
    span = ot_trace.get_current_span()
    ctx = span.get_span_context()
    if ctx.is_valid:
        event_dict["trace_id"] = format(ctx.trace_id, "032x")
        event_dict["span_id"] = format(ctx.span_id, "016x")
    return event_dict

# добавляешь add_trace_context в processors structlog

Теперь в каждой записи лога есть trace_id, и в Grafana можно настроить переход «лог → трасса».

Sentry для исключений

Логи с трассами хороши, но не заменяют отдельный канал для ошибок. Sentry (или его аналог) даёт удобную группировку, контекст, deduplication.

import sentry_sdk
from sentry_sdk.integrations.fastapi import FastApiIntegration
from sentry_sdk.integrations.starlette import StarletteIntegration

sentry_sdk.init(
    dsn="https://...",
    environment="production",
    traces_sample_rate=0.05,
    integrations=[FastApiIntegration(), StarletteIntegration()],
)

В прод не лей все ошибки — фильтруй по уровням и не отправляй ожидаемые исключения (валидация, ожидаемые HTTPException). Sentry — про неожиданное.

Чек-лист «у нас normal observability»

  • Все логи в JSON, идут в централизованное хранилище.
  • В каждой записи есть request_id и (если применимо) user_id.
  • Эндпоинты отдают X-Request-ID в ответе.
  • Есть /metrics с базовыми и бизнес-метриками.
  • Tracing включён, спаны генерируются автоматически для FastAPI/БД/HTTP.
  • В каждой записи лога есть trace_id.
  • Sentry ловит неожиданные исключения.
  • Все 4xx и 5xx видны на дашборде в реальном времени.

Когда хоть один пункт не выполнен — на инциденте чувствуешь, как этого не хватает. Особенно request_id и trace_id. Поднять observability на уровень «всё видно за секунды» стоит пары дней работы, и эта инвестиция окупается на каждой ночной багалии.

Не пытайся сразу включить всё. Иди по списку: structured logs → request_id → метрики → tracing → корреляция → Sentry. Каждый шаг даёт результат, который можно проверить и почувствовать.

Комментарии 0

  • Будьте первым, кто оставит комментарий.

Войдите, чтобы оставить комментарий.