Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions grafana/dashboards/github-prs.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,18 @@
{
"name": "scope",
"label": "Scope",
"type": "custom",
"query": "All repos : org:JSONbored, metagraphed : repo:JSONbored/metagraphed, gittensory : repo:JSONbored/gittensory, awesome-claude : repo:JSONbored/awesome-claude, metagraphed-ui : repo:JSONbored/metagraphed-ui",
"type": "query",
"datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" },
"description": "Dynamic, never hardcoded: built from whichever repos are actually tracked in this self-hoster's own reporting DB. 'All repos' assumes a single-owner setup (the same assumption the dashboard's own GitHub-search-syntax scope always required, since GitHub search can't OR multiple repo: qualifiers in one query) — it widens to org:<owner> of the first tracked repo. A self-hoster tracking repos across multiple owners should pick a specific repo instead of 'All repos'.",
"query": {
"queryType": "table",
"rawQueryText": "SELECT * FROM (SELECT 0 AS ord, 'All repos' AS \"__text\", (SELECT 'org:' || substr(repo, 1, instr(repo, '/') - 1) FROM review_targets LIMIT 1) AS \"__value\" UNION ALL SELECT 1 AS ord, repo AS \"__text\", 'repo:' || repo AS \"__value\" FROM (SELECT DISTINCT repo FROM review_targets ORDER BY repo)) ORDER BY ord, \"__text\""
},
"current": { "text": "All repos", "value": "org:JSONbored" },
"includeAll": false,
"multi": false
"multi": false,
"refresh": 2,
"sort": 0
}
]
},
Expand Down
117 changes: 3 additions & 114 deletions grafana/dashboards/gittensory.json
Original file line number Diff line number Diff line change
Expand Up @@ -507,42 +507,6 @@
}
},
"gridPos": { "h": 4, "w": 6, "x": 0, "y": 33 },
"id": 13,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": ["lastNotNull"],
"fields": "",
"values": false
},
"textMode": "auto"
},
"title": "Orb Events Recorded",
"type": "stat",
"targets": [
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"expr": "gittensory_orb_events_recorded_total or vector(0)",
"legendFormat": "recorded"
}
]
},
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"fieldConfig": {
"defaults": {
"color": { "mode": "thresholds" },
"thresholds": {
"mode": "absolute",
"steps": [{ "color": "green", "value": null }]
},
"unit": "short"
}
},
"gridPos": { "h": 4, "w": 6, "x": 6, "y": 33 },
"id": 14,
"options": {
"colorMode": "value",
Expand Down Expand Up @@ -581,7 +545,7 @@
"unit": "short"
}
},
"gridPos": { "h": 4, "w": 6, "x": 12, "y": 33 },
"gridPos": { "h": 4, "w": 6, "x": 6, "y": 33 },
"id": 15,
"options": {
"colorMode": "background",
Expand All @@ -605,42 +569,6 @@
}
]
},
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"fieldConfig": {
"defaults": {
"color": { "mode": "thresholds" },
"thresholds": {
"mode": "absolute",
"steps": [{ "color": "green", "value": null }]
},
"unit": "short"
}
},
"gridPos": { "h": 4, "w": 6, "x": 18, "y": 33 },
"id": 16,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": ["lastNotNull"],
"fields": "",
"values": false
},
"textMode": "auto"
},
"title": "Orb Installations",
"type": "stat",
"targets": [
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"expr": "gittensory_orb_installs_total or vector(0)",
"legendFormat": "installs"
}
]
},
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"fieldConfig": {
Expand All @@ -650,7 +578,7 @@
"unit": "ops"
}
},
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 37 },
"gridPos": { "h": 8, "w": 24, "x": 0, "y": 37 },
"id": 17,
"options": {
"legend": {
Expand All @@ -663,11 +591,6 @@
"title": "Orb Event Rate",
"type": "timeseries",
"targets": [
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"expr": "rate(gittensory_orb_events_recorded_total[5m]) or vector(0)",
"legendFormat": "recorded/s"
},
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"expr": "rate(gittensory_orb_events_exported_total[5m]) or vector(0)",
Expand All @@ -680,40 +603,6 @@
}
]
},
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"fieldConfig": {
"defaults": {
"color": { "mode": "palette-classic" },
"custom": { "lineWidth": 2, "fillOpacity": 10 },
"unit": "short"
}
},
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 37 },
"id": 18,
"options": {
"legend": {
"calcs": ["mean", "last"],
"displayMode": "table",
"placement": "bottom"
},
"tooltip": { "mode": "multi" }
},
"title": "Orb Pending vs Exported",
"type": "timeseries",
"targets": [
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"expr": "(gittensory_orb_events_recorded_total or vector(0)) - (gittensory_orb_events_exported_total or vector(0))",
"legendFormat": "pending"
},
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"expr": "gittensory_orb_events_exported_total or vector(0)",
"legendFormat": "exported (cumulative)"
}
]
},
{
"collapsed": false,
"gridPos": {
Expand Down Expand Up @@ -2504,7 +2393,7 @@
]
},
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 170 },
"id": 158,
"id": 205,
"options": {
"legend": { "calcs": ["lastNotNull"], "displayMode": "table", "placement": "bottom" },
"tooltip": { "mode": "multi", "sort": "desc" }
Expand Down
12 changes: 6 additions & 6 deletions prometheus/rules/alerts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -411,12 +411,12 @@ groups:
description: "Either the consecutive registration-failure streak has reached {{ $value | printf \"%.0f\" }}, or the pull-mode drain loop hasn't completed in over 30m. A lone registration timeout alone would not trigger this."
runbook: "Check gittensory_orb_relay_register_total{result=\"failed\"} by mode for the failure pattern, and confirm ORB_BROKER_URL / ORB_ENROLLMENT_SECRET are still valid. If pull mode, verify the drain loop itself isn't crash-looping (selfhost_orb_relay_register_failed logs at level=error)."

# ── HTTP serving health (uses the PLANNED status label + duration histogram) ─
# NOTE: gittensory_http_requests_total is gaining a status="2xx|3xx|4xx|5xx" label,
# and gittensory_http_request_duration_seconds (a histogram) is being added. Both
# rules below tolerate the metric/label being absent on older data: the ratio uses a
# `> 0` denominator guard, and an absent series simply yields no result (rule stays
# inactive) rather than erroring. They start firing the moment the new metrics exist.
# ── HTTP serving health (status label + duration histogram, both live in src/server.ts) ─
# gittensory_http_requests_total carries a status="2xx|3xx|4xx|5xx" label (seeded at zero per
# class so every series exists from boot), and gittensory_http_request_duration_seconds is a
# histogram of request duration. Both rules below still tolerate an absent metric/label on an
# older running build: the ratio uses a `> 0` denominator guard, and an absent series simply
# yields no result (rule stays inactive) rather than erroring.
- name: gittensory-http
rules:
- alert: GittensoryHighHttp5xxRatio
Expand Down
8 changes: 8 additions & 0 deletions scripts/setup-github-datasource.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@
# maintainer dashboards. Done over the API rather than file-provisioning on purpose: a backend datasource
# whose plugin/token isn't ready at boot would crash Grafana's provisioning, so we add it after Grafana is up.
#
# KNOWN, ACCEPTED trade-off: unlike the other 4 file-provisioned datasources (each `editable: false` in their
# YAML), this one is API-managed and therefore NOT locked read-only — Grafana's `readOnly` field is computed
# from whether a datasource came from file provisioning and cannot be set via the datasource API itself
# (verified empirically: a PUT with `"readOnly": true` in the payload is silently ignored, the response still
# reports `false`). Moving this to YAML provisioning to close that gap would reintroduce the exact boot-crash
# risk documented above, so this datasource's connection settings remain editable via the Grafana UI, unlike
# every other one in this stack.
#
# Prereqs: --profile observability running, the grafana-github-datasource plugin installed (GF_INSTALL_PLUGINS),
# and a read-only fine-grained PAT (Pull requests: read, Issues: read, Contents: read) on the repos.
#
Expand Down
63 changes: 60 additions & 3 deletions test/unit/selfhost-grafana-dashboard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const tmpRoots: string[] = [];
const dashboardPath = join(process.cwd(), "grafana/dashboards/maintainer-reviews.json");
const selfhostDashboardPath = join(process.cwd(), "grafana/dashboards/gittensory.json");
const selfhostAlertsPath = join(process.cwd(), "prometheus/rules/alerts.yml");
const githubPrsPath = join(process.cwd(), "grafana/dashboards/github-prs.json");
const timeFrom = "${__from:date:seconds}";
const timeTo = "${__to:date:seconds}";

Expand Down Expand Up @@ -118,11 +119,31 @@ describe("Gittensory Self-Host Grafana dashboard", () => {
const dashboard = readDashboard(selfhostDashboardPath);
const targets = dashboard.panels.flatMap((panel) => panel.targets ?? []);

expect(targets.some((target) => target.expr === "gittensory_orb_events_recorded_total or vector(0)")).toBe(true);
expect(targets.some((target) => target.expr === "gittensory_orb_events_exported_total or vector(0)")).toBe(true);
expect(targets.some((target) => target.expr === "gittensory_orb_installs_total or vector(0)")).toBe(true);
expect(targets.some((target) => target.expr === "gittensory_orb_export_errors_total or vector(0)")).toBe(true);
expect(targets.some((target) => target.expr === "sum by (result) (rate(gittensory_orb_webhook_total[5m])) or vector(0)")).toBe(true);
expect(targets.some((target) => target.expr === "(gittensory_orb_events_recorded_total or vector(0)) - (gittensory_orb_events_exported_total or vector(0))")).toBe(true);
});

it("no longer references gittensory_orb_events_recorded_total / gittensory_orb_installs_total, retired with the per-instance Orb App in #1256 but never cleaned out of the dashboard (2026-07 fix)", () => {
const dashboard = readDashboard(selfhostDashboardPath);
const targets = dashboard.panels.flatMap((panel) => panel.targets ?? []);
const titles = dashboard.panels.map((panel) => panel.title);

for (const target of targets) {
expect(target.expr ?? "").not.toContain("gittensory_orb_events_recorded_total");
expect(target.expr ?? "").not.toContain("gittensory_orb_installs_total");
}
expect(titles).not.toContain("Orb Events Recorded");
expect(titles).not.toContain("Orb Installations");
expect(titles).not.toContain("Orb Pending vs Exported");
});

it("assigns every panel a unique id (regression: 'Maintenance Admission Deferrals (total)' and 'Orb Relay Registration: Streak vs Drain Progress' both used id 158, 2026-07 fix)", () => {
const dashboard = readDashboard(selfhostDashboardPath);
const ids = dashboard.panels.map((panel) => panel.id).filter((id): id is number => id !== undefined);
const duplicates = ids.filter((id, index) => ids.indexOf(id) !== index);

expect(duplicates).toEqual([]);
});

it("surfaces the onMerge/combine/reviewer-count floor-clamp counter on a panel and an alert (#3901)", () => {
Expand Down Expand Up @@ -499,3 +520,39 @@ describe("maintainer Reviews & PRs Grafana dashboard", () => {
expect(tableRows).not.toContain("cut v1.0.0");
});
});

describe("github-prs.json: $scope is dynamic, never a hardcoded repo list (2026-07 fix)", () => {
function readGithubPrsDashboard(): {
templating: { list: Array<{ name: string; type: string; datasource?: { type?: string }; query?: { rawQueryText?: string } }> };
} {
return JSON.parse(readFileSync(githubPrsPath, "utf8"));
}

it("declares $scope as a dynamic, query-backed variable against the local reporting DB, not a hardcoded custom list", () => {
const vars = readGithubPrsDashboard().templating.list;
const scope = vars.find((v) => v.name === "scope");

expect(scope?.type).toBe("query");
expect(scope?.datasource?.type).toBe("frser-sqlite-datasource");
expect(scope?.query?.rawQueryText).toContain("review_targets");
expect(scope?.query?.rawQueryText).not.toContain("JSONbored");
});

(sqliteCliAvailable ? it : it.skip)("builds 'All repos' as org:<owner> of the first tracked repo, plus one repo: option per distinct tracked repo", () => {
const root = tmpRoot();
const db = join(root, "reporting.sqlite");
sqlite(
db,
`
CREATE TABLE review_targets (repo TEXT NOT NULL);
INSERT INTO review_targets (repo) VALUES ('acme/widgets'), ('acme/gadgets'), ('acme/widgets');
`,
);

const rawQueryText = readGithubPrsDashboard().templating.list.find((v) => v.name === "scope")?.query?.rawQueryText;
if (!rawQueryText) throw new Error("missing $scope rawQueryText");
const rows = sqlite(db, rawQueryText).split("\n");

expect(rows).toEqual(["0|All repos|org:acme", "1|acme/gadgets|repo:acme/gadgets", "1|acme/widgets|repo:acme/widgets"]);
});
});
Loading