feat(grafana): six per-service dashboards
This commit is contained in:
15
infra/grafana/dashboards/ci.json
Normal file
15
infra/grafana/dashboards/ci.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"title": "CI",
|
||||
"uid": "ci",
|
||||
"tags": ["homelab", "stack"],
|
||||
"timezone": "browser",
|
||||
"schemaVersion": 39,
|
||||
"time": {"from": "now-6h", "to": "now"},
|
||||
"refresh": "30s",
|
||||
"panels": [
|
||||
{"id": 1, "title": "Active CI jobs (24h)", "type": "stat", "gridPos": {"x": 0, "y": 0, "w": 4, "h": 4}, "datasource": {"uid": "loki"}, "targets": [{"expr": "count_over_time({container=\"act-runner\", ci_event=\"job started\"}[24h])"}]},
|
||||
{"id": 2, "title": "Job outcomes (24h)", "type": "timeseries", "gridPos": {"x": 4, "y": 0, "w": 20, "h": 8}, "datasource": {"uid": "loki"}, "targets": [{"expr": "sum by (ci_event) (count_over_time({container=\"act-runner\", ci_event=~\"job (started|finished|failed)\"}[24h]))"}]},
|
||||
{"id": 3, "title": "Gitea push events", "type": "timeseries", "gridPos": {"x": 0, "y": 8, "w": 12, "h": 8}, "datasource": {"uid": "loki"}, "targets": [{"expr": "sum(rate({container=\"git\", gitea_event=\"push\"}[5m]))"}]},
|
||||
{"id": 4, "title": "Webhook events", "type": "timeseries", "gridPos": {"x": 12, "y": 8, "w": 12, "h": 8}, "datasource": {"uid": "loki"}, "targets": [{"expr": "sum(rate({container=\"git\", gitea_event=\"webhook\"}[5m]))"}]}
|
||||
]
|
||||
}
|
||||
16
infra/grafana/dashboards/data-lake.json
Normal file
16
infra/grafana/dashboards/data-lake.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"title": "Data Lake",
|
||||
"uid": "data-lake",
|
||||
"tags": ["homelab", "stack"],
|
||||
"timezone": "browser",
|
||||
"schemaVersion": 39,
|
||||
"time": {"from": "now-6h", "to": "now"},
|
||||
"refresh": "30s",
|
||||
"panels": [
|
||||
{"id": 1, "title": "Trino queries (status)", "type": "timeseries", "gridPos": {"x": 0, "y": 0, "w": 12, "h": 8}, "datasource": {"uid": "loki"}, "targets": [{"expr": "sum by (trino_event) (rate({container=\"trino\"}[1m]))"}]},
|
||||
{"id": 2, "title": "Trino query duration p95", "type": "timeseries", "gridPos": {"x": 12, "y": 0, "w": 12, "h": 8}, "datasource": {"uid": "prometheus"}, "targets": [{"expr": "histogram_quantile(0.95, sum by (le) (rate(trino_query_execution_seconds_bucket[5m])))"}]},
|
||||
{"id": 3, "title": "Nessie/Polaris HTTP rate", "type": "timeseries", "gridPos": {"x": 0, "y": 8, "w": 12, "h": 8}, "datasource": {"uid": "prometheus"}, "targets": [{"expr": "sum by (job) (rate(http_server_requests_seconds_count{job=~\"nessie|polaris\"}[1m]))"}]},
|
||||
{"id": 4, "title": "Nessie/Polaris error rate", "type": "timeseries", "gridPos": {"x": 12, "y": 8, "w": 12, "h": 8}, "datasource": {"uid": "prometheus"}, "targets": [{"expr": "sum by (job) (rate(http_server_requests_seconds_count{job=~\"nessie|polaris\",status=~\"5..\"}[1m]))"}]},
|
||||
{"id": 5, "title": "RustFS S3 ops by op", "type": "timeseries", "gridPos": {"x": 0, "y": 16, "w": 24, "h": 8}, "datasource": {"uid": "loki"}, "targets": [{"expr": "sum by (s3_op) (rate({container=\"rustfs\"} | json [1m]))"}]}
|
||||
]
|
||||
}
|
||||
16
infra/grafana/dashboards/data-pipelines.json
Normal file
16
infra/grafana/dashboards/data-pipelines.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"title": "Data Pipelines",
|
||||
"uid": "data-pipelines",
|
||||
"tags": ["homelab", "stack"],
|
||||
"timezone": "browser",
|
||||
"schemaVersion": 39,
|
||||
"time": {"from": "now-6h", "to": "now"},
|
||||
"refresh": "30s",
|
||||
"panels": [
|
||||
{"id": 1, "title": "API request rate (status)", "type": "timeseries", "gridPos": {"x": 0, "y": 0, "w": 12, "h": 8}, "datasource": {"uid": "prometheus"}, "targets": [{"expr": "sum by (status) (rate(stack_http_server_requests_total[1m]))"}]},
|
||||
{"id": 2, "title": "Mail-poller poll cycles", "type": "timeseries", "gridPos": {"x": 12, "y": 0, "w": 12, "h": 8}, "datasource": {"uid": "loki"}, "targets": [{"expr": "sum(rate({container=\"mail-poller\", mail_event=\"poll\"}[5m]))"}]},
|
||||
{"id": 3, "title": "Mail-poller ingest count", "type": "timeseries", "gridPos": {"x": 0, "y": 8, "w": 12, "h": 8}, "datasource": {"uid": "loki"}, "targets": [{"expr": "sum(rate({container=\"mail-poller\", mail_event=\"ingest\"}[5m]))"}]},
|
||||
{"id": 4, "title": "Pipeline errors", "type": "timeseries", "gridPos": {"x": 12, "y": 8, "w": 12, "h": 8}, "datasource": {"uid": "loki"}, "targets": [{"expr": "sum by (pipeline_name) (rate({container=\"api\", api_event=\"error\"}[5m]))"}]},
|
||||
{"id": 5, "title": "Pipeline error tail", "type": "logs", "gridPos": {"x": 0, "y": 16, "w": 24, "h": 8}, "datasource": {"uid": "loki"}, "targets": [{"expr": "{container=\"api\", level=\"error\"}"}]}
|
||||
]
|
||||
}
|
||||
16
infra/grafana/dashboards/gateway-auth.json
Normal file
16
infra/grafana/dashboards/gateway-auth.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"title": "Gateway & Auth",
|
||||
"uid": "gateway-auth",
|
||||
"tags": ["homelab", "stack"],
|
||||
"timezone": "browser",
|
||||
"schemaVersion": 39,
|
||||
"time": {"from": "now-6h", "to": "now"},
|
||||
"refresh": "30s",
|
||||
"panels": [
|
||||
{"id": 1, "title": "Traefik request rate by status", "type": "timeseries", "gridPos": {"x": 0, "y": 0, "w": 12, "h": 8}, "datasource": {"uid": "prometheus"}, "targets": [{"expr": "sum by (code) (rate(traefik_service_requests_total[1m]))"}]},
|
||||
{"id": 2, "title": "Traefik latency p95 (s)", "type": "timeseries", "gridPos": {"x": 12, "y": 0, "w": 12, "h": 8}, "datasource": {"uid": "prometheus"}, "targets": [{"expr": "histogram_quantile(0.95, sum by (le) (rate(traefik_service_request_duration_seconds_bucket[5m])))"}]},
|
||||
{"id": 3, "title": "oauth2-proxy auth events", "type": "timeseries", "gridPos": {"x": 0, "y": 8, "w": 12, "h": 8}, "datasource": {"uid": "loki"}, "targets": [{"expr": "sum by (oauth_event) (rate({container=\"oauth2-proxy\"} | json [1m]))"}]},
|
||||
{"id": 4, "title": "Gitea login + push events", "type": "timeseries", "gridPos": {"x": 12, "y": 8, "w": 12, "h": 8}, "datasource": {"uid": "loki"}, "targets": [{"expr": "sum by (gitea_event) (rate({container=\"git\"} | regexp `(?P<gitea_event>login|push|webhook)` [1m]))"}]},
|
||||
{"id": 5, "title": "Failed auth tail", "type": "logs", "gridPos": {"x": 0, "y": 16, "w": 24, "h": 8}, "datasource": {"uid": "loki"}, "targets": [{"expr": "{container=~\"oauth2-proxy|git\"} |~ \"(?i)(failure|forbidden|401|403)\""}]}
|
||||
]
|
||||
}
|
||||
17
infra/grafana/dashboards/gpu-notebooks.json
Normal file
17
infra/grafana/dashboards/gpu-notebooks.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"title": "GPU & Notebooks",
|
||||
"uid": "gpu-notebooks",
|
||||
"tags": ["homelab", "stack"],
|
||||
"timezone": "browser",
|
||||
"schemaVersion": 39,
|
||||
"time": {"from": "now-6h", "to": "now"},
|
||||
"refresh": "30s",
|
||||
"panels": [
|
||||
{"id": 1, "title": "GPU utilization %", "type": "timeseries", "gridPos": {"x": 0, "y": 0, "w": 12, "h": 8}, "datasource": {"uid": "prometheus"}, "targets": [{"expr": "DCGM_FI_DEV_GPU_UTIL"}]},
|
||||
{"id": 2, "title": "GPU memory used (MiB)", "type": "timeseries", "gridPos": {"x": 12, "y": 0, "w": 12, "h": 8}, "datasource": {"uid": "prometheus"}, "targets": [{"expr": "DCGM_FI_DEV_FB_USED"}]},
|
||||
{"id": 3, "title": "GPU temp (°C)", "type": "timeseries", "gridPos": {"x": 0, "y": 8, "w": 12, "h": 8}, "datasource": {"uid": "prometheus"}, "targets": [{"expr": "DCGM_FI_DEV_GPU_TEMP"}]},
|
||||
{"id": 4, "title": "GPU power (W)", "type": "timeseries", "gridPos": {"x": 12, "y": 8, "w": 12, "h": 8}, "datasource": {"uid": "prometheus"}, "targets": [{"expr": "DCGM_FI_DEV_POWER_USAGE"}]},
|
||||
{"id": 5, "title": "Notebooks/Zotero CPU %", "type": "timeseries", "gridPos": {"x": 0, "y": 16, "w": 12, "h": 8}, "datasource": {"uid": "prometheus"}, "targets": [{"expr": "rate(container_cpu_usage_seconds_total{name=~\"notebooks|zotero\"}[1m]) * 100", "legendFormat": "{{name}}"}]},
|
||||
{"id": 6, "title": "Notebooks/Zotero memory", "type": "timeseries", "gridPos": {"x": 12, "y": 16, "w": 12, "h": 8}, "datasource": {"uid": "prometheus"}, "targets": [{"expr": "container_memory_working_set_bytes{name=~\"notebooks|zotero\"} / 1024 / 1024", "legendFormat": "{{name}}"}]}
|
||||
]
|
||||
}
|
||||
17
infra/grafana/dashboards/homelab-overview.json
Normal file
17
infra/grafana/dashboards/homelab-overview.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"title": "Homelab Overview",
|
||||
"uid": "homelab-overview",
|
||||
"tags": ["homelab", "stack"],
|
||||
"timezone": "browser",
|
||||
"schemaVersion": 39,
|
||||
"time": {"from": "now-6h", "to": "now"},
|
||||
"refresh": "30s",
|
||||
"panels": [
|
||||
{"id": 1, "title": "Containers up", "type": "stat", "gridPos": {"x": 0, "y": 0, "w": 4, "h": 4}, "datasource": {"uid": "prometheus"}, "targets": [{"expr": "count(container_last_seen{name!=\"\"})"}]},
|
||||
{"id": 2, "title": "Log volume / 1m", "type": "stat", "gridPos": {"x": 4, "y": 0, "w": 4, "h": 4}, "datasource": {"uid": "loki"}, "targets": [{"expr": "sum(rate({container=~\".+\"}[1m]))"}]},
|
||||
{"id": 3, "title": "Error rate / 1m", "type": "stat", "gridPos": {"x": 8, "y": 0, "w": 4, "h": 4}, "datasource": {"uid": "loki"}, "targets": [{"expr": "sum(rate({level=\"error\"}[1m]))"}]},
|
||||
{"id": 4, "title": "Container CPU %", "type": "timeseries", "gridPos": {"x": 0, "y": 4, "w": 12, "h": 8}, "datasource": {"uid": "prometheus"}, "targets": [{"expr": "rate(container_cpu_usage_seconds_total{name!=\"\"}[1m]) * 100", "legendFormat": "{{name}}"}]},
|
||||
{"id": 5, "title": "Container memory (MiB)", "type": "timeseries", "gridPos": {"x": 12, "y": 4, "w": 12, "h": 8}, "datasource": {"uid": "prometheus"}, "targets": [{"expr": "container_memory_working_set_bytes{name!=\"\"} / 1024 / 1024", "legendFormat": "{{name}}"}]},
|
||||
{"id": 6, "title": "Top error sources (1h)", "type": "logs", "gridPos": {"x": 0, "y": 12, "w": 24, "h": 8}, "datasource": {"uid": "loki"}, "targets": [{"expr": "{level=\"error\"} | json"}]}
|
||||
]
|
||||
}
|
||||
@@ -5,6 +5,7 @@ refs docs/superpowers/specs/2026-05-01-observability-port-design.md
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
@@ -276,3 +277,26 @@ class TestSubdomainSwap:
|
||||
class TestJaegerRemoved:
|
||||
def test_jaeger_service_gone(self):
|
||||
assert "jaeger" not in _services()
|
||||
|
||||
|
||||
EXPECTED_DASHBOARDS = {
|
||||
"homelab-overview",
|
||||
"gateway-auth",
|
||||
"data-lake",
|
||||
"data-pipelines",
|
||||
"ci",
|
||||
"gpu-notebooks",
|
||||
}
|
||||
|
||||
|
||||
class TestDashboards:
|
||||
def test_all_dashboards_exist_and_parse(self):
|
||||
dash_dir = ROOT / "infra/grafana/dashboards"
|
||||
files = {p.stem for p in dash_dir.glob("*.json")}
|
||||
missing = EXPECTED_DASHBOARDS - files
|
||||
assert not missing, f"missing dashboards: {missing}"
|
||||
for name in EXPECTED_DASHBOARDS:
|
||||
with (dash_dir / f"{name}.json").open() as f:
|
||||
data = json.load(f)
|
||||
assert "panels" in data, f"{name}: no panels[]"
|
||||
assert data["panels"], f"{name}: empty panels[]"
|
||||
|
||||
Reference in New Issue
Block a user