# Стек наблюдаемости — АГЕНТСКАЯ сторона. Поднимается на КАЖДОМ хосте: и на 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: # v0.55.1, не v0.52.x: Docker 29 работает через containerd-snapshotter # (Storage Driver = overlayfs), а 0.52 искал метаданные слоя в легаси # /var/lib/docker/image//layerdb, которого при снапшоттере # просто нет. Итог был — ни одной метрики по контейнерам. image: gcr.io/cadvisor/cadvisor:v0.55.1 container_name: gendesign-cadvisor restart: unless-stopped privileged: true devices: - /dev/kmsg:/dev/kmsg command: # Урезаем набор метрик: полный cAdvisor выдаёт тысячи рядов на контейнер и # раздувает TSDB на порядок ради данных, которые никто не смотрит. - "--docker_only=true" - "--housekeeping_interval=30s" # disk/diskIO добавлены 27.08 по замеру. Именно они держат fsHandler, # который в логе честно отчитывается: «disk usage and inodes count on # following dirs took 1.3–1.8s» — обход overlay-слоёв на каждом цикле. # Память растёт вместе с числом слоёв, а слои копятся от сборок. # # Прод: на продуктовом хосте cadvisor доходил до 359 МиБ из 384 (93 % # лимита), на инфраструктурном за два часа поднялся со 105 до 291 МиБ. # До OOM не дошло, но запас был 25 МиБ — то есть вопрос времени. # # Теряем метрики использования диска ПО КОНТЕЙНЕРАМ. Проверено, что их # никто не использует: ни правила Prometheus, ни дашборды. Алерты по # диску построены на node_exporter (`node_filesystem_*`), то есть на # уровне файловой системы хоста — а это как раз то измерение, которое # и нужно, чтобы вовремя увидеть кончающееся место. - "--disable_metrics=disk,diskIO,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