Add GeoIP support for unique IP aggregation
- 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:
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user