gendesign/ops/metrics/grafana/dashboards/apps.json
bot-backend 412d78f357
All checks were successful
CI Trade-In / changes (pull_request) Successful in 13s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 16s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
fix(ops): панель классов ответов больше не стекируется — красная линия и есть число 5xx
11.09 панель «Запросы по классам ответов» дала ложную тревогу: при stacking=normal
верхняя, красная линия рисуется на высоте суммы всех классов, и её положение
читается как объём пятисоток. Фактически за то окно их было шесть.

Стек здесь ничего не даёт: суммарный трафик уже показан отдельной панелью
«Запросов в минуту», а от этой нужна форма каждого класса по отдельности.
Заливка снижена, линия утолщена — без стека 25% заливки перекрывают друг друга.

Описание панели теперь прямо говорит, что линии независимы.

Refs #3471
2026-09-12 13:49:45 +03:00

296 lines
18 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-apps",
"title": "Приложения",
"description": "Птица и МЕРА глазами их собственных счётчиков. Отвечает на класс вопросов, невидимый для GlitchTip: сервис отвечает, исключений нет, а продукт при этом не работает — потому что медленно, или потому что поток 401 и 429.",
"tags": ["gendesign", "apps"],
"timezone": "browser",
"editable": false,
"schemaVersion": 39,
"refresh": "1m",
"time": { "from": "now-6h", "to": "now" },
"templating": {
"list": [
{
"name": "app",
"label": "Приложение",
"type": "query",
"datasource": { "type": "prometheus", "uid": "prometheus" },
"query": "label_values(app_build_info, app)",
"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": "Скрейп удался или нет. Ноль здесь не то же самое, что «нет запросов»: это агент не смог снять метрики вовсе, то есть процесс не отвечает по сети.",
"datasource": { "type": "prometheus", "uid": "prometheus" },
"gridPos": { "h": 5, "w": 6, "x": 0, "y": 1 },
"targets": [
{ "refId": "A", "expr": "up{job=\"app\", app=~\"$app\"}", "legendFormat": "{{app}}" }
],
"fieldConfig": {
"defaults": {
"mappings": [
{ "type": "value", "options": { "0": { "text": "не отвечает", "color": "red", "index": 0 }, "1": { "text": "отвечает", "color": "green", "index": 1 } } }
],
"thresholds": { "mode": "absolute", "steps": [ { "color": "red", "value": null }, { "color": "green", "value": 1 } ] }
},
"overrides": []
},
"options": { "colorMode": "background", "graphMode": "none", "textMode": "value_and_name" }
},
{
"type": "stat",
"title": "Запросов в минуту",
"description": "Обвал до нуля при живом процессе — тоже отказ: значит, до приложения перестали доходить запросы (Caddy, сеть, фронт), и снаружи это выглядит как неработающий сайт.",
"datasource": { "type": "prometheus", "uid": "prometheus" },
"gridPos": { "h": 5, "w": 6, "x": 6, "y": 1 },
"targets": [
{ "refId": "A", "expr": "sum by (app) (rate(http_requests_total{app=~\"$app\"}[5m])) * 60", "legendFormat": "{{app}}" }
],
"fieldConfig": {
"defaults": { "unit": "short", "decimals": 1, "thresholds": { "mode": "absolute", "steps": [ { "color": "text", "value": null } ] } },
"overrides": []
},
"options": { "colorMode": "none", "graphMode": "area", "textMode": "value_and_name" }
},
{
"type": "stat",
"title": "Доля 5xx за час",
"description": "Ошибка сервера — это всегда несделанная работа пользователя. Порог оранжевого стоит на 1 %: при нашем трафике это единицы запросов, и они должны быть заметны, а не тонуть в проценте от большого числа.",
"datasource": { "type": "prometheus", "uid": "prometheus" },
"gridPos": { "h": 5, "w": 6, "x": 12, "y": 1 },
"targets": [
{ "refId": "A", "expr": "sum by (app) (increase(http_requests_total{app=~\"$app\", status=~\"5..\"}[1h])) / clamp_min(sum by (app) (increase(http_requests_total{app=~\"$app\"}[1h])), 1)", "legendFormat": "{{app}}" }
],
"fieldConfig": {
"defaults": {
"unit": "percentunit",
"min": 0,
"thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "orange", "value": 0.01 }, { "color": "red", "value": 0.05 } ] }
},
"overrides": []
},
"options": { "colorMode": "value", "graphMode": "area", "textMode": "value_and_name" }
},
{
"type": "stat",
"title": "95-й перцентиль за час",
"description": "Не среднее: среднее прячет как раз тех, кому плохо. Считается по корзинам гистограммы, поэтому точность ограничена их границами — это нормально для порога, но не для точного измерения.",
"datasource": { "type": "prometheus", "uid": "prometheus" },
"gridPos": { "h": 5, "w": 6, "x": 18, "y": 1 },
"targets": [
{ "refId": "A", "expr": "histogram_quantile(0.95, sum by (le, app) (rate(http_request_duration_seconds_bucket{app=~\"$app\"}[1h])))", "legendFormat": "{{app}}" }
],
"fieldConfig": {
"defaults": {
"unit": "s",
"thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "orange", "value": 1 }, { "color": "red", "value": 5 } ] }
},
"overrides": []
},
"options": { "colorMode": "value", "graphMode": "area", "textMode": "value_and_name" }
},
{ "type": "row", "title": "Трафик и отказы", "gridPos": { "h": 1, "w": 24, "x": 0, "y": 6 } },
{
"type": "timeseries",
"title": "Запросы по классам ответов",
"description": "Классы, а не отдельные коды: форма графика важнее точного номера. Всплеск 4xx без 5xx — обычно сканер или сломанный клиент; всплеск 5xx — наша ошибка. Линии НЕ стекируются: высота красной линии — это и есть число пятисоток, а не сумма со всем, что под ней.",
"datasource": { "type": "prometheus", "uid": "prometheus" },
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 7 },
"targets": [
{ "refId": "A", "expr": "sum by (app) (rate(http_requests_total{app=~\"$app\", status=~\"2..\"}[5m]))", "legendFormat": "{{app}} · 2xx" },
{ "refId": "B", "expr": "sum by (app) (rate(http_requests_total{app=~\"$app\", status=~\"3..\"}[5m]))", "legendFormat": "{{app}} · 3xx" },
{ "refId": "C", "expr": "sum by (app) (rate(http_requests_total{app=~\"$app\", status=~\"4..\"}[5m]))", "legendFormat": "{{app}} · 4xx" },
{ "refId": "D", "expr": "sum by (app) (rate(http_requests_total{app=~\"$app\", status=~\"5..\"}[5m]))", "legendFormat": "{{app}} · 5xx" }
],
"fieldConfig": {
"defaults": { "unit": "reqps", "min": 0, "custom": { "fillOpacity": 8, "stacking": { "mode": "none" }, "showPoints": "never", "lineWidth": 2 } },
"overrides": [
{ "matcher": { "id": "byRegexp", "options": ".*2xx.*" }, "properties": [ { "id": "color", "value": { "mode": "fixed", "fixedColor": "green" } } ] },
{ "matcher": { "id": "byRegexp", "options": ".*3xx.*" }, "properties": [ { "id": "color", "value": { "mode": "fixed", "fixedColor": "blue" } } ] },
{ "matcher": { "id": "byRegexp", "options": ".*4xx.*" }, "properties": [ { "id": "color", "value": { "mode": "fixed", "fixedColor": "orange" } } ] },
{ "matcher": { "id": "byRegexp", "options": ".*5xx.*" }, "properties": [ { "id": "color", "value": { "mode": "fixed", "fixedColor": "red" } } ] }
]
}
},
{
"type": "timeseries",
"title": "Отказы авторизации и лимитера",
"description": "401 и 403 считаются потому, что вход у «Меры» и «Птицы» общий, а инцидентов вокруг него уже было достаточно (#2552, эпик единого входа). 429 — срабатывания ограничителя частоты: устойчивая линия означает, что кому-то из живых пользователей регулярно отказывают.",
"datasource": { "type": "prometheus", "uid": "prometheus" },
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 7 },
"targets": [
{ "refId": "A", "expr": "sum by (app) (rate(http_requests_total{app=~\"$app\", status=\"401\"}[5m]))", "legendFormat": "{{app}} · 401 не авторизован" },
{ "refId": "B", "expr": "sum by (app) (rate(http_requests_total{app=~\"$app\", status=\"403\"}[5m]))", "legendFormat": "{{app}} · 403 запрещено" },
{ "refId": "C", "expr": "sum by (app) (rate(http_requests_total{app=~\"$app\", status=\"429\"}[5m]))", "legendFormat": "{{app}} · 429 лимитер" }
],
"fieldConfig": {
"defaults": { "unit": "reqps", "min": 0, "custom": { "fillOpacity": 10, "showPoints": "never", "lineWidth": 2 } },
"overrides": [
{ "matcher": { "id": "byRegexp", "options": ".*429.*" }, "properties": [ { "id": "color", "value": { "mode": "fixed", "fixedColor": "red" } } ] }
]
}
},
{
"type": "timeseries",
"title": "Ошибки сервера по маршрутам (топ-10)",
"description": "Здесь видно, какая именно ручка ломается. Метка route — шаблон маршрута, а не путь: идентификаторы в него не попадают намеренно, иначе каждый участок и каждая заявка давали бы отдельный ряд.",
"datasource": { "type": "prometheus", "uid": "prometheus" },
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 15 },
"targets": [
{ "refId": "A", "expr": "topk(10, sum by (app, route, status) (rate(http_requests_total{app=~\"$app\", status=~\"5..\"}[5m])))", "legendFormat": "{{app}} · {{route}} · {{status}}" }
],
"fieldConfig": {
"defaults": { "unit": "reqps", "min": 0, "custom": { "fillOpacity": 20, "showPoints": "never", "lineWidth": 1, "drawStyle": "bars" } },
"overrides": []
}
},
{
"type": "table",
"title": "Самые медленные маршруты (95-й перцентиль за час)",
"description": "Считается по маршрутам, у которых за окно был хоть какой-то трафик. Пустая таблица означает тишину, а не быстроту.",
"datasource": { "type": "prometheus", "uid": "prometheus" },
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 15 },
"targets": [
{
"refId": "A",
"instant": true,
"format": "table",
"expr": "topk(12, histogram_quantile(0.95, sum by (le, app, route) (rate(http_request_duration_seconds_bucket{app=~\"$app\"}[1h]))))"
}
],
"transformations": [
{
"id": "organize",
"options": {
"excludeByName": { "Time": true, "job": true, "instance": true, "host": true, "cluster": true, "le": true },
"renameByName": { "app": "Приложение", "route": "Маршрут", "Value": "95-й перцентиль" }
}
},
{ "id": "sortBy", "options": { "fields": {}, "sort": [ { "field": "95-й перцентиль", "desc": true } ] } }
],
"fieldConfig": {
"defaults": { "custom": { "align": "auto", "cellOptions": { "type": "auto" } } },
"overrides": [
{
"matcher": { "id": "byName", "options": "95-й перцентиль" },
"properties": [
{ "id": "unit", "value": "s" },
{ "id": "custom.cellOptions", "value": { "type": "color-text" } },
{ "id": "thresholds", "value": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "orange", "value": 1 }, { "color": "red", "value": 5 } ] } }
]
}
]
}
},
{ "type": "row", "title": "Задержка", "gridPos": { "h": 1, "w": 24, "x": 0, "y": 23 } },
{
"type": "timeseries",
"title": "Перцентили времени ответа",
"description": "Три линии вместе, потому что расхождение между ними и есть сигнал: ровный 50-й при растущем 99-м означает, что плохо становится части пользователей, а не всем, — и по среднему это не увидеть никогда.",
"datasource": { "type": "prometheus", "uid": "prometheus" },
"gridPos": { "h": 9, "w": 16, "x": 0, "y": 24 },
"targets": [
{ "refId": "A", "expr": "histogram_quantile(0.5, sum by (le, app) (rate(http_request_duration_seconds_bucket{app=~\"$app\"}[5m])))", "legendFormat": "{{app}} · 50-й" },
{ "refId": "B", "expr": "histogram_quantile(0.9, sum by (le, app) (rate(http_request_duration_seconds_bucket{app=~\"$app\"}[5m])))", "legendFormat": "{{app}} · 90-й" },
{ "refId": "C", "expr": "histogram_quantile(0.99, sum by (le, app) (rate(http_request_duration_seconds_bucket{app=~\"$app\"}[5m])))", "legendFormat": "{{app}} · 99-й" }
],
"fieldConfig": {
"defaults": { "unit": "s", "min": 0, "custom": { "fillOpacity": 5, "showPoints": "never", "lineWidth": 2 } },
"overrides": []
}
},
{
"type": "timeseries",
"title": "Запросов в работе",
"description": "Растущая линия при неизменном трафике означает, что запросы копятся: приложение принимает быстрее, чем отвечает. Обычно это блокирующий вызов в обработчике — ровно тот случай, что был в #1202.",
"datasource": { "type": "prometheus", "uid": "prometheus" },
"gridPos": { "h": 9, "w": 8, "x": 16, "y": 24 },
"targets": [
{ "refId": "A", "expr": "http_requests_in_progress{app=~\"$app\"}", "legendFormat": "{{app}}" }
],
"fieldConfig": {
"defaults": { "unit": "short", "min": 0, "custom": { "fillOpacity": 20, "showPoints": "never", "lineWidth": 2 } },
"overrides": []
}
},
{ "type": "row", "title": "Процесс", "gridPos": { "h": 1, "w": 24, "x": 0, "y": 33 } },
{
"type": "timeseries",
"title": "Память процесса",
"description": "Резидентная память самого процесса Python, а не контейнера. Монотонный рост между перезапусками — утечка; по контейнерной метрике её легко списать на кэш.",
"datasource": { "type": "prometheus", "uid": "prometheus" },
"gridPos": { "h": 8, "w": 8, "x": 0, "y": 34 },
"targets": [
{ "refId": "A", "expr": "process_resident_memory_bytes{job=\"app\", app=~\"$app\"}", "legendFormat": "{{app}}" }
],
"fieldConfig": {
"defaults": { "unit": "bytes", "min": 0, "custom": { "fillOpacity": 10, "showPoints": "never", "lineWidth": 2 } },
"overrides": []
}
},
{
"type": "timeseries",
"title": "Открытые дескрипторы",
"description": "Незакрытые соединения и файлы упираются в лимит и дают отказы, которые выглядят как что угодно, кроме своей причины. Линия, ползущая вверх и не спадающая, — это она.",
"datasource": { "type": "prometheus", "uid": "prometheus" },
"gridPos": { "h": 8, "w": 8, "x": 8, "y": 34 },
"targets": [
{ "refId": "A", "expr": "process_open_fds{job=\"app\", app=~\"$app\"}", "legendFormat": "{{app}} · открыто" },
{ "refId": "B", "expr": "process_max_fds{job=\"app\", app=~\"$app\"}", "legendFormat": "{{app}} · предел" }
],
"fieldConfig": {
"defaults": { "unit": "short", "min": 0, "custom": { "fillOpacity": 5, "showPoints": "never", "lineWidth": 2 } },
"overrides": [
{ "matcher": { "id": "byRegexp", "options": ".*предел.*" }, "properties": [ { "id": "custom.lineStyle", "value": { "fill": "dash", "dash": [ 8, 6 ] } }, { "id": "color", "value": { "mode": "fixed", "fixedColor": "red" } } ] }
]
}
},
{
"type": "table",
"title": "Что задеплоено",
"description": "Версия, отвечавшая в выбранном окне. Первый вопрос при разборе — «а что там было в этот момент», и ответ на него должен быть в той же картинке, а не в истории деплоев.",
"datasource": { "type": "prometheus", "uid": "prometheus" },
"gridPos": { "h": 8, "w": 8, "x": 16, "y": 34 },
"targets": [
{ "refId": "A", "instant": true, "format": "table", "expr": "app_build_info{app=~\"$app\"}" }
],
"transformations": [
{
"id": "organize",
"options": {
"excludeByName": { "Time": true, "job": true, "instance": true, "host": true, "cluster": true, "Value": true },
"renameByName": { "app": "Приложение", "release": "Версия" }
}
}
],
"fieldConfig": {
"defaults": { "custom": { "align": "auto", "cellOptions": { "type": "auto" } } },
"overrides": []
}
}
]
}