gendesign/docker-compose.metrics-agent.yml
bot-backend c6e15954ba
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
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 / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
fix(observability): контейнерные метрики терялись целиком + два хвоста стека
Три независимых дефекта, найденных на живом проде после подъёма стека метрик.

1. cAdvisor-метрики не доезжали ВООБЩЕ. В Prometheus ноль имён container_*
   при 2034 именах всего, хотя cAdvisor отдаёт 880 рядов, scrape-таргет в
   alloy health=up с последним скрейпом 10 мс назад, а remote_write рабочий
   (node/postgres идут через него же и доезжают). Методом исключения — потери
   в prometheus.relabel.cadvisor_trim, во втором правиле:

       rule { source_labels = ["name"], regex = "", action = "drop" }

   Замысел был выкинуть безымянные cgroup-ряды (id="/"). Но regex в Alloy
   документированно дефолтится в (.*), и пустая строка неотличима от
   незаданного значения — такой drop рискует выкидывать вообще всё, что и
   наблюдалось. Заменено на однозначное keep regex=".+" — тот же замысел,
   без зависимости от того, как трактуется пустой regex.

2. healthcheck alloy не мог пройти никогда: дёргал wget, которого в образе
   grafana/alloy нет (как и curl, и nc). Контейнер вечно unhealthy при
   полностью исправном alloy — ложная тревога, маскирующая настоящие сбои.
   Заменено на сырой HTTP через bash /dev/tcp, без внешних утилит.

3. Prometheus раз в минуту писал "lookup alertmanager: no such host" и держал
   up{job="alertmanager"}=0. Alertmanager намеренно за профилем alerts до
   решения #3078 — дефект не в профиле, а в безусловной ссылке на сервис.
   Оба места (alerting.alertmanagers и job_name: alertmanager) переведены на
   file_sd_configs с файлом целей, по умолчанию пустым: целей нет — ошибок
   тоже нет. Prometheus перечитывает file_sd на лету, поэтому включение
   профиля сведётся к наполнению файла, без рестарта и правки конфига.
   Файл целей смонтирован в сервис prometheus явным volume.

Проверено на живом хосте, не на глаз:
- alloy fmt обоих .alloy в одноразовом контейнере grafana/alloy:v1.6.1 - exit 0
- promtool check config в prom/prometheus:v3.1.0 - valid, 16 rules found
- механизм нового healthcheck выполнен внутри работающего gendesign-alloy:
  первая строка ответа "HTTP/1.0 200 OK", grep матчится, RESULT=HEALTHY
- наличие bash/head/grep/printf в образе alloy подтверждено command -v
2026-08-26 13:14:28 +03:00

237 lines
12 KiB
YAML
Raw 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.

