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
332 lines
13 KiB
JSON
332 lines
13 KiB
JSON
{
|
||
"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": []
|
||
}
|
||
}
|
||
]
|
||
}
|