Перейти к содержимому

Prometheus, Grafana и Loki на одном сервере

Константин Потапов
16 мин

Прод упал в три ночи, письмо клиента пришло в девять. Стек на вечер: метрики, логи, алерты без Datadog и без отдельного DevOps.

Prometheus, Grafana и Loki на одном сервере

Прод упал в три ночи. Узнали в девять из письма клиента. В 2023-м консультировал стартап на $2M ARR: алертов не было, мониторинг это SSH и tail -f. Диск кончился, база встала, шесть часов работы и $15k выручки.

Нормальный контур поднимается за вечер. Без Kubernetes, без Datadog за три тысячи в месяц, без отдельной команды.

Три случая, когда слепота уже стоила денег

Fintech, 2024. API тормозил, «что-то не так» все знали, что именно никто. За вечер поставили Prometheus. Одна точка делала 300+ SQL на один HTTP. Убрали N+1, 2.5 s стало 120 ms.

Django-магазин, 2025. Раз в два-три дня прод тупил без рисунка. htop молчал. Grafana по памяти: Celery worker съел 16 GB, OOM-killer его убивал. Утечка в обработке картинок. Два часа на фикс.

B2B SaaS, 2023. Клиенты жаловались на странные тормоза. Раз в шесть часов пик латентности. Loki нашёл pg_dump без nice на той же машине. CPU в потолок, приложение задыхалось. Бэкапы уехали на другой сервер.

Без метрик о проблеме сообщает клиент или касса.

Метрики: числа. CPU, память, RPS, latency, error rate. Логи: события. Трейсы: путь запроса, здесь не трогаю, это уже про микросервисы.

Спидометр, запись «двигатель перегрелся в 14:35», регистратор. Без первых двух ведёшь машину вслепую и узнаёшь о поломке, когда она уже стоит.

Почему этот стек

Пробовал Zabbix, Nagios, ELK, Datadog, New Relic. Для большинства проектов хватает Prometheus, Grafana и Loki.

Бесплатно, без vendor lock. Живёт на сервере с 2 GB RAM. Exporters есть на Postgres, Redis, Nginx, Node. Я поднимал даже на Hetzner CX21 за пять долларов.

КритерийPrometheus StackELK StackDatadog/New RelicZabbix
СтоимостьБесплатноБесплатно$100-5000/месБесплатно
Простота setup1-2 часа4-8 часов30 минут2-4 часа
Ресурсы (RAM)1-2GB8-16GBSaaS2-4GB
МетрикиОтличноНе фокусОтличноХорошо
ЛогиLokiОтлично (ES)ОтличноБазово
АлертыAlertmanagerСложноОтличноХорошо
ДашбордыGrafanaKibanaКрасивоСлабее

До десяти серверов беру этот стек. Нужен Elasticsearch под аудит: ELK. Бюджет есть и возиться лень: Datadog. Zabbix уже стоит: не трогаю.

PrometheusGrafanaLokiDockerNode Exporter
┌─────────────────────────────────────────────────┐
│  Ваш сервер (2-4GB RAM)                         │
├─────────────────────────────────────────────────┤
│                                                 │
│  ┌─────────────────┐  ┌──────────────┐          │
│  │ Ваше приложение │  │ PostgreSQL   │          │
│  │ (FastAPI/Django)│  │ / Redis      │          │
│  └────┬────────────┘  └────┬─────────┘          │
│       │                    │                    │
│       │ metrics            │ metrics            │
│       │ + logs             │ (exporter)         │
│       ▼                    ▼                    │
│  ┌─────────────────────────────────┐            │
│  │ Prometheus                      │            │
│  │ (собирает метрики каждые 15s)   │            │
│  └───────────┬─────────────────────┘            │
│              │                                  │
│              │ query                            │
│              ▼                                  │
│  ┌─────────────────────────────────┐            │
│  │ Grafana                         │            │
│  │ (визуализация + алерты)         │            │
│  └───────────┬─────────────────────┘            │
│              │ query                            │
│  ┌───────────▼─────────────────────┐            │
│  │ Loki                            │            │
│  │ (хранит логи)                   │            │
│  └─────────────────────────────────┘            │
│                                                 │
└─────────────────────────────────────────────────┘

Prometheus ходит по HTTP раз в 15-30 секунд. Grafana рисует. Loki индексирует логи. node_exporter, postgres_exporter, redis_exporter, nginx_exporter отдают метрики в его формате.

