From 02f3e90b32677ca097343f1c014d39ae9989c91a Mon Sep 17 00:00:00 2001 From: pg9182 <96569817+pg9182@users.noreply.github.com> Date: Sun, 27 Nov 2022 01:19:53 -0500 Subject: pkg/api/api0: Implement geo metrics --- pkg/api/api0/api.go | 26 +++++++++++++++++++++++++- pkg/api/api0/client.go | 12 +++++++++++- pkg/api/api0/metrics.go | 20 +++++++++++++++++++- pkg/api/api0/server.go | 17 +++++++++++++++++ pkg/api/api0/serverlist.go | 37 +++++++++++++++++++++++++++++++++++++ 5 files changed, 109 insertions(+), 3 deletions(-) (limited to 'pkg/api/api0') diff --git a/pkg/api/api0/api.go b/pkg/api/api0/api.go index fb557ad..66f9a93 100644 --- a/pkg/api/api0/api.go +++ b/pkg/api/api0/api.go @@ -26,6 +26,7 @@ import ( "github.com/klauspost/compress/gzip" "github.com/pg9182/ip2x" + "github.com/r2northstar/atlas/pkg/metricsx" "github.com/r2northstar/atlas/pkg/origin" "github.com/rs/zerolog/hlog" "golang.org/x/mod/semver" @@ -82,7 +83,8 @@ type Handler struct { AllowGameServerIPv6 bool // LookupIP looks up an IP2Location record for an IP. If not provided, - // server regions are disabled. + // server regions and geo metrics are disabled. If it doesn't include latlon + // info, geo metrics will be disabled too. LookupIP func(netip.Addr) (ip2x.Record, error) // GetRegion gets the region name from an IP2Location record. If not @@ -212,6 +214,28 @@ func (h *Handler) extractLauncherVersion(r *http.Request) string { return "" } +// geoCounter2 increments a [metricsx.GeoCounter2] for the location of r. +func (h *Handler) geoCounter2(r *http.Request, ctr *metricsx.GeoCounter2) { + a, err := netip.ParseAddrPort(r.RemoteAddr) + if err != nil { + return + } + + c, err := h.LookupIP(a.Addr()) + if err != nil { + return + } + + lat, _ := c.GetFloat32(ip2x.Latitude) + lon, _ := c.GetFloat32(ip2x.Longitude) + + if lat != 0 && lon != 0 { + ctr.Inc(float64(lat), float64(lon)) + } else { + ctr.IncUnknown() + } +} + // respFail writes a {success:false,error:ErrorObj} response with the provided // response status. func respFail(w http.ResponseWriter, r *http.Request, status int, obj ErrorObj) { diff --git a/pkg/api/api0/client.go b/pkg/api/api0/client.go index 87d5345..b5659ed 100644 --- a/pkg/api/api0/client.go +++ b/pkg/api/api0/client.go @@ -62,6 +62,7 @@ func (h *Handler) handleMainMenuPromos(w http.ResponseWriter, r *http.Request) { } h.m().client_mainmenupromos_requests_total.success(h.extractLauncherVersion(r)).Inc() + h.geoCounter2(r, h.m().client_mainmenupromos_requests_map) var p MainMenuPromos if h.MainMenuPromos != nil { @@ -293,6 +294,8 @@ func (h *Handler) handleClientOriginAuth(w http.ResponseWriter, r *http.Request) } h.m().client_originauth_requests_total.success.Inc() + h.geoCounter2(r, h.m().client_originauth_requests_map) + respJSON(w, r, http.StatusOK, map[string]any{ "success": true, "token": acct.AuthToken, @@ -610,7 +613,14 @@ func (h *Handler) handleClientServers(w http.ResponseWriter, r *http.Request) { h.m().client_servers_response_size_bytes.none.Update(float64(len(buf))) } - h.m().client_servers_requests_total.success(h.extractLauncherVersion(r)).Inc() + lver := h.extractLauncherVersion(r) + h.m().client_servers_requests_total.success(lver).Inc() + if lver != "" { + h.geoCounter2(r, h.m().client_servers_requests_map.northstar) + } else { + h.geoCounter2(r, h.m().client_servers_requests_map.other) + } + w.Header().Set("Content-Length", strconv.Itoa(len(buf))) w.WriteHeader(http.StatusOK) if r.Method != http.MethodHead { diff --git a/pkg/api/api0/metrics.go b/pkg/api/api0/metrics.go index 57e3b0a..8f9a0b5 100644 --- a/pkg/api/api0/metrics.go +++ b/pkg/api/api0/metrics.go @@ -6,6 +6,7 @@ import ( "reflect" "github.com/VictoriaMetrics/metrics" + "github.com/r2northstar/atlas/pkg/metricsx" ) // note: for results, fail_ prefix is for errors which are likely a problem with the backend, and reject_ are for client errors @@ -55,7 +56,8 @@ type apiMetrics struct { success func(version string) *metrics.Counter http_method_not_allowed *metrics.Counter } - client_originauth_requests_total struct { + client_mainmenupromos_requests_map *metricsx.GeoCounter2 + client_originauth_requests_total struct { success *metrics.Counter reject_bad_request *metrics.Counter reject_versiongate *metrics.Counter @@ -68,6 +70,7 @@ type apiMetrics struct { fail_other_error *metrics.Counter http_method_not_allowed *metrics.Counter } + client_originauth_requests_map *metricsx.GeoCounter2 client_originauth_stryder_auth_duration_seconds *metrics.Histogram client_originauth_origin_username_lookup_duration_seconds *metrics.Histogram client_originauth_origin_username_lookup_calls_total struct { @@ -106,6 +109,10 @@ type apiMetrics struct { success func(version string) *metrics.Counter http_method_not_allowed *metrics.Counter } + client_servers_requests_map struct { + northstar *metricsx.GeoCounter2 + other *metricsx.GeoCounter2 + } client_servers_response_size_bytes struct { gzip *metrics.Histogram none *metrics.Histogram @@ -163,6 +170,13 @@ func (h *Handler) WritePrometheus(w io.Writer) { h.m().set.WritePrometheus(w) } +func (h *Handler) WritePrometheusGeo(w io.Writer) { + h.m().client_mainmenupromos_requests_map.WritePrometheus(w) + h.m().client_originauth_requests_map.WritePrometheus(w) + h.m().client_servers_requests_map.northstar.WritePrometheus(w) + h.m().client_servers_requests_map.other.WritePrometheus(w) +} + // m gets metrics objects for h. // // We use it instead of using a *metrics.Set directly because: @@ -213,6 +227,7 @@ func (h *Handler) m() *apiMetrics { } mo.client_mainmenupromos_requests_total.success("unknown") mo.client_mainmenupromos_requests_total.http_method_not_allowed = mo.set.NewCounter(`atlas_api0_client_servers_response_size_bytes{result="http_method_not_allowed"}`) + mo.client_mainmenupromos_requests_map = metricsx.NewGeoCounter2(`atlas_api0_client_mainmenupromos_requests_map`) mo.client_originauth_requests_total.success = mo.set.NewCounter(`atlas_api0_client_originauth_requests_total{result="success"}`) mo.client_originauth_requests_total.reject_bad_request = mo.set.NewCounter(`atlas_api0_client_originauth_requests_total{result="reject_bad_request"}`) mo.client_originauth_requests_total.reject_versiongate = mo.set.NewCounter(`atlas_api0_client_originauth_requests_total{result="reject_versiongate"}`) @@ -224,6 +239,7 @@ func (h *Handler) m() *apiMetrics { mo.client_originauth_requests_total.fail_stryder_error = mo.set.NewCounter(`atlas_api0_client_originauth_requests_total{result="fail_stryder_error"}`) mo.client_originauth_requests_total.fail_other_error = mo.set.NewCounter(`atlas_api0_client_originauth_requests_total{result="fail_other_error"}`) mo.client_originauth_requests_total.http_method_not_allowed = mo.set.NewCounter(`atlas_api0_client_originauth_requests_total{result="http_method_not_allowed"}`) + mo.client_originauth_requests_map = metricsx.NewGeoCounter2(`atlas_api0_client_originauth_requests_map`) mo.client_originauth_stryder_auth_duration_seconds = mo.set.NewHistogram(`atlas_api0_client_originauth_stryder_auth_duration_seconds`) mo.client_originauth_origin_username_lookup_duration_seconds = mo.set.NewHistogram(`atlas_api0_client_originauth_origin_username_lookup_duration_seconds`) mo.client_originauth_origin_username_lookup_calls_total.success = mo.set.NewCounter(`atlas_api0_client_originauth_origin_username_lookup_calls_total{result="success"}`) @@ -260,6 +276,8 @@ func (h *Handler) m() *apiMetrics { } mo.client_servers_requests_total.success("unknown") mo.client_servers_requests_total.http_method_not_allowed = mo.set.NewCounter(`atlas_api0_client_servers_requests_total{result="http_method_not_allowed"}`) + mo.client_servers_requests_map.northstar = metricsx.NewGeoCounter2(`atlas_api0_client_servers_requests_map{user_agent="northstar"}`) + mo.client_servers_requests_map.other = metricsx.NewGeoCounter2(`atlas_api0_client_servers_requests_map{user_agent="other"}`) mo.client_servers_response_size_bytes.gzip = mo.set.NewHistogram(`atlas_api0_client_servers_response_size_bytes{compression="gzip"}`) mo.client_servers_response_size_bytes.none = mo.set.NewHistogram(`atlas_api0_client_servers_response_size_bytes{compression="none"}`) mo.server_upsert_requests_total.success_updated = func(action string) *metrics.Counter { diff --git a/pkg/api/api0/server.go b/pkg/api/api0/server.go index ba04bc0..ad7133c 100644 --- a/pkg/api/api0/server.go +++ b/pkg/api/api0/server.go @@ -11,6 +11,7 @@ import ( "strings" "time" + "github.com/pg9182/ip2x" "github.com/r2northstar/atlas/pkg/a2s" "github.com/r2northstar/atlas/pkg/api/api0/api0gameserver" "github.com/rs/zerolog/hlog" @@ -161,6 +162,22 @@ func (h *Handler) handleServerUpsert(w http.ResponseWriter, r *http.Request) { if canCreate || canUpdate { if h.LookupIP != nil && h.GetRegion != nil { if rec, err := h.LookupIP(raddr.Addr()); err == nil { + var lat, lon float64 + if v, _ := rec.GetFloat32(ip2x.Latitude); v != 0 { + lat = float64(v) + } + if v, _ := rec.GetFloat32(ip2x.Longitude); v != 0 { + lon = float64(v) + } + if canCreate { + s.Latitude = lat + s.Longitude = lon + } + if canUpdate { + u.Latitude = &lat + u.Longitude = &lon + } + region, err := h.GetRegion(raddr.Addr(), rec) if err == nil || region != "" { // if an error occurs, we may still have a best-effort region if canCreate { diff --git a/pkg/api/api0/serverlist.go b/pkg/api/api0/serverlist.go index 9725bf9..3a11c21 100644 --- a/pkg/api/api0/serverlist.go +++ b/pkg/api/api0/serverlist.go @@ -17,6 +17,7 @@ import ( "unicode/utf8" "github.com/klauspost/compress/gzip" + "github.com/r2northstar/atlas/pkg/metricsx" "github.com/r2northstar/atlas/pkg/nstypes" ) @@ -84,6 +85,9 @@ type Server struct { Description string Password string // blank for none + Latitude float64 + Longitude float64 + VerificationDeadline time.Time // zero once verified LastHeartbeat time.Time @@ -124,6 +128,8 @@ type ServerUpdate struct { Name *string Region *string Description *string + Latitude *float64 + Longitude *float64 PlayerCount *int MaxPlayers *int Map *string @@ -681,6 +687,31 @@ func (s *ServerList) WritePrometheus(w io.Writer) { w.Write(s.GetMetrics()) } +// WritePrometheusGeo writes location metrics for s to w. +func (s *ServerList) WritePrometheusGeo(w io.Writer) { + t := s.now() + + // take a read lock on the server list + s.mu.RLock() + defer s.mu.RUnlock() + + if s.servers1 == nil { + return + } + + ctr := metricsx.NewGeoCounter2(`atlas_api0sl_map`) + for _, srv := range s.servers1 { + if s.serverState(srv, t) == serverListStateAlive { + if srv.Latitude != 0 && srv.Longitude != 0 { + ctr.Inc(srv.Latitude, srv.Longitude) + } else { + ctr.IncUnknown() + } + } + } + ctr.WritePrometheus(w) +} + // GetLiveServers loops over live (i.e., not dead/ghost) servers until fn // returns false. The order of the servers is non-deterministic. func (s *ServerList) GetLiveServers(fn func(*Server) bool) { @@ -820,6 +851,12 @@ func (s *ServerList) ServerHybridUpdatePut(u *ServerUpdate, c *Server, l ServerL if u.Description != nil { esrv.Description, changed = *u.Description, true } + if u.Latitude != nil { + esrv.Latitude, changed = *u.Latitude, true + } + if u.Longitude != nil { + esrv.Longitude, changed = *u.Longitude, true + } if u.Map != nil { esrv.Map, changed = *u.Map, true } -- cgit v1.2.3