Skip to content

Admin → Traffic: daily request counts per repository and client, with country and ASN from MaxMind - #711

Merged
schmitch merged 4 commits into
mainfrom
schmitch/determined-cerf-nk319p
Oct 30, 2026
Merged

schmitch merged 4 commits into
mainfrom
schmitch/determined-cerf-nk319p

Conversation

@schmitch

@schmitch schmitch commented Oct 30, 2026 •

Copy link
Copy Markdown
Member

Fixes #709

Every instance counts the requests it answers by day, repository and client network. Once a minute it writes the counts to Forge's bucket as a Parquet file of its own. Admin → Traffic reads every instance's files back, so it shows all servers (or one) with no calls between them and no Redis. Clients are located with MaxMind databases (country and autonomous system), so you can compare where each repository's requests come from.

What is recorded

  • One file per instance and day: traffic/v1/days/<date>/<instance>.parquet. Each row is a repository and client network, with country, ASN, AS organisation, requests and bytes.
  • The client address is the one the login lockout resolves (the socket peer, or a trusted proxy's X-Forwarded-For). IPv6 is counted by its /64.
  • The repository is noted by the package dispatcher and the image registry once the repository has been found. Requests to no repository (UI, APIs, sign-in) are counted under none.
  • Filling them in. The outermost track middleware opens a request-scoped slot (forge_traffic::Slot), and the principal layer and the dispatchers fill it in. The request path does no I/O.
  • Bounds. At most 100k keys per minute and 500k rows per day per instance; past that, new clients are counted as other.
  • Client addresses off. The address is looked up in memory and dropped; rows keep repository, country and AS only.
  • Shutdown. A graceful shutdown writes the last minute.

Geolocation (MaxMind)

  • Where the databases live. traffic/v1/geoip/country.mmdb and asn.mmdb in the bucket. Every instance loads them, again when their ETag changes (checked every ten minutes). The maxminddb crate reads them: Country or City editions for countries, ASN or ISP editions for autonomous systems. A file of another edition is refused.
  • Download.
    • An admin enters a MaxMind account ID and license key in the UI. The key is sealed as a secret forge_setting (traffic_maxmind_license_key, added to SECRET_SETTINGS, so DEK rotation reseals it).
    • One instance claims the download with a compare-and-swap on the state setting. It fetches GeoLite2-Country and GeoLite2-ASN (.tar.gz), checks them, and stores them in the bucket.
    • It downloads every 3 days, and waits 6 hours after an attempt, so failures are not retried hourly against MaxMind's daily limit.
    • traffic.maxmind_url can point at a mirror.
  • By hand. Operators can also put the .mmdb files in the bucket themselves (air-gapped installs, GeoIP2 editions).
  • Credit. The GeoLite credit is shown next to the countries and networks.

API and UI

  • API:
    • GET /admin/traffic?range=today|7d|30d|90d|365d&instance=&repository=&network=&country=&asn=, or from and to. It answers the days, totals, top repositories, countries, ASes and clients. repository=- and country=- select none or unknown.
    • GET/PUT /admin/settings/traffic: enabled, client addresses, retention days (default 90).
    • GET/PUT/DELETE /admin/traffic/geoip and POST /admin/traffic/geoip/update. The license key is never answered back.
  • Admin → Traffic (/manage/traffic):
    • range and instance filters;
    • stats: requests, data, clients, countries;
    • requests, data or clients by day, as an SVG chart or a table;
    • repositories, countries, networks (ASes) and top clients. Every row narrows the whole view; filters are removable chips kept in the URL.
  • Settings (/manage/traffic/settings): recording, retention, and geolocation (account, download now, turn off after confirming).

Docs

  • docs/deployment.md → Traffic statistics and Countries and networks (MaxMind).
  • forge.toml.example → [traffic].
  • Changelog fragment: docs/changes/2.2.0/709.feature.md.

Tests

  • cargo test -p forge-traffic (14):
    • counting and flushing over days;
    • rows without addresses;
    • pruning;
    • Parquet round trip;
    • geo lookups from MMDB databases built by the crate's test writer (testing feature); wrong editions refused; refresh from the bucket;
    • overview totals and every filter over two instances.
  • cargo test -p forge-server --test traffic (3, Postgres, versitygw and a wiremock MaxMind):
    • real requests are counted, flushed and read back with repository, client and filters; admins only;
    • settings validation;
    • the MaxMind account: a wrong key's 401 is reported, the key is sealed at rest, the right key downloads both .tar.gz databases, clients are located, and turning off removes everything.
  • Server unit tests (254), forge-db --test encryption (13), and the server suites admin, dispatch, npm, raw, registry, telemetry and otlp_export all pass.
  • pnpm tsc, pnpm test (529, including 6 for the traffic screens) and pnpm build.
  • cargo fmt --check, cargo clippy --workspace --all-targets -D warnings, cargo xtask changelog --check and cargo xtask docs --check.
  • Rendered in Chromium against mocked data in light, dark and phone widths, with no horizontal overflow.

🤖 Generated with Claude Code

https://claude.ai/code/session_012e6DJbpgZmQnhz5Jjjhug6

…in the bucket

Every instance counts the requests it answers in memory (by minute,
surface, repository, method and status, with a latency histogram), keeps
the top client networks of each minute in bounded memory, and logs each
request. Once a minute it writes Parquet files of its own under
traffic/v1/ in Forge's bucket; the admin API lists and reads every
instance's files from there, so Admin -> Traffic shows all instances
without calls between them.

- crates/traffic (forge-traffic): the recorder, the request-scoped slot
  the principal layer and the dispatchers note the client, account and
  repository in, the Parquet rows (pure-Rust parquet, Snappy), the file
  layout, flushing, pruning and the queries.
- BlobStore: put_bytes, get_bytes and list (ListObjectsV2).
- /admin/traffic, /admin/traffic/client and /admin/settings/traffic;
  the traffic worker flushes every minute and prunes every hour, and the
  last counts are written on shutdown.
- Admin -> Traffic in the web UI: totals, an SVG chart or table over time,
  top clients with their request logs, repositories, surfaces, statuses,
  and the settings.

Fixes #709

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012e6DJbpgZmQnhz5Jjjhug6
…s with MaxMind

The statistics only need a count per day for each repository and client
address, to compare where the requests come from, so the minute and hour
files, the latency histograms, the top-client lists and the request log
are replaced by one Parquet file per instance and day: a row per
repository and client network with its requests and bytes.

Clients are located with MaxMind databases: their country and autonomous
system go on each row. The databases are kept in the bucket
(traffic/v1/geoip/{country,asn}.mmdb) and loaded by every instance from
there. Forge downloads GeoLite2-Country and GeoLite2-ASN every three days
with a MaxMind account ID and license key set in the web UI (the key
sealed like the other secret settings, the download URL configurable as
traffic.maxmind_url), or an operator puts the files there.

- forge-traffic: daily recorder and flusher, geo lookups (maxminddb),
  queries filtered by instance, repository, client, country and AS, and
  a small MaxMind database writer for tests (feature `testing`).
- /admin/traffic takes the filters; /admin/traffic/geoip shows the
  databases and sets, downloads or removes the MaxMind account.
- Admin -> Traffic: requests by day, repositories, countries, networks
  and top clients, each narrowing the view; the settings get a
  geolocation panel.

Fixes #709

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012e6DJbpgZmQnhz5Jjjhug6
@schmitch schmitch changed the title Admin → Traffic: request counts, top clients and a request log as Parquet in the bucket Admin → Traffic: daily request counts per repository and client, with country and ASN from MaxMind Oct 30, 2026
@schmitch
schmitch enabled auto-merge October 30, 2026 09:30
… the stack

The raw group test overflowed its 2 MiB test thread in CI: a debug build
of a request that fetches a file through a group's proxy and stores it
needed about 1.95 MiB of stack on main already, and the traffic
statistics' per-request layers took it over. Every future up that
request held the AWS SDK's upload future inline (put_if_absent, the
CAS commit, the file store, the proxy fill, the group walk, the plugin
dispatch), so each of their poll frames carried a copy.

put_if_absent and the conditional PUT now return boxed futures: the
SDK's future lives in one heap allocation, and the request needs about
1.55 MiB, measured with RUST_MIN_STACK on the test. Nothing else
changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012e6DJbpgZmQnhz5Jjjhug6

Copy link
Copy Markdown
Member Author

CI: test failed on raw::groups_fall_through_their_members_by_priority (stack overflow). Fixed in 96b85cc.

The test's thread has 2 MiB of stack. In a debug build, the request that fetches a file through the group's proxy and stores it already needed about 1.95 MiB on main, measured with RUST_MIN_STACK (it passes at 2048 KiB and overflows at 1900 KiB). This PR adds a few KiB per request: the traffic slot in track. That was enough to overflow on the CI runner.

gdb at the overflow showed where the stack goes. The AWS SDK's upload future was held inline by every future up that request: put_if_absent, the CAS commit, the file store, the proxy fill, the group walk and the plugin dispatch. Each of their poll frames carried a copy (frames of 45–190 KB).

96b85cc makes BlobStore::put_if_absent and the conditional PUT return boxed futures. The SDK future now lives in one heap allocation, and the same test needs about 1.55 MiB: it passes at 1600 KiB and overflows at 1500 KiB. Nothing else changes.

The branch also has main merged in. On the merged code, raw, registry, npm, dispatch, admin, import, telemetry and traffic, the storage tests, clippy and the web tests all pass locally.


Generated by Claude Code

@schmitch
schmitch added this pull request to the merge queue Oct 30, 2026
Merged via the queue into main with commit ccc2787 Oct 30, 2026
20 checks passed
@schmitch
schmitch deleted the schmitch/determined-cerf-nk319p branch October 30, 2026 10:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Admin → Traffic: daily request counts per repository and client, with country and ASN, over all instances

2 participants