Add GeoIP support for unique IP aggregation
Publish telemt-api gateway Docker image / test (push) Failing after 17s
Publish telemt-api gateway Docker image / build-and-push (push) Has been skipped

- Introduced GeoIP configuration options in config.example.yaml to enable geolocation lookups for the /api/agg/unique-ips endpoint.
- Updated the aggregate handler to include optional GeoIP data in responses, enriching unique IP information with country and city details, as well as ASN data if available.
- Enhanced documentation in AGGREGATE.md and README.md to reflect the new GeoIP functionality and its usage.
- Added a dependency on the geoip2-golang library in go.mod for GeoIP lookups.
- Modified tests to accommodate the new GeoIP integration in the aggregate handler.
This commit is contained in:
Denozordec
2026-03-30 01:47:08 +07:00
parent 4a4f15bd0b
commit d7a63f0da3
15 changed files with 449 additions and 12 deletions
+41
View File
@@ -0,0 +1,41 @@
package aggregate
import (
"github.com/telemt/telemt-api/internal/geoip"
)
// EnrichUniqueIPsGeo fills country/city on each IP when lookup succeeds.
func EnrichUniqueIPsGeo(rows []UniqueIPsRow, g *geoip.Service) {
if g == nil || len(rows) == 0 {
return
}
for i := range rows {
for j := range rows[i].IPs {
ip := rows[i].IPs[j].IP
res, ok := g.Lookup(ip)
if !ok {
continue
}
if res.CountryCode != "" {
s := res.CountryCode
rows[i].IPs[j].CountryCode = &s
}
if res.CountryName != "" {
s := res.CountryName
rows[i].IPs[j].CountryName = &s
}
if res.CityName != "" {
s := res.CityName
rows[i].IPs[j].CityName = &s
}
if res.ASN != 0 {
a := res.ASN
rows[i].IPs[j].ASN = &a
}
if res.ASOrg != "" {
o := res.ASOrg
rows[i].IPs[j].ASOrganization = &o
}
}
}
}
+8 -2
View File
@@ -9,6 +9,7 @@ import (
"time"
"github.com/telemt/telemt-api/internal/config"
"github.com/telemt/telemt-api/internal/geoip"
)
const pathPrefix = "/api/agg"
@@ -17,11 +18,13 @@ const pathPrefix = "/api/agg"
type Handler struct {
Parsed *config.Parsed
Client *http.Client
Geo *geoip.Service
}
// NewHandler builds an aggregate handler; client must use a non-nil Transport (e.g. gateway shared transport).
func NewHandler(p *config.Parsed, client *http.Client) *Handler {
return &Handler{Parsed: p, Client: client}
// Geo may be nil (no GeoLite2 lookups).
func NewHandler(p *config.Parsed, client *http.Client, geo *geoip.Service) *Handler {
return &Handler{Parsed: p, Client: client, Geo: geo}
}
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
@@ -154,6 +157,9 @@ func (h *Handler) handleUniqueIPs(w http.ResponseWriter, r *http.Request) {
defer cancel()
results := FetchStatsUsers(ctx, h.Client, h.Parsed, aliases)
data := BuildUniqueIPs(results)
if h.Geo != nil && !strings.EqualFold(r.URL.Query().Get("geo"), "false") {
EnrichUniqueIPsGeo(data, h.Geo)
}
writeOK(w, data)
}
+2 -2
View File
@@ -35,7 +35,7 @@ func TestHandlerResolveAndFetch(t *testing.T) {
if err != nil {
t.Fatal(err)
}
h := NewHandler(parsed, up.Client())
h := NewHandler(parsed, up.Client(), nil)
req := httptest.NewRequest(http.MethodGet, "/api/agg/summary?aliases=test", nil)
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
@@ -65,7 +65,7 @@ func TestHandlerMethodNotAllowed(t *testing.T) {
if err != nil {
t.Fatal(err)
}
h := NewHandler(parsed, http.DefaultClient)
h := NewHandler(parsed, http.DefaultClient, nil)
req := httptest.NewRequest(http.MethodPost, "/api/agg/summary", nil)
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
+10 -4
View File
@@ -62,10 +62,16 @@ type UniqueIPsRow struct {
// IPAssignments per-IP server visibility (snapshot-based).
type IPAssignments struct {
IP string `json:"ip"`
ActiveOnServers []string `json:"active_on_servers"`
RecentOnServers []string `json:"recent_on_servers"`
PrimaryServer *string `json:"primary_server,omitempty"`
IP string `json:"ip"`
ActiveOnServers []string `json:"active_on_servers"`
RecentOnServers []string `json:"recent_on_servers"`
PrimaryServer *string `json:"primary_server,omitempty"`
// GeoIP when gateway geoip.enabled and lookup succeeds:
CountryCode *string `json:"country_code,omitempty"`
CountryName *string `json:"country_name,omitempty"`
CityName *string `json:"city_name,omitempty"`
ASN *uint64 `json:"asn,omitempty"` // GeoLite2-ASN
ASOrganization *string `json:"as_organization,omitempty"` // ASN org name
}
// UsersRow merged user view with optional per-server detail and links.