CLI for collecting unique user IPs from telemt servers and aggregating them by country/city using MaxMind GeoLite2.
Requires Go 1.26+.
make build # .bin/tctl (host OS)
make linux # .bin/tctl-linux-amd64# 1. one-time: pull MaxMind databases into mmdb/
./tctl-update-mmdb
# 2. collect unique IPs from telemt servers (repeat on cadence)
tctl c
# 3. regenerate the country/city aggregate
tctl c acollect is incremental — each run merges new IPs into collected_ips.yaml, so running it repeatedly grows the dataset. aggregate is idempotent; rerun it whenever you want a fresh aggregated_geo.json.
Reads .tctl.yaml by default (override with -c/--conf):
collect_file_path: /var/lib/tctl/%Y/%m/%d/%H.yaml
aggregate_file_path: /var/lib/tctl/%Y/%m/%d/%H.json
mmdb_city: /var/lib/tctl/mmdb/GeoLite2-City.mmdb
mmdb_asn: /var/lib/tctl/mmdb/GeoLite2-ASN.mmdb
mmdb_country: /var/lib/tctl/mmdb/GeoLite2-Country.mmdb
telemt_servers:
- base_url: https://s1.example.com:9091
token: <bearer-token>
- base_url: https://s2.example.com:9091
token: <bearer-token>| Field | Used by | Notes |
|---|---|---|
collect_file_path |
collect, aggregate |
output of collect, input of aggregate |
aggregate_file_path |
aggregate |
output of aggregate |
mmdb_city |
aggregate |
required for city/country lookups |
mmdb_asn, mmdb_country |
reserved | declared for future commands |
telemt_servers |
collect |
list of {base_url, token} |
collect_file_path and aggregate_file_path support date placeholders expanded against the current UTC time on each run:
| Placeholder | Meaning |
|---|---|
%Y |
4-digit year |
%m |
2-digit month |
%d |
2-digit day |
%H |
2-digit hour (24h) |
%M |
2-digit minute |
%S |
2-digit second |
%% |
literal % |
Missing parent directories are created automatically. With the example above and %H.yaml, you get a separate accumulator file per hour.
For /v1/users to return a meaningful recent_unique_ips_list, telemt should be configured to track IPs in a sliding window:
[access]
user_max_unique_ips_mode = "time_window"
user_max_unique_ips_window_secs = 120Without this, the IP list per user is either never rotated or empty, depending on the default mode.
The window must cover your collect cadence with some margin: at one run per minute, 120 seconds is enough. If you collect less often, raise user_max_unique_ips_window_secs accordingly — IPs that age out between runs are lost.
Hits GET /v1/users with Authorization: Bearer <token> on each server in telemt_servers, merges all recent_unique_ips_list values with the IPs already stored at the resolved collect_file_path (deduplicated), and rewrites the file with an updated timestamp.
tctl collectThe output path comes from collect_file_path in the config (no -o flag); see Configuration for placeholder semantics.
Reads the file at collect_file_path (with current-UTC placeholders expanded), looks up each IP in mmdb_city, groups by (country, city), and writes the result to aggregate_file_path sorted by count descending. Parent directories are created automatically.
tctl collect aggregate
tctl c a # chained aliasesAll paths come from the config; there are no input/output/db flags. If collect and aggregate run in the same hour (with the example %H granularity), they line up on the same time-stamped pair.
Sample entry in the aggregate JSON:
{
"country": "Russia",
"city": "Moscow",
"latitude": 55.7487,
"longitude": 37.6187,
"count": 35
}country and city may be null when the MaxMind record lacks the corresponding field.
tctl-update-mmdb downloads the latest GeoLite2-{ASN,City,Country}.mmdb from the P3TERX/GeoLite.mmdb releases. Place them where mmdb_city / mmdb_asn / mmdb_country point.
./tctl-update-mmdb| File | Purpose |
|---|---|
.tctl.yaml |
config (paths + telemt servers) |
collect_file_path target |
accumulated unique IPs (output of collect, input of aggregate); carries count, created_at and last_update metadata |
aggregate_file_path target |
aggregated country/city summary |
mmdb_* targets |
MaxMind databases |