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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ scrape.
| `github_runner_up` | `runner`, `os` | 1 if the runner is registered and online, 0 if offline |
| `github_runner_busy` | `runner`, `os` | 1 if the runner is currently executing a job, 0 if idle |
| `github_org_up` | | 1 if the last org/repo stats poll succeeded, 0 if a stale cache is being served or the first poll is still running (about a minute after startup at ~25 repos) |
| `github_org_info` | `org` | Always 1; the org this exporter watches. The dashboard reads it to link to github.com |
| `github_org_repos_total` | `visibility` | Number of non-archived repos, by `public`/`private` |
| `github_rate_limit_remaining` | | Remaining core API budget in the scarcest active window: the one that runs out first |
| `github_rate_limit_limit` | | Total core API budget of that window |
Expand Down Expand Up @@ -153,7 +154,7 @@ images to GHCR with SLSA provenance attestation on push to `main`/tags.
[`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;
- CI health per repo and workflow, open PRs, Dependabot alerts by severity and by repo, and the API rate limit; the PR and Dependabot panels link through to the lists on github.com;
- CI history: average and p95 run time per job, queue time per runner pool and per runner, and workflow runs by conclusion.

Import it in Grafana (Dashboards → New → Import) and pick your Prometheus data source.
Expand Down
167 changes: 159 additions & 8 deletions dashboards/github-actions-runner-exporter.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"prometheus"
],
"schemaVersion": 39,
"version": 6,
"version": 7,
"editable": true,
"time": {
"from": "now-24h",
Expand All @@ -35,6 +35,23 @@
"hide": 0,
"refresh": 1
},
{
"name": "org",
"label": "GitHub org",
"type": "query",
"datasource": {
"type": "prometheus",
"uid": "${datasource}"
},
"query": {
"query": "label_values(github_org_info, org)",
"refId": "org"
},
"definition": "label_values(github_org_info, org)",
"refresh": 1,
"hide": 2,
"current": {}
},
{
"name": "repo",
"label": "Repo",
Expand Down Expand Up @@ -661,6 +678,13 @@
"range": false,
"refId": "A"
}
],
"links": [
{
"title": "All open PRs in the org",
"url": "https://github.com/pulls?q=is%3Aopen+is%3Apr+org%3A${org}",
"targetBlank": true
}
]
},
{
Expand All @@ -669,7 +693,7 @@
"type": "bargauge",
"gridPos": {
"h": 8,
"w": 12,
"w": 8,
"x": 0,
"y": 18
},
Expand Down Expand Up @@ -697,12 +721,21 @@
"color": "red"
}
]
}
},
"noValue": "No open alerts"
}
},
"options": {
"orientation": "horizontal",
"displayMode": "gradient"
"displayMode": "gradient",
"sizing": "manual",
"minVizHeight": 16,
"maxVizHeight": 28,
"text": {
"titleSize": 14,
"valueSize": 16
},
"namePlacement": "top"
},
"targets": [
{
Expand All @@ -712,16 +745,108 @@
"range": false,
"refId": "A"
}
],
"description": "Open Dependabot alerts across the org. Click the title link for the list on GitHub.",
"links": [
{
"title": "Dependabot alerts on GitHub",
"url": "https://github.com/orgs/${org}/security/alerts/dependabot",
"targetBlank": true
}
]
},
{
"id": 25,
"title": "Dependabot Alerts by Repo",
"type": "table",
"datasource": {
"type": "prometheus",
"uid": "${datasource}"
},
"description": "Open alerts per repo and severity. Click a repo to open its Dependabot alerts on GitHub.",
"gridPos": {
"h": 8,
"w": 8,
"x": 8,
"y": 18
},
"targets": [
{
"expr": "max by (repo, severity) (github_repo_dependabot_alerts_open)",
"format": "table",
"instant": true,
"range": false,
"refId": "A"
}
],
"transformations": [
{
"id": "groupingToMatrix",
"options": {
"columnField": "severity",
"rowField": "repo",
"valueField": "Value",
"emptyValue": "zero"
}
},
{
"id": "organize",
"options": {
"renameByName": {
"repo\\severity": "repo"
},
"indexByName": {
"repo\\severity": 0,
"critical": 1,
"high": 2,
"medium": 3,
"low": 4
}
}
}
],
"fieldConfig": {
"defaults": {
"noValue": "No open alerts",
"unit": "short",
"custom": {
"align": "auto"
}
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "repo"
},
"properties": [
{
"id": "links",
"value": [
{
"title": "Dependabot alerts in ${__data.fields.repo}",
"url": "https://github.com/${org}/${__data.fields.repo}/security/dependabot",
"targetBlank": true
}
]
}
]
}
]
},
"options": {
"showHeader": true,
"cellHeight": "sm"
}
},
{
"id": 15,
"title": "Open PRs by Repo",
"type": "bargauge",
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"w": 8,
"x": 16,
"y": 18
},
"datasource": {
Expand All @@ -741,12 +866,30 @@
"color": "blue"
}
]
}
},
"displayName": "${__field.labels.repo}",
"links": [
{
"title": "Open PRs in ${__field.labels.repo}",
"url": "https://github.com/${org}/${__field.labels.repo}/pulls",
"targetBlank": true
}
]
}
},
"options": {
"orientation": "horizontal",
"displayMode": "gradient"
"displayMode": "gradient",
"namePlacement": "top",
"showUnfilled": true,
"valueMode": "color",
"sizing": "manual",
"minVizHeight": 16,
"maxVizHeight": 28,
"text": {
"titleSize": 14,
"valueSize": 16
}
},
"targets": [
{
Expand All @@ -756,6 +899,14 @@
"range": false,
"refId": "A"
}
],
"description": "Click a bar to open that repo's pull requests on GitHub.",
"links": [
{
"title": "All open PRs in the org",
"url": "https://github.com/pulls?q=is%3Aopen+is%3Apr+org%3A${org}",
"targetBlank": true
}
]
},
{
Expand Down
11 changes: 9 additions & 2 deletions internal/collector/collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@ import (
const namespace = "github"

type Collector struct {
org string
runnerFetcher *fetch.Fetcher[runners.Summary]
orgFetcher *fetch.Fetcher[orgstats.Summary]
rateLimitFn func() map[time.Time]github.RateLimit

runnerUp *prometheus.Desc
runnerBusy *prometheus.Desc
runnersUp *prometheus.Desc
orgInfo *prometheus.Desc
orgUp *prometheus.Desc
reposTotal *prometheus.Desc
rateLimit *prometheus.Desc
Expand All @@ -37,14 +39,15 @@ type Collector struct {
}

// New builds the collector. The intervals only feed HELP text; polling is the Fetchers' job.
func New(runnerFetcher *fetch.Fetcher[runners.Summary], orgFetcher *fetch.Fetcher[orgstats.Summary], rateLimits func() map[time.Time]github.RateLimit, runnerCacheTTL, orgCacheTTL time.Duration) *Collector {
func New(org string, runnerFetcher *fetch.Fetcher[runners.Summary], orgFetcher *fetch.Fetcher[orgstats.Summary], rateLimits func() map[time.Time]github.RateLimit, runnerCacheTTL, orgCacheTTL time.Duration) *Collector {
runnerNote := fmt.Sprintf(" Polled every %s.", runnerCacheTTL)
orgNote := fmt.Sprintf(" Polled every %s - not real-time by design, see internal/orgstats.", orgCacheTTL)
desc := func(subsystem, name, help string, labels []string) *prometheus.Desc {
return prometheus.NewDesc(prometheus.BuildFQName(namespace, subsystem, name), help, labels, nil)
}

return &Collector{
org: org,
runnerFetcher: runnerFetcher,
orgFetcher: orgFetcher,
rateLimitFn: rateLimits,
Expand All @@ -58,6 +61,9 @@ func New(runnerFetcher *fetch.Fetcher[runners.Summary], orgFetcher *fetch.Fetche

orgUp: desc("org", "up",
"Whether the last scrape of the org/repo stats succeeded (1) or a stale cache is being served (0)."+orgNote, nil),
// lets dashboards build github.com links without a hand-set variable
orgInfo: desc("org", "info",
"Always 1; the GitHub org this exporter watches is in the org label.", []string{"org"}),
reposTotal: desc("org", "repos_total",
"Number of non-archived repos in the org."+orgNote, []string{"visibility"}),
rateLimit: desc("rate_limit", "remaining",
Expand Down Expand Up @@ -88,7 +94,7 @@ func New(runnerFetcher *fetch.Fetcher[runners.Summary], orgFetcher *fetch.Fetche
func (c *Collector) Describe(ch chan<- *prometheus.Desc) {
for _, d := range []*prometheus.Desc{
c.runnersUp, c.runnerUp, c.runnerBusy,
c.orgUp, c.reposTotal, c.rateLimit, c.rateLimitCap, c.rateWindow,
c.orgInfo, c.orgUp, c.reposTotal, c.rateLimit, c.rateLimitCap, c.rateWindow,
c.repoOpenPRs, c.repoCILastRunConclusion, c.repoCILastRunAt, c.repoCIDuration, c.repoDependabot,
} {
ch <- d
Expand All @@ -99,6 +105,7 @@ func (c *Collector) Collect(ch chan<- prometheus.Metric) {
c.collectRunners(ch)
c.collectOrgStats(ch)
c.collectRateLimits(ch)
ch <- prometheus.MustNewConstMetric(c.orgInfo, prometheus.GaugeValue, 1, c.org)
}

// collectRateLimits reports every active core window plus the scarcest one,
Expand Down
2 changes: 1 addition & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func main() {
}, cfg.OrgCacheTTL, cfg.OrgCacheMaxStale, log)

registry := prometheus.NewRegistry()
registry.MustRegister(collector.New(runnerFetcher, orgFetcher, client.RateLimits, cfg.RunnerCacheTTL, cfg.OrgCacheTTL), feed)
registry.MustRegister(collector.New(cfg.GitHubOrg, runnerFetcher, orgFetcher, client.RateLimits, cfg.RunnerCacheTTL, cfg.OrgCacheTTL), feed)

mux := http.NewServeMux()
mux.Handle("/metrics", promhttp.HandlerFor(registry, promhttp.HandlerOpts{}))
Expand Down
Loading