diff --git a/README.md b/README.md index bb086bc..1a1f772 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,17 @@ never a real GitHub org). The Trivy image scan runs separately (`security.yml`, on PRs and weekly on main). `build.yml` pushes multi-arch images to GHCR with SLSA provenance attestation on push to `main`/tags. +## Grafana dashboard + +![Grafana dashboard: runners online/offline/busy, poll health, per-runner online and busy timelines, org repo counts, API rate limit and open PRs](docs/dashboard.png) + +[`dashboards/github-actions-runner-exporter.json`](dashboards/github-actions-runner-exporter.json) covers every metric above: +- runners online, offline and busy, with per-runner state timelines; +- both exporter polls' health, so a stale cache is visible; +- CI health per repo and workflow, open PRs, Dependabot alerts by severity, and the API rate limit. + +Import it in Grafana (Dashboards → New → Import) and pick your Prometheus data source. + ## How it was made Built with the help of an AI coding assistant (Claude). I review and test what gets published. diff --git a/dashboards/github-actions-runner-exporter.json b/dashboards/github-actions-runner-exporter.json new file mode 100644 index 0000000..8830323 --- /dev/null +++ b/dashboards/github-actions-runner-exporter.json @@ -0,0 +1,911 @@ +{ + "title": "GitHub Actions Runners & Org Health", + "uid": "github-actions-runner-exporter", + "description": "Dashboard for drumandbytes/github-actions-runner-exporter: runner online/busy state, CI health per workflow, open PRs, Dependabot alerts and API rate limit.", + "tags": [ + "github", + "github-actions", + "prometheus" + ], + "schemaVersion": 39, + "version": 1, + "editable": true, + "time": { + "from": "now-24h", + "to": "now" + }, + "refresh": "1m", + "timezone": "browser", + "links": [ + { + "title": "github-actions-runner-exporter", + "url": "https://github.com/drumandbytes/github-actions-runner-exporter", + "type": "link", + "targetBlank": true + } + ], + "templating": { + "list": [ + { + "name": "datasource", + "label": "Prometheus", + "type": "datasource", + "query": "prometheus", + "current": {}, + "hide": 0, + "refresh": 1 + } + ] + }, + "annotations": { + "list": [] + }, + "panels": [ + { + "type": "row", + "title": "Runners", + "collapsed": false, + "gridPos": { + "x": 0, + "y": 0, + "w": 24, + "h": 1 + }, + "panels": [], + "id": 1 + }, + { + "type": "stat", + "title": "Runners online", + "description": "Registered runners currently online", + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "gridPos": { + "x": 0, + "y": 1, + "w": 5, + "h": 4 + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum(max by (runner) (github_runner_up))", + "instant": true + } + ], + "options": { + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "colorMode": "background", + "graphMode": "none", + "textMode": "value" + }, + "fieldConfig": { + "defaults": { + "unit": "none", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "green", + "value": 1 + } + ] + } + }, + "overrides": [] + }, + "id": 2 + }, + { + "type": "stat", + "title": "Runners offline", + "description": "", + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "gridPos": { + "x": 5, + "y": 1, + "w": 5, + "h": 4 + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "count(max by (runner) (github_runner_up) == 0) or vector(0)", + "instant": true + } + ], + "options": { + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "colorMode": "background", + "graphMode": "none", + "textMode": "value" + }, + "fieldConfig": { + "defaults": { + "unit": "none", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 1 + } + ] + } + }, + "overrides": [] + }, + "id": 3 + }, + { + "type": "stat", + "title": "Busy now", + "description": "Runners executing a job", + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "gridPos": { + "x": 10, + "y": 1, + "w": 5, + "h": 4 + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum(max by (runner) (github_runner_busy))", + "instant": true + } + ], + "options": { + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "colorMode": "background", + "graphMode": "none", + "textMode": "value" + }, + "fieldConfig": { + "defaults": { + "unit": "none", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "id": 4 + }, + { + "type": "stat", + "title": "Runner poll", + "description": "0 means the exporter is serving a stale cache", + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "gridPos": { + "x": 15, + "y": 1, + "w": 4, + "h": 4 + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "max(github_runners_up)", + "instant": true + } + ], + "options": { + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "colorMode": "background", + "graphMode": "none", + "textMode": "value" + }, + "fieldConfig": { + "defaults": { + "unit": "none", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "green", + "value": 1 + } + ] + }, + "mappings": [ + { + "type": "value", + "options": { + "0": { + "text": "stale" + }, + "1": { + "text": "ok" + } + } + } + ] + }, + "overrides": [] + }, + "id": 5 + }, + { + "type": "stat", + "title": "Org stats poll", + "description": "0 means org/repo stats are from a stale cache", + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "gridPos": { + "x": 19, + "y": 1, + "w": 5, + "h": 4 + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "max(github_org_up)", + "instant": true + } + ], + "options": { + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "colorMode": "background", + "graphMode": "none", + "textMode": "value" + }, + "fieldConfig": { + "defaults": { + "unit": "none", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "green", + "value": 1 + } + ] + }, + "mappings": [ + { + "type": "value", + "options": { + "0": { + "text": "stale" + }, + "1": { + "text": "ok" + } + } + } + ] + }, + "overrides": [] + }, + "id": 6 + }, + { + "id": 7, + "title": "Runner Online", + "type": "state-timeline", + "gridPos": { + "x": 0, + "y": 5, + "w": 12, + "h": 8 + }, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "mappings": [ + { + "type": "value", + "options": { + "0": { + "text": "Offline", + "color": "red" + }, + "1": { + "text": "Online", + "color": "green" + } + } + } + ] + } + }, + "targets": [ + { + "expr": "max by (runner, os) (github_runner_up)", + "legendFormat": "{{runner}}", + "range": true, + "instant": false, + "refId": "A" + } + ] + }, + { + "id": 8, + "title": "Runner Busy", + "type": "state-timeline", + "gridPos": { + "x": 12, + "y": 5, + "w": 12, + "h": 8 + }, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "mappings": [ + { + "type": "value", + "options": { + "0": { + "text": "Idle", + "color": "blue" + }, + "1": { + "text": "Running job", + "color": "orange" + } + } + } + ] + } + }, + "targets": [ + { + "expr": "max by (runner, os) (github_runner_busy)", + "legendFormat": "{{runner}}", + "range": true, + "instant": false, + "refId": "A" + } + ] + }, + { + "type": "row", + "title": "Organization", + "collapsed": false, + "gridPos": { + "x": 0, + "y": 13, + "w": 24, + "h": 1 + }, + "panels": [], + "id": 9 + }, + { + "id": 10, + "title": "Public Repos", + "type": "stat", + "gridPos": { + "h": 4, + "w": 6, + "x": 0, + "y": 14 + }, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "unit": "short" + } + }, + "targets": [ + { + "expr": "max by (visibility) (github_org_repos_total{visibility=\"public\"})", + "instant": true, + "range": false, + "refId": "A" + } + ] + }, + { + "id": 11, + "title": "Private Repos", + "type": "stat", + "gridPos": { + "h": 4, + "w": 6, + "x": 6, + "y": 14 + }, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "unit": "short" + } + }, + "targets": [ + { + "expr": "max by (visibility) (github_org_repos_total{visibility=\"private\"})", + "instant": true, + "range": false, + "refId": "A" + } + ] + }, + { + "id": 12, + "title": "API Rate Limit Remaining", + "type": "stat", + "gridPos": { + "h": 4, + "w": 6, + "x": 12, + "y": 14 + }, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "unit": "short", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": null, + "color": "red" + }, + { + "value": 200, + "color": "yellow" + }, + { + "value": 1000, + "color": "green" + } + ] + } + } + }, + "targets": [ + { + "expr": "max(github_rate_limit_remaining)", + "instant": true, + "range": false, + "refId": "A" + } + ] + }, + { + "id": 13, + "title": "Open PRs (Org-wide)", + "type": "stat", + "gridPos": { + "h": 4, + "w": 6, + "x": 18, + "y": 14 + }, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "unit": "short", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": null, + "color": "green" + }, + { + "value": 10, + "color": "yellow" + }, + { + "value": 25, + "color": "red" + } + ] + } + } + }, + "targets": [ + { + "expr": "sum(max by (repo) (github_repo_open_prs))", + "instant": true, + "range": false, + "refId": "A" + } + ] + }, + { + "id": 14, + "title": "Dependabot Alerts by Severity (Org-wide)", + "type": "bargauge", + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 18 + }, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "unit": "short", + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": null, + "color": "green" + }, + { + "value": 1, + "color": "yellow" + }, + { + "value": 5, + "color": "red" + } + ] + } + } + }, + "options": { + "orientation": "horizontal", + "displayMode": "gradient" + }, + "targets": [ + { + "expr": "sum by (severity) (max by (repo, severity) (github_repo_dependabot_alerts_open))", + "legendFormat": "{{severity}}", + "instant": true, + "range": false, + "refId": "A" + } + ] + }, + { + "id": 15, + "title": "Open PRs by Repo", + "type": "bargauge", + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 18 + }, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "unit": "short", + "min": 0, + "noValue": "No open PRs", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": null, + "color": "blue" + } + ] + } + } + }, + "options": { + "orientation": "horizontal", + "displayMode": "gradient" + }, + "targets": [ + { + "expr": "max by (repo) (github_repo_open_prs) > 0", + "legendFormat": "{{repo}}", + "instant": true, + "range": false, + "refId": "A" + } + ] + }, + { + "id": 16, + "title": "CI Health (per repo/workflow)", + "description": "One row per repo/workflow, showing the raw GitHub conclusion (green success, red failure/timed_out/action_required, blue cancelled/skipped/stale, purple neutral) rather than a collapsed pass/fail. Only the latest run per workflow is tracked (not full history).", + "type": "table", + "gridPos": { + "h": 16, + "w": 24, + "x": 0, + "y": 26 + }, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "targets": [ + { + "expr": "max by (repo, workflow, url, conclusion) (github_repo_ci_last_run_conclusion)", + "format": "table", + "instant": true, + "range": false, + "refId": "A" + }, + { + "expr": "max by (repo, workflow) (github_repo_ci_last_run_timestamp_seconds) * 1000", + "format": "table", + "instant": true, + "range": false, + "refId": "B" + } + ], + "transformations": [ + { + "id": "merge", + "options": {} + }, + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true, + "Value #A": true + }, + "renameByName": { + "conclusion": "Status", + "Value #B": "Last Run" + }, + "indexByName": { + "repo": 0, + "workflow": 1, + "Status": 2, + "Last Run": 3 + } + } + } + ], + "fieldConfig": { + "defaults": {}, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Status" + }, + "properties": [ + { + "id": "mappings", + "value": [ + { + "type": "value", + "options": { + "success": { + "text": "Passing", + "color": "green", + "index": 0 + }, + "failure": { + "text": "Failing", + "color": "red", + "index": 1 + }, + "timed_out": { + "text": "Timed Out", + "color": "red", + "index": 2 + }, + "action_required": { + "text": "Action Required", + "color": "orange", + "index": 3 + }, + "cancelled": { + "text": "Cancelled", + "color": "blue", + "index": 4 + }, + "skipped": { + "text": "Skipped", + "color": "blue", + "index": 5 + }, + "neutral": { + "text": "Neutral", + "color": "purple", + "index": 6 + }, + "stale": { + "text": "Stale", + "color": "blue", + "index": 7 + } + } + } + ] + }, + { + "id": "custom.cellOptions", + "value": { + "type": "color-background" + } + }, + { + "id": "links", + "value": [ + { + "title": "View run on GitHub", + "url": "${__data.fields.url}", + "targetBlank": true + } + ] + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Last Run" + }, + "properties": [ + { + "id": "unit", + "value": "dateTimeFromNow" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "url" + }, + "properties": [ + { + "id": "custom.hidden", + "value": true + } + ] + } + ] + }, + "options": { + "sortBy": [ + { + "desc": true, + "displayName": "Last Run" + } + ], + "cellHeight": "sm" + } + }, + { + "id": 17, + "title": "API Rate Limit Remaining Over Time", + "type": "timeseries", + "gridPos": { + "h": 6, + "w": 24, + "x": 0, + "y": 42 + }, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "unit": "short", + "custom": { + "fillOpacity": 10 + } + } + }, + "targets": [ + { + "expr": "max(github_rate_limit_remaining)", + "legendFormat": "remaining", + "range": true, + "instant": false, + "refId": "A" + } + ] + } + ] +} diff --git a/docs/dashboard.png b/docs/dashboard.png new file mode 100644 index 0000000..e03a300 Binary files /dev/null and b/docs/dashboard.png differ