A source IP address on its own answers almost none of the questions an analyst asks at triage. Did this login attempt come from a country where we have no users? Is the source a residential connection or a hosting provider? Log enrichment with GeoIP answers these questions before anyone has to ask them by resolving each IP address against a geolocation database and writing the results — country, city, coordinates, network owner — directly into the event record.
In this post, we show you how to implement GeoIP enrichment at the collection layer with NXLog Agent, using a single enrichment function that works for both Linux web server logs and Windows logon events. We also cover the accuracy limits and maintenance work you should plan for.
What’s in a GeoIP database
GeoIP databases map IP address ranges to geographic and network metadata. MaxMind’s databases, one of the most widely used options, come in two tiers with different capabilities. The free GeoLite2 databases — available with a MaxMind account and acceptance of the GeoLite2 EULA — cover country, city, postal code, and approximate coordinates, with autonomous system data provided in a separate GeoLite2 ASN database. The commercial GeoIP products add higher accuracy, ISP-level data, and anonymizer detection for identifying proxy, VPN, and hosting IP addresses — the latter through the GeoIP Anonymous IP database and Insights service, not the standard City or Country databases.
IP geolocation is imprecise, and MaxMind says so itself: the company cautions that locations often resolve to the center of a population area, and you shouldn’t use a GeoIP result to identify a particular address or household. Country-level results are dependable enough for detection logic. City-level results and coordinates are best treated as supporting evidence during an investigation, not proof.
Why enrich at the agent instead of the SIEM
You can run GeoIP lookups in your SIEM at query time; many platforms support this. Enriching at the collection layer is the stronger default for three reasons.
First, every consumer sees the same data. When the agent adds the geo fields, your SIEM, long-term storage, and dashboards all receive the same fields with the same names. There is no drift between what one query-time lookup returns and another.
Second, the lookup records history correctly. IP allocations change hands. A lookup performed at collection time records what the address mapped to when the event happened. A lookup performed six months later during an investigation may return a different answer.
Third, it costs less. The lookup runs once per event against a local database file rather than repeatedly at query time. Enriched fields also let you filter and route events by geography at the edge, before they reach per-gigabyte SIEM pricing.
Agent-side enrichment adds a small per-event processing cost and puts a database file on each collector that you must keep updated. Both are manageable, and we cover them under operational considerations below.
How GeoIP enrichment works in NXLog Agent
NXLog Agent does not ship a dedicated GeoIP module — you can confirm this in the module catalog. Instead, enrichment runs through the language extension modules, most naturally the Python extension, which lets you call a function on every event as it passes through a route. The same approach works with the Perl extension, while the Go extension loads a compiled shared library rather than a script, so the pattern carries over with more build setup.
The enrichment logic lives in one script, independent of the data source. Whether the IP address came from an nginx access log or a Windows Security event, the same function enriches it.
Step 1: The NXLog Agent configuration
This configuration reads nginx access logs with the File input module, extracts the client IP, calls the Python enrichment function, and forwards the result as JSON:
<Extension json>
Module xm_json
</Extension>
<Extension geoip>
Module xm_python
PythonCode /opt/nxlog/etc/geoip_enrich.py
</Extension>
<Input access_log>
Module im_file
File '/var/log/nginx/access.log'
<Exec>
if ($raw_event =~ /^(\S+)/) {
$SrcIP = $1;
}
python_call("enrich_geoip");
</Exec>
</Input>
<Output siem>
Module om_tcp
Host siem.example.com:1514
Exec to_json();
</Output>
<Route nginx_to_siem>
Path access_log => siem
</Route>
Step 2: The enrichment script
The script opens both databases once, when NXLog Agent loads the module, using the geoip2 Python library with the free GeoLite2 City and ASN databases:
import nxlog
import geoip2.database
import geoip2.errors
city_reader = geoip2.database.Reader('/opt/nxlog/etc/GeoLite2-City.mmdb')
asn_reader = geoip2.database.Reader('/opt/nxlog/etc/GeoLite2-ASN.mmdb')
def enrich_geoip(event):
ip = event.get_field('SrcIP')
if ip is None:
return
try:
city = city_reader.city(ip)
event.set_field('SrcGeoCountry', city.country.iso_code)
event.set_field('SrcGeoCity', city.city.name)
event.set_field('SrcGeoLat', city.location.latitude)
event.set_field('SrcGeoLon', city.location.longitude)
asn = asn_reader.asn(ip)
event.set_field('SrcASN', asn.autonomous_system_number)
event.set_field('SrcASOrg', asn.autonomous_system_organization)
except (geoip2.errors.AddressNotFoundError, ValueError):
nxlog.log_debug('No GeoIP record for %s' % ip)
The ASN organization field lets you distinguish a residential ISP from a hosting or cloud provider.
Catching ValueError alongside AddressNotFoundError keeps the function safe when a corrupt log line puts something that isn’t an IP address into the field.
Step 3: Skip addresses that can’t resolve
Private and loopback addresses never return a useful GeoIP result, so filter them out before the lookup.
RFC 1918 defines the private ranges.
RFC 1122 defines the loopback range (127.0.0.0/8).
Replace the python_call line in the input block with:
if (defined($SrcIP) and not ($SrcIP =~ /^(10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[01])\.|127\.)/)) {
python_call("enrich_geoip");
}
IPv6 sources such as ::1 pass through this IPv4-only filter, but the script’s exception handling covers any address that the databases can’t resolve.
The same enrichment for Windows logon events
To enrich Windows Security logon events instead, only the input block changes.
The Event Log for Windows input module collects successful (4624) and failed (4625) logons and parses the source address into the $IpAddress field:
<Input windows_logons>
Module im_msvistalog
<QueryXML>
<QueryList>
<Query Id="0" Path="Security">
<Select Path="Security">
*[System[(EventID=4624 or EventID=4625)]]
</Select>
</Query>
</QueryList>
</QueryXML>
<Exec>
if (defined($IpAddress) and $IpAddress != '-') {
$SrcIP = $IpAddress;
}
if (defined($SrcIP) and not ($SrcIP =~ /^(10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[01])\.|127\.)/)) {
python_call("enrich_geoip");
}
</Exec>
</Input>
The Python script does not change. You maintain one function, and every data source across your fleet produces the same field names.
Operational considerations
- Keep the databases current
-
IP allocations move between networks and countries, and a stale database produces false geographic anomalies. MaxMind provides the geoipupdate client to automate downloads and publishes its update schedule: the commercial databases update every weekday (several times daily), while the free GeoLite2 City and Country databases update on Tuesdays and Fridays. The GeoLite2 ASN database updates every weekday. Schedule updates on your collectors, and alert on failed updates rather than letting them fail silently.
- Open the readers once
-
The example above initializes both database readers at module load. Opening them per event would repeat file I/O millions of times a day for no benefit.
- Check the license terms
-
GeoLite2 is free to use, but the EULA does more than govern redistribution — it also requires you to keep the data current, deleting old database copies within 30 days of an updated release. Updating is a contractual obligation, not just an accuracy practice. Redistribution terms still matter too, especially if you bake the
.mmdbfiles into golden images or container layers that leave your organization.
What SecOps teams do with enriched logs
Once geo fields exist for every event, detection and hunting become simpler. Common patterns include alerting on logins from countries where your organization has no users or business presence, flagging accounts that authenticate from two distant locations within an implausible time window, and separating residential traffic from hosting-provider sources during phishing and credential-stuffing investigations — the ASN organization field from the walkthrough above makes hosting and cloud networks easy to spot. Flagging VPNs and anonymizing proxies specifically requires MaxMind’s commercial Anonymous IP data, which the free databases don’t include. Geographic dashboards built on the enriched fields also show you where your exposure comes from.
Because NXLog Agent adds these fields before the data leaves the endpoint, you can also act on them in the pipeline itself — routing events from high-risk regions to a priority index, or dropping noisy scanner traffic from hosting ASNs before it reaches your SIEM. If you manage a large fleet, NXLog Platform lets you maintain the enrichment configuration centrally, so every agent applies the same logic and your geo fields stay consistent across thousands of endpoints.
Conclusion
GeoIP enrichment turns an opaque IP address into context your analysts and detection rules can act on. Doing it at the collection layer keeps field names consistent across all consumers and records which address mapped to when the event happened. With NXLog Agent, one short Python function enriches every event you collect.
Ready to try it in your environment? Get started with NXLog Platform or explore the NXLog documentation for more configuration examples. If you have questions, we’re here to help — reach out through our support portals.