← все статьи
9 мин

Observability для бедных: логи, метрики, трейсы без Datadog

Datadog — $23 за хост в месяц плюс $0.10 за миллион log-событий плюс APM на каждый сервис. Для стартапа с 20 сервисами это быстро превращается в $1 500–3 000/мес ещё до того, как вы написали первый production-баг. New Relic и Dynatrace не дешевле. Но observability — не опция, это обязательное условие работы в production. Разберём, как собрать полноценный стек за $20/мес на VPS.

Три столпа: что и зачем

Observability строится на трёх типах данных, и каждый решает свою задачу:

  • Логи — что именно произошло. Структурированные события с контекстом: кто вызвал, с какими параметрами, что вернулось.
  • Метрики — как часто и насколько быстро. Счётчики, гистограммы, gauge-значения. Идеальны для алертинга и трендов.
  • Трейсы — путь запроса через систему. Распределённые системы без трейсов — это дебаггинг вслепую.

Ключевая проблема большинства команд: они используют один инструмент для всего. Grep по логам вместо трейсов, Prometheus без логов — получают неполную картину. Полноценная observability требует всех трёх.

Архитектура стека

Выбор компонентов прямолинеен: берём OpenTelemetry как vendor-neutral SDK для инструментации, Grafana Alloy как коллектор (пришёл на смену Grafana Agent в 2024), и три специализированных хранилища из экосистемы Grafana:

# Общая схема потока данных:
#
# App (Python/FastAPI)
#   └─► OpenTelemetry SDK
#         ├─► Alloy (collector, порт 4317 OTLP/gRPC)
#         │     ├─► Loki   (логи,    порт 3100)
#         │     ├─► Mimir  (метрики, порт 9009)
#         │     └─► Tempo  (трейсы,  порт 3200)
#         └─► Grafana (визуализация, порт 3000)
#               ├── datasource: Loki
#               ├── datasource: Mimir (Prometheus-compatible)
#               └── datasource: Tempo

Loki хранит логи как потоки с лейблами — не индексирует содержимое, что делает его в разы дешевле Elasticsearch. Mimir — горизонтально масштабируемое хранилище метрик, совместимое с Prometheus Remote Write. Tempo — distributed tracing backend с поддержкой OTLP, Jaeger и Zipkin форматов.

Docker Compose: поднимаем стек за 5 минут

# docker-compose.yml
version: "3.9"

volumes:
  loki_data:
  mimir_data:
  tempo_data:
  grafana_data:

networks:
  obs:
    driver: bridge

services:
  alloy:
    image: grafana/alloy:v1.3.1
    volumes:
      - ./config/alloy/config.alloy:/etc/alloy/config.alloy
    ports:
      - "4317:4317"   # OTLP gRPC
      - "4318:4318"   # OTLP HTTP
      - "12345:12345" # Alloy UI
    command: run --server.http.listen-addr=0.0.0.0:12345 /etc/alloy/config.alloy
    networks: [obs]

  loki:
    image: grafana/loki:3.2.0
    volumes:
      - loki_data:/loki
      - ./config/loki/loki.yaml:/etc/loki/local-config.yaml
    ports:
      - "3100:3100"
    command: -config.file=/etc/loki/local-config.yaml
    networks: [obs]

  mimir:
    image: grafana/mimir:2.13.0
    volumes:
      - mimir_data:/data
      - ./config/mimir/mimir.yaml:/etc/mimir/mimir.yaml
    ports:
      - "9009:9009"
    command: --config.file=/etc/mimir/mimir.yaml
    networks: [obs]

  tempo:
    image: grafana/tempo:2.6.0
    volumes:
      - tempo_data:/var/tempo
      - ./config/tempo/tempo.yaml:/etc/tempo.yaml
    ports:
      - "3200:3200"
    command: -config.file=/etc/tempo.yaml
    networks: [obs]

  grafana:
    image: grafana/grafana:11.3.0
    environment:
      - GF_AUTH_ANONYMOUS_ENABLED=true
      - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
    volumes:
      - grafana_data:/var/lib/grafana
      - ./config/grafana/provisioning:/etc/grafana/provisioning
    ports:
      - "3000:3000"
    depends_on: [loki, mimir, tempo]
    networks: [obs]

Конфигурация Grafana Alloy

Alloy использует язык конфигурации River. Принимаем OTLP-данные и маршрутизируем по типу:

// config/alloy/config.alloy

// OTLP-приёмник (gRPC + HTTP)
otelcol.receiver.otlp "default" {
  grpc {
    endpoint = "0.0.0.0:4317"
  }
  http {
    endpoint = "0.0.0.0:4318"
  }

  output {
    metrics = [otelcol.exporter.prometheus.mimir.input]
    logs    = [otelcol.exporter.loki.default.input]
    traces  = [otelcol.exporter.otlp.tempo.input]
  }
}

// Метрики → Mimir (Prometheus Remote Write)
otelcol.exporter.prometheus "mimir" {
  forward_to = [prometheus.remote_write.mimir.receiver]
}

prometheus.remote_write "mimir" {
  endpoint {
    url = "http://mimir:9009/api/v1/push"
  }
}

// Логи → Loki
otelcol.exporter.loki "default" {
  forward_to = [loki.write.default.receiver]
}

loki.write "default" {
  endpoint {
    url = "http://loki:3100/loki/api/v1/push"
  }
}

// Трейсы → Tempo
otelcol.exporter.otlp "tempo" {
  client {
    endpoint = "tempo:4317"
    tls {
      insecure = true
    }
  }
}

