registry-stats

command module
v1.0.32 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 16, 2026 License: GPL-2.0, GPL-3.0 Imports: 20 Imported by: 0

README

registry-stats

Image Size Platforms Go Report Card Test coverage Mutation OpenSSF Best Practices OpenSSF Scorecard SBOM

Track how many times your container images are pulled — with a ready-made Grafana dashboard.

What it does

When you publish a container image to Docker Hub or GitHub Container Registry (GHCR), each registry tracks how many times that image has been downloaded — but there's no built-in way to see those numbers over time, compare trends, or get alerts. Registry Stats solves this by polling the registries on a schedule, recording the download counts, and making the data available for dashboards and scripts.

  • Prometheus metrics (/metrics) — pull counts as gauges, scraped by Alloy/Prometheus into Mimir/Thanos for native Grafana dashboards
  • JSON API (/api/*) — raw data for scripts, automation, or any tool that speaks HTTP
  • Supports both explicit repos (myuser/myapp) and owner wildcards (myuser/*) to automatically discover and track all public repos for an owner. Wildcards are resolved on each poll cycle, so newly published images are picked up automatically.
  • Either surface can be disabled independently via environment variables (ENABLE_METRICS, ENABLE_JSON_API) to save resources.
Why this design
  • Minimal dependencies — no non-cplieger runtime deps beyond golang.org/x/sync; the cplieger httpx / health / metrics / atomicfile libraries supply retry/backoff, the health probe, Prometheus exposition, and crash-safe snapshot writes. Small, auditable supply chain.
  • Distroless, rootless container — runs as nonroot on gcr.io/distroless/static with no shell or package manager, minimising attack surface.
  • Public repos only — avoids credential management entirely; Docker Hub uses the unauthenticated API and GHCR counts are scraped from public package pages.
  • Dual output (Prometheus + JSON) — lets you choose the ingestion path that fits your stack without running two tools.
Limitations
  • Public repositories only. Docker Hub uses the unauthenticated API. GHCR download counts are scraped from public package pages. Private repositories and packages are not supported.
  • GHCR scraping is fragile. Download counts and package listings are extracted from GitHub's HTML, not an official API. If GitHub changes their page structure, scraping will break. The container logs a clear error with a link to open an issue when this happens.
  • No historical backfill. The registries only expose current totals. Time-series data is built locally as snapshots accumulate. If you start today, you only have data from today forward.

Quick start

The image is published to both ghcr.io/cplieger/registry-stats and docker.io/cplieger/registry-stats — use whichever registry you prefer.

services:
  registry-stats:
    image: ghcr.io/cplieger/registry-stats:latest
    container_name: registry-stats
    restart: unless-stopped
    user: "1000:1000"  # match your host user

    environment:
      TZ: "Europe/Paris"
      DOCKERHUB_REPOS: ""  # owner/repo or owner/* format, comma-separated
      GHCR_REPOS: ""  # owner/package or owner/* format, comma-separated
      LOG_LEVEL: "info"
      POLL_INTERVAL_HOURS: "1"  # 0 = collect once then serve
      RETENTION_DAYS: "90"  # 0 = keep forever

    ports:
      - "9100:9100"

    volumes:
      - "/opt/appdata/registry-stats:/data"  # daily JSON snapshots

Configuration reference

Environment variables
Variable Description Default Required
TZ Container timezone Europe/Paris No
DOCKERHUB_REPOS Comma-separated list of Docker Hub repositories to track. Use owner/repo for specific repos or owner/* to auto-discover all public repos for an owner (e.g. myuser/*,otheruser/specific-app) `` No
GHCR_REPOS Comma-separated list of public GHCR packages to track. Use owner/package for specific packages or owner/* to auto-discover all public packages for an owner (e.g. myuser/*,otheruser/specific-app) `` No
LOG_LEVEL - info No
POLL_INTERVAL_HOURS Hours between collection cycles. Set to 0 to collect once and then only serve the API (no recurring polls). Wildcards are re-expanded on each cycle, picking up newly published images 1 No
RETENTION_DAYS Number of days to keep snapshot files. Older snapshots are automatically deleted. Set to 0 to keep all snapshots forever 90 No
ENABLE_METRICS Enable Prometheus metrics endpoint true No
ENABLE_JSON_API Enable JSON API and daily snapshot files true No
Volumes
Mount Description
/data Snapshot storage directory. Contains one JSON file per day (e.g. 2025-01-15.json) within the configured retention period. Size is minimal — typically under 2 MB for 90 days of data.
Ports
Port Description
9100 HTTP API for Grafana and other consumers

API reference

The HTTP API serves JSON on port 9100. All endpoints return [] (not null) for empty results and use ISO 8601 timestamps.

Filtering

All data endpoints support these query parameters:

Parameter Description Example
registry Filter by registry (dockerhub or ghcr) ?registry=dockerhub
repo Filter by package name ?repo=myuser/myapp

Omitting a filter returns all data. Multiple repos can be comma-separated or passed as repeated parameters.

Endpoints
GET /api/health

Returns {"status":"ok"} when healthy, or {"status":"unready","reason":"..."} with HTTP 503 during startup (before the first successful collect). Used as the Docker healthcheck endpoint.

GET /metrics

Prometheus text format metrics. Includes:

  • registrystats_image_pulls_total{registry,owner,repo} — current pull count per image
  • registrystats_image_tags{registry,owner,repo} — tag count per image
  • registrystats_http_requests_total{method,path,status} — HTTP request counters
  • registrystats_http_request_duration_seconds — request latency histogram
  • registrystats_collects_total{source} — successful collects per source
  • registrystats_collect_errors_total{source} — failed collects per source
  • registrystats_collect_duration_seconds — collect cycle duration histogram
  • process_goroutines, process_heap_bytes, process_uptime_seconds — runtime metrics

Disabled when ENABLE_METRICS=false.

GET /api/summary

Current snapshot overview — one row per package per registry.

[
  {"registry":"dockerhub","name":"myuser/myapp","pull_count":1234,"tag_count":5},
  {"registry":"ghcr","name":"myuser/myapp","pull_count":567,"tag_count":0}
]
GET /api/pulls

Cumulative pull counts over time — one row per package per day. When both registries track the same package, their counts are merged (summed) per day.

[
  {"timestamp":"2025-01-15T00:00:00Z","repo":"myuser/myapp","pull_count":1801}
]
GET /api/pulls/daily

Daily download deltas — the difference in pull counts between consecutive days. Counter resets are clamped to zero. Missing days (transient scrape failures) are smoothed by dividing the delta across the gap, so a one-day outage doesn't show as a spike the day after.

When a repo is present in both registries and one scrape transiently fails, the missing registry's value is carried forward from the last successful day so the merged daily delta reflects only the real per-registry change.

The first day a repo appears in the retained snapshot window reports daily_pulls: 0 (no previous day to compare) and carries a first_seen: true flag so dashboards can annotate it rather than misread the zero as a drop in activity.

[
  {"timestamp":"2025-01-15T00:00:00Z","repo":"myuser/myapp","daily_pulls":0,"first_seen":true},
  {"timestamp":"2025-01-16T00:00:00Z","repo":"myuser/myapp","daily_pulls":42}
]
GET /api/snapshot

Raw snapshot for debugging. Returns the full snapshot file including all Docker Hub tag metadata and GHCR download counts. Accepts ?date=YYYY-MM-DD to fetch a specific day (defaults to the most recent snapshot).

Using without Grafana

The API returns standard JSON that any HTTP client, dashboard tool, or script can consume. Examples:

# Total downloads across all repos
curl -s http://localhost:9100/api/summary | jq '[.[].pull_count] | add'

# Daily deltas for a specific repo
curl -s 'http://localhost:9100/api/pulls/daily?repo=myuser/myapp' | jq .

# Docker Hub repos only
curl -s 'http://localhost:9100/api/summary?registry=dockerhub' | jq .

# Export raw snapshot for backup or external processing
curl -s http://localhost:9100/api/snapshot > backup.json

For periodic reporting, point a cron job at /api/summary and pipe the output to your notification system, spreadsheet, or monitoring tool.

Grafana integration

Registry Stats exposes Prometheus metrics at /metrics. The included grafana-dashboard.json uses PromQL and requires only a standard Prometheus datasource — no plugins needed.

Setup
  1. Add a scrape target for registry-stats:9100 in your Prometheus/Alloy/Grafana Agent config
  2. Import grafana-dashboard.json in Grafana
  3. Select your Prometheus/Mimir datasource when prompted

Alloy example:

prometheus.scrape "registry_stats" {
  targets         = [{ __address__ = "registry-stats:9100" }]
  forward_to      = [prometheus.remote_write.default.receiver]
  scrape_interval = "60s"
  job_name        = "registry-stats"
  metrics_path    = "/metrics"
}

Prometheus example (prometheus.yml):

scrape_configs:
  - job_name: registry-stats
    scrape_interval: 60s
    static_configs:
      - targets: ["registry-stats:9100"]

The dashboard shows cumulative downloads, daily deltas, package overview, and tracked package count — all via standard PromQL.

To save disk I/O, set ENABLE_JSON_API=false once the Prometheus path is confirmed working. This stops writing daily JSON snapshot files to disk.

Healthcheck

The container includes a built-in Docker healthcheck using a marker file at /tmp/.healthy. After each successful collection cycle the main process creates this file; if all configured registries fail or the snapshot cannot be written, the file is removed. The health subcommand (/registry-stats health) checks for this file and exits 0 when healthy. On startup the container collects immediately — if both registries are unreachable on first boot it starts unhealthy and recovers automatically on the next successful poll. Partial failures are tolerated: one successful repo keeps the container healthy. Wildcard expansion failures alone do not cause unhealthy status if explicit repos still succeed.

Security

No vulnerabilities found. All scans clean across the full scanner battery.

Tool Result
govulncheck No vulnerabilities in call graph
golangci-lint (gosec) 0 issues
trivy 0 vulnerabilities (distroless base)
grype 0 vulnerabilities
gitleaks No secrets detected
semgrep 1 info (false positive)
hadolint Clean

Read-only JSON API designed for internal Grafana consumption. No authentication required (standard for internal metrics APIs). Minimal dependencies (no non-cplieger runtime deps beyond golang.org/x/sync; uses the cplieger httpx / health / metrics / atomicfile libraries). Runs as nonroot on a distroless base image with no shell. The HTTP client follows redirects only within a host allowlist (httpx.DockerGitHubRedirectPolicy: docker.com / github.com / githubusercontent.com, 5-hop cap) so a compromised or misconfigured upstream cannot bounce the polling request to an arbitrary third-party host (the registries legitimately redirect to their own CDNs/blob stores).

Details for advanced users: URL path segments validated via isSafeURLSegment (rejects /%\?#@:). Snapshot filenames are date-format-validated before disk access (prevents path traversal). Response bodies capped via io.LimitReader (10 MB JSON, 4 MB HTML). HTTP server sets all five timeouts. Atomic writes (temp file + rename) prevent snapshot corruption; stale temp files from interrupted writes are swept on startup. Retry-After response headers are honoured on 429/503 responses (capped at the configured retry backoff ceiling). Semgrep flags math/rand/v2 usage, which is correct for jitter timing (not crypto).

Dependencies

All dependencies are updated automatically via Renovate and pinned by digest or version for reproducibility.

Dependency Source
golang Go
Distroless static Distroless
golang.org/x/sync Go stdlib
pgregory.net/rapid pkg.go.dev

Credits

This is an original tool that builds upon Docker Hub API.

Contributing

Issues and pull requests are welcome. Please open an issue first for larger changes so the approach can be discussed before implementation.

Disclaimer

These images are built with care and follow security best practices, but they are intended for homelab use. No guarantees of fitness for production environments. Use at your own risk.

This project was built with AI-assisted tooling using Claude Opus and Kiro. The human maintainer defines architecture, supervises implementation, and makes all final decisions.

License

This project is licensed under the GNU General Public License v3.0.

Documentation

The Go Gopher

There is no documentation for this package.

Directories

Path Synopsis
internal
api
Package api holds the cross-package interfaces that registry-stats subpackages depend on.
Package api holds the cross-package interfaces that registry-stats subpackages depend on.
collect
Package collect is the registry-stats collection orchestrator.
Package collect is the registry-stats collection orchestrator.
config
Package config parses registry-stats configuration from environment variables.
Package config parses registry-stats configuration from environment variables.
dockerhub
Package dockerhub is the Docker Hub RegistrySource implementation.
Package dockerhub is the Docker Hub RegistrySource implementation.
ghcr
Package ghcr is the GitHub Container Registry source for registry-stats.
Package ghcr is the GitHub Container Registry source for registry-stats.
metrics
Package metrics is a thin wrapper around github.com/cplieger/metrics/v2 holding the registry-stats-specific metric instances and the SetImageMetrics adapter (which converts a per-cycle slice of ImageMetric records into the equivalent labeled-gauge state).
Package metrics is a thin wrapper around github.com/cplieger/metrics/v2 holding the registry-stats-specific metric instances and the SetImageMetrics adapter (which converts a per-cycle slice of ImageMetric records into the equivalent labeled-gauge state).
model
Package model holds the pure data types that describe a registry-stats snapshot.
Package model holds the pure data types that describe a registry-stats snapshot.
store
Package store persists registry-stats snapshots on the local filesystem.
Package store persists registry-stats snapshots on the local filesystem.
urlsafe
Package urlsafe holds URL path segment safety predicates.
Package urlsafe holds URL path segment safety predicates.
webapi
Package webapi holds the HTTP API surface for registry-stats: the five Grafana-facing handlers (/api/health, /api/snapshot, /api/pulls, /api/pulls/daily, /api/summary), the lifecycle wiring (access log, graceful shutdown), and the pure filter/query helpers the handlers share.
Package webapi holds the HTTP API surface for registry-stats: the five Grafana-facing handlers (/api/health, /api/snapshot, /api/pulls, /api/pulls/daily, /api/summary), the lifecycle wiring (access log, graceful shutdown), and the pure filter/query helpers the handlers share.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL