Skip to content

feat(traffic): add city analytics with coordinates - #51

Merged
andrasbacsai merged 14 commits into
mainfrom
city-analytics
Oct 14, 2026
Merged

andrasbacsai merged 14 commits into
mainfrom
city-analytics

Conversation

@adiologydev

@adiologydev adiologydev commented Oct 13, 2026 •

Copy link
Copy Markdown
Member

Summary

Adds a city breakdown dimension to traffic analytics, with coordinates, so Coolify can draw a city map.

  • GeoIP now uses city databases by default (GeoLite2-City mirror, then DB-IP city lite). GEOIP_MAXMIND_EDITION defaults to GeoLite2-City.
  • New city dimension. Values are CC|subdivision|City (for example DE|BE|Berlin).
  • GET .../traffic/breakdown/city and the dashboards return lat / lon on city entries. Other dimensions serialize as before.
  • New traffic_city_coords table, written in the same transaction as the minute flush.

Behaviour changes

  • No city, no row. A request with no known city makes no city row, as an unknown country makes no country row. A country-only database gives country data only.
  • The city key uses the GeoIP country. The country dimension still prefers a trusted CF-IPCountry header, so the two can differ for one request.
  • GeoIP no longer delays startup. A database left by a previous run is mapped from disk (geoip.meta.json records the file, the source and the HTTP validators). With none, the lookup starts empty. The download runs in the background, and is retried with backoff while no database is loaded (15 minutes, doubled after each failure, up to 24 hours). On the first start, requests have no country or city until the download completes.
  • TRAFFIC_IP_MODE=off records no city. At start, Sentinel deletes the stored city rows in this mode.
  • Retention prunes traffic_city_coords. Coordinates that no city row uses are deleted, and the coordinates of a city update when the database changes.
  • The sidecar stores the source URL with the MaxMind license key masked. A database from a source that the configuration no longer uses is downloaded again.

Resource usage

  • The download is spooled to a temporary file and unpacked as a stream on a blocking thread. The database is never held in memory.
  • The lookup decodes only the fields that Sentinel keeps.
  • The city database is about 48 MiB on disk (memory-mapped).

Benchmark (sentinel-bench analytics --require-geoip --cf-header-pct 0 --events 1000000 --minutes 60, one machine):

Measurement Result
Ingest with GeoIP, old decode about 121,000 events/s
Ingest with GeoIP, this PR about 135,000 events/s
Ingest with no GeoIP about 181,000 events/s
GeoIP load on a restart under 0.1 ms from disk, no download
Store queries with the city query (p50 / p99) 0.014 ms / 0.027 ms

The benchmark now uses a pool of 2048 client addresses that resolve to cities, and reports city counts, coordinate rows and the GeoIP load time. BENCHMARK.md describes this.

How to test

  • cargo test --workspace --features traffic
  • cargo test -p traffic -- --ignored geoip (uses the network)
  • Start Sentinel with TRAFFIC_ENABLED=true and an empty database directory. The API answers at once, and the log shows "geoip database loaded" a moment later.
  • Send requests, wait one minute, then call /api/traffic/breakdown/city?from=...&to=.... City entries have lat and lon, and no value ends with ||.
  • Restart. The log shows "geoip database loaded from disk".

adiologydev and others added 5 commits October 13, 2026 10:15
Strip request URLs from download errors to protect license keys, apply
size limits to raw GeoIP databases, and drop out-of-range coordinates.
Add regression tests for redaction, database limits, and coordinate bounds.
Suppress and delete city breakdowns when TRAFFIC_IP_MODE is off.
Cache GeoIP lookups by database generation and prefer subdivision names.

Serialize GeoIP refreshes, clean partial downloads, reuse older DB-IP
caches, and add retry backoff with shutdown cancellation. Upgrade
maxminddb to 0.32.

Optimize breakdown indexes and coordinate queries, update stored
coordinates, prune unused coordinates, and amortize aggregation pruning.
Match benchmark city ranking to the API and add regression coverage.
Mark optional latitude and longitude fields as non-nullable in OpenAPI.

Use local proxy peers and public visitor IPs in Cloudflare benchmark logs,
and test that enrichment trusts their country headers. Advance daily
compaction by two days so benchmark queries include daily rollups.
@andrasbacsai
andrasbacsai merged commit 35d2eb3 into main Oct 14, 2026
3 checks passed
@andrasbacsai
andrasbacsai deleted the city-analytics branch October 14, 2026 09:14
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.

2 participants