Your access logs, on a live map.

GeoMetrikks tails your reverse proxy's access log, looks every request up in GeoLite2, and draws it on a world map within seconds. Traffic analytics, a searchable history, and CrowdSec bans next to the traffic that caused them. On your own hardware.

  • MIT
  • ghcr.io/gilbn/geometrikks
  • amd64 + arm64
  • no telemetry
geometrikks · ingestion
access.log 203.0.113.7 - - [02/Sep/2026:23:41:07 +0000] "GET /wp-login.php HTTP/1.1" 403 153 "-" home.example.org "Mozilla/5.0 zgrab/0.x" "0.001" "-"detect format=nginx (positional, $time_local)parse method=GET path=/wp-login.php status=403 host=home.example.org bytes=153 rt=1msgeoip 203.0.113.7 → Hanoi, VN 21.03, 105.85 AS135905 (hosting)store access_logs +1 geo_events +1 hypertable chunk 2026-09-02notify /ws/live → 1 client map: +1 marker, route → homecrowdsec decision for 203.0.113.7: ban (http-probing, 4h) still knocking: yes access.log {"client_ip":"198.51.100.24","timestamp":"2026-09-02T23:41:09+00:00","method":"GET","path":"/","status":"200","host":"blog.example.org","request_time":"0.018"}detect format=geometrikks-jsongeoip 198.51.100.24 → Toronto, CA 43.65, -79.38 AS812notify /ws/live → 1 client map: +1 marker

Sources

Point it at the log file your proxy already writes.

The format is detected per file. Rotated files are followed. Archives, plain or gzipped, can be backfilled through the same pipeline with one command.

nginx.confgeometrikks-jsonnginxcombined
log_format geometrikks_json escape=json
  '{'
    '"client_ip":"$remote_addr",'
    '"timestamp":"$time_iso8601",'
    '"method":"$request_method",'
    '"path":"$request_uri",'
    '"protocol":"$server_protocol",'
    '"status":"$status",'
    '"bytes":"$body_bytes_sent",'
    '"host":"$host",'
    '"referrer":"$http_referer",'
    '"user_agent":"$http_user_agent",'
    '"remote_user":"$remote_user",'
    '"request_time":"$request_time",'
    '"upstream_time":"$upstream_response_time",'
    '"request_raw":"$request"'
  '}';

access_log /config/log/nginx/access.log geometrikks_json;

In .env

ACCESS_LOG_DIR=/var/log/nginx
LOGPARSER_LOG_PATHS=/var/log/access/access.log

Worth knowing

  • nginx 1.11.8 or later, for escape=json.
  • The older positional format and nginx's built-in combined parse too, so archives import without a config change.
  • Behind a CDN, set real_ip so $remote_addr is the visitor and not the CDN.
nginx setup in the docs

Inside GeoMetrikks

The logs must flow.

geometrikks.home.lan/map

Live map. Show requests as markers, clusters or a heatmap on a flat map or globe, or switch to the Banned IPs view to see where CrowdSec bans come from. Live routes end at the server location you configure.

Request details

Search, filter and inspect the requests behind the map.

Live tail

Watch new requests arrive on the access logs page. The feed pauses while you hover so rows do not move under the cursor.

Access logs

Search every request and filter by status, method, IP, host, country or source. Results are paginated on the server.

IP inspector

Open an address to see its request count, bandwidth, status codes, user agents, hosts and paths. Check its CrowdSec status or ban it there.

Geo logs

Group traffic by country and city, compare activity over time, and see the top IPs and locations for the selected range.

Batch import

Import rotated or archived logs, including gzip files, with one command. Checksums prevent the same file from being imported twice.

Debug logs

Inspect the raw line for each parsed request. Parse failures appear here too, so you can find log lines GeoMetrikks could not read.

CrowdSec

Watch hostile traffic fuck around and find out.

Connect the Local API and the map's Banned IPs view shows where the bans come from. Open any of them in the IP inspector to see what that address did to earn it.

The map in its Banned IPs view: red markers count banned addresses per location across Europe, a popup shows one address with its CrowdSec decision, and the controls list the top banned IPs
The IP inspector for a banned address, showing when the ban expires, a warning for requests seen after the ban, request counts, response codes over time and recent requests
Every ban with the traffic behind it
The IP inspector shows an address's request counts, bandwidth, response codes over time, hosts, paths, user agents and recent requests, with its CrowdSec decision and how long is left on it. Requests that arrive after the ban are called out.
Ban and unban from the UI
With machine credentials, a shield button appears next to IPs on the map, in the tables and in the IP inspector, with a duration picker from an hour to forever. Every ban and unban is audit-logged with the acting user.
Ban changes appear within seconds
GeoMetrikks follows CrowdSec's live updates, including bans and unbans made outside GeoMetrikks. The map and the badges refresh together.
Two lines to connect
# prints the API key
docker exec crowdsec cscli bouncers add geometrikks

CROWDSEC_LAPI_URL=http://crowdsec:8080
CROWDSEC_BOUNCER_API_KEY=<key>
CrowdSec decides; a bouncer in front of your stack still does the blocking. GeoMetrikks shows and manages decisions, it does not block traffic. CrowdSec setup in the docs

Agent mode

Several proxies, one map.

When traffic comes in through more than one proxy or host, run one full instance plus an agent next to each log file. The agent tails, parses, geolocates and writes locally. The full instance owns everything else: the UI, the API, migrations, the scheduler and CrowdSec.

Every writer publishes committed events over PostgreSQL LISTEN/NOTIFY, so the live map shows a request the moment any agent stores it. Each source gets its own home beacon, and every page filters by recording hostname.

An agent needs four things: the mode, database credentials for the shared instance, MaxMind credentials, and its log mount. No admin password, no UI, no migrations.

APP_MODE=agent
DB_HOST=timescale.example.internal
LOGPARSER_LOG_PATHS=/var/log/access/access.log
LOGPARSER_HOST_NAME=edge-01
Multi-source setup in the docs

Install

Run GeoMetrikks with Docker Compose.

The Compose file starts GeoMetrikks and TimescaleDB on amd64 or arm64. Set the log path and API keys, then open the app on port 8000.

# Docker. Images for amd64 and arm64.
mkdir geometrikks && cd geometrikks
curl -LO https://raw.githubusercontent.com/GilbN/geometrikks/main/docker-compose.yml
curl -Lo .env https://raw.githubusercontent.com/GilbN/geometrikks/main/.env.example
$EDITOR .env      # set APP_ADMIN_PASSWORD, MaxMind key, CARTO key, log path
docker compose up -d
# open http://localhost:8000
  • Docker. The compose file brings TimescaleDB along.
  • A free MaxMind account. GeoLite2 City and ASN download at startup under your key and refresh weekly. You never touch .mmdb files.
  • A free CARTO key. The basemap comes from CARTO, and their terms want a key per deployment. Optional today; keyless tiles may stop working at any time.
  • A log file. Not stdout. Traefik and Caddy need one directive to write one.
  • One admin password. Single-user session login. An authenticating proxy or a tailnet can take over, and the built-in login switches off.
  • No CrowdSec needed. Optional. Add a LAPI key later and the Security page lights up.
  • No telemetry. Outbound traffic is the GeoLite2 download and, unless disabled, one public-IP lookup for the map's home.

Built with

What it runs on.