Documentation

Monitor usage and cache health

peryx counts everything it serves, off the request path. Four surfaces show the same numbers: the dashboard, the admin status page, the /+stats JSON endpoint, and Prometheus /metrics.

Drill down from the dashboard

Each index card on the dashboard shows its pages, downloads, and bytes served, with a usage link. The link opens the drill-down: totals for the index and a table of its projects, each project linking to its per-file counters. The same pages live at /stats, /stats?index={route}, and /stats?index={route}&project={name}.

The usage drill-down for one index: totals up top, busiest projects first The usage drill-down for one index: totals up top, busiest projects first
The usage drill-down for one index: totals up top, busiest projects first
One project drilled to file level: downloads, bytes, and metadata hits per artifact One project drilled to file level: downloads, bytes, and metadata hits per artifact
One project drilled to file level: downloads, bytes, and metadata hits per artifact

Query the counters

curl -s http://127.0.0.1:4433/+stats | jq
curl -s 'http://127.0.0.1:4433/+stats?index=root/pypi' | jq '.projects | to_entries | sort_by(-.value.downloads)[:5]'
curl -s 'http://127.0.0.1:4433/+stats?index=root/pypi&project=numpy' | jq .files

These counters live in memory and reset on restart; for durable time series, scrape /metrics, and see daily version and source usage for the one aggregate that survives a restart. Prometheus sums repositories by the bounded ecosystem and role labels. A query can split PyPI from OCI traffic without storing repository names:

peryx_artifacts_served_total{ecosystem="pypi",role="virtual"}
peryx_artifacts_served_total{ecosystem="oci",role="virtual"}

Repository, project, file, user, path, error, credential, token, and URL values are excluded from metric names and labels. Use /+stats for repository and package drill-down. The endpoint reference lists renamed series and replacement queries.

Daily version and source usage

Alongside the in-memory counters, peryx keeps one durable aggregate that survives a restart: daily download totals broken down by version and routed source. Each successful artifact response folds into a bucket keyed by repository, project, distribution version, routed source, and the UTC day it landed on, holding that bucket's download count and byte total. The fold, its persistence, and retention all run on the aggregator thread, so nothing here touches the request path.

The dimensions stay narrow. peryx parses version from the artifact filename, so a PyPI wheel or sdist carries its release and a content-addressed OCI layer carries none. source is the named upstream a cache miss routed to; a response served from the local store carries no source, because it routed to no upstream. A bucket never holds a client's identity, address, or credential, and never an unbounded label. The label set stays bounded by the configured repositories, their projects and versions, the configured sources, and the retention window.

Counting rule

One fully delivered response is one download, counted once when its last expected byte leaves the body:

ResponseCounted
Full 200 delivered to the last byteOnce
206 range delivered to the last byteOnce, for the bytes sent
Truncated, cancelled, or disconnectedNo
4xx/5xx, unauthorized, or policy-deniedNo

A range request counts once and records only the bytes it transmitted, so a resumed download is not double-counted against the whole artifact. A transfer that fails its digest is rejected, not counted.

Retention

usage_retention_days bounds how many days of buckets to keep; leave it unset to keep them without limit. Expiry drops whole expired days on the aggregator thread and never touches a retained day's totals, so tightening the window only reclaims durable storage. Existing durable snapshots that predate this aggregate, carry missing dimensions, or fail to parse rebuild from zero rather than blocking startup.

Check operational status

/admin/status combines GET /+status?details=admin with top-level GET /+stats. It shows the configured index topology next to the same cache-health counters listed below, plus observed project counts, upload counts, recent uploads, cached index URLs, and redacted token/authentication state. The page does not fetch upstreams or read artifacts while it renders.

The top-level blob_storage object names the selected backend. Its fields report durability and a support level for each operation, including local staging. The backend implements native operations; peryx composes emulated operations from lower-level calls. filesystem durability reports acknowledgment from the configured filesystem, whose mount determines crash and replication guarantees. health.blob_store reports the current reachability check.

What the cache-health counters mean

CounterSignal
refreshesRevalidations against upstream, on demand or from the background sweep
changedRevalidations that found new upstream content; also logged at info
stale_servedPages served from cache because upstream was down; rising means an outage
upstream_errorsUpstream failures with nothing cached to fall back to; clients saw an error
rejectedDownloads whose bytes did not match the advertised digest; never cached

A steady refreshes with zero changed is the normal idle state. rejected above zero deserves attention: either the upstream served corrupt bytes or something rewrote them in transit.

Inspect the disk cache

Use the cache CLI when you need the state on disk, not request counters. It works the same for either ecosystem: the blob store is content-addressed and shared, and --index selects a PyPI or an OCI index by route.

peryx cache size --data-dir /var/lib/peryx
peryx cache list --data-dir /var/lib/peryx --stale
peryx cache list --data-dir /var/lib/peryx --index dockerhub
peryx cache fsck --data-dir /var/lib/peryx

cache size reports cached pages, stale pages, blob files, bytes, and metadata row counts. cache list --stale lists the stale pages with age and freshness lifetime. cache fsck validates metadata row shapes and stream-hashes blob files against their sha256 path.

Use two steps for project purge. First print the plan, then rerun with --yes if the row counts match what you expect.

peryx cache purge project --data-dir /var/lib/peryx --index pypi --project flask
peryx cache purge project --data-dir /var/lib/peryx --index pypi --project flask --yes
peryx cache purge orphaned-blobs --data-dir /var/lib/peryx
peryx cache purge orphaned-blobs --data-dir /var/lib/peryx --yes

Project purge removes metadata rows and leaves blobs in place. Orphaned-blob purge removes blob files that no metadata row references.

On this page