10K+
A lightweight Prometheus exporter for Semaphore UIā written in Go.
It polls the Semaphore REST API on a configurable interval, stores the data in a
local file-backed cache, and exposes all metrics via a standard /metrics endpoint
ā so that every Prometheus scrape reads from cache and never hammers the API.
| Metric | Labels | Description |
|---|---|---|
semaphore_up | ā | 1 if data has been fetched at least once, 0 otherwise |
semaphore_cache_age_seconds | ā | Age of the cached data in seconds |
semaphore_cache_last_update_timestamp_seconds | ā | Unix timestamp of the last successful cache update |
| Metric | Labels | Description |
|---|---|---|
semaphore_project_info | project_id, project_name, alert_chat, created | Project metadata (value is always 1) |
semaphore_project_max_parallel_tasks | project_id, project_name | Maximum parallel tasks allowed per project |
| Metric | Labels | Description |
|---|---|---|
semaphore_task_info | task_id, project_id, template_id, status, playbook, message, debug, dry_run, diff, created | Task metadata (value is always 1) |
semaphore_task_duration_seconds | task_id, project_id, template_id, status | Task wall-clock duration in seconds (-1 if still running or no end time recorded) |
semaphore_task_status_total | project_id, status | Task count per project and status combination |
| Metric | Labels | Description |
|---|---|---|
semaphore_template_info | template_id, project_id, name, playbook, description, type | Template metadata (value is always 1) |
semaphore_template_count | project_id, project_name | Total number of templates per project |
| Metric | Labels | Description |
|---|---|---|
semaphore_schedule_info | schedule_id, project_id, template_id, cron_format, name, active, delete_after_run | Schedule metadata (value is always 1) |
semaphore_schedule_count | project_id, project_name | Total number of schedules per project |
| Metric | Labels | Description |
|---|---|---|
semaphore_event_info | object_type, object_id, project_id, description, user_id, user_name, username, created | Audit event metadata ā last N events (configurable via MAX_EVENTS) |
| Metric | Labels | Description |
|---|---|---|
semaphore_user_info | user_id, name, username, email, admin, external | User metadata (value is always 1) |
semaphore_user_count | ā | Total number of users |
Note: Fetching users requires an admin API token. If a non-admin token is used, user metrics will be empty but all other metrics will still work.
cp .env.example .env
# Edit .env ā set at minimum SEMAPHORE_URL and SEMAPHORE_API_TOKEN
docker compose up -d
Metrics are available at http://localhost:9090/metricsā .
All settings are controlled via environment variables:
| Variable | Default | Description |
|---|---|---|
SEMAPHORE_URL | (required) | Base URL of your Semaphore instance, e.g. http://semaphore:3000 |
SEMAPHORE_API_TOKEN | (required) | API token ā Semaphore UI ā Your Profile ā API Tokens |
LISTEN_ADDRESS | :9090 | Address the HTTP server binds to |
SCRAPE_INTERVAL | 30m | How often to fetch from Semaphore (Go duration: 30s, 5m, 1h) |
MAX_EVENTS | 100 | Number of audit events to fetch and expose |
HTTP_TIMEOUT | 30s | Timeout for HTTP requests to Semaphore |
INSECURE_SKIP_VERIFY | false | Skip TLS certificate verification (not recommended in production) |
CACHE_FILE | /opt/semaphore-prometheus-exporter/data/cache.json | Path of the JSON cache file inside the container |
CACHE_DATA_PATH | ./data | Docker Compose only ā host path mounted as the cache volume |
EXPORTER_PORT | 9090 | Docker Compose only ā host port the metrics endpoint is exposed on |
LOG_LEVEL | info | Log verbosity: debug, info, warn, error |
SEMAPHORE_API_TOKENscrape_configs:
- job_name: semaphore
static_configs:
- targets: ["semaphore-prometheus-exporter:9090"]
scrape_interval: 1m # Can be faster than SCRAPE_INTERVAL ā reads from cache
A ready-to-import Grafana dashboard is included at grafana-dashboard.json.
Import steps:
grafana-dashboard.jsonThe dashboard covers:
All log output is in JSON format following the Elastic Common Schema (ECS) 1.12ā , making it compatible with Wazuh, Elasticsearch, and other ECS-aware SIEM systems.
Example log entry:
{
"@timestamp": "2026-03-01T19:23:26.123456789Z",
"log": { "level": "info" },
"message": "Fetched events",
"count": 100,
"service": { "name": "semaphore-prometheus-exporter", "type": "metrics" },
"ecs": { "version": "1.12.0" }
}
Log level is controlled via the LOG_LEVEL environment variable (debug, info, warn, error).
go mod download
go build -ldflags="-X main.Version=1.0.0" -o semaphore-prometheus-exporter .
./semaphore-prometheus-exporter
The application version is defined in version.goā :
const Version = "1.0.0"
Update this value manually before each release. The CI/CD pipeline reads it automatically and applies it as a Docker image tag alongside latest and the current date:
docker.io/vremenar/semaphore-prometheus-exporter:latest
docker.io/vremenar/semaphore-prometheus-exporter:2026-03-01
docker.io/vremenar/semaphore-prometheus-exporter:1.0.0
The cache directory /opt/semaphore-prometheus-exporter/data is declared as a Docker volume.
On restart the exporter loads the last-known data from disk immediately and serves it
until the first successful API fetch completes.
To use a custom host path, set CACHE_DATA_PATH in your .env file:
CACHE_DATA_PATH=/var/lib/semaphore-prometheus-exporter
GET /healthz ā 200 OK (always, as long as the process is alive)
GET / ā HTML index page with links to /metrics, /healthz and GitHub
Content type
Image
Digest
sha256:94712f36fā¦
Size
11.9 MB
Last updated
4 days ago
docker pull vremenar/semaphore-prometheus-exporter