All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 10s
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 / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 1m58s
CI / backend-tests (pull_request) Successful in 17m33s
postgres_exporter при неудачном скрейпе печатает полный DSN вместе с паролем. Замер по Loki за сутки: 104 такие строки на инфра-хосте и 106 на продуктовом. При ретенции 30 дней это порядка 6000 строк с паролями БД в хранилище, доступ к которому даёт вход в Grafana - причём внешний basic_auth с витрины сегодня же снят (#3113). Проверено экспериментом, а не предположено. Напрашивалось передать пароль отдельно от строки подключения (DATA_SOURCE_URI + DATA_SOURCE_USER + DATA_SOURCE_PASS_FILE). Прогон на скретч-контейнерах с настоящим файлом запросов показал: НЕ помогает - экспортер собирает строку сам и логирует её целиком. Первые три попытки воспроизведения были неинформативны (контейнер падал сразу; скрейпа не было; не подключён файл кастомных запросов) - утечка воспроизводится только при неудачном скрейпе с нашим queries.yml. Стало: ступень loki.process между источником журнала и loki.write, маскирует пароль в любом URL вида scheme://user:pass@host. Пользователь и адрес остаются - без них строка ошибки перестаёт годиться для диагностики. Ровно одна группа захвата: Alloy заменяет содержимое групп, вторая затёрла бы имя пользователя. Это защита в глубину, а не замена причине. Конкретно эта ошибка уходит грантом pg_monitor (запрос pg_wal_bytes в нашем queries.yml требует pg_ls_waldir) - это боевая БД и остаётся за владельцем. Скруббер же ловит любой пароль в URL, включая компоненты, о которых мы ещё не знаем. Регулярка без экранирования намеренно: Alloy не принимает \s в строке (unknown escape sequence), поэтому класс задан явным пробелом. Оба конфига прогнаны через `alloy fmt` образом grafana/alloy:v1.6.1 - синтаксис ok. Тесты (8) берут выражение ИЗ КОНФИГА и применяют к настоящей строке из прода: пароль исчезает; пользователь и адрес остаются; обычные URL и почтовые адреса не портятся; журнал направлен в скруббер, а не мимо него. Фальсификация: на исходных конфигах краснеют все 8. tests/ops целиком - 43 passed.
169 lines
8.2 KiB
Text
169 lines
8.2 KiB
Text
// 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 = "***"
|
||
}
|
||
}
|
||
|
||
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"
|
||
}
|