# Стек наблюдаемости — АГЕНТСКАЯ сторона. Поднимается на КАЖДОМ хосте: и на Beget
# (рядом с сервером), и на Poincare (рядом с продуктом).
#
# docker compose -p gendesign-metrics-agent -f docker-compose.metrics-agent.yml up -d
#
# Что делает: собирает метрики хоста (node-exporter), контейнеров (cAdvisor), баз
# (postgres-exporter) и логи journald, после чего Alloy отправляет всё на приёмник.
#
# ПОЧЕМУ АГЕНТ, А НЕ СКРЕЙП ИЗ ЦЕНТРА. Prometheus на Beget не может дотянуться до
# экспортеров на Poincare, не открыв там входящие порты — а ufw на Poincare намеренно
# держит только 22/80/443. Агент решает это push'ем: исходящий HTTPS уже разрешён.
# Побочный выигрыш — при обрыве канала Alloy копит в WAL и досылает, тогда как
# pull-скрейп просто теряет точки.
#
# КОНФИГУРАЦИЯ ЗАВИСИТ ОТ ХОСТА — задаётся переменными окружения, файл один:
#
# На Beget (инфраструктура, приёмник рядом):
# METRICS_ROLE=infra
# METRICS_ALLOY_CONFIG=alloy-infra.alloy # пишет напрямую в prometheus:9090 / loki:3100
# COMPOSE_PROFILES=infra
#
# На Poincare (продукт, приёмник за интернетом):
# METRICS_ROLE=apps
# METRICS_ALLOY_CONFIG=alloy-apps.alloy # пишет в https://metrics.gendsgn.ru под basic_auth
# COMPOSE_PROFILES=apps
# METRICS_INGEST_USER / METRICS_INGEST_PASSWORD — учётка приёмника
#
# Профили решают, какие postgres-exporter'ы поднимать: на Poincare две базы
# (Птица + МЕРА), на Beget одна (infra-postgres с forgejo и glitchtip).
x-logging: &default-logging
driver: journald
services:
# ── Alloy: единый агент метрик и логов ───────────────────────────────────────
# Почему Alloy, а не Promtail: у Promtail EOL 2 марта 2026. Alloy читает journald
# нативно и умеет одновременно скрейпить Prometheus-эндпоинты.
alloy:
image: grafana/alloy:v1.6.1
container_name: gendesign-alloy
restart: unless-stopped
# root нужен для чтения /var/log/journal. Штатный пользователь `alloy` требует
# членства в группах adm и systemd-journal — внутри контейнера этих групп с
# правильными gid хоста нет, и агент молча читает НОЛЬ записей. Проверено как
# известные грабли: отказ выглядит как «логов просто нет».
user: root
command:
- "run"
- "--server.http.listen-addr=0.0.0.0:12345"
- "--storage.path=/var/lib/alloy/data"
- "/etc/alloy/config.alloy"
env_file:
- path: ./backend/.env.runtime
required: false
- path: ./backend/.env
required: false
environment:
# Метка хоста попадает во все ряды и логи — без неё графики двух машин
# сливаются в один и разобрать, где что, невозможно.
METRICS_HOST_LABEL: ${METRICS_ROLE:?zadaj METRICS_ROLE=infra ili apps}
METRICS_INGEST_USER: ${METRICS_INGEST_USER:-}
METRICS_INGEST_PASSWORD: ${METRICS_INGEST_PASSWORD:-}
volumes:
- ./ops/metrics/alloy/${METRICS_ALLOY_CONFIG:?zadaj METRICS_ALLOY_CONFIG}:/etc/alloy/config.alloy:ro
# Явный путь к журналу обязателен. Без него libsystemd применяет
# SD_JOURNAL_LOCAL_ONLY и хостовые логи из контейнера не видны — при этом
# ошибки нет, просто пустой поток.
- /var/log/journal:/var/log/journal:ro
- /etc/machine-id:/etc/machine-id:ro
- alloy_data:/var/lib/alloy/data
expose:
- "12345"
networks:
- shared
mem_limit: 512m
logging: *default-logging
healthcheck:
# В образе grafana/alloy нет wget/curl/nc, только bash — шлём
# сырой HTTP-запрос через /dev/tcp и проверяем код 200 в ответе.
test:
[
"CMD-SHELL",
"bash -c 'exec 3<>/dev/tcp/localhost/12345 && printf \"GET /-/ready HTTP/1.0\r\n\r\n\" >&3 && head -1 <&3 | grep -q 200' || exit 1",
]
interval: 30s
timeout: 10s
retries: 5
start_period: 30s
# ── node-exporter: CPU, память, диск, сеть, IO хоста ─────────────────────────
node-exporter:
image: prom/node-exporter:v1.8.2
container_name: gendesign-node-exporter
restart: unless-stopped
command:
- "--path.procfs=/host/proc"
- "--path.sysfs=/host/sys"
- "--path.rootfs=/host/root"
# Без исключения оверлеев метрика файловой системы засоряется десятками
# слоёв docker, и «свободное место на диске» перестаёт читаться глазами.
- "--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc|var/lib/docker/.+)($$|/)"
- "--collector.filesystem.fs-types-exclude=^(autofs|binfmt_misc|cgroup2?|configfs|debugfs|devpts|devtmpfs|fusectl|hugetlbfs|mqueue|nsfs|overlay|proc|procfs|pstore|rpc_pipefs|securityfs|selinuxfs|squashfs|sysfs|tracefs)$$"
pid: host
volumes:
- /proc:/host/proc:ro
- /sys:/host/sys:ro
- /:/host/root:ro,rslave
expose:
- "9100"
networks:
- shared
mem_limit: 128m
logging: *default-logging
# ── cAdvisor: память и CPU по контейнерам ────────────────────────────────────
# Читает докер-сокет, поэтому видит ВСЕ контейнеры хоста независимо от сетей —
# отдельно подключать его к gendesign_default не нужно.
cadvisor:
image: gcr.io/cadvisor/cadvisor:v0.52.1
container_name: gendesign-cadvisor
restart: unless-stopped
privileged: true
devices:
- /dev/kmsg:/dev/kmsg
command:
# Урезаем набор метрик: полный cAdvisor выдаёт тысячи рядов на контейнер и
# раздувает TSDB на порядок ради данных, которые никто не смотрит.
- "--docker_only=true"
- "--housekeeping_interval=30s"
- "--disable_metrics=percpu,sched,tcp,udp,advtcp,process,hugetlb,referenced_memory,cpu_topology,resctrl"
- "--store_container_labels=false"
volumes:
- /:/rootfs:ro
- /var/run:/var/run:ro
- /sys:/sys:ro
- /var/lib/docker/:/var/lib/docker:ro
- /dev/disk/:/dev/disk:ro
expose:
- "8080"
networks:
- shared
mem_limit: 384m
logging: *default-logging
# ── postgres-exporter: Птица (только на Poincare) ────────────────────────────
# WAL/сутки, n_tup_upd против n_tup_hot_upd, рост TOAST, коннекты, горизонт
# vacuum — то, из-за отсутствия чего раздутие копилось 91 день незамеченным.
postgres-exporter-gendesign:
image: quay.io/prometheuscommunity/postgres-exporter:v0.16.0
container_name: gendesign-pg-exporter-gendesign
restart: unless-stopped
profiles: ["apps"]
env_file:
- path: ./backend/.env.runtime
required: false
environment:
# `:-`, а НЕ `:?`. Compose интерполирует ВЕСЬ файл до фильтрации по
# профилям, поэтому `:?` роняет команду из-за переменной сервиса, который
# на этом хосте вообще не поднимается: продуктовый агент падал на
# INFRA_EXPORTER_DSN (профиль infra), инфраструктурный упал бы на двух
# продуктовых. Громкость не потеряна — перенесена в deploy-metrics.yml,
# где роль ИЗВЕСТНА и проверяется ровно тот DSN, который нужен здесь.
DATA_SOURCE_NAME: ${GENDESIGN_EXPORTER_DSN:-}
PG_EXPORTER_EXTEND_QUERY_PATH: /etc/pg-queries.yml
volumes:
- ./ops/metrics/postgres/queries.yml:/etc/pg-queries.yml:ro
expose:
- "9187"
networks:
- shared
- product
mem_limit: 128m
logging: *default-logging
# ── postgres-exporter: МЕРА (только на Poincare) ─────────────────────────────
postgres-exporter-tradein:
image: quay.io/prometheuscommunity/postgres-exporter:v0.16.0
container_name: gendesign-pg-exporter-tradein
restart: unless-stopped
profiles: ["apps"]
env_file:
- path: ./backend/.env.runtime
required: false
environment:
# `:-`, а не `:?` — см. пояснение у экспортера Птицы выше.
DATA_SOURCE_NAME: ${TRADEIN_EXPORTER_DSN:-}
PG_EXPORTER_EXTEND_QUERY_PATH: /etc/pg-queries.yml
volumes:
- ./ops/metrics/postgres/queries.yml:/etc/pg-queries.yml:ro
expose:
- "9187"
networks:
- shared
mem_limit: 128m
logging: *default-logging
# ── postgres-exporter: инфраструктурная БД (только на Beget) ─────────────────
# forgejo + glitchtip. Нужен и сам по себе, и как страховка: рост базы glitchtip
# ничем не ограничен — политики ретенции у GlitchTip нет вообще.
postgres-exporter-infra:
image: quay.io/prometheuscommunity/postgres-exporter:v0.16.0
container_name: gendesign-pg-exporter-infra
restart: unless-stopped
profiles: ["infra"]
env_file:
- path: ./backend/.env.runtime
required: false
environment:
# `:-`, а не `:?` — см. пояснение у экспортера Птицы выше.
DATA_SOURCE_NAME: ${INFRA_EXPORTER_DSN:-}
PG_EXPORTER_EXTEND_QUERY_PATH: /etc/pg-queries.yml
volumes:
- ./ops/metrics/postgres/queries.yml:/etc/pg-queries.yml:ro
expose:
- "9187"
networks:
- shared
- infra
mem_limit: 128m
logging: *default-logging
volumes:
alloy_data:
networks:
shared:
external: true
name: gendesign_shared
# Сеть продуктового стека Птицы — в ней gendesign-postgres-1 на Poincare.
product:
external: true
name: gendesign_default
# На Beget это та же по имени сеть, но с инфраструктурными контейнерами.
# Разные алиасы в файле нужны, чтобы профили не тянули лишнюю сеть на чужом хосте.
infra:
external: true
name: gendesign_default