Skip to content

CLI commands

Besides the server, the image ships maintenance commands under the litestar CLI. Run them inside the container with docker compose exec -u geometrikks app litestar <command> (or docker compose run --rm app litestar <command> when the stack is stopped). The image sets LITESTAR_APP so the bare command works there; outside the container, point the CLI at the app yourself: uv run litestar --app geometrikks.server.core:create_app <command>. Every command supports --help.

Live tailing only picks up lines written after the app starts. To backfill rotated or archived access logs (nginx, Traefik JSON or Caddy JSON, plain or gzip), use import-logs:

Terminal window
docker compose exec -u geometrikks app litestar import-logs /var/log/access/access.log.1.gz

It reuses the live ingestion pipeline (same parsing, GeoIP lookup and DB writes), uses the timestamps in each log line rather than wall-clock time, and refreshes the continuous aggregates for the imported range when done. The log format is auto-detected per file, as with live tailing; pass --format geometrikks-json, --format nginx, --format traefik-json or --format caddy-json to pin it. You can pass several files in one invocation. Paths are container paths, and the import runs as the non-root geometrikks user (PUID:PGID, default 1000:1000), so host files must be readable by it (-u geometrikks keeps exec from running the import as root).

exec requires the app service to be running. If the stack is stopped, use run --rm instead:

Terminal window
docker compose run --rm app litestar import-logs /var/log/access/access.log.1.gz

[!IMPORTANT] Import archived (rotated) files only. Importing a file that is also being live-tailed double-counts its lines.

  • Each imported file is fingerprinted by content checksum; importing the same content again (even under a different filename) is skipped. Pass --force to re-import. That updates the bookkeeping row but does not delete rows written by the earlier import.
  • A file that matches no supported log format is rejected up front, before anything is written.
  • Without --format, the format is detected per file. If detection can only match the relaxed IP-and-timestamp pattern, the file imports as map events with no access-log rows. Pin the format (--format geometrikks-json or --format nginx) to require a full parse; lines that do not match then show up in the skipped count instead.
  • The TimescaleDB retention policy drops rows older than the raw retention window (ANALYTICS_RAW_RETENTION_DAYS, default 180 days), so history beyond that window will not persist. Raise the retention setting before importing older archives if you want to keep them.

backfill-hostname: fix up historical hostnames

Section titled “backfill-hostname: fix up historical hostnames”

Every ingested row records which GeoMetrikks instance wrote it (LOGPARSER_HOST_NAME; defaults to the machine hostname, which the compose file pins to geometrikks). Rows ingested by older versions have no hostname, so the access-logs hostname filter cannot see them. backfill-hostname stamps them retroactively:

Terminal window
docker compose exec -u geometrikks app litestar backfill-hostname myhost

The plain form fills only rows with no hostname. It is idempotent, cannot overwrite stamped values, and runs immediately without a confirmation prompt.

If your database has accumulated many bogus hostnames, add --consolidate to rewrite all existing hostnames to the given name as well. The classic cause is running in Docker with LOGPARSER_HOST_NAME unset before the compose file pinned a hostname: every container recreation minted a new 12-hex container-ID “hostname”. Consolidate lists every hostname it will rewrite, with row counts, and asks for confirmation first (--yes skips the prompt):

Terminal window
docker compose exec -u geometrikks app litestar backfill-hostname myhost --consolidate

Either form decompresses compressed history chunks first (a full-table update would trip TimescaleDB’s tuple decompression limit), so disk usage grows until the compression policy recompresses them. It then refreshes the affected continuous aggregates so the filter dropdowns update. It may run for minutes on a large database.

backfill-asn: fill in ASN data for historical rows

Section titled “backfill-asn: fill in ASN data for historical rows”

Rows ingested before the ASN feature (or while the ASN database was missing) have no ASN data. backfill-asn resolves their IPs against the local GeoLite2 ASN database and stamps them retroactively:

Terminal window
docker compose exec -u geometrikks app litestar backfill-asn

It fills only rows with no ASN data (idempotent, never overwrites stamped values) and asks for confirmation after reporting how many rows and distinct IPs are affected (--yes skips the prompt). IPs the database cannot resolve stay empty. Like backfill-hostname, it decompresses compressed history chunks first (disk usage grows until the compression policy recompresses them) and refreshes the ASN continuous aggregates afterwards so the Top ASNs view picks up the history. It may run for minutes on a large database.

If the aggregate refresh fails, the command exits non-zero and names the stale aggregates: the rows are stamped, but the Top ASNs view will not show the backfilled range until they refresh. Rerunning is safe.

Today’s ASN database describes today’s network ownership; stamping years-old traffic with it is an approximation.

backfill-timings: clear placeholder response times

Section titled “backfill-timings: clear placeholder response times”

Archives in nginx’s built-in combined format have no $request_time. Older versions stored those rows with a response time of 0.0, which dragged every average and percentile toward zero. Rows ingested by this version store no timing at all for such lines; backfill-timings does the same for the rows that predate it:

Terminal window
docker compose exec -u geometrikks app litestar backfill-timings

It only touches rows the legacy nginx format wrote without a host, which is how a combined line looks after import (the custom format always logs $host), and whose response time is exactly 0. A genuine sub-millisecond timing on a row with a host is left alone. --hostname NAME and --before 2026-08-20 narrow the set; the command prints the row count and time span and asks for confirmation (--yes skips it). Like the other backfills it decompresses history chunks first and refreshes the affected continuous aggregates afterwards, so it may run for minutes on a large database.

Bulk operations (import-logs over months of archives, either backfill command on a database with real history) write WAL much faster than live tailing does. With PostgreSQL’s default max_wal_size of 1GB the database checkpoints every few seconds and logs:

LOG: checkpoints are occurring too frequently (21 seconds apart)
HINT: Consider increasing the configuration parameter "max_wal_size".

Nothing is at risk, but each checkpoint re-triggers full-page writes for the pages the run touches next, so the operation slows down the longer this goes on. Both settings reload without a restart, so you can raise them while a backfill is already running:

Terminal window
docker compose exec timescale_db psql -U geouser -d geometrikks \
-c "ALTER SYSTEM SET max_wal_size = '4GB';" \
-c "ALTER SYSTEM SET checkpoint_timeout = '15min';" \
-c "SELECT pg_reload_conf();"

Expect up to max_wal_size of extra disk used for WAL during the run, on top of the temporarily decompressed chunks. To keep the settings permanently, add max_wal_size=4GB and checkpoint_timeout=15min to the database service’s command: block in the compose file, then run ALTER SYSTEM RESET max_wal_size and ALTER SYSTEM RESET checkpoint_timeout once. Command-line flags override ALTER SYSTEM values, and keeping both means the compose file no longer tells the whole truth.