gendesign/ops/metrics/grafana/dashboards/host.json
bot-backend 309d273f3f
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
feat(observability): стек метрик и логов — Prometheus, Loki, Grafana, агенты на обоих хостах
Метрик в проекте не было ни одной: ни экспортеров, ни /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
2026-08-26 10:45:54 +03:00

332 lines
13 KiB
JSON
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.

{
"uid": "gendesign-host",
"title": "Хост и контейнеры",
"description": "CPU, память, диск и контейнеры обоих хостов. Метка host: infra = Beget (Forgejo, GlitchTip, мониторинг), apps = Poincare (Птица и МЕРА).",
"tags": ["gendesign", "infra"],
"timezone": "browser",
"editable": false,
"schemaVersion": 39,
"refresh": "1m",
"time": { "from": "now-6h", "to": "now" },
"templating": {
"list": [
{
"name": "host",
"type": "query",
"datasource": { "type": "prometheus", "uid": "prometheus" },
"query": "label_values(node_uname_info, host)",
"refresh": 1,
"includeAll": true,
"multi": true,
"current": { "text": "All", "value": "$__all" }
}
]
},
"panels": [
{
"type": "row",
"title": "Сводка",
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 }
},
{
"type": "stat",
"title": "Свободно на корневом разделе",
"description": "Место, оставшееся на /. При заполнении диска Postgres останавливается — это не «медленно», а полная остановка записи.",
"datasource": { "type": "prometheus", "uid": "prometheus" },
"gridPos": { "h": 5, "w": 6, "x": 0, "y": 1 },
"targets": [
{
"refId": "A",
"expr": "node_filesystem_avail_bytes{mountpoint=\"/\", host=~\"$host\"}",
"legendFormat": "{{host}}"
}
],
"fieldConfig": {
"defaults": {
"unit": "bytes",
"thresholds": {
"mode": "absolute",
"steps": [
{ "color": "red", "value": null },
{ "color": "orange", "value": 10737418240 },
{ "color": "green", "value": 32212254720 }
]
}
},
"overrides": []
},
"options": { "colorMode": "value", "graphMode": "area", "textMode": "auto" }
},
{
"type": "stat",
"title": "Доступно памяти",
"description": "MemAvailable — то, что ядро реально может отдать под новую нагрузку. Отличается от «free»: кэш страниц отдаётся по требованию и в нехватку не считается.",
"datasource": { "type": "prometheus", "uid": "prometheus" },
"gridPos": { "h": 5, "w": 6, "x": 6, "y": 1 },
"targets": [
{
"refId": "A",
"expr": "node_memory_MemAvailable_bytes{host=~\"$host\"}",
"legendFormat": "{{host}}"
}
],
"fieldConfig": {
"defaults": {
"unit": "bytes",
"thresholds": {
"mode": "absolute",
"steps": [
{ "color": "red", "value": null },
{ "color": "orange", "value": 1073741824 },
{ "color": "green", "value": 3221225472 }
]
}
},
"overrides": []
},
"options": { "colorMode": "value", "graphMode": "area", "textMode": "auto" }
},
{
"type": "stat",
"title": "Загрузка (load1 на ядро)",
"description": "Нормировано на число ядер: 1.0 означает «занято ровно столько, сколько есть». Без нормировки число несравнимо между хостами с разным CPU.",
"datasource": { "type": "prometheus", "uid": "prometheus" },
"gridPos": { "h": 5, "w": 6, "x": 12, "y": 1 },
"targets": [
{
"refId": "A",
"expr": "node_load1{host=~\"$host\"} / count by (host) (node_cpu_seconds_total{mode=\"idle\", host=~\"$host\"})",
"legendFormat": "{{host}}"
}
],
"fieldConfig": {
"defaults": {
"unit": "short",
"decimals": 2,
"thresholds": {
"mode": "absolute",
"steps": [
{ "color": "green", "value": null },
{ "color": "orange", "value": 0.8 },
{ "color": "red", "value": 1.5 }
]
}
},
"overrides": []
},
"options": { "colorMode": "value", "graphMode": "area", "textMode": "auto" }
},
{
"type": "stat",
"title": "Аптайм",
"description": "Время с последней загрузки. Внезапное обнуление — перезагрузка, о которой стоит знать.",
"datasource": { "type": "prometheus", "uid": "prometheus" },
"gridPos": { "h": 5, "w": 6, "x": 18, "y": 1 },
"targets": [
{
"refId": "A",
"expr": "time() - node_boot_time_seconds{host=~\"$host\"}",
"legendFormat": "{{host}}"
}
],
"fieldConfig": {
"defaults": { "unit": "s", "decimals": 0 },
"overrides": []
},
"options": { "colorMode": "none", "graphMode": "none", "textMode": "auto" }
},
{
"type": "row",
"title": "Ресурсы во времени",
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 6 }
},
{
"type": "timeseries",
"title": "CPU по режимам",
"description": "iowait отдельно от user/system: высокий iowait означает, что процессор ждёт диск, и добавлять ядер бесполезно.",
"datasource": { "type": "prometheus", "uid": "prometheus" },
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 7 },
"targets": [
{
"refId": "A",
"expr": "sum by (host, mode) (rate(node_cpu_seconds_total{mode!=\"idle\", host=~\"$host\"}[5m])) / on (host) group_left count by (host) (node_cpu_seconds_total{mode=\"idle\", host=~\"$host\"})",
"legendFormat": "{{host}} · {{mode}}"
}
],
"fieldConfig": {
"defaults": {
"unit": "percentunit",
"min": 0,
"custom": { "fillOpacity": 20, "stacking": { "mode": "normal" }, "showPoints": "never" }
},
"overrides": []
}
},
{
"type": "timeseries",
"title": "Память",
"description": "Занято = всего минус доступно. Своп показан отдельно: его рост означает, что рабочий набор перестал помещаться.",
"datasource": { "type": "prometheus", "uid": "prometheus" },
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 7 },
"targets": [
{
"refId": "A",
"expr": "node_memory_MemTotal_bytes{host=~\"$host\"} - node_memory_MemAvailable_bytes{host=~\"$host\"}",
"legendFormat": "{{host}} · занято"
},
{
"refId": "B",
"expr": "node_memory_SwapTotal_bytes{host=~\"$host\"} - node_memory_SwapFree_bytes{host=~\"$host\"}",
"legendFormat": "{{host}} · своп"
}
],
"fieldConfig": {
"defaults": {
"unit": "bytes",
"min": 0,
"custom": { "fillOpacity": 15, "showPoints": "never" }
},
"overrides": []
}
},
{
"type": "timeseries",
"title": "Занятость дисков",
"description": "По точкам монтирования. Порог алерта — 85 %, критический — 93 %.",
"datasource": { "type": "prometheus", "uid": "prometheus" },
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 15 },
"targets": [
{
"refId": "A",
"expr": "1 - node_filesystem_avail_bytes{fstype!~\"tmpfs|overlay\", host=~\"$host\"} / node_filesystem_size_bytes{fstype!~\"tmpfs|overlay\", host=~\"$host\"}",
"legendFormat": "{{host}} · {{mountpoint}}"
}
],
"fieldConfig": {
"defaults": {
"unit": "percentunit",
"min": 0,
"max": 1,
"custom": { "fillOpacity": 10, "showPoints": "never", "thresholdsStyle": { "mode": "dashed" } },
"thresholds": {
"mode": "absolute",
"steps": [
{ "color": "green", "value": null },
{ "color": "orange", "value": 0.85 },
{ "color": "red", "value": 0.93 }
]
}
},
"overrides": []
}
},
{
"type": "timeseries",
"title": "Дисковый ввод-вывод",
"description": "Байты чтения и записи по устройствам. Всплеск записи по ночам — это бэкапы (00:30 и 01:30 UTC), так и должно быть.",
"datasource": { "type": "prometheus", "uid": "prometheus" },
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 15 },
"targets": [
{
"refId": "A",
"expr": "sum by (host) (rate(node_disk_read_bytes_total{host=~\"$host\"}[5m]))",
"legendFormat": "{{host}} · чтение"
},
{
"refId": "B",
"expr": "sum by (host) (rate(node_disk_written_bytes_total{host=~\"$host\"}[5m]))",
"legendFormat": "{{host}} · запись"
}
],
"fieldConfig": {
"defaults": {
"unit": "Bps",
"custom": { "fillOpacity": 10, "showPoints": "never" }
},
"overrides": []
}
},
{
"type": "row",
"title": "Контейнеры",
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 23 }
},
{
"type": "timeseries",
"title": "Память по контейнерам (топ-15)",
"description": "working set, а не usage: usage включает переиспользуемый кэш и завышает картину. Именно по этой метрике сравнивается фактическое потребление с mem_limit.",
"datasource": { "type": "prometheus", "uid": "prometheus" },
"gridPos": { "h": 10, "w": 14, "x": 0, "y": 24 },
"targets": [
{
"refId": "A",
"expr": "topk(15, container_memory_working_set_bytes{name!=\"\", host=~\"$host\"})",
"legendFormat": "{{host}} · {{name}}"
}
],
"fieldConfig": {
"defaults": {
"unit": "bytes",
"custom": { "fillOpacity": 10, "showPoints": "never" }
},
"overrides": []
}
},
{
"type": "table",
"title": "Приближение к mem_limit",
"description": "Доля от заданного лимита. Пустая строка означает, что лимит не выставлен вовсе — тогда потребление сверху не ограничено ничем, кроме памяти хоста.",
"datasource": { "type": "prometheus", "uid": "prometheus" },
"gridPos": { "h": 10, "w": 10, "x": 14, "y": 24 },
"targets": [
{
"refId": "A",
"instant": true,
"format": "table",
"expr": "sort_desc(container_memory_working_set_bytes{name!=\"\", host=~\"$host\"} / (container_spec_memory_limit_bytes{name!=\"\", host=~\"$host\"} > 0))",
"legendFormat": "{{name}}"
}
],
"transformations": [
{ "id": "organize", "options": { "excludeByName": { "Time": true, "job": true, "instance": true, "id": true, "image": true } } }
],
"fieldConfig": {
"defaults": {
"unit": "percentunit",
"custom": { "align": "auto", "cellOptions": { "type": "gauge" } },
"thresholds": {
"mode": "absolute",
"steps": [
{ "color": "green", "value": null },
{ "color": "orange", "value": 0.8 },
{ "color": "red", "value": 0.9 }
]
}
},
"overrides": []
}
},
{
"type": "timeseries",
"title": "Перезапуски контейнеров",
"description": "Число стартов за полчаса. Больше трёх — цикл перезапусков: снаружи такой контейнер выглядит поднятым, а деплой при этом зелёный.",
"datasource": { "type": "prometheus", "uid": "prometheus" },
"gridPos": { "h": 8, "w": 24, "x": 0, "y": 34 },
"targets": [
{
"refId": "A",
"expr": "changes(container_start_time_seconds{name!=\"\", host=~\"$host\"}[30m]) > 0",
"legendFormat": "{{host}} · {{name}}"
}
],
"fieldConfig": {
"defaults": {
"unit": "short",
"min": 0,
"custom": { "drawStyle": "bars", "fillOpacity": 60, "showPoints": "never" }
},
"overrides": []
}
}
]
}