Минимум: 2 GB RAM, 2 ядра, 20 GB диска. Лучше 4 GB и 50 GB. Ubuntu 22.04/24.04 или Debian 12 с Docker.

Поднимаем

mkdir -p /opt/monitoring
cd /opt/monitoring
services:
  # Prometheus: собирает метрики
  prometheus:
    image: prom/prometheus:v3.0.0
    container_name: prometheus
    command:
      - "--config.file=/etc/prometheus/prometheus.yml"
      - "--storage.tsdb.path=/prometheus"
      - "--storage.tsdb.retention.time=30d" # Храним метрики 30 дней
      - "--web.enable-lifecycle" # API для hot-reload конфига
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
      - ./prometheus/alerts.yml:/etc/prometheus/alerts.yml
      - prometheus_data:/prometheus
    restart: unless-stopped
    networks:
      - monitoring
 
  # Grafana: визуализация
  grafana:
    image: grafana/grafana:11.4.0
    container_name: grafana
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=your_secure_password # ПОМЕНЯЙТЕ!
      - GF_INSTALL_PLUGINS=grafana-piechart-panel
    volumes:
      - grafana_data:/var/lib/grafana
      - ./grafana/provisioning:/etc/grafana/provisioning
    restart: unless-stopped
    networks:
      - monitoring
 
  # Loki: логи
  loki:
    image: grafana/loki:3.3.2
    container_name: loki
    ports:
      - "3100:3100"
    command: -config.file=/etc/loki/local-config.yaml
    volumes:
      - loki_data:/loki
    restart: unless-stopped
    networks:
      - monitoring
 
  # Node Exporter: метрики сервера
  node_exporter:
    image: prom/node-exporter:v1.8.2
    container_name: node_exporter
    command:
      - "--path.rootfs=/host"
    ports:
      - "9100:9100"
    volumes:
      - /:/host:ro,rslave
    restart: unless-stopped
    networks:
      - monitoring
 
volumes:
  prometheus_data:
  grafana_data:
  loki_data:
 
networks:
  monitoring:
    driver: bridge

prometheus/prometheus.yml:

global:
  scrape_interval: 15s # Собираем метрики каждые 15 секунд
  evaluation_interval: 15s # Проверяем правила алертов каждые 15 секунд
 
# Алерты (создадим позже)
rule_files:
  - "/etc/prometheus/alerts.yml"
 
# Откуда собираем метрики
scrape_configs:
  # Сам Prometheus
  - job_name: "prometheus"
    static_configs:
      - targets: ["localhost:9090"]
 
  # Метрики сервера (CPU, RAM, Disk)
  - job_name: "node"
    static_configs:
      - targets: ["node_exporter:9100"]
 
  # Ваше приложение (FastAPI, Django, etc.)
  # Раскомментируйте и укажите адрес вашего приложения
  # - job_name: 'app'
  #   static_configs:
  #     - targets: ['app:8000']
 
  # PostgreSQL (если используете postgres_exporter)
  # - job_name: 'postgres'
  #   static_configs:
  #     - targets: ['postgres_exporter:9187']
 
  # Redis (если используете redis_exporter)
  # - job_name: 'redis'
  #   static_configs:
  #     - targets: ['redis_exporter:9121']

Пустой prometheus/alerts.yml:

groups:
  - name: basic_alerts
    interval: 30s
    rules: []
docker compose up -d
docker compose ps

Четыре контейнера Up: prometheus, grafana, loki, node_exporter.

В Grafana: Connections → Add data source → Prometheus, URL http://prometheus:9090. То же для Loki: http://loki:3100.

Готовый дашборд не рисую. Dashboards → Import → ID 1860 (Node Exporter Full). Postgres: 9628. Redis: 11835. Nginx: 12708. Docker: 893.

Метрики приложения

pip install prometheus-client
from fastapi import FastAPI
from prometheus_client import Counter, Histogram, make_asgi_app
 
app = FastAPI()
 
# Метрики
REQUEST_COUNT = Counter(
    'app_requests_total',
    'Total HTTP requests',
    ['method', 'endpoint', 'status']
)
 
REQUEST_DURATION = Histogram(
    'app_request_duration_seconds',
    'HTTP request duration',
    ['method', 'endpoint']
)
 
