IP lookup: add best-effort geolocation from geojs with a DB-IP City Lite fallback, cached on disk #239

Closed
opened 2026-11-10 10:15:09 +01:00 by clawbot · 1 comment
Collaborator

Part of #203, from item 5 and "The two caches" of the proposal sneak agreed to (#203 (comment), #203 (comment)); he chose "city lite download ok". Depends on the PTR issue, which creates cache.db, linked from the plan on that issue.

Change.

  1. New package internal/geo, doing all of this:
    • Asks https://get.geojs.io/v1/ip/geo.json?ip=a,b,c, one batch for all addresses of a lookup, 1 second in all. latitude and longitude come as strings and are "nil" when unknown.
    • Falls back on a timeout or an error to DB-IP City Lite, read with github.com/oschwald/maxminddb-golang/v2.
    • Keeps the geojs answers in a new table in cache.db (internal/cachedb) and in memory, loaded at startup, at most 250,000 entries in memory, least recently used dropped first. An entry expires 7 days after its fetch. A sweep every 10 minutes deletes expired rows. A memory miss reads the table before asking geojs.
  2. It downloads the database from https://download.db-ip.com/free/dbip-city-lite-YYYY-MM.mmdb.gz, once a month, into the state directory, at most 200 MiB, replacing the old file only once the new one opens. Check the current URL first.
  3. Every result gains geo. A part the source does not know is null; geo is null only when neither source answers. Before the first download finishes, only geojs answers.
  4. The home page footer carries "IP Geolocation by DB-IP" with a link to https://db-ip.com, as the licence requires.
before: (nothing)
after:  "geo": {"country_code": "DE", "country": "Germany", "continent_code": "EU", "region": "Brandenburg", "city": "Brandenburg an der Havel", "latitude": 52.6171, "longitude": 13.1207, "accuracy_radius_km": 20, "time_zone": "Europe/Berlin", "source": "geojs", "updated": "2026-10-27T20:37:28Z"}

The image does not pre-fetch the database: a pre-fetch needs a pinned hash and the file changes every month, so the first start downloads it.

Tests. A fake geojs over httptest: a batch, "nil" coordinates, a timeout falling back; a small database built with github.com/maxmind/mmdbwriter; the cache surviving a restart; expiry and the memory limit. Addresses from documentation ranges.

Done when. A real lookup returns geo from geojs, and from the database with geojs unreachable (one line on the PR); the README describes the sources, the cache, what is sent outside and the database's memory use.

Model: opus-5-5

Part of https://git.eeqj.de/sneak/routewatch/issues/203, from item 5 and "The two caches" of the proposal sneak agreed to (https://git.eeqj.de/sneak/routewatch/issues/203#issuecomment-159864, https://git.eeqj.de/sneak/routewatch/issues/203#issuecomment-165190); he chose "city lite download ok". Depends on the PTR issue, which creates `cache.db`, linked from the plan on that issue. **Change.** 1. New package `internal/geo`, doing all of this: - Asks https://get.geojs.io/v1/ip/geo.json?ip=a,b,c, one batch for all addresses of a lookup, 1 second in all. `latitude` and `longitude` come as strings and are `"nil"` when unknown. - Falls back on a timeout or an error to DB-IP City Lite, read with `github.com/oschwald/maxminddb-golang/v2`. - Keeps the geojs answers in a new table in `cache.db` (`internal/cachedb`) and in memory, loaded at startup, at most 250,000 entries in memory, least recently used dropped first. An entry expires 7 days after its fetch. A sweep every 10 minutes deletes expired rows. A memory miss reads the table before asking geojs. 2. It downloads the database from `https://download.db-ip.com/free/dbip-city-lite-YYYY-MM.mmdb.gz`, once a month, into the state directory, at most 200 MiB, replacing the old file only once the new one opens. Check the current URL first. 3. Every result gains `geo`. A part the source does not know is null; `geo` is null only when neither source answers. Before the first download finishes, only geojs answers. 4. The home page footer carries "IP Geolocation by DB-IP" with a link to https://db-ip.com, as the licence requires. ```json before: (nothing) after: "geo": {"country_code": "DE", "country": "Germany", "continent_code": "EU", "region": "Brandenburg", "city": "Brandenburg an der Havel", "latitude": 52.6171, "longitude": 13.1207, "accuracy_radius_km": 20, "time_zone": "Europe/Berlin", "source": "geojs", "updated": "2026-10-27T20:37:28Z"} ``` The image does not pre-fetch the database: a pre-fetch needs a pinned hash and the file changes every month, so the first start downloads it. **Tests.** A fake geojs over `httptest`: a batch, `"nil"` coordinates, a timeout falling back; a small database built with `github.com/maxmind/mmdbwriter`; the cache surviving a restart; expiry and the memory limit. Addresses from documentation ranges. **Done when.** A real lookup returns `geo` from geojs, and from the database with geojs unreachable (one line on the PR); the README describes the sources, the cache, what is sent outside and the database's memory use. Model: opus-5-5
clawbot self-assigned this 2026-11-10 10:15:09 +01:00
Author
Collaborator

Built in #249: internal/geo adds geo to every IP lookup result, from geojs with DB-IP City Lite as the fallback, cached in memory and in cache.db for 7 days. The PR body lists the judgement calls.Model: opus-5-5

Built in https://git.eeqj.de/sneak/routewatch/pulls/249: `internal/geo` adds `geo` to every IP lookup result, from geojs with DB-IP City Lite as the fallback, cached in memory and in `cache.db` for 7 days. The PR body lists the judgement calls.Model: opus-5-5
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/routewatch#239