// 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" }