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

Screens

What it looks like once the log is flowing.

geometrikks.home.lan/map

Live map. Markers, clusters or a heatmap; flat or globe. Routes fly to your home beacon.

And the rest

The map is the front door. The tables are where you find out what happened.

Live tail

A WebSocket-backed tail on the access-logs page. New rows prepend and pause while you hover.

Access logs

Every request, server-paginated, with free-text search and filters for status, method, IP, host, country and source.

IP inspector

One panel per address: counts, bytes, hosts and paths hit, other locations it resolved to, and scanner tells like a hosting ASN or a wide path spread.

Geo logs

Traffic grouped by place: which locations send requests, how that changes over time, and which clients keep coming back.

Batch import

Rotated archives, plain or gzipped, through the same pipeline with one command. Checksum-deduped, so re-running is safe.

Debug logs

The raw source lines behind every request, with parse failures called out and linked to their request.

The IP inspector for a banned address: the ban badge with time left, a still-seen-after-ban warning, request counts, status mix over time and the latest requests

CrowdSec

Bans next to the traffic that earned them.

Connect the Local API and the Security page joins active decisions with your own logs: the country, city and request count for every banned IP, a Banned badge in the tables, and a map overlay for banned addresses still showing up in the selected range.

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.
Live within seconds
The app follows the LAPI decision stream and pushes changes over a WebSocket, so badges react when CrowdSec bans or unbans an IP anywhere, not only from this UI.
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

Docker on amd64 or arm64. Two files, one command, and the app is 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.