gendesign/ops/metrics/alloy/alloy-infra.alloy
bot-backend 655652ae1c
All checks were successful
CI Trade-In / changes (pull_request) Successful in 11s
CI / changes (pull_request) Successful in 14s
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
feat(ops): алерты уровня приложения и три слепые зоны мониторинга
- AppHighErrorRate / AppHighLatencyP95 (job="app", severity=critical,
  host="apps") — доля 5xx и p95 задержки по http_requests_total /
  http_request_duration_seconds_bucket теперь ловятся Prometheus'ом, а не
  только постфактум в GlitchTip. Пара critical+apps обязательна для
  маршрута telegram-clients в alertmanager.yml.tmpl.
- Inhibit по HostAgentDown больше не гасит critical того же хоста —
  target_matchers сужен до severity="warning" (падение node-exporter
  раньше молча забирало с собой PostgresLongTransactionCritical и
  critical-алерты cAdvisor).
- cAdvisor: keep-фильтр в alloy-infra.alloy резал у него `up` наравне с
  container_*-мусором — job "cadvisor" не публиковал свою же серию `up`.
  Пропущены up/scrape_samples_scraped, добавлен CadvisorDown.
- TradeInBackgroundContainerMissing по absent(container_last_seen) на
  tradein-tgbot/tradein-scraper — эти контейнеры не HTTP-сервисы и в
  up{} не участвуют вовсе; крэш без рестарта раньше не алертился.

Не закрыто: живой, но зависший процесс tgbot/scraper (container_last_seen
не про внутренний прогресс, а про то, что Docker видит контейнер running).

Refs #3471
2026-09-12 13:57:54 +03:00

191 lines
10 KiB
Text
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Alloy — агент на ИНФРАСТРУКТУРНОМ хосте (Beget). Приёмник стоит рядом, на этом же
// хосте, поэтому пишем напрямую по docker-сети: без TLS, без basic_auth, без интернета.
//
// Вариант для продуктового хоста — alloy-apps.alloy: там тот же набор источников, но
// доставка идёт исходящим HTTPS через metrics.gendsgn.ru.
//
// Метка host проставляется здесь, один раз, в external_labels — если ставить её ещё и
// на стороне Prometheus, результат зависит от honor_labels и перестаёт быть очевидным.
logging {
level = "warn"
format = "logfmt"
}
// ═══ КУДА ОТПРАВЛЯЕМ ═══════════════════════════════════════════════════════════
prometheus.remote_write "central" {
endpoint {
url = "http://prometheus:9090/api/v1/write"
}
external_labels = {
host = sys.env("METRICS_HOST_LABEL"),
}
}
loki.write "central" {
endpoint {
url = "http://loki:3100/loki/api/v1/push"
}
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"
}
// cAdvisor отдаёт ряды на каждый слой файловой системы и каждое устройство.
// Без прополки TSDB растёт на порядок ради данных, которые никто не открывает.
prometheus.relabel "cadvisor_trim" {
forward_to = [prometheus.remote_write.central.receiver]
// `up`/`scrape_samples_scraped` — служебные ряды самого скрейпа, не
// container_*-метрики. Без явного допуска этот keep-фильтр резал их вместе
// с прочим шумом, и у cAdvisor как job'а не было своей серии `up` вообще —
// его смерть выглядела так же, как «ничего не изменилось» (#3471).
rule {
source_labels = ["__name__"]
regex = "up|scrape_samples_scraped|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.
//
// `up`/`scrape_samples_scraped` лейбла `name` не несут вовсе (они не про
// конкретный контейнер, а про сам скрейп) — фильтр по нему вырезал бы и их.
// Поэтому здесь смотрим на пару (__name__, name): для служебных рядов
// достаточно самого __name__, для контейнерных метрик по-прежнему обязателен
// непустой name.
rule {
source_labels = ["__name__", "name"]
regex = "up;.*|scrape_samples_scraped;.*|container_[^;]*;.+"
action = "keep"
}
}
// ═══ МЕТРИКИ ИНФРАСТРУКТУРНОЙ БД ═══════════════════════════════════════════════
// Профиль infra в docker-compose.metrics-agent.yml поднимает ровно один экспортер —
// по кластеру с базами forgejo и glitchtip.
prometheus.scrape "postgres_infra" {
targets = [
{ __address__ = "gendesign-pg-exporter-infra:9187", job = "postgres", db = "infra" },
]
forward_to = [prometheus.remote_write.central.receiver]
scrape_interval = "60s"
}
// ═══ ЛОГИ ══════════════════════════════════════════════════════════════════════
loki.source.journal "host" {
// Путь задан ЯВНО. Без него libsystemd включает SD_JOURNAL_LOCAL_ONLY, и из
// контейнера хостовый журнал не виден — при этом ошибки нет, просто пустой поток.
path = "/var/log/journal"
// При рестарте агента не тянем всю историю заново: journald держит ~13 суток,
// а повторная заливка создала бы дубликаты и упёрлась в reject_old_samples.
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) ─────────────────────────────────────────
// То же выражение, что в alloy-apps.alloy, и по той же причине — подробности
// там. Здесь оно нужно не меньше: на инфра-хосте живут Forgejo и GlitchTip,
// у обоих есть ручки с `?token=` в адресе, и их логи идут в тот же Loki.
//
// Группа захвата — на ЗНАЧЕНИИ: Alloy заменяет содержимое групп, а не весь
// совпавший фрагмент (как у DSN выше, где группа на пароле).
stage.replace {
expression = "(?i)[?&][\\w.-]*(?:secret|token|api[-_]?key|apikey|access[-_]?token|password|signature|sig)=([^&\\s\"'<>]+)"
replace = "***"
}
}
loki.relabel "journal" {
forward_to = []
// Внутренние поля journald приходят с префиксом __ и без переименования
// отбрасываются. Оставляем ровно те, по которым потом фильтруют.
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"
}