gendesign/ops/metrics/alloy/alloy-infra.alloy
bot-backend ed71292f03
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
fix(metrics): маскировать секреты из query-строки в Alloy до отправки в Loki
Скруббер #3115 знал одну форму — пароль в DSN (scheme://user:pass@host).
Секрет в query-строке (`?secret=<64 hex>` вебхука GlitchTip, #3154) проходил
насквозь и оседал в Loki на 30 суток ретенции.

Приложение чистит это у себя (#3353), но фильтр стоит на логгере ОДНОГО
процесса. Второй слой на сборщике закрывает всё, что придёт мимо: sidecar,
чужой процесс, будущий логгер без фильтра. Множество имён параметров взято
из log_scrub.py дословно, включая суффиксные client_secret/refresh_token.

Группа захвата стоит на значении, а не на имени параметра: Alloy заменяет
содержимое ГРУПП, а не весь совпавший фрагмент. Группа вокруг `?secret=`
затёрла бы имя и оставила сам секрет — то есть ровно наоборот.

Стадия добавлена и в alloy-infra.alloy: на инфра-хосте Forgejo и GlitchTip
с их `?token=` в адресах пишут в тот же Loki, дефект там тот же.

Closes #3354
2026-09-06 01:46:53 +05:00

181 lines
9.2 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]
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"
}
}
// ═══ МЕТРИКИ ИНФРАСТРУКТУРНОЙ БД ═══════════════════════════════════════════════
// Профиль 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"
}