Prometheus exporter for GitHub self-hosted Actions runners, authenticated as a GitHub App installation.
A background poller refreshes the organization's runner state from the GitHub
API every POLL_INTERVAL and caches it. Prometheus scrapes are served from that
cache, so GitHub API load stays constant no matter how many Prometheus replicas
scrape the exporter.
| Metric | Type | Labels | Description |
|---|---|---|---|
github_runner_status |
gauge | org, runner, name, os |
1 if the runner is online, else 0 |
github_runner_busy |
gauge | org, runner, name, os |
1 if the runner is currently running a job, else 0 |
github_runners_total |
gauge | org |
Total number of registered runners |
github_runners_online_total |
gauge | org |
Number of online runners |
github_runners_busy_total |
gauge | org |
Number of busy runners |
github_scrape_success |
gauge | org |
1 if the last GitHub API poll succeeded, else 0 |
The number of free runners (a proxy for spare queue capacity) can be derived in PromQL as
github_runners_online_total - github_runners_busy_total.
Standard Go runtime and process collectors are also exposed.
All settings come from environment variables.
| Variable | Required | Default | Description |
|---|---|---|---|
APP_ID |
yes | — | GitHub App ID |
INSTALLATION_ID |
yes | — | GitHub App installation ID |
PRIVATE_KEY |
yes | — | Path to the App's PEM private key |
GITHUB_ORG |
yes | — | Organization whose runners are scraped |
LISTEN_ADDRESS |
no | :9101 |
HTTP listen address |
METRICS_PATH |
no | /metrics |
Metrics endpoint path |
GITHUB_API_URL |
no | public github.com | API base URL for GitHub Enterprise Server |
SCRAPE_TIMEOUT |
no | 30s |
Timeout for a single GitHub API request (Go duration) |
POLL_INTERVAL |
no | 30s |
How often the background poller refreshes runner state (Go duration) |
The GitHub App needs the Self-hosted runners: Read-only organization permission.
GET /metrics— Prometheus metricsGET /healthz— liveness probe:200while the process is aliveGET /readyz— readiness probe:200once runner state has been fetched successfully,503(with the reason) before the first successful poll or after a failed oneGET /— landing page
A GitHub API outage makes the exporter not ready (it has no fresh runner data to serve) but still alive — restarting the pod would not fix an upstream outage, so only the readiness probe fails.
livenessProbe:
httpGet:
path: /healthz
port: 9101
readinessProbe:
httpGet:
path: /readyz
port: 9101
periodSeconds: 10export APP_ID=123456
export INSTALLATION_ID=7891011
export PRIVATE_KEY=/etc/github-exporter/app.pem
export GITHUB_ORG=my-org
go run ./cmddocker build -t github-exporter .
docker run --rm -p 9101:9101 \
-e APP_ID=123456 \
-e INSTALLATION_ID=7891011 \
-e GITHUB_ORG=my-org \
-e PRIVATE_KEY=/etc/github-exporter/app.pem \
-v /path/to/app.pem:/etc/github-exporter/app.pem:ro \
github-exporterThe image is a multi-stage build on distroless/static (~19 MB), running as a
non-root user with a read-only root filesystem.
The chart lives in .helm. Build and deploy with werf:
werf converge --env production \
--set config.appId=123456 \
--set config.installationId=7891011 \
--set config.org=my-org \
--set-file privateKey.value=/path/to/app.pemOr render/apply the chart with plain Helm (set the image explicitly):
helm template .helm \
--set image.repository=registry.example.com/github-exporter \
--set image.tag=v0.1.0 \
--set config.appId=123456 \
--set config.installationId=7891011 \
--set config.org=my-org \
--set-file privateKey.value=/path/to/app.pem | kubectl apply -f -Provide the GitHub App PEM key in one of two ways:
privateKey.value— the chart creates aSecretfrom it.privateKey.existingSecret(+privateKey.existingSecretKey) — reference a pre-createdSecret(recommended for production; keep the key out of values).
Enable the ServiceMonitor (Prometheus Operator) with
--set serviceMonitor.enabled=true. Without the operator, add a scrape config
pointing at the Service on port 9101, path /metrics.
go build ./...
go vet ./...
go test ./...