Learning Platform
Глоссарий Troubleshooting
Урок 20.03 · 25 мин
Средний
dockerobservabilityprometheusgrafanalokimonitoring

Observability стек: Prometheus + Grafana + Loki

В production у тебя 5+ контейнеров: Airflow webserver, scheduler, Postgres, Kafka, ClickHouse. Один из них тормозит — какой? Почему OOM Killer убил scheduler в 3 ночи? Сколько raw events в час уходит в Kafka?

Эти вопросы решает observability — три столпа:

  • Metrics — цифры во времени (CPU, RAM, queue depth, requests/sec). Хранятся как time-series.
  • Logs — текстовые события. INFO, ERROR, stacktraces.
  • Traces — путь запроса через сервисы. Где задержка — в API, БД, или в очереди?

Стандарт 2026 — стек на open-source: Prometheus (метрики) + Grafana (UI) + Loki (логи) + Tempo (traces) + cAdvisor (per-container metrics). Все они живут в compose-файле, который ты можешь скопировать и поднять за 5 минут.


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

Observability: 5 сервисов, единая Grafana
cAdvisorcontainer metricsDaemon от Google. Скрейпит per-container CPU, memory, network, fs I/O. Экспортирует Prometheus-формат на /metrics.
scrape
PrometheusTSDBTime-series database. Каждые 15 секунд скрейпит /metrics со всех target'ов. Хранит в свой TSDB-format.
query
GrafanaUI :3000Дашборды и алерты. Подключается к Prometheus как data source, к Loki для логов, к Tempo для traces.
Loki :3100Loki -- log aggregation. Не индексирует содержимое (как ElasticSearch), индексирует только metadata-labels.
PromtailPromtail -- агент. Читает /var/log и docker logs, отправляет в Loki с labels.
Tempo :3200Tempo -- distributed tracing. Принимает spans через OTLP / Jaeger / Zipkin protocols.

Шаг 1: Prometheus + cAdvisor

Как /proc и cgroups экспортируют метрики процессов
# compose.observability.yml
services:
  prometheus:
    image: prom/prometheus:v3.0.1
    container_name: prometheus
    ports:
      - "9090:9090"
    volumes:
      - ./observability/prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - prom-data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.path=/prometheus'
      - '--storage.tsdb.retention.time=15d'
      - '--web.enable-lifecycle'   # для reload через POST /-/reload
    networks:
      - obs-net

  cadvisor:
    image: gcr.io/cadvisor/cadvisor:v0.49.1
    container_name: cadvisor
    ports:
      - "8081:8080"
    volumes:
      - /:/rootfs:ro
      - /var/run:/var/run:ro
      - /sys:/sys:ro
      - /var/lib/docker/:/var/lib/docker:ro
      - /dev/disk/:/dev/disk:ro
    privileged: true
    devices:
      - /dev/kmsg
    networks:
      - obs-net

  node-exporter:
    image: prom/node-exporter:v1.8.2
    container_name: node-exporter
    ports:
      - "9100:9100"
    volumes:
      - /proc:/host/proc:ro
      - /sys:/host/sys:ro
      - /:/rootfs:ro
    command:
      - '--path.procfs=/host/proc'
      - '--path.rootfs=/rootfs'
      - '--path.sysfs=/host/sys'
    networks:
      - obs-net

volumes:
  prom-data:

networks:
  obs-net:

prometheus.yml:

global:
  scrape_interval: 15s
  evaluation_interval: 15s

scrape_configs:
  - job_name: 'prometheus'
    static_configs:
      - targets: ['localhost:9090']

  - job_name: 'cadvisor'
    static_configs:
      - targets: ['cadvisor:8080']

  - job_name: 'node-exporter'
    static_configs:
      - targets: ['node-exporter:9100']

  - job_name: 'airflow'
    metrics_path: '/admin/metrics/'
    static_configs:
      - targets: ['airflow-webserver:8080']
    # Airflow требует enable-Prometheus metrics через провайдер

  - job_name: 'clickhouse'
    static_configs:
      - targets: ['clickhouse:9363']   # ClickHouse Prometheus port

Что мы получаем:

  • cAdvisor — метрики каждого контейнера (CPU, RAM, network I/O). Самое полезное для DE: видеть, какой контейнер ест больше всех.
  • node-exporter — метрики хоста (load avg, disk usage, NIC).
  • Airflow / ClickHouse — application metrics (DAG-run-count, query-duration, и т.д.).

Шаг 2: Grafana

services:
  grafana:
    image: grafana/grafana:11.3.1
    container_name: grafana
    ports:
      - "3000:3000"
    environment:
      GF_SECURITY_ADMIN_PASSWORD: admin
      GF_USERS_ALLOW_SIGN_UP: 'false'
    volumes:
      - grafana-data:/var/lib/grafana
      - ./observability/grafana/provisioning:/etc/grafana/provisioning:ro
    depends_on:
      - prometheus
    networks:
      - obs-net