@app.middleware("http")
async def prometheus_middleware(request, call_next):
    method = request.method
    endpoint = request.url.path
 
    with REQUEST_DURATION.labels(method, endpoint).time():
        response = await call_next(request)
 
    REQUEST_COUNT.labels(method, endpoint, response.status_code).inc()
    return response
 
# Endpoint для Prometheus
metrics_app = make_asgi_app()
app.mount("/metrics", metrics_app)

Метрики на http://your-app:8000/metrics.

pip install django-prometheus
# settings.py
INSTALLED_APPS = [
    'django_prometheus',
    # ...
]
 
MIDDLEWARE = [
    'django_prometheus.middleware.PrometheusBeforeMiddleware',
    # ... остальные middleware
    'django_prometheus.middleware.PrometheusAfterMiddleware',
]
 
# urls.py
urlpatterns = [
    path('', include('django_prometheus.urls')),
    # ...
]
npm install prom-client
const express = require("express");
const client = require("prom-client");
 
const app = express();
 
// Создаём registry
const register = new client.Registry();
 
// Собираем дефолтные метрики (CPU, memory, event loop)
client.collectDefaultMetrics({ register });
 
// Кастомные метрики
const httpRequestDuration = new client.Histogram({
  name: "http_request_duration_seconds",
  help: "Duration of HTTP requests in seconds",
  labelNames: ["method", "route", "status_code"],
  registers: [register],
});
 
// Middleware для метрик
app.use((req, res, next) => {
  const start = Date.now();
  res.on("finish", () => {
    const duration = (Date.now() - start) / 1000;
    httpRequestDuration
      .labels(req.method, req.route?.path || req.path, res.statusCode)
      .observe(duration);
  });
  next();
});
 
// Endpoint для Prometheus
app.get("/metrics", async (req, res) => {
  res.set("Content-Type", register.contentType);
  res.end(await register.metrics());
});
 
app.listen(3000);

В prometheus.yml:

scrape_configs:
  # ... existing jobs
 
  - job_name: "myapp"
    static_configs:
      - targets: ["host.docker.internal:8000"] # Ваше приложение

Если приложение в том же Compose, цель myapp:8000.

docker compose restart prometheus

Status → Targets: приложение должно быть UP.

Логи

Promtail забирает файлы и толкает в Loki.

promtail:
  image: grafana/promtail:3.3.2
  container_name: promtail
  volumes:
    - /var/log:/var/log:ro # Логи системы
    - ./promtail/config.yml:/etc/promtail/config.yml
    - ./logs:/app/logs:ro # Логи вашего приложения
  command: -config.file=/etc/promtail/config.yml
  restart: unless-stopped
  networks:
    - monitoring
server:
  http_listen_port: 9080
  grpc_listen_port: 0
 
positions:
  filename: /tmp/positions.yaml
 
clients:
  - url: http://loki:3100/loki/api/v1/push
 
