{ "annotations": { "list": [] }, "editable": true, "graphTooltip": 0, "id": null, "links": [], "panels": [ { "datasource": { "type": "loki", "uid": "loki" }, "description": "Успешные входы (HTTP 200) по access-логу core (HttpAccessLogMiddleware: поля Path/StatusCode). Тенант — POST /api/auth/login, оператор — POST /api/operator/auth/login.", "fieldConfig": { "defaults": { "custom": { "drawStyle": "line", "fillOpacity": 10, "lineWidth": 1, "showPoints": "never" }, "unit": "short" }, "overrides": [] }, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 0 }, "id": 1, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "targets": [ { "datasource": { "type": "loki", "uid": "loki" }, "editorMode": "code", "expr": "sum(count_over_time({service=\"core\"} | json | Path=\"/api/auth/login\" | StatusCode=200 [1m]))", "legendFormat": "тенант", "refId": "A" }, { "datasource": { "type": "loki", "uid": "loki" }, "editorMode": "code", "expr": "sum(count_over_time({service=\"core\"} | json | Path=\"/api/operator/auth/login\" | StatusCode=200 [1m]))", "legendFormat": "оператор", "refId": "B" } ], "title": "Успешные входы, /мин", "type": "timeseries" }, { "datasource": { "type": "loki", "uid": "loki" }, "description": "Неудачные входы по HTTP-коду ответа: 401 — неверные учётные данные, 403 — тенант приостановлен, 429 — сработал LoginAttemptGuard (5 неудач ip|login за 15 мин). По access-логу core: поля Path/StatusCode.", "fieldConfig": { "defaults": { "custom": { "drawStyle": "bars", "fillOpacity": 60, "lineWidth": 1, "showPoints": "never" }, "unit": "short" }, "overrides": [] }, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 0 }, "id": 2, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "targets": [ { "datasource": { "type": "loki", "uid": "loki" }, "editorMode": "code", "expr": "sum by (StatusCode) (count_over_time({service=\"core\"} | json | Path=~\"/api/auth/login|/api/operator/auth/login\" | StatusCode >= 400 [1m]))", "legendFormat": "HTTP {{StatusCode}}", "refId": "A" } ], "title": "Неудачные входы по коду ответа, /мин", "type": "timeseries" }, { "datasource": { "type": "loki", "uid": "loki" }, "description": "Выходы из системы (POST /api/auth/logout — тенант, POST /api/operator/auth/logout — оператор) по access-логу core. Ручка всегда отвечает 200.", "fieldConfig": { "defaults": { "custom": { "drawStyle": "line", "fillOpacity": 10, "lineWidth": 1, "showPoints": "never" }, "unit": "short" }, "overrides": [] }, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 8 }, "id": 3, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "targets": [ { "datasource": { "type": "loki", "uid": "loki" }, "editorMode": "code", "expr": "sum by (Path) (count_over_time({service=\"core\"} | json | Path=~\"/api/auth/logout|/api/operator/auth/logout\" [1m]))", "legendFormat": "{{Path}}", "refId": "A" } ], "title": "Выходы (logout), /мин", "type": "timeseries" }, { "datasource": { "type": "loki", "uid": "loki" }, "description": "Активация приглашений (POST /api/join) по коду ответа: 200 — успешная активация, 4xx — неверный/просроченный код, занятый email или приостановленный тенант.", "fieldConfig": { "defaults": { "custom": { "drawStyle": "bars", "fillOpacity": 60, "lineWidth": 1, "showPoints": "never" }, "unit": "short" }, "overrides": [] }, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 8 }, "id": 4, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "targets": [ { "datasource": { "type": "loki", "uid": "loki" }, "editorMode": "code", "expr": "sum by (StatusCode) (count_over_time({service=\"core\"} | json | Path=\"/api/join\" [1m]))", "legendFormat": "HTTP {{StatusCode}}", "refId": "A" } ], "title": "Активация инвайтов (/api/join), /мин", "type": "timeseries" }, { "datasource": { "type": "loki", "uid": "loki" }, "description": "Успешные входы тенанта и оператора за последний час.", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] }, "unit": "short" }, "overrides": [] }, "gridPos": { "h": 5, "w": 6, "x": 0, "y": 16 }, "id": 5, "options": { "colorMode": "value", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "textMode": "auto" }, "targets": [ { "datasource": { "type": "loki", "uid": "loki" }, "editorMode": "code", "expr": "sum(count_over_time({service=\"core\"} | json | Path=~\"/api/auth/login|/api/operator/auth/login\" | StatusCode=200 [1h]))", "legendFormat": "успешных входов", "refId": "A" } ], "title": "Успешные входы за 1ч", "type": "stat" }, { "datasource": { "type": "loki", "uid": "loki" }, "description": "Неудачные входы тенанта и оператора за последний час; >0 — повод посмотреть Explore (текст ответа) и аудит-ленту оператора.", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 1 } ] }, "unit": "short" }, "overrides": [] }, "gridPos": { "h": 5, "w": 6, "x": 6, "y": 16 }, "id": 6, "options": { "colorMode": "value", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "textMode": "auto" }, "targets": [ { "datasource": { "type": "loki", "uid": "loki" }, "editorMode": "code", "expr": "sum(count_over_time({service=\"core\"} | json | Path=~\"/api/auth/login|/api/operator/auth/login\" | StatusCode >= 400 [1h]))", "legendFormat": "неудачных входов", "refId": "A" } ], "title": "Неудачные входы за 1ч", "type": "stat" }, { "gridPos": { "h": 5, "w": 12, "x": 12, "y": 16 }, "id": 7, "options": { "content": "**Кто именно входил — в аудите, не в логах.** Access-лог core не пишет идентичность (login/tenant/operator) — только метод/путь/код, поэтому панели выше различают лишь контур (тенант vs оператор) по префиксу пути.\n\nПолная лента входов/выходов/неудач с актором, тенантом и IP — в PostgreSQL (`public.audit_log`, append-only, события `tenant_login_ok`/`tenant_login_failed`/`operator_login_*`/`*_logout`/`invite_activated`) через операторские ручки `GET /api/operator/audit` или экран «Аудит» в оператор-консоли.", "mode": "markdown" }, "title": "Где искать актора", "type": "text" } ], "refresh": "1m", "schemaVersion": 39, "tags": [ "deal" ], "templating": { "list": [] }, "time": { "from": "now-24h", "to": "now" }, "timepicker": {}, "timezone": "", "title": "Deal-Auth", "uid": "deal-auth", "version": 1, "weekStart": "" }