volumes:
  grafana-data:

Provisioning datasource через файл ./observability/grafana/provisioning/datasources/prometheus.yml:

apiVersion: 1
datasources:
  - name: Prometheus
    type: prometheus
    url: http://prometheus:9090
    access: proxy
    isDefault: true

  - name: Loki
    type: loki
    url: http://loki:3100
    access: proxy

  - name: Tempo
    type: tempo
    url: http://tempo:3200
    access: proxy

При старте Grafana прочитает этот файл и автоматически создаст data sources — ничего руками настраивать не нужно.

Доступ: http://localhost:3000, login admin/admin.


Шаг 3: Loki + Promtail для логов

services:
  loki:
    image: grafana/loki:3.3.1
    container_name: loki
    ports:
      - "3100:3100"
    volumes:
      - ./observability/loki-config.yaml:/etc/loki/local-config.yaml:ro
      - loki-data:/loki
    command: -config.file=/etc/loki/local-config.yaml
    networks:
      - obs-net

  promtail:
    image: grafana/promtail:3.3.1
    container_name: promtail
    volumes:
      - /var/log:/var/log:ro
      - /var/lib/docker/containers:/var/lib/docker/containers:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./observability/promtail-config.yaml:/etc/promtail/config.yml:ro
    command: -config.file=/etc/promtail/config.yml
    depends_on:
      - loki
    networks:
      - obs-net

volumes:
  loki-data:

promtail-config.yaml:

server:
  http_listen_port: 9080

clients:
  - url: http://loki:3100/loki/api/v1/push

scrape_configs:
  - job_name: docker
    docker_sd_configs:
      - host: unix:///var/run/docker.sock
        refresh_interval: 5s
    relabel_configs:
      - source_labels: ['__meta_docker_container_name']
        regex: '/(.*)'
        target_label: 'container'
      - source_labels: ['__meta_docker_container_log_stream']
        target_label: 'stream'

Promtail отписывается на Docker events через docker.sock и автоматически собирает логи всех контейнеров. В Loki они приходят с label’ами container=..., stream=stdout|stderr.

В Grafana -> Explore -> Loki:

{container="airflow-scheduler"} |~ "ERROR"

Найдёт все ERROR-логи scheduler’а за последний час.

TIP

Loki НЕ индексирует содержимое логов (как ElasticSearch). Индексируется только labels (container, stream, namespace). Поэтому поиск по тексту — линейный scan, но запись и storage в десятки раз дешевле. Стратегия: label = metadata, |~ “regex” — для поиска.


Шаг 4: Tempo для traces

services:
  tempo:
    image: grafana/tempo:2.6.1
    container_name: tempo
    ports:
      - "3200:3200"     # query
      - "4317:4317"     # OTLP gRPC
      - "4318:4318"     # OTLP HTTP
    volumes:
      - ./observability/tempo-config.yaml:/etc/tempo.yaml:ro
      - tempo-data:/tmp/tempo
    command: ["-config.file=/etc/tempo.yaml"]
    networks:
      - obs-net

volumes:
  tempo-data:

tempo-config.yaml:

server:
  http_listen_port: 3200

distributor:
  receivers:
    otlp:
      protocols:
        grpc:
          endpoint: 0.0.0.0:4317
        http:
          endpoint: 0.0.0.0:4318

storage:
  trace:
    backend: local
    local:
      path: /tmp/tempo/traces
    wal:
      path: /tmp/tempo/wal

Tempo принимает spans через OTLP (OpenTelemetry Protocol). Твой Python-код может слать spans:

# pip install opentelemetry-api opentelemetry-sdk opentelemetry-exporter-otlp
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter

provider = TracerProvider()
provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter(endpoint="http://tempo:4317", insecure=True)))
trace.set_tracer_provider(provider)

tracer = trace.get_tracer("my-etl")

with tracer.start_as_current_span("etl-job") as span:
    span.set_attribute("rows_processed", 100000)
    # ... твой код

В Grafana traces ищутся по trace ID или service name.


Application metrics: prometheus_client в Python

В твоём ETL-коде хочешь видеть бизнес-метрики (rows-processed, lag, error count):

# pip install prometheus_client
from prometheus_client import Counter, Histogram, Gauge, start_http_server

ROWS_PROCESSED = Counter('etl_rows_processed_total', 'Total rows processed', ['job_name'])
JOB_DURATION = Histogram('etl_job_duration_seconds', 'ETL job duration', ['job_name'])
ACTIVE_WORKERS = Gauge('etl_active_workers', 'Active worker count')

# Запускаем HTTP-сервер на 9000 для Prometheus scrape
start_http_server(9000)

def etl_job():
    with JOB_DURATION.labels(job_name='users').time():
        ACTIVE_WORKERS.inc()
        try:
            rows = process_users()
            ROWS_PROCESSED.labels(job_name='users').inc(rows)
        finally:
            ACTIVE_WORKERS.dec()

В Dockerfile:

FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
EXPOSE 9000
CMD ["python", "main.py"]

В Prometheus config:

- job_name: 'etl-app'
  static_configs:
    - targets: ['my-etl:9000']

Теперь в Grafana:

rate(etl_rows_processed_total[5m])

— покажет rows/sec в реальном времени.


Дашборды: что важно мониторить

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

  1. Container resources (CPU, memory) — из cAdvisor. Шаблон 14282 в grafana.com — готовый дашборд.

  2. Host resources (disk, network) — из node-exporter. Шаблон 1860.

  3. Application metrics — сколько rows прошло, lag consumer’а, время DAG-run’а.

  4. Logs — count ERROR за последний час, top sources of error.

  5. Alerts — через Grafana Alerting:

    • memory > 90% за 5 min
    • DAG run failed
    • Kafka consumer lag > 10000
WARNING

ВНИМАНИЕ: observability-stack сам потребляет ресурсы. Prometheus с 15-day retention на 10 контейнеров: ~5GB диска. Loki с логами: 10-30GB/неделя. На маленьком стенде это сравнимо с самой data-нагрузкой. Используй retention настройки агрессивно.


Полный compose-overlay

# compose.observability.yml -- запускается поверх основного стека
services:
  prometheus:
    image: prom/prometheus:v3.0.1
    profiles: ["observability"]
    ports: ["9090:9090"]
    volumes:
      - ./observability/prometheus.yml:/etc/prometheus/prometheus.yml:ro
    networks: [de-net]

  grafana:
    image: grafana/grafana:11.3.1
    profiles: ["observability"]
    ports: ["3000:3000"]
    volumes:
      - grafana-data:/var/lib/grafana
      - ./observability/grafana/provisioning:/etc/grafana/provisioning:ro
    networks: [de-net]

  cadvisor:
    image: gcr.io/cadvisor/cadvisor:v0.49.1
    profiles: ["observability"]
    volumes:
      - /:/rootfs:ro
      - /var/run:/var/run:ro
      - /sys:/sys:ro
      - /var/lib/docker/:/var/lib/docker:ro
    privileged: true
    networks: [de-net]

  loki:
    image: grafana/loki:3.3.1
    profiles: ["observability"]
    networks: [de-net]

  promtail:
    image: grafana/promtail:3.3.1
    profiles: ["observability"]
    volumes:
      - /var/lib/docker/containers:/var/lib/docker/containers:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
    networks: [de-net]

volumes:
  grafana-data:

networks:
  de-net:
    external: true

Поднимать:

docker compose --profile observability up -d
# Стек с Airflow + observability одновременно

# Только основной (без observability)
docker compose up -d

# Только observability (если основной стек уже live)
docker compose --profile observability up -d

Попробуй сам

# 1. Создай ./observability/prometheus.yml с минимальной конфой
mkdir -p observability
cat > observability/prometheus.yml <<EOF
global:
  scrape_interval: 15s
scrape_configs:
  - job_name: prometheus
    static_configs:
      - targets: [localhost:9090]
  - job_name: cadvisor
    static_configs:
      - targets: [cadvisor:8080]
EOF

# 2. Подними небольшую часть
docker compose -f compose.observability.yml --profile observability up -d prometheus grafana cadvisor

# 3. Открой Prometheus
open http://localhost:9090
# Status -> Targets -- должны быть UP

# 4. Открой Grafana
open http://localhost:3000   # admin/admin

# 5. Импортируй дашборд:
# Dashboards -> New -> Import -> 14282 (cAdvisor exporter)

# 6. Симулируй нагрузку:
docker run --rm -d --name stress alpine sh -c "yes > /dev/null"

# 7. В дашборде увидишь, как растёт CPU stress-контейнера

# 8. Cleanup
docker rm -f stress
docker compose -f compose.observability.yml --profile observability down -v

Проверка знанийKnowledge check
Ты поднял Prometheus + Grafana + cAdvisor, но в дашборде "Docker Containers" нет данных от твоего airflow-scheduler контейнера. В Prometheus -> Status -> Targets cadvisor показывает UP. В чём проблема?
ОтветAnswer
cAdvisor скрейпит контейнеры с того же docker daemon, что и сам запущен на. Если airflow-scheduler в другой compose-сети или на другом хосте -- cAdvisor его не видит, потому что не имеет доступа к /var/lib/docker. Решения: (1) Убедись, что cAdvisor запущен с volumes /var/lib/docker:/var/lib/docker:ro и privileged: true -- без этого нет доступа к namespace/cgroup-info других контейнеров. (2) Если контейнеры на разных хостах -- нужен cAdvisor на каждом хосте, в Prometheus добавь target для каждого. (3) Проверь labels в Prometheus -- cAdvisor может возвращать данные, но дашборд фильтрует по name="airflow-scheduler" точно; если у контейнера imя другое (через compose название -- "myproject-airflow-scheduler-1"), фильтр не сработает.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Что такое три столпа observability и какие OSS-инструменты их реализуют?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 4