Прод упал в три ночи. Узнали в девять из письма клиента. В 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 Stack | ELK Stack | Datadog/New Relic | Zabbix |
|---|---|---|---|---|
| Стоимость | Бесплатно | Бесплатно | $100-5000/мес | Бесплатно |
| Простота setup | 1-2 часа | 4-8 часов | 30 минут | 2-4 часа |
| Ресурсы (RAM) | 1-2GB | 8-16GB | SaaS | 2-4GB |
| Метрики | Отлично | Не фокус | Отлично | Хорошо |
| Логи | Loki | Отлично (ES) | Отлично | Базово |
| Алерты | Alertmanager | Сложно | Отлично | Хорошо |
| Дашборды | Grafana | Kibana | Красиво | Слабее |
До десяти серверов беру этот стек. Нужен Elasticsearch под аудит: ELK. Бюджет есть и возиться лень: Datadog. Zabbix уже стоит: не трогаю.
┌─────────────────────────────────────────────────┐
│ Ваш сервер (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/monitoringservices:
# 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: bridgeprometheus/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.
- Prometheus: http://your-server-ip:9090
- Grafana: http://your-server-ip:3000, логин
admin, пароль из compose - Loki: http://your-server-ip:3100/ready должен вернуть
ready
В 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-clientfrom 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-clientconst 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 prometheusStatus → 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:
- monitoringserver:
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/syslogdocker compose up -dИли прямо из Python:
pip install python-logging-lokiimport 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 пишется до второго такого письма, не после.



