All checks were successful
CI Trade-In / changes (pull_request) Successful in 13s
CI / changes (pull_request) Successful in 15s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
Prometheus не видел ни одной серии celery_*/redis_* — переполнение очереди Site Finder и залипший воркер снаружи выглядели одинаково, тишиной (issue #3471). Добавлено (только на продуктовом хосте, профиль apps): - redis-exporter (oliver006/redis_exporter) — здоровье общего Redis (db0 celery-брокер Site Finder, db1 SearchCache trade-in, db2 glitchtip), адрес через alias gendesign-redis на сети shared, без нового сетевого доступа. - celery-exporter (danihodovic/celery-exporter) — глубина очереди, число живых воркеров, счётчик неуспешных задач. Выбран вместо redis-exporter --check-keys, потому что дефолтная очередь "celery" дала бы только глубину, но не воркеров и не failures. - Скрейп обоих в alloy-apps.alloy. - Алерты в infra.yml: RedisDown, NoActiveCeleryWorkers, CeleryQueueGrowing (порог 150 предварительный — реальных данных по глубине очереди ещё нет, пересмотр через неделю наблюдений). Имена метрик celery-exporter (celery_queue_length, celery_worker_up, celery_task_failed_total) — по документации проекта, без прогона на реальном брокере; сверить после первого деплоя, см. комментарий у сервиса. promtool check rules — 23 правила, SUCCESS. Refs #3471
247 lines
12 KiB
Text
247 lines
12 KiB
Text
// Alloy — агент на ПРОДУКТОВОМ хосте (Poincare). Приёмник стоит у другого провайдера,
|
||
// поэтому доставка идёт исходящим HTTPS через metrics.gendsgn.ru под basic_auth.
|
||
//
|
||
// ПОЧЕМУ PUSH. Альтернатива — открыть на Poincare входящие порты под скрейп из центра.
|
||
// ufw там держит только 22/80/443, и расширять периметр ради мониторинга — плохой
|
||
// размен. Исходящий HTTPS уже разрешён, ничего открывать не нужно.
|
||
//
|
||
// ПРИ ОБРЫВЕ КАНАЛА агент копит в WAL и досылает. Pull-скрейп в этой ситуации просто
|
||
// потерял бы точки — а обрыв между площадками это ровно тот случай, ради которого
|
||
// мониторинг и заводится.
|
||
|
||
logging {
|
||
level = "warn"
|
||
format = "logfmt"
|
||
}
|
||
|
||
// ═══ КУДА ОТПРАВЛЯЕМ ═══════════════════════════════════════════════════════════
|
||
|
||
prometheus.remote_write "central" {
|
||
endpoint {
|
||
url = "https://metrics.gendsgn.ru/ingest/prometheus/api/v1/write"
|
||
|
||
basic_auth {
|
||
username = sys.env("METRICS_INGEST_USER")
|
||
password = sys.env("METRICS_INGEST_PASSWORD")
|
||
}
|
||
|
||
// Очередь на случай недоступности приёмника. Дефолты рассчитаны на облако с
|
||
// быстрым каналом; здесь между площадками 17 мс и приёмник может уехать в
|
||
// рестарт при деплое — даём запас, чтобы не терять точки на ровном месте.
|
||
queue_config {
|
||
capacity = 10000
|
||
max_shards = 5
|
||
min_shards = 1
|
||
max_samples_per_send = 2000
|
||
batch_send_deadline = "10s"
|
||
retry_on_http_429 = true
|
||
}
|
||
}
|
||
|
||
// Досыл после обрыва. Держим сутки: суточного окна хватает и на ночной рестарт
|
||
// приёмника, и на разбор утром.
|
||
wal {
|
||
truncate_frequency = "2h"
|
||
max_keepalive_time = "24h"
|
||
}
|
||
|
||
external_labels = {
|
||
host = sys.env("METRICS_HOST_LABEL"),
|
||
}
|
||
}
|
||
|
||
loki.write "central" {
|
||
endpoint {
|
||
url = "https://metrics.gendsgn.ru/ingest/loki/loki/api/v1/push"
|
||
|
||
basic_auth {
|
||
username = sys.env("METRICS_INGEST_USER")
|
||
password = sys.env("METRICS_INGEST_PASSWORD")
|
||
}
|
||
}
|
||
|
||
external_labels = {
|
||
host = sys.env("METRICS_HOST_LABEL"),
|
||
}
|
||
}
|
||
|
||
// ═══ МЕТРИКИ ХОСТА И КОНТЕЙНЕРОВ ═══════════════════════════════════════════════
|
||
|
||
prometheus.scrape "node" {
|
||
targets = [
|
||
{ __address__ = "node-exporter:9100", job = "node" },
|
||
]
|
||
forward_to = [prometheus.remote_write.central.receiver]
|
||
scrape_interval = "30s"
|
||
}
|
||
|
||
prometheus.scrape "cadvisor" {
|
||
targets = [
|
||
{ __address__ = "cadvisor:8080", job = "cadvisor" },
|
||
]
|
||
forward_to = [prometheus.relabel.cadvisor_trim.receiver]
|
||
scrape_interval = "30s"
|
||
}
|
||
|
||
prometheus.relabel "cadvisor_trim" {
|
||
forward_to = [prometheus.remote_write.central.receiver]
|
||
|
||
rule {
|
||
source_labels = ["__name__"]
|
||
regex = "container_(memory_(usage_bytes|working_set_bytes|rss)|cpu_(usage_seconds_total|cfs_throttled_seconds_total)|network_(receive|transmit)_bytes_total|fs_(usage|limit)_bytes|last_seen|spec_memory_limit_bytes|start_time_seconds|processes)"
|
||
action = "keep"
|
||
}
|
||
|
||
// Служебные ряды без имени (корневой cgroup, id="/") только зашумляют графики.
|
||
// keep .+ вместо drop с пустым regex — та же мысль, но без зависимости от
|
||
// трактовки пустого regex: в Alloy он неотличим от незаданного, а незаданный
|
||
// дефолтится в (.*).
|
||
//
|
||
// NB: на пустые панели это правило НЕ влияло. Причина была в cAdvisor 0.52 на
|
||
// Docker 29 — до сюда доезжал ровно один ряд, корневой. Лечится версией 0.55.1.
|
||
rule {
|
||
source_labels = ["name"]
|
||
regex = ".+"
|
||
action = "keep"
|
||
}
|
||
}
|
||
|
||
// ═══ МЕТРИКИ ОБЕИХ БОЕВЫХ БД ═══════════════════════════════════════════════════
|
||
// Именно здесь живут ряды, из-за отсутствия которых раздутие `listings` копилось
|
||
// 91 день незамеченным: WAL в сутки, n_tup_upd против n_tup_hot_upd, рост TOAST.
|
||
|
||
prometheus.scrape "postgres" {
|
||
targets = [
|
||
{ __address__ = "gendesign-pg-exporter-gendesign:9187", job = "postgres", db = "gendesign" },
|
||
{ __address__ = "gendesign-pg-exporter-tradein:9187", job = "postgres", db = "tradein" },
|
||
]
|
||
forward_to = [prometheus.remote_write.central.receiver]
|
||
scrape_interval = "60s"
|
||
}
|
||
|
||
// ═══ REDIS И ОЧЕРЕДЬ CELERY (#3471) ═════════════════════════════════════════════
|
||
// До этой правки ни одной серии redis_* / celery_* в Prometheus не было: глубина
|
||
// очереди, число живых воркеров и потеря соединения с брокером были невидимы —
|
||
// переполнение очереди и залипший воркер снаружи выглядели одинаково, тишиной.
|
||
|
||
prometheus.scrape "redis" {
|
||
targets = [
|
||
{ __address__ = "gendesign-redis-exporter:9121", job = "redis" },
|
||
]
|
||
forward_to = [prometheus.remote_write.central.receiver]
|
||
scrape_interval = "30s"
|
||
}
|
||
|
||
prometheus.scrape "celery" {
|
||
targets = [
|
||
{ __address__ = "gendesign-celery-exporter:9808", job = "celery" },
|
||
]
|
||
forward_to = [prometheus.remote_write.central.receiver]
|
||
scrape_interval = "30s"
|
||
}
|
||
|
||
// ═══ МЕТРИКИ ПРИЛОЖЕНИЙ ════════════════════════════════════════════════════════
|
||
// Эндпоинты появляются в части 3. До этого скрейп просто отдаёт `up 0` — и это
|
||
// правильно: цель видна как недоступная, а не отсутствует молча.
|
||
|
||
prometheus.scrape "apps" {
|
||
targets = [
|
||
{ __address__ = "gendesign-backend-1:8000", job = "app", app = "sitefinder" },
|
||
{ __address__ = "tradein-backend:8000", job = "app", app = "mera" },
|
||
]
|
||
metrics_path = "/metrics"
|
||
forward_to = [prometheus.remote_write.central.receiver]
|
||
scrape_interval = "30s"
|
||
scrape_timeout = "20s"
|
||
}
|
||
|
||
// ═══ ЛОГИ ══════════════════════════════════════════════════════════════════════
|
||
|
||
loki.source.journal "host" {
|
||
path = "/var/log/journal"
|
||
max_age = "12h"
|
||
format_as_json = false
|
||
labels = {
|
||
job = "journal",
|
||
}
|
||
relabel_rules = loki.relabel.journal.rules
|
||
forward_to = [loki.process.scrub_credentials.receiver]
|
||
}
|
||
|
||
// ── Скруббер учётных данных (#3114) ──────────────────────────────────────────
|
||
// Прод-факт: postgres_exporter при неудачном скрейпе печатает ПОЛНЫЙ DSN вместе
|
||
// с паролем. Замер по Loki за сутки — 104 строки на инфра-хосте и 106 на
|
||
// продуктовом, то есть при ретенции 30 дней в хранилище копится порядка 6000
|
||
// строк с паролями БД. Доступ к ним даёт вход в Grafana.
|
||
//
|
||
// Проверено на скретч-контейнерах: передача пароля отдельно
|
||
// (DATA_SOURCE_URI + DATA_SOURCE_USER + DATA_SOURCE_PASS_FILE) НЕ помогает —
|
||
// экспортер собирает строку подключения сам и логирует её целиком. Поэтому
|
||
// чистим на нашей стороне, до отправки в Loki.
|
||
//
|
||
// Это защита в глубину, а не замена причине: конкретно эта ошибка уходит
|
||
// грантом pg_monitor (#3114). Скруббер же ловит ЛЮБОЙ пароль в URL — в том
|
||
// числе из компонентов, о которых мы ещё не знаем.
|
||
//
|
||
// Маскируется только пароль: пользователь и адрес остаются, без них строка
|
||
// ошибки перестала бы годиться для диагностики. Одна группа захвата — Alloy
|
||
// заменяет содержимое групп, и вторая группа затёрла бы имя пользователя.
|
||
loki.process "scrub_credentials" {
|
||
forward_to = [loki.write.central.receiver]
|
||
|
||
stage.replace {
|
||
expression = "://[^:@/ ]+:([^@ ]+)@"
|
||
replace = "***"
|
||
}
|
||
|
||
// ── Секреты в query-строке (#3354) ─────────────────────────────────────────
|
||
// Прод-факт (#3154): uvicorn пишет в access-log ПОЛНЫЙ путь с query, и туда
|
||
// уезжал `?secret=<64 hex>` вебхука GlitchTip. Приложение чистит это у себя
|
||
// (tradein-mvp/backend/app/core/log_scrub.py), здесь — второй слой на случай
|
||
// строки, пришедшей мимо фильтра: другой процесс, sidecar, будущий логгер.
|
||
//
|
||
// Множество имён держим ОДИНАКОВЫМ с log_scrub.py. Префикс `[\w.-]*` перед
|
||
// альтернацией — ради суффиксных имён (`client_secret`, `refresh_token`,
|
||
// `webhook_secret`); значение обрывается на `&`, пробеле или кавычке, потому
|
||
// что access-строка uvicorn обрамляет запрос кавычками.
|
||
//
|
||
// Группа захвата стоит на ЗНАЧЕНИИ, а не на имени параметра: Alloy заменяет
|
||
// содержимое групп, а не весь совпавший фрагмент (ровно как у DSN выше — там
|
||
// группа на пароле, поэтому пользователь и хост уцелевают). Обернуть группой
|
||
// `?secret=` значило бы затереть имя и оставить сам секрет.
|
||
stage.replace {
|
||
expression = "(?i)[?&][\\w.-]*(?:secret|token|api[-_]?key|apikey|access[-_]?token|password|signature|sig)=([^&\\s\"'<>]+)"
|
||
replace = "***"
|
||
}
|
||
}
|
||
|
||
loki.relabel "journal" {
|
||
forward_to = []
|
||
|
||
rule {
|
||
source_labels = ["__journal__systemd_unit"]
|
||
target_label = "unit"
|
||
}
|
||
rule {
|
||
source_labels = ["__journal__hostname"]
|
||
target_label = "node"
|
||
}
|
||
rule {
|
||
source_labels = ["__journal_container_name"]
|
||
target_label = "container"
|
||
}
|
||
rule {
|
||
source_labels = ["__journal_priority_keyword"]
|
||
target_label = "level"
|
||
}
|
||
}
|
||
|
||
// ═══ САМОКОНТРОЛЬ ══════════════════════════════════════════════════════════════
|
||
|
||
prometheus.exporter.self "alloy" {}
|
||
|
||
prometheus.scrape "alloy_self" {
|
||
targets = prometheus.exporter.self.alloy.targets
|
||
forward_to = [prometheus.remote_write.central.receiver]
|
||
scrape_interval = "60s"
|
||
}
|