Инструментация Python / FastAPI

OpenTelemetry для Python даёт автоинструментацию ASGI-фреймворков, SQLAlchemy, httpx и redis одной строкой. Добавляем зависимости:

pip install \
  opentelemetry-sdk \
  opentelemetry-exporter-otlp-proto-grpc \
  opentelemetry-instrumentation-fastapi \
  opentelemetry-instrumentation-sqlalchemy \
  opentelemetry-instrumentation-httpx

Настраиваем SDK и подключаем к FastAPI:

# telemetry.py
import logging
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._logs import set_logger_provider
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter
from opentelemetry.sdk.resources import Resource

OTLP_ENDPOINT = "http://alloy:4317"

def setup_telemetry(service_name: str) -> None:
    resource = Resource.create({
        "service.name": service_name,
        "service.version": "1.0.0",
        "deployment.environment": "production",
    })

    # Трейсы
    tracer_provider = TracerProvider(resource=resource)
    tracer_provider.add_span_processor(
        BatchSpanProcessor(OTLPSpanExporter(endpoint=OTLP_ENDPOINT, insecure=True))
    )
    trace.set_tracer_provider(tracer_provider)

    # Метрики
    metric_reader = PeriodicExportingMetricReader(
        OTLPMetricExporter(endpoint=OTLP_ENDPOINT, insecure=True),
        export_interval_millis=15_000,
    )
    meter_provider = MeterProvider(resource=resource, metric_readers=[metric_reader])
    metrics.set_meter_provider(meter_provider)

    # Логи через OTLP
    logger_provider = LoggerProvider(resource=resource)
    logger_provider.add_log_record_processor(
        BatchLogRecordProcessor(OTLPLogExporter(endpoint=OTLP_ENDPOINT, insecure=True))
    )
    set_logger_provider(logger_provider)

    handler = LoggingHandler(level=logging.INFO, logger_provider=logger_provider)
    logging.getLogger().addHandler(handler)
    logging.getLogger().setLevel(logging.INFO)
# main.py
from fastapi import FastAPI
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
from telemetry import setup_telemetry

setup_telemetry("api-service")

app = FastAPI()
FastAPIInstrumentor.instrument_app(app)
SQLAlchemyInstrumentor().instrument()

# Кастомные метрики
from opentelemetry import metrics
meter = metrics.get_meter("api-service")
request_counter = meter.create_counter(
    "http.requests.total",
    description="Число HTTP-запросов",
)
processing_time = meter.create_histogram(
    "task.processing.duration",
    description="Время обработки задачи (сек)",
    unit="s",
)

@app.get("/orders/{order_id}")
async def get_order(order_id: int):
    import logging, time
    log = logging.getLogger(__name__)

    start = time.time()
    request_counter.add(1, {"endpoint": "/orders", "method": "GET"})

    # Бизнес-логика...
    result = {"order_id": order_id, "status": "processing"}

    processing_time.record(time.time() - start, {"endpoint": "/orders"})
    log.info("Order fetched", extra={"order_id": order_id, "status": result["status"]})
    return result

Grafana: провижининг дашбордов

Провижининг datasource'ов через YAML — никакого ручного кликанья в UI:

# config/grafana/provisioning/datasources/datasources.yaml
apiVersion: 1

datasources:
  - name: Loki
    type: loki
    url: http://loki:3100
    isDefault: false
    jsonData:
      maxLines: 1000

  - name: Mimir
    type: prometheus
    url: http://mimir:9009/prometheus
    isDefault: true
    jsonData:
      timeInterval: "15s"

  - name: Tempo
    type: tempo
    url: http://tempo:3200
    isDefault: false
    jsonData:
      tracesToLogsV2:
        datasourceUid: loki
        spanStartTimeShift: "-1h"
        spanEndTimeShift: "1h"
        filterByTraceID: true
      serviceMap:
        datasourceUid: mimir
      nodeGraph:
        enabled: true

Связка Tempo → Loki позволяет из трейса одним кликом прыгнуть в логи того же сервиса за тот же промежуток времени. Это убивает необходимость переключаться между инструментами.

Реальная стоимость

Весь стек работает на одном VPS с 4 vCPU / 8 GB RAM. Для нагрузки до 50 rps и retention 30 дней этого хватает.

# Примерное потребление ресурсов в idle:
# alloy:   ~150 MB RAM, ~0.1 CPU
# loki:    ~300 MB RAM, ~0.2 CPU
# mimir:   ~400 MB RAM, ~0.3 CPU
# tempo:   ~250 MB RAM, ~0.1 CPU
# grafana: ~200 MB RAM, ~0.1 CPU
# Итого:   ~1.3 GB RAM, ~0.8 CPU

# Стоимость VPS (Hetzner CX21, 4 vCPU / 8 GB):
# ~6 EUR/мес ≈ $6.5
# + объектное хранилище для долгосрочного retention (опционально):
# Hetzner Object Storage: $5/мес за 1 TB
# Итого: ~$12–20/мес против $1500–3000/мес у Datadog

При росте нагрузки Loki, Mimir и Tempo поддерживают горизонтальное масштабирование и S3-compatible object storage в качестве бэкенда — архитектура не упирается в потолок.

Vendor lock-in в observability — это двойная ловушка: платите много сейчас и платите ещё больше за миграцию потом. OpenTelemetry SDK на стороне приложения и open-source хранилища снимают обе проблемы. $20/мес VPS даёт полноценные три столпа observability — с retention, алертингом и красивыми дашбордами.


← все статьи Следующая →HTMX: когда не нужен React