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
Три независимых дефекта, найденных на живом проде после подъёма стека метрик.
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
237 lines
12 KiB
YAML
237 lines
12 KiB
YAML
# Стек наблюдаемости — АГЕНТСКАЯ сторона. Поднимается на КАЖДОМ хосте: и на 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
|