scrape_configs:
  # Логи вашего приложения
  - job_name: app
    static_configs:
      - targets:
          - localhost
        labels:
          job: app
          __path__: /app/logs/*.log
 
  # Системные логи (опционально)
  - job_name: system
    static_configs:
      - targets:
          - localhost
        labels:
          job: syslog
          __path__: /var/log/syslog
docker compose up -d

Или прямо из Python:

pip install python-logging-loki
import logging
from logging_loki import LokiHandler
 
logger = logging.getLogger("my-app")
logger.setLevel(logging.INFO)
 
loki_handler = LokiHandler(
    url="http://loki:3100/loki/api/v1/push",
    tags={"application": "my-app", "environment": "production"},
    version="1",
)
 
logger.addHandler(loki_handler)
 
logger.info("Application started")
logger.error("Something went wrong", extra={"user_id": 123})

В Grafana: Explore → Loki → {job="app"}.

# Все логи приложения
{job="app"}
 
# Только ошибки
{job="app"} |= "ERROR"
 
# Логи конкретного пользователя
{job="app"} | json | user_id="123"
 
# Rate ошибок за последние 5 минут
rate({job="app"} |= "ERROR" [5m])

Алерты

prometheus/alerts.yml:

groups:
  - name: critical_alerts
    interval: 30s
    rules:
      # Сервер недоступен
      - alert: InstanceDown
        expr: up == 0
        for: 1m
        labels:
          severity: critical
        annotations:
          summary: "Instance {{ $labels.instance }} is down"
          description: "{{ $labels.job }} has been down for more than 1 minute."
 
      # CPU больше 80%
      - alert: HighCPU
        expr: 100 - (avg by(instance) (irate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 80
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "High CPU usage on {{ $labels.instance }}"
          description: "CPU usage is above 80% for 5 minutes (current: {{ $value }}%)"
 
      # RAM больше 90%
      - alert: HighMemory
        expr: (1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) * 100 > 90
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "High memory usage on {{ $labels.instance }}"
          description: "Memory usage is above 90% (current: {{ $value }}%)"
 
      # Диск больше 85%
      - alert: DiskSpaceLow
        expr: (1 - (node_filesystem_avail_bytes{fstype!~"tmpfs|fuse.lxcfs"} / node_filesystem_size_bytes)) * 100 > 85
        for: 10m
        labels:
          severity: warning
        annotations:
          summary: "Low disk space on {{ $labels.instance }}"
          description: "Disk {{ $labels.mountpoint }} is {{ $value }}% full"
 
      # Высокий error rate (>5% запросов с ошибками)
      - alert: HighErrorRate
        expr: rate(app_requests_total{status=~"5.."}[5m]) / rate(app_requests_total[5m]) > 0.05
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "High error rate in {{ $labels.job }}"
          description: "Error rate is {{ $value | humanizePercentage }} for 5 minutes"
 
      # Медленные запросы (p95 latency > 1s)
      - alert: SlowRequests
        expr: histogram_quantile(0.95, rate(app_request_duration_seconds_bucket[5m])) > 1
        for: 10m
        labels:
          severity: warning
        annotations:
          summary: "Slow requests in {{ $labels.job }}"
          description: "95th percentile latency is {{ $value }}s"
curl -X POST http://localhost:9090/-/reload

Проверка: http://your-server-ip:9090/alerts

Alertmanager кладёт письма в Telegram, Slack, почту.

alertmanager:
  image: prom/alertmanager:v0.27.0
  container_name: alertmanager
  ports:
    - "9093:9093"
  volumes:
    - ./alertmanager/config.yml:/etc/alertmanager/config.yml
    - alertmanager_data:/alertmanager
  command:
    - "--config.file=/etc/alertmanager/config.yml"
  restart: unless-stopped
  networks:
    - monitoring
 
volumes:
  # ... existing volumes
  alertmanager_data:

В начало prometheus.yml:

# Добавьте в начало файла
alerting:
  alertmanagers:
    - static_configs:
        - targets: ["alertmanager:9093"]
global:
  resolve_timeout: 5m
 
route:
  group_by: ["alertname", "cluster"]
  group_wait: 10s
  group_interval: 10s
  repeat_interval: 12h
  receiver: "telegram"
 
receivers:
  # Telegram (рекомендуется)
  - name: "telegram"
    telegram_configs:
      - bot_token: "YOUR_BOT_TOKEN" # Получите у @BotFather
        chat_id: YOUR_CHAT_ID # Ваш chat_id
        parse_mode: "HTML"
        message: |
          <b>{{ .Status | toUpper }}</b>
          {{ range .Alerts }}
          <b>Alert:</b> {{ .Labels.alertname }}
          <b>Severity:</b> {{ .Labels.severity }}
          <b>Summary:</b> {{ .Annotations.summary }}
          <b>Description:</b> {{ .Annotations.description }}
          {{ end }}
 
  # Slack (альтернатива)
  # - name: 'slack'
  #   slack_configs:
  #     - api_url: 'https://hooks.slack.com/services/YOUR/WEBHOOK/URL'
  #       channel: '#alerts'
  #       title: 'Alert: {{ .GroupLabels.alertname }}'
  #       text: '{{ range .Alerts }}{{ .Annotations.description }}{{ end }}'
 
  # Email (альтернатива)
  # - name: 'email'
  #   email_configs:
  #     - to: 'your-email@example.com'
  #       from: 'alerts@yourapp.com'
  #       smarthost: 'smtp.gmail.com:587'
  #       auth_username: 'your-email@gmail.com'
  #       auth_password: 'your-app-password'
docker compose up -d

Бот: @BotFather → /newbot → токен. Потом /start боту и https://api.telegram.org/bot<bot_token>/getUpdates, в ответе "chat":{"id":123456789}.

Проверка CPU:

# Загружаем CPU
yes > /dev/null &
yes > /dev/null &
yes > /dev/null &
yes > /dev/null &
 
# Через 5 минут должен сработать алерт HighCPU
# Проверьте: http://your-server-ip:9090/alerts
 
# Остановите нагрузку:
killall yes

Через пять-шесть минут должно прийти в Telegram.

Дашборды, которые смотрю сам

RPS: rate(app_requests_total[1m]).

Error rate: (rate(app_requests_total{status=~"5.."}[5m]) / rate(app_requests_total[5m])) * 100. Пороги: 1% warning, 5% critical.

Latency:

histogram_quantile(0.50, rate(app_request_duration_seconds_bucket[5m]))  # p50
histogram_quantile(0.95, rate(app_request_duration_seconds_bucket[5m]))  # p95
histogram_quantile(0.99, rate(app_request_duration_seconds_bucket[5m]))  # p99

Топ медленных: topk(5, histogram_quantile(0.95, rate(app_request_duration_seconds_bucket[5m]))).

CPU: 100 - (avg by(instance) (irate(node_cpu_seconds_total{mode="idle"}[5m])) * 100).

Память: (1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) * 100.

Диск: rate(node_disk_read_bytes_total[5m]) и rate(node_disk_written_bytes_total[5m]).

Сеть: rate(node_network_receive_bytes_total[5m]) и rate(node_network_transmit_bytes_total[5m]).

Что делать в три ночи

HighCPU: график CPU, рядом RPS. Если пик трафика, виноват код. В Loki {job="app"} | json | line_format "{{.endpoint}} {{.duration}}". В Prometheus topk(5, rate(app_request_duration_seconds_sum[5m])).

HighMemory: process_resident_memory_bytes, логи OutOfMemory / MemoryError. Временно docker compose restart app. Дальше memory_profiler или py-spy.

HighErrorRate: какие эндпоинты отдают 5xx, в Loki ERROR и стектрейсы. База, чужой API, таймаут.

SlowRequests: p95 по эндпоинтам, конкретные запросы в логах, pg_stat_statements.

На каждый алерт нужен короткий runbook. В три ночи память хуже, чем днём у доски.

Что я больше не делаю

Дефолтные 15 дней хранения мало. Ставлю 30-90:

command:
  - "--storage.tsdb.retention.time=90d"
# loki-config.yaml
limits_config:
  retention_period: 30d

Не мониторю клики по кнопкам. Это аналитика. Смотрю RPS, ошибки, latency, CPU, RAM, диск.

Пятьдесят писем в минуту хуже тишины. Группирую. Critical звонит, warning идёт в Telegram, info остаётся в логах. На время деплоя алерты глушу, иначе ложные срабатывания.

Конфиги в Git. Порты 9090 и 3000 наружу не торчат: Nginx и пароль. Дефолтный пароль Grafana меняю в тот же вечер. Prometheus умеет показать лишнее.

Если метрики нужны годами: Thanos, Cortex или Grafana Cloud (бесплатно до 10k series).

В Kubernetes: Prometheus Operator и ServiceMonitor.

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: myapp
spec:
  selector:
    matchLabels:
      app: myapp
  endpoints:
    - port: metrics
      interval: 30s

Для микросервисов рядом кладут Tempo:

tempo:
  image: grafana/tempo:latest
  command: ["-config.file=/etc/tempo.yaml"]
  volumes:
    - ./tempo.yaml:/etc/tempo.yaml
  ports:
    - "3200:3200" # Tempo UI
    - "4317:4317" # OTLP gRPC

Дальше OpenTelemetry SDK в приложении.

Сколько это стоит

Hetzner CX31 4 GB: $7. Ещё 50 GB: $5. Setup один раз, 4-8 часов. Поддержка: 1-2 часа в месяц. Итого $12-15 и два часа.

Datadog $100-500. New Relic $99-749. Grafana Cloud от нуля до $299.

Главная экономия не в счёте за SaaS. Час простоя стоит от тысячи до десяти, смотря что за продукт. У меня стек окупился на первой неделе.

Дальше обычно добавляю базу, кеш, очереди и внешние API. Если обещан 99.9% uptime, сорок три минуты простоя в месяц уже инцидент. Runbook пишется до второго такого письма, не после.