// 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" } // ═══ МЕТРИКИ ПРИЛОЖЕНИЙ ════════════════════════════════════════════════════════ // Эндпоинты появляются в части 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 = "***" } } 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" }