All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 9s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
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 / openapi-codegen-check (pull_request) Has been skipped
Метрик в проекте не было ни одной: ни экспортеров, ни /metrics в бэкендах, единственный канал наблюдения — journald, единственный сигнал об аварии — исключение в GlitchTip. Из-за этого целый класс отказов невидим в принципе: задача рапортует done, строк ноль, исключения нет. Так протухли данные на семь месяцев (#2998), 34 дня был мёртв house_imv_backfill (#2698), 8 суток писал ноль newbuilding_enrich (#2767), 91 день копилось раздутие listings (#2992). Grafana не заменяет GlitchTip: ошибки остаются там. Grafana OSS не принимает Sentry DSN ни одним компонентом, а скрубберы в before_send — требование 152-ФЗ. Здесь появляется другой класс данных: ряды и алерты по трендам. Наблюдатель поставлен у ДРУГОГО провайдера, чем наблюдаемое: серверная сторона на Beget, рядом с GlitchTip. Если ляжет Poincare, мониторинг должен об этом сказать, а не лечь вместе с ним. Транспорт push, а не pull: агент на Poincare шлёт remote_write и логи исходящим HTTPS, поэтому там не открывается ни одного входящего порта сверх 22/80/443. При обрыве канала Alloy копит в WAL и досылает — pull-скрейп в той же ситуации терял бы точки именно в аварии, ради которой мониторинг и нужен. Два контура доступа с разными учётками. Пароль приёмника по построению лежит открытым на продуктовом хосте, значит его компрометация неизбежна вместе с хостом; будь это учётка витрины, утёк бы и доступ к дашбордам. GlitchTip читается прямым SQL, а не Sentry-плагином: у плагина на 6.1.6 stats_v2 отдаёт 500 (баг GlitchTip #381), Events/Discover — 404 (#416), а в grafana/sentry-datasource слово glitchtip не встречается ни разу. Схема сверена на живой базе: колонка времени называется timestamp, а не received, и отдельной таблицы IssueIndex не существует — агрегаты лежат на самой issue_events_issue. Алерты за профилем alerts: канал доставки — открытый вопрос #3078, и стек не должен на нём стоять. Деплой предупреждает, что уведомлять пока некому. Каждая настройка, способная отказать молча, закрыта явно: ретенция Prometheus задана и по времени и по размеру, retention_enabled у компактора Loki (без него retention_period не работает вовсе), путь к журналу и запуск Alloy от root (иначе агент читает ноль записей без ошибки), проверка Caddy до перезагрузки (на этом хосте тот же Caddy держит git, errors и obsidian). Refs #3078
116 lines
7.8 KiB
YAML
116 lines
7.8 KiB
YAML
# Дополнительные запросы для postgres_exporter (PG_EXPORTER_EXTEND_QUERY_PATH).
|
||
#
|
||
# Здесь только то, отсутствие чего уже стоило времени. Каждый блок — ответ на
|
||
# конкретный разбор постфактум, а не «полезно иметь».
|
||
#
|
||
# cache_seconds стоит у всех: экспортер скрейпится раз в 60 с, а часть запросов
|
||
# трогает pg_class по всей базе. Кэш держит нагрузку на уровне шума.
|
||
|
||
# ── Раздутие: обновления, идущие МИМО HOT ────────────────────────────────────
|
||
# Разбор #2992/#2989: у `listings` 198 апдейтов на строку при доле HOT 0,43 %.
|
||
# Каждый не-HOT апдейт переписывает строку во все индексы и заново тостит
|
||
# описание — отсюда TOAST 15 ГБ при ~230 МБ живого содержимого. Копилось 91 день,
|
||
# потому что смотреть было не на что.
|
||
pg_table_write_amplification:
|
||
query: |
|
||
SELECT
|
||
schemaname AS schema,
|
||
relname AS table,
|
||
n_tup_ins AS tup_ins,
|
||
n_tup_upd AS tup_upd,
|
||
n_tup_del AS tup_del,
|
||
n_tup_hot_upd AS tup_hot_upd,
|
||
n_live_tup AS live_tup,
|
||
n_dead_tup AS dead_tup,
|
||
COALESCE(EXTRACT(EPOCH FROM (now() - last_autovacuum)), -1) AS last_autovacuum_age_s,
|
||
COALESCE(EXTRACT(EPOCH FROM (now() - last_autoanalyze)), -1) AS last_autoanalyze_age_s
|
||
FROM pg_stat_user_tables
|
||
WHERE n_tup_upd > 0 OR n_live_tup > 10000
|
||
cache_seconds: 60
|
||
metrics:
|
||
- schema: { usage: "LABEL", description: "Схема" }
|
||
- table: { usage: "LABEL", description: "Таблица" }
|
||
- tup_ins: { usage: "COUNTER", description: "Вставлено строк" }
|
||
- tup_upd: { usage: "COUNTER", description: "Обновлено строк" }
|
||
- tup_del: { usage: "COUNTER", description: "Удалено строк" }
|
||
- tup_hot_upd: { usage: "COUNTER", description: "Из них HOT — не трогают индексы" }
|
||
- live_tup: { usage: "GAUGE", description: "Живых строк" }
|
||
- dead_tup: { usage: "GAUGE", description: "Мёртвых строк — работа для vacuum" }
|
||
- last_autovacuum_age_s: { usage: "GAUGE", description: "Секунд с последнего autovacuum, -1 если не было" }
|
||
- last_autoanalyze_age_s: { usage: "GAUGE", description: "Секунд с последнего autoanalyze, -1 если не было" }
|
||
|
||
# ── Размеры: куда именно уходит диск ─────────────────────────────────────────
|
||
# Отдельно heap, индексы и TOAST. Суммарный размер таблицы этого не показывает,
|
||
# а именно разделение объясняло, почему `listings` весила 19 ГБ при 230 МБ данных.
|
||
pg_table_size_detail:
|
||
query: |
|
||
SELECT
|
||
n.nspname AS schema,
|
||
c.relname AS table,
|
||
pg_relation_size(c.oid) AS heap_bytes,
|
||
pg_indexes_size(c.oid) AS index_bytes,
|
||
COALESCE(pg_total_relation_size(c.reltoastrelid), 0) AS toast_bytes,
|
||
pg_total_relation_size(c.oid) AS total_bytes
|
||
FROM pg_class c
|
||
JOIN pg_namespace n ON n.oid = c.relnamespace
|
||
WHERE c.relkind = 'r'
|
||
AND n.nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
|
||
AND pg_total_relation_size(c.oid) > 10485760
|
||
cache_seconds: 300
|
||
metrics:
|
||
- schema: { usage: "LABEL", description: "Схема" }
|
||
- table: { usage: "LABEL", description: "Таблица" }
|
||
- heap_bytes: { usage: "GAUGE", description: "Сами строки" }
|
||
- index_bytes: { usage: "GAUGE", description: "Индексы" }
|
||
- toast_bytes: { usage: "GAUGE", description: "TOAST — вынесенные длинные значения" }
|
||
- total_bytes: { usage: "GAUGE", description: "Итого с учётом всего" }
|
||
|
||
# ── WAL: сколько журнала генерится ───────────────────────────────────────────
|
||
# Замер 20.08: 7,02 ГБ/сутки при примерно четырёх пользовательских расчётах в
|
||
# сутки. Диспропорция такого масштаба и есть симптом — но заметить её можно
|
||
# только имея ряд.
|
||
pg_wal_bytes:
|
||
query: |
|
||
SELECT
|
||
CAST(pg_wal_lsn_diff(pg_current_wal_lsn(), '0/0') AS BIGINT) AS wal_bytes_total,
|
||
(SELECT count(*) FROM pg_ls_waldir()) AS wal_segments
|
||
cache_seconds: 60
|
||
metrics:
|
||
- wal_bytes_total: { usage: "COUNTER", description: "Позиция WAL от начала — рост даёт байт/сек" }
|
||
- wal_segments: { usage: "GAUGE", description: "Сегментов в pg_wal сейчас" }
|
||
|
||
# ── Горизонт vacuum и долгие транзакции ──────────────────────────────────────
|
||
# Разбор #2607: осиротевшие запросы висели 46 часов и держали горизонт, из-за
|
||
# чего vacuum не мог убрать мёртвые строки во всей базе. Одна забытая транзакция
|
||
# отравляет весь кластер, и снаружи это выглядит просто как «база пухнет».
|
||
pg_activity_horizon:
|
||
query: |
|
||
SELECT
|
||
COALESCE(MAX(EXTRACT(EPOCH FROM (now() - xact_start))), 0) AS oldest_xact_age_s,
|
||
COALESCE(MAX(EXTRACT(EPOCH FROM (now() - query_start))), 0) AS oldest_query_age_s,
|
||
count(*) FILTER (WHERE state = 'idle in transaction') AS idle_in_transaction,
|
||
count(*) FILTER (WHERE wait_event_type = 'Lock') AS waiting_on_lock,
|
||
count(*) FILTER (WHERE backend_type = 'client backend') AS client_backends
|
||
FROM pg_stat_activity
|
||
WHERE backend_type = 'client backend'
|
||
cache_seconds: 30
|
||
metrics:
|
||
- oldest_xact_age_s: { usage: "GAUGE", description: "Возраст самой старой транзакции, сек" }
|
||
- oldest_query_age_s: { usage: "GAUGE", description: "Возраст самого старого запроса, сек" }
|
||
- idle_in_transaction: { usage: "GAUGE", description: "Открыта транзакция и ничего не делает — держит горизонт" }
|
||
- waiting_on_lock: { usage: "GAUGE", description: "Ждут блокировку" }
|
||
- client_backends: { usage: "GAUGE", description: "Клиентских соединений" }
|
||
|
||
# ── Размер баз ───────────────────────────────────────────────────────────────
|
||
# У GlitchTip нет политики ретенции вообще (проверено: grep по retention даёт
|
||
# ноль попаданий), база растёт без ограничения. Нужен ряд, чтобы поймать это
|
||
# до того, как кончится диск.
|
||
pg_database_size_bytes_detail:
|
||
query: |
|
||
SELECT datname AS database, pg_database_size(datname) AS bytes
|
||
FROM pg_database
|
||
WHERE datistemplate = false AND datallowconn = true
|
||
cache_seconds: 300
|
||
metrics:
|
||
- database: { usage: "LABEL", description: "База" }
|
||
- bytes: { usage: "GAUGE", description: "Размер, байт" }
|