diff options
-rw-r--r-- | go.mod | 3 | ||||
-rw-r--r-- | go.sum | 6 | ||||
-rw-r--r-- | pkg/api/api0/accounts.go | 40 | ||||
-rw-r--r-- | pkg/api/api0/api.go | 31 | ||||
-rw-r--r-- | pkg/api/api0/client.go | 77 | ||||
-rw-r--r-- | pkg/api/api0/metrics.go | 430 | ||||
-rw-r--r-- | pkg/api/api0/playerinfo.go | 23 | ||||
-rw-r--r-- | pkg/api/api0/server.go | 60 | ||||
-rw-r--r-- | pkg/api/api0/serverlist.go | 1 |
9 files changed, 655 insertions, 16 deletions
@@ -3,6 +3,7 @@ module github.com/pg9182/atlas go 1.19 require ( + github.com/VictoriaMetrics/metrics v1.22.2 github.com/andybalholm/cascadia v1.3.1 github.com/cardigann/harhar v0.0.0-20161005032312-acb91b7a8682 github.com/rs/zerolog v1.28.0 @@ -15,5 +16,7 @@ require ( github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-isatty v0.0.14 // indirect github.com/rs/xid v1.4.0 // indirect + github.com/valyala/fastrand v1.1.0 // indirect + github.com/valyala/histogram v1.2.0 // indirect golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab // indirect ) @@ -1,3 +1,5 @@ +github.com/VictoriaMetrics/metrics v1.22.2 h1:A6LsNidYwkAHetxsvNFaUWjtzu5ltdgNEoS6i7Bn+6I= +github.com/VictoriaMetrics/metrics v1.22.2/go.mod h1:rAr/llLpEnAdTehiNlUxKgnjcOuROSzpw0GvjpEbvFc= github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c= github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= github.com/cardigann/harhar v0.0.0-20161005032312-acb91b7a8682 h1:Ce5LRUcDnICPpYjWych45AXKaV61l9oqqfMd1hORNPg= @@ -15,6 +17,10 @@ github.com/rs/zerolog v1.28.0 h1:MirSo27VyNi7RJYP3078AA1+Cyzd2GB66qy3aUHvsWY= github.com/rs/zerolog v1.28.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/valyala/fastrand v1.1.0 h1:f+5HkLW4rsgzdNoleUOB69hyT9IlD2ZQh9GyDMfb5G8= +github.com/valyala/fastrand v1.1.0/go.mod h1:HWqCzkrkg6QXT8V2EXWvXCoow7vLwOFN002oeRzjapQ= +github.com/valyala/histogram v1.2.0 h1:wyYGAZZt3CpwUiIb9AU/Zbllg1llXyrtApRS815OLoQ= +github.com/valyala/histogram v1.2.0/go.mod h1:Hb4kBwb4UxsaNbbbh+RRz8ZR6pdodR57tzWUS3BUzXY= golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= diff --git a/pkg/api/api0/accounts.go b/pkg/api/api0/accounts.go index 5167978..f6bb731 100644 --- a/pkg/api/api0/accounts.go +++ b/pkg/api/api0/accounts.go @@ -12,6 +12,7 @@ import ( func (h *Handler) handleAccountsWritePersistence(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodOptions && r.Method != http.MethodPost { + h.m().accounts_writepersistence_requests_total.http_method_not_allowed.Inc() http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) return } @@ -29,18 +30,21 @@ func (h *Handler) handleAccountsWritePersistence(w http.ResponseWriter, r *http. } if err := r.ParseMultipartForm(2 << 20); err != nil { + h.m().accounts_writepersistence_requests_total.reject_bad_request.Inc() respFail(w, r, http.StatusNotFound, ErrorCode_BAD_REQUEST.MessageObjf("failed to parse multipart form: %v", err)) return } pf, pfHdr, err := r.FormFile("pdata") if err != nil { + h.m().accounts_writepersistence_requests_total.reject_bad_request.Inc() respFail(w, r, http.StatusNotFound, ErrorCode_BAD_REQUEST.MessageObjf("missing pdata file: %v", err)) return } defer pf.Close() if pfHdr.Size > (2 << 20) { + h.m().accounts_writepersistence_requests_total.reject_too_large.Inc() respFail(w, r, http.StatusNotFound, ErrorCode_BAD_REQUEST.MessageObjf("pdata file is too large")) return } @@ -50,6 +54,7 @@ func (h *Handler) handleAccountsWritePersistence(w http.ResponseWriter, r *http. hlog.FromRequest(r).Error(). Err(err). Msgf("failed to read uploaded data file (size: %d)", pfHdr.Size) + h.m().accounts_writepersistence_requests_total.fail_other_error.Inc() respFail(w, r, http.StatusInternalServerError, ErrorCode_INTERNAL_SERVER_ERROR.MessageObj()) return } @@ -59,6 +64,7 @@ func (h *Handler) handleAccountsWritePersistence(w http.ResponseWriter, r *http. hlog.FromRequest(r).Warn(). Err(err). Msgf("invalid pdata rejected") + h.m().accounts_writepersistence_requests_total.reject_invalid_pdata.Inc() respFail(w, r, http.StatusBadRequest, ErrorCode_BAD_REQUEST.MessageObjf("invalid pdata")) return } @@ -67,18 +73,23 @@ func (h *Handler) handleAccountsWritePersistence(w http.ResponseWriter, r *http. hlog.FromRequest(r).Warn(). Err(err). Msgf("pdata with too much trailing junk rejected") + h.m().accounts_writepersistence_requests_total.reject_too_much_extradata.Inc() respFail(w, r, http.StatusBadRequest, ErrorCode_BAD_REQUEST.MessageObjf("invalid pdata")) return } + h.m().accounts_writepersistence_extradata_size_bytes.Update(float64(len(pd.ExtraData))) + uidQ := r.URL.Query().Get("id") if uidQ == "" { + h.m().accounts_writepersistence_requests_total.reject_bad_request.Inc() respFail(w, r, http.StatusBadRequest, ErrorCode_BAD_REQUEST.MessageObjf("id param is required")) return } uid, err := strconv.ParseUint(uidQ, 10, 64) if err != nil { + h.m().accounts_writepersistence_requests_total.reject_bad_request.Inc() respFail(w, r, http.StatusNotFound, ErrorCode_PLAYER_NOT_FOUND.MessageObj()) return } @@ -90,6 +101,7 @@ func (h *Handler) handleAccountsWritePersistence(w http.ResponseWriter, r *http. hlog.FromRequest(r).Error(). Err(err). Msgf("failed to parse remote ip %q", r.RemoteAddr) + h.m().accounts_writepersistence_requests_total.fail_other_error.Inc() respFail(w, r, http.StatusInternalServerError, ErrorCode_INTERNAL_SERVER_ERROR.MessageObj()) return } @@ -100,30 +112,36 @@ func (h *Handler) handleAccountsWritePersistence(w http.ResponseWriter, r *http. Err(err). Uint64("uid", uid). Msgf("failed to read account from storage") + h.m().accounts_writepersistence_requests_total.fail_storage_error_account.Inc() respFail(w, r, http.StatusInternalServerError, ErrorCode_INTERNAL_SERVER_ERROR.MessageObj()) return } if acct == nil { + h.m().accounts_writepersistence_requests_total.reject_player_not_found.Inc() respFail(w, r, http.StatusNotFound, ErrorCode_PLAYER_NOT_FOUND.MessageObj()) return } if acct.IsOnOwnServer() { if acct.AuthIP != raddr.Addr() { + h.m().accounts_writepersistence_requests_total.reject_unauthorized.Inc() respFail(w, r, http.StatusForbidden, ErrorCode_UNAUTHORIZED_GAMESERVER.MessageObj()) return } } else { srv := h.ServerList.GetServerByID(serverID) if srv == nil { + h.m().accounts_writepersistence_requests_total.reject_unauthorized.Inc() respFail(w, r, http.StatusForbidden, ErrorCode_UNAUTHORIZED_GAMESERVER.MessageObjf("no such game server")) return } if srv.Addr.Addr() != raddr.Addr() { + h.m().accounts_writepersistence_requests_total.reject_unauthorized.Inc() respFail(w, r, http.StatusForbidden, ErrorCode_UNAUTHORIZED_GAMESERVER.MessageObj()) return } if acct.LastServerID != srv.ID { + h.m().accounts_writepersistence_requests_total.reject_unauthorized.Inc() respFail(w, r, http.StatusForbidden, ErrorCode_UNAUTHORIZED_GAMESERVER.MessageObj()) return } @@ -134,15 +152,18 @@ func (h *Handler) handleAccountsWritePersistence(w http.ResponseWriter, r *http. Err(err). Uint64("uid", uid). Msgf("failed to save pdata") + h.m().accounts_writepersistence_requests_total.fail_storage_error_pdata.Inc() respFail(w, r, http.StatusInternalServerError, ErrorCode_INTERNAL_SERVER_ERROR.MessageObj()) return } + h.m().accounts_writepersistence_requests_total.success.Inc() respJSON(w, r, http.StatusOK, nil) } func (h *Handler) handleAccountsLookupUID(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodOptions && r.Method != http.MethodHead && r.Method != http.MethodGet { + h.m().accounts_lookupuid_requests_total.http_method_not_allowed.Inc() http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) return } @@ -160,6 +181,7 @@ func (h *Handler) handleAccountsLookupUID(w http.ResponseWriter, r *http.Request username := r.URL.Query().Get("username") if username == "" { + h.m().accounts_lookupuid_requests_total.reject_bad_request.Inc() respJSON(w, r, http.StatusBadRequest, map[string]any{ "success": false, "username": "", @@ -179,6 +201,7 @@ func (h *Handler) handleAccountsLookupUID(w http.ResponseWriter, r *http.Request hlog.FromRequest(r).Error(). Err(err). Msgf("failed to find account uids from storage for %q", username) + h.m().accounts_lookupuid_requests_total.fail_storage_error_account.Inc() respJSON(w, r, http.StatusInternalServerError, map[string]any{ "success": false, "username": username, @@ -188,6 +211,14 @@ func (h *Handler) handleAccountsLookupUID(w http.ResponseWriter, r *http.Request return } + switch len(uids) { + case 0: + h.m().accounts_lookupuid_requests_total.success_nomatch.Inc() + case 1: + h.m().accounts_lookupuid_requests_total.success_singlematch.Inc() + default: + h.m().accounts_lookupuid_requests_total.success_multimatch.Inc() + } respJSON(w, r, http.StatusOK, map[string]any{ "success": false, "username": username, @@ -197,6 +228,7 @@ func (h *Handler) handleAccountsLookupUID(w http.ResponseWriter, r *http.Request func (h *Handler) handleAccountsGetUsername(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodOptions && r.Method != http.MethodHead && r.Method != http.MethodGet { + h.m().accounts_getusername_requests_total.http_method_not_allowed.Inc() http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) return } @@ -214,6 +246,7 @@ func (h *Handler) handleAccountsGetUsername(w http.ResponseWriter, r *http.Reque uidQ := r.URL.Query().Get("uid") if uidQ == "" { + h.m().accounts_getusername_requests_total.reject_bad_request.Inc() respJSON(w, r, http.StatusBadRequest, map[string]any{ "success": false, "uid": "", @@ -225,6 +258,7 @@ func (h *Handler) handleAccountsGetUsername(w http.ResponseWriter, r *http.Reque uid, err := strconv.ParseUint(uidQ, 10, 64) if err != nil { + h.m().accounts_getusername_requests_total.reject_bad_request.Inc() respJSON(w, r, http.StatusNotFound, map[string]any{ "success": false, "uid": strconv.FormatUint(uid, 10), @@ -240,6 +274,7 @@ func (h *Handler) handleAccountsGetUsername(w http.ResponseWriter, r *http.Reque Err(err). Uint64("uid", uid). Msgf("failed to read account from storage") + h.m().accounts_getusername_requests_total.fail_storage_error_account.Inc() respJSON(w, r, http.StatusInternalServerError, map[string]any{ "success": false, "uid": strconv.FormatUint(uid, 10), @@ -253,6 +288,11 @@ func (h *Handler) handleAccountsGetUsername(w http.ResponseWriter, r *http.Reque if acct != nil { username = acct.Username } + if username == "" { + h.m().accounts_getusername_requests_total.success_match.Inc() + } else { + h.m().accounts_getusername_requests_total.success_missing.Inc() + } respJSON(w, r, http.StatusOK, map[string]any{ "success": true, "uid": strconv.FormatUint(uid, 10), diff --git a/pkg/api/api0/api.go b/pkg/api/api0/api.go index 180bbfc..d52603e 100644 --- a/pkg/api/api0/api.go +++ b/pkg/api/api0/api.go @@ -21,6 +21,7 @@ import ( "net/http" "strconv" "strings" + "sync" "time" "github.com/pg9182/atlas/pkg/origin" @@ -77,10 +78,20 @@ type Handler struct { // AllowGameServerIPv6 controls whether to allow game servers to use IPv6. AllowGameServerIPv6 bool + + metricsInit sync.Once + metricsObj apiMetrics } // ServeHTTP routes requests to Handler. func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + var notPanicked bool // this lets us catch panics without swallowing them + defer func() { + if !notPanicked { + h.m().request_panics_total.Inc() + } + }() + w.Header().Set("Server", "Atlas") switch r.URL.Path { @@ -110,9 +121,11 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if h.NotFound == nil { http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) } else { + notPanicked = true h.NotFound.ServeHTTP(w, r) } } + notPanicked = true } // checkLauncherVersion checks if the r was made by NorthstarLauncher and if it @@ -126,6 +139,7 @@ func (h *Handler) checkLauncherVersion(r *http.Request) bool { rver = x } } else { + h.m().versiongate_checks_total.reject_notns.Inc() return false // deny: not R2Northstar } @@ -135,17 +149,31 @@ func (h *Handler) checkLauncherVersion(r *http.Request) bool { mver = "v" + mver } } else { + h.m().versiongate_checks_total.success_ok.Inc() return true // allow: no minimum version } if !semver.IsValid(mver) { hlog.FromRequest(r).Warn().Msgf("not checking invalid minimum version %q", mver) + h.m().versiongate_checks_total.success_ok.Inc() return true // allow: invalid minimum version } if strings.HasSuffix(rver, "+dev") { + h.m().versiongate_checks_total.success_dev.Inc() return true // allow: dev versions } - return semver.Compare(rver, mver) >= 0 + if !semver.IsValid(rver) { + h.m().versiongate_checks_total.reject_invalid.Inc() + return false // deny: invalid version + } + + if semver.Compare(rver, mver) < 0 { + h.m().versiongate_checks_total.reject_old.Inc() + return false // deny: too old + } + + h.m().versiongate_checks_total.success_ok.Inc() + return true } // extractLauncherVersion extracts the launcher version from r, returning an @@ -194,6 +222,7 @@ func respJSON(w http.ResponseWriter, r *http.Request, status int, obj any) { if err != nil { panic(err) } + hlog.FromRequest(r).Trace().Msgf("json api response %.2048s", string(buf)) buf = append(buf, '\n') w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Content-Length", strconv.Itoa(len(buf))) diff --git a/pkg/api/api0/client.go b/pkg/api/api0/client.go index d5cd659..ea3ce9c 100644 --- a/pkg/api/api0/client.go +++ b/pkg/api/api0/client.go @@ -46,6 +46,7 @@ type MainMenuPromosButtonSmall struct { func (h *Handler) handleMainMenuPromos(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodOptions && r.Method != http.MethodHead && r.Method != http.MethodGet { + h.m().client_mainmenupromos_requests_total.http_method_not_allowed.Inc() http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) return } @@ -60,6 +61,8 @@ func (h *Handler) handleMainMenuPromos(w http.ResponseWriter, r *http.Request) { return } + h.m().client_mainmenupromos_requests_total.success(h.extractLauncherVersion(r)).Inc() + var p MainMenuPromos if h.MainMenuPromos != nil { p = h.MainMenuPromos(r) @@ -69,6 +72,7 @@ func (h *Handler) handleMainMenuPromos(w http.ResponseWriter, r *http.Request) { func (h *Handler) handleClientOriginAuth(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodOptions && r.Method != http.MethodGet { // no HEAD support intentionally + h.m().client_originauth_requests_total.http_method_not_allowed.Inc() http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) return } @@ -84,18 +88,21 @@ func (h *Handler) handleClientOriginAuth(w http.ResponseWriter, r *http.Request) } if !h.checkLauncherVersion(r) { + h.m().client_originauth_requests_total.reject_versiongate.Inc() respFail(w, r, http.StatusBadRequest, ErrorCode_UNSUPPORTED_VERSION.MessageObj()) return } uidQ := r.URL.Query().Get("id") if uidQ == "" { + h.m().client_originauth_requests_total.reject_bad_request.Inc() respFail(w, r, http.StatusBadRequest, ErrorCode_BAD_REQUEST.MessageObjf("id param is required")) return } uid, err := strconv.ParseUint(uidQ, 10, 64) if err != nil { + h.m().client_originauth_requests_total.reject_bad_request.Inc() respFail(w, r, http.StatusNotFound, ErrorCode_PLAYER_NOT_FOUND.MessageObj()) return } @@ -105,6 +112,7 @@ func (h *Handler) handleClientOriginAuth(w http.ResponseWriter, r *http.Request) hlog.FromRequest(r).Error(). Err(err). Msgf("failed to parse remote ip %q", r.RemoteAddr) + h.m().client_originauth_requests_total.fail_other_error.Inc() respFail(w, r, http.StatusInternalServerError, ErrorCode_INTERNAL_SERVER_ERROR.MessageObj()) return } @@ -112,17 +120,33 @@ func (h *Handler) handleClientOriginAuth(w http.ResponseWriter, r *http.Request) if !h.InsecureDevNoCheckPlayerAuth { token := r.URL.Query().Get("token") if token == "" { + h.m().client_originauth_requests_total.reject_bad_request.Inc() respFail(w, r, http.StatusBadRequest, ErrorCode_BAD_REQUEST.MessageObjf("token param is required")) return } + stryderStart := time.Now() + stryderCtx, cancel := context.WithTimeout(r.Context(), time.Second*5) defer cancel() stryderRes, err := stryder.NucleusAuth(stryderCtx, token, uid) + h.m().client_originauth_stryder_auth_duration_seconds.UpdateDuration(stryderStart) if err != nil { switch { case errors.Is(err, stryder.ErrInvalidGame): + h.m().client_originauth_requests_total.reject_stryder_invalidgame.Inc() + case errors.Is(err, stryder.ErrInvalidToken): + h.m().client_originauth_requests_total.reject_stryder_invalidtoken.Inc() + case errors.Is(err, stryder.ErrMultiplayerNotAllowed): + h.m().client_originauth_requests_total.reject_stryder_mpnotallowed.Inc() + case errors.Is(err, stryder.ErrStryder): + h.m().client_originauth_requests_total.reject_stryder_other.Inc() + default: + h.m().client_originauth_requests_total.fail_stryder_error.Inc() + } + switch { + case errors.Is(err, stryder.ErrInvalidGame): fallthrough case errors.Is(err, stryder.ErrInvalidToken): fallthrough @@ -159,32 +183,39 @@ func (h *Handler) handleClientOriginAuth(w http.ResponseWriter, r *http.Request) var username string if h.OriginAuthMgr != nil { + originStart := time.Now() if tok, ours, err := h.OriginAuthMgr.OriginAuth(false); err == nil { var notfound bool if ui, err := origin.GetUserInfo(r.Context(), tok, uid); err == nil { if len(ui) == 1 { username = ui[0].EAID + h.m().client_originauth_origin_username_lookup_calls_total.success.Inc() } else { notfound = true + h.m().client_originauth_origin_username_lookup_calls_total.notfound.Inc() } } else if errors.Is(err, origin.ErrAuthRequired) { if tok, ours, err := h.OriginAuthMgr.OriginAuth(true); err == nil { if ui, err := origin.GetUserInfo(r.Context(), tok, uid); err == nil { if len(ui) == 1 { username = ui[0].EAID + h.m().client_originauth_origin_username_lookup_calls_total.success.Inc() } else { notfound = true + h.m().client_originauth_origin_username_lookup_calls_total.notfound.Inc() } } } else if ours { hlog.FromRequest(r).Error(). Err(err). Msgf("origin auth token refresh failure") + h.m().client_originauth_origin_username_lookup_calls_total.fail_authtok_refresh.Inc() } } else { hlog.FromRequest(r).Error(). Err(err). Msgf("failed to get origin user info") + h.m().client_originauth_origin_username_lookup_calls_total.fail_other_error.Inc() } if notfound { hlog.FromRequest(r).Warn(). @@ -196,7 +227,9 @@ func (h *Handler) handleClientOriginAuth(w http.ResponseWriter, r *http.Request) hlog.FromRequest(r).Error(). Err(err). Msgf("origin auth token refresh failure") + h.m().client_originauth_origin_username_lookup_calls_total.fail_authtok_refresh.Inc() } + h.m().client_originauth_origin_username_lookup_duration_seconds.UpdateDuration(originStart) } // note: there's small chance of race conditions here if there are multiple @@ -210,6 +243,7 @@ func (h *Handler) handleClientOriginAuth(w http.ResponseWriter, r *http.Request) Err(err). Uint64("uid", uid). Msgf("failed to read account from storage") + h.m().client_originauth_requests_total.fail_storage_error_account.Inc() respJSON(w, r, http.StatusInternalServerError, map[string]any{ "success": false, "error": ErrorCode_INTERNAL_SERVER_ERROR, @@ -231,6 +265,7 @@ func (h *Handler) handleClientOriginAuth(w http.ResponseWriter, r *http.Request) hlog.FromRequest(r).Error(). Err(err). Msgf("failed to generate random token") + h.m().client_originauth_requests_total.fail_other_error.Inc() respFail(w, r, http.StatusInternalServerError, ErrorCode_INTERNAL_SERVER_ERROR.MessageObj()) return } else { @@ -248,10 +283,12 @@ func (h *Handler) handleClientOriginAuth(w http.ResponseWriter, r *http.Request) Err(err). Uint64("uid", uid). Msgf("failed to save account to storage") + h.m().client_originauth_requests_total.fail_storage_error_account.Inc() respFail(w, r, http.StatusInternalServerError, ErrorCode_INTERNAL_SERVER_ERROR.MessageObj()) return } + h.m().client_originauth_requests_total.success.Inc() respJSON(w, r, http.StatusOK, map[string]any{ "success": true, "token": acct.AuthToken, @@ -260,6 +297,7 @@ func (h *Handler) handleClientOriginAuth(w http.ResponseWriter, r *http.Request) func (h *Handler) handleClientAuthWithServer(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodOptions && r.Method != http.MethodPost { + h.m().client_authwithserver_requests_total.http_method_not_allowed.Inc() http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) return } @@ -275,18 +313,21 @@ func (h *Handler) handleClientAuthWithServer(w http.ResponseWriter, r *http.Requ } if !h.checkLauncherVersion(r) { + h.m().client_authwithserver_requests_total.reject_versiongate.Inc() respFail(w, r, http.StatusBadRequest, ErrorCode_UNSUPPORTED_VERSION.MessageObj()) return } uidQ := r.URL.Query().Get("id") if uidQ == "" { + h.m().client_authwithserver_requests_total.reject_bad_request.Inc() respFail(w, r, http.StatusBadRequest, ErrorCode_BAD_REQUEST.MessageObjf("id param is required")) return } uid, err := strconv.ParseUint(uidQ, 10, 64) if err != nil { + h.m().client_authwithserver_requests_total.reject_bad_request.Inc() respFail(w, r, http.StatusNotFound, ErrorCode_PLAYER_NOT_FOUND.MessageObj()) return } @@ -297,6 +338,7 @@ func (h *Handler) handleClientAuthWithServer(w http.ResponseWriter, r *http.Requ srv := h.ServerList.GetServerByID(server) if srv == nil || srv.Password != password { + h.m().client_authwithserver_requests_total.reject_password.Inc() respFail(w, r, http.StatusUnauthorized, ErrorCode_UNAUTHORIZED_PWD.MessageObj()) return } @@ -307,16 +349,19 @@ func (h *Handler) handleClientAuthWithServer(w http.ResponseWriter, r *http.Requ Err(err). Uint64("uid", uid). Msgf("failed to read account from storage") + h.m().client_authwithserver_requests_total.fail_storage_error_account.Inc() respFail(w, r, http.StatusInternalServerError, ErrorCode_INTERNAL_SERVER_ERROR.MessageObj()) return } if acct == nil { + h.m().client_authwithserver_requests_total.reject_player_not_found.Inc() respFail(w, r, http.StatusNotFound, ErrorCode_PLAYER_NOT_FOUND.MessageObj()) return } if !h.InsecureDevNoCheckPlayerAuth { if playerToken != acct.AuthToken || !time.Now().Before(acct.AuthTokenExpiry) { + h.m().client_authwithserver_requests_total.reject_masterserver_token.Inc() respFail(w, r, http.StatusUnauthorized, ErrorCode_INVALID_MASTERSERVER_TOKEN.MessageObj()) return } @@ -327,6 +372,7 @@ func (h *Handler) handleClientAuthWithServer(w http.ResponseWriter, r *http.Requ hlog.FromRequest(r).Error(). Err(err). Msgf("failed to generate random token") + h.m().client_authwithserver_requests_total.fail_other_error.Inc() respFail(w, r, http.StatusInternalServerError, ErrorCode_INTERNAL_SERVER_ERROR.MessageObj()) return } else { @@ -339,6 +385,7 @@ func (h *Handler) handleClientAuthWithServer(w http.ResponseWriter, r *http.Requ Err(err). Uint64("uid", acct.UID). Msgf("failed to read pdata from storage") + h.m().client_authwithserver_requests_total.fail_storage_error_pdata.Inc() respFail(w, r, http.StatusInternalServerError, ErrorCode_INTERNAL_SERVER_ERROR.MessageObj()) return } else if !exists { @@ -348,29 +395,37 @@ func (h *Handler) handleClientAuthWithServer(w http.ResponseWriter, r *http.Requ } { + authStart := time.Now() + ctx, cancel := context.WithTimeout(r.Context(), time.Second*5) defer cancel() if err := api0gameserver.AuthenticateIncomingPlayer(ctx, srv.AuthAddr(), acct.UID, acct.Username, authToken, srv.ServerAuthToken, pbuf); err != nil { + h.m().client_authwithserver_gameserverauth_duration_seconds.UpdateDuration(authStart) if errors.Is(err, context.DeadlineExceeded) { err = fmt.Errorf("request timed out") } switch { case errors.Is(err, api0gameserver.ErrAuthFailed): + h.m().client_authwithserver_requests_total.reject_gameserverauth.Inc() respFail(w, r, http.StatusInternalServerError, ErrorCode_JSON_PARSE_ERROR.MessageObj()) // this is kind of misleading... but it's what the original master server did case errors.Is(err, api0gameserver.ErrInvalidResponse): hlog.FromRequest(r).Error(). Err(err). Msgf("failed to make gameserver auth request") + h.m().client_authwithserver_requests_total.fail_gameserverauth.Inc() respFail(w, r, http.StatusInternalServerError, ErrorCode_BAD_GAMESERVER_RESPONSE.MessageObj()) default: hlog.FromRequest(r).Error(). Err(err). Msgf("failed to make gameserver auth request") + h.m().client_authwithserver_requests_total.fail_gameserverauth.Inc() respFail(w, r, http.StatusInternalServerError, ErrorCode_INTERNAL_SERVER_ERROR.MessageObj()) } return } + + h.m().client_authwithserver_gameserverauth_duration_seconds.UpdateDuration(authStart) } acct.LastServerID = srv.ID @@ -380,10 +435,12 @@ func (h *Handler) handleClientAuthWithServer(w http.ResponseWriter, r *http.Requ Err(err). Uint64("uid", uid). Msgf("failed to save account to storage") + h.m().client_authwithserver_requests_total.fail_storage_error_account.Inc() respFail(w, r, http.StatusInternalServerError, ErrorCode_INTERNAL_SERVER_ERROR.MessageObj()) return } + h.m().client_authwithserver_requests_total.success.Inc() respJSON(w, r, http.StatusOK, map[string]any{ "success": true, "ip": srv.Addr.Addr().String(), @@ -394,6 +451,7 @@ func (h *Handler) handleClientAuthWithServer(w http.ResponseWriter, r *http.Requ func (h *Handler) handleClientAuthWithSelf(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodOptions && r.Method != http.MethodPost { + h.m().client_authwithself_requests_total.http_method_not_allowed.Inc() http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) return } @@ -409,18 +467,21 @@ func (h *Handler) handleClientAuthWithSelf(w http.ResponseWriter, r *http.Reques } if !h.checkLauncherVersion(r) { + h.m().client_authwithself_requests_total.reject_versiongate.Inc() respFail(w, r, http.StatusBadRequest, ErrorCode_UNSUPPORTED_VERSION.MessageObj()) return } uidQ := r.URL.Query().Get("id") if uidQ == "" { + h.m().client_authwithself_requests_total.reject_bad_request.Inc() respFail(w, r, http.StatusBadRequest, ErrorCode_BAD_REQUEST.MessageObjf("id param is required")) return } uid, err := strconv.ParseUint(uidQ, 10, 64) if err != nil { + h.m().client_authwithself_requests_total.reject_bad_request.Inc() respFail(w, r, http.StatusNotFound, ErrorCode_PLAYER_NOT_FOUND.MessageObj()) return } @@ -433,16 +494,19 @@ func (h *Handler) handleClientAuthWithSelf(w http.ResponseWriter, r *http.Reques Err(err). Uint64("uid", uid). Msgf("failed to read account from storage") + h.m().client_authwithself_requests_total.fail_storage_error_account.Inc() respFail(w, r, http.StatusInternalServerError, ErrorCode_INTERNAL_SERVER_ERROR.MessageObj()) return } if acct == nil { + h.m().client_authwithself_requests_total.reject_player_not_found.Inc() respFail(w, r, http.StatusNotFound, ErrorCode_PLAYER_NOT_FOUND.MessageObj()) return } if !h.InsecureDevNoCheckPlayerAuth { if playerToken != acct.AuthToken || !time.Now().Before(acct.AuthTokenExpiry) { + h.m().client_authwithself_requests_total.reject_masterserver_token.Inc() respFail(w, r, http.StatusUnauthorized, ErrorCode_INVALID_MASTERSERVER_TOKEN.MessageObj()) return } @@ -455,6 +519,7 @@ func (h *Handler) handleClientAuthWithSelf(w http.ResponseWriter, r *http.Reques Err(err). Uint64("uid", uid). Msgf("failed to save account to storage") + h.m().client_authwithself_requests_total.fail_storage_error_account.Inc() respFail(w, r, http.StatusInternalServerError, ErrorCode_INTERNAL_SERVER_ERROR.MessageObj()) return } @@ -470,6 +535,7 @@ func (h *Handler) handleClientAuthWithSelf(w http.ResponseWriter, r *http.Reques Err(err). Uint64("uid", acct.UID). Msgf("failed to read pdata from storage") + h.m().client_authwithself_requests_total.fail_storage_error_pdata.Inc() respFail(w, r, http.StatusInternalServerError, ErrorCode_INTERNAL_SERVER_ERROR.MessageObj()) return } else if !exists { @@ -484,17 +550,20 @@ func (h *Handler) handleClientAuthWithSelf(w http.ResponseWriter, r *http.Reques hlog.FromRequest(r).Error(). Err(err). Msgf("failed to generate random token") + h.m().client_authwithself_requests_total.fail_other_error.Inc() respFail(w, r, http.StatusInternalServerError, ErrorCode_INTERNAL_SERVER_ERROR.MessageObj()) return } else { obj["authToken"] = v } + h.m().client_authwithself_requests_total.success.Inc() respJSON(w, r, http.StatusOK, obj) } func (h *Handler) handleClientServers(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodOptions && r.Method != http.MethodHead && r.Method != http.MethodGet { + h.m().client_servers_requests_total.http_method_not_allowed.Inc() http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) return } @@ -511,19 +580,27 @@ func (h *Handler) handleClientServers(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8") + var compressed bool buf := h.ServerList.csGetJSON() for _, e := range strings.Split(r.Header.Get("Accept-Encoding"), ",") { if t, _, _ := strings.Cut(e, ";"); strings.TrimSpace(t) == "gzip" { if zbuf, ok := h.ServerList.csGetJSONGzip(); ok { buf = zbuf w.Header().Set("Content-Encoding", "gzip") + compressed = true } else { hlog.FromRequest(r).Error().Msg("failed to gzip server list") } break } } + if compressed { + h.m().client_servers_response_size_bytes.gzip.Update(float64(len(buf))) + } else { + h.m().client_servers_response_size_bytes.none.Update(float64(len(buf))) + } + h.m().client_servers_requests_total.success(h.extractLauncherVersion(r)).Inc() 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 new file mode 100644 index 0000000..dd98000 --- /dev/null +++ b/pkg/api/api0/metrics.go @@ -0,0 +1,430 @@ +package api0 + +import ( + "fmt" + "io" + "reflect" + + "github.com/VictoriaMetrics/metrics" +) + +// note: for results, fail_ prefix is for errors which are likely a problem with the backend, and reject_ are for client errors + +type apiMetrics struct { + set *metrics.Set + request_panics_total *metrics.Counter + versiongate_checks_total struct { + success_ok *metrics.Counter + success_dev *metrics.Counter + reject_old *metrics.Counter + reject_invalid *metrics.Counter + reject_notns *metrics.Counter + } + accounts_writepersistence_extradata_size_bytes *metrics.Histogram // only includes successful updates + accounts_writepersistence_requests_total struct { + success *metrics.Counter + reject_too_much_extradata *metrics.Counter + reject_too_large *metrics.Counter + reject_invalid_pdata *metrics.Counter + reject_bad_request *metrics.Counter + reject_player_not_found *metrics.Counter + reject_unauthorized *metrics.Counter + fail_storage_error_account *metrics.Counter + fail_storage_error_pdata *metrics.Counter + fail_other_error *metrics.Counter + http_method_not_allowed *metrics.Counter + } + accounts_lookupuid_requests_total struct { + success_singlematch *metrics.Counter + success_multimatch *metrics.Counter + success_nomatch *metrics.Counter + reject_bad_request *metrics.Counter + fail_storage_error_account *metrics.Counter + http_method_not_allowed *metrics.Counter + } + accounts_getusername_requests_total struct { + success_match *metrics.Counter + success_missing *metrics.Counter + reject_bad_request *metrics.Counter + reject_player_not_found *metrics.Counter + fail_storage_error_account *metrics.Counter + http_method_not_allowed *metrics.Counter + } + client_mainmenupromos_requests_total struct { + success func(version string) *metrics.Counter + http_method_not_allowed *metrics.Counter + } + client_originauth_requests_total struct { + success *metrics.Counter + reject_bad_request *metrics.Counter + reject_versiongate *metrics.Counter + reject_stryder_invalidgame *metrics.Counter + reject_stryder_invalidtoken *metrics.Counter + reject_stryder_mpnotallowed *metrics.Counter + reject_stryder_other *metrics.Counter + fail_storage_error_account *metrics.Counter + fail_stryder_error *metrics.Counter + fail_other_error *metrics.Counter + http_method_not_allowed *metrics.Counter + } + 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 { + success *metrics.Counter + notfound *metrics.Counter + fail_authtok_refresh *metrics.Counter + fail_other_error *metrics.Counter + } + client_authwithserver_requests_total struct { + success *metrics.Counter + reject_bad_request *metrics.Counter + reject_versiongate *metrics.Counter + reject_player_not_found *metrics.Counter + reject_masterserver_token *metrics.Counter + reject_password *metrics.Counter + reject_gameserverauth *metrics.Counter + fail_gameserverauth *metrics.Counter + fail_storage_error_account *metrics.Counter + fail_storage_error_pdata *metrics.Counter + fail_other_error *metrics.Counter + http_method_not_allowed *metrics.Counter + } + client_authwithserver_gameserverauth_duration_seconds *metrics.Histogram + client_authwithself_requests_total struct { + success *metrics.Counter + reject_bad_request *metrics.Counter + reject_versiongate *metrics.Counter + reject_player_not_found *metrics.Counter + reject_masterserver_token *metrics.Counter + fail_storage_error_account *metrics.Counter + fail_storage_error_pdata *metrics.Counter + fail_other_error *metrics.Counter + http_method_not_allowed *metrics.Counter + } + client_servers_requests_total struct { + success func(version string) *metrics.Counter + http_method_not_allowed *metrics.Counter + } + client_servers_response_size_bytes struct { + gzip *metrics.Histogram + none *metrics.Histogram + } + server_upsert_requests_total struct { + success_updated func(action string) *metrics.Counter + success_verified func(action string) *metrics.Counter + reject_versiongate func(action string) *metrics.Counter + reject_ipv6 func(action string) *metrics.Counter + reject_bad_request func(action string) *metrics.Counter + reject_unauthorized_ip func(action string) *metrics.Counter + reject_server_not_found func(action string) *metrics.Counter + reject_duplicate_auth_addr func(action string) *metrics.Counter + reject_limits_exceeded func(action string) *metrics.Counter + reject_verify_authtimeout func(action string) *metrics.Counter + reject_verify_authresp func(action string) *metrics.Counter + reject_verify_autherr func(action string) *metrics.Counter + reject_verify_udptimeout func(action string) *metrics.Counter + reject_verify_udperr func(action string) *metrics.Counter + fail_other_error func(action string) *metrics.Counter + fail_serverlist_error func(action string) *metrics.Counter + http_method_not_allowed func(action string) *metrics.Counter + } + server_upsert_modinfo_parse_errors_total func(action string) *metrics.Counter + server_upsert_verify_time_seconds struct { + success *metrics.Histogram + failure *metrics.Histogram + } + server_remove_requests_total struct { + success *metrics.Counter + reject_unauthorized_ip *metrics.Counter + reject_bad_request *metrics.Counter + reject_server_not_found *metrics.Counter + fail_other_error *metrics.Counter + http_method_not_allowed *metrics.Counter + } + player_pdata_requests_total struct { + success func(filter string) *metrics.Counter + reject_bad_request *metrics.Counter + reject_player_not_found *metrics.Counter + fail_storage_error_pdata *metrics.Counter + fail_pdata_invalid *metrics.Counter + fail_other_error *metrics.Counter + http_method_not_allowed *metrics.Counter + } +} + +func (h *Handler) Metrics() *metrics.Set { + return h.m().set +} + +func (h *Handler) WritePrometheus(w io.Writer) { + h.m().set.WritePrometheus(w) +} + +// m gets metrics objects for h. +// +// We use it instead of using a *metrics.Set directly because: +// - It means we don't need to keep checking if a set is nil. +// - It means we don't have the overhead of checking/creating each individual metric during requests. +// - It makes typos less likely. +// - It means that metrics still get included in the output instead of being undefined even if they start at zero. +func (h *Handler) m() *apiMetrics { + h.metricsInit.Do(func() { + mo := &h.metricsObj + mo.set = metrics.NewSet() + mo.request_panics_total = mo.set.NewCounter(`atlas_api0_request_panics_total`) + mo.versiongate_checks_total.success_ok = mo.set.NewCounter(`atlas_api0_versiongate_checks_total{result="success_ok"}`) + mo.versiongate_checks_total.success_dev = mo.set.NewCounter(`atlas_api0_versiongate_checks_total{result="success_dev"}`) + mo.versiongate_checks_total.reject_old = mo.set.NewCounter(`atlas_api0_versiongate_checks_total{result="reject_old"}`) + mo.versiongate_checks_total.reject_invalid = mo.set.NewCounter(`atlas_api0_versiongate_checks_total{result="reject_invalid"}`) + mo.versiongate_checks_total.reject_notns = mo.set.NewCounter(`atlas_api0_versiongate_checks_total{result="reject_notns"}`) + mo.accounts_writepersistence_extradata_size_bytes = mo.set.NewHistogram(`atlas_api0_accounts_writepersistence_extradata_size_bytes`) + mo.accounts_writepersistence_requests_total.success = mo.set.NewCounter(`atlas_api0_accounts_writepersistence_requests_total{result="success"}`) + mo.accounts_writepersistence_requests_total.reject_too_much_extradata = mo.set.NewCounter(`atlas_api0_accounts_writepersistence_requests_total{result="reject_too_much_extradata"}`) + mo.accounts_writepersistence_requests_total.reject_too_large = mo.set.NewCounter(`atlas_api0_accounts_writepersistence_requests_total{result="reject_too_large"}`) + mo.accounts_writepersistence_requests_total.reject_invalid_pdata = mo.set.NewCounter(`atlas_api0_accounts_writepersistence_requests_total{result="reject_invalid_pdata"}`) + mo.accounts_writepersistence_requests_total.reject_bad_request = mo.set.NewCounter(`atlas_api0_accounts_writepersistence_requests_total{result="reject_bad_request"}`) + mo.accounts_writepersistence_requests_total.reject_player_not_found = mo.set.NewCounter(`atlas_api0_accounts_writepersistence_requests_total{result="reject_player_not_found"}`) + mo.accounts_writepersistence_requests_total.reject_unauthorized = mo.set.NewCounter(`atlas_api0_accounts_writepersistence_requests_total{result="reject_unauthorized"}`) + mo.accounts_writepersistence_requests_total.fail_storage_error_account = mo.set.NewCounter(`atlas_api0_accounts_writepersistence_requests_total{result="fail_storage_error_account"}`) + mo.accounts_writepersistence_requests_total.fail_storage_error_pdata = mo.set.NewCounter(`atlas_api0_accounts_writepersistence_requests_total{result="fail_storage_error_pdata"}`) + mo.accounts_writepersistence_requests_total.fail_other_error = mo.set.NewCounter(`atlas_api0_accounts_writepersistence_requests_total{result="fail_other_error"}`) + mo.accounts_writepersistence_requests_total.http_method_not_allowed = mo.set.NewCounter(`atlas_api0_accounts_writepersistence_requests_total{result="http_method_not_allowed"}`) + mo.accounts_lookupuid_requests_total.success_singlematch = mo.set.NewCounter(`atlas_api0_accounts_lookupuid_requests_total{result="success_singlematch"}`) + mo.accounts_lookupuid_requests_total.success_multimatch = mo.set.NewCounter(`atlas_api0_accounts_lookupuid_requests_total{result="success_multimatch"}`) + mo.accounts_lookupuid_requests_total.success_nomatch = mo.set.NewCounter(`atlas_api0_accounts_lookupuid_requests_total{result="success_nomatch"}`) + mo.accounts_lookupuid_requests_total.reject_bad_request = mo.set.NewCounter(`atlas_api0_accounts_lookupuid_requests_total{result="reject_bad_request"}`) + mo.accounts_lookupuid_requests_total.fail_storage_error_account = mo.set.NewCounter(`atlas_api0_accounts_lookupuid_requests_total{result="fail_storage_error_account"}`) + mo.accounts_lookupuid_requests_total.http_method_not_allowed = mo.set.NewCounter(`atlas_api0_accounts_lookupuid_requests_total{result="http_method_not_allowed"}`) + mo.accounts_getusername_requests_total.success_match = mo.set.NewCounter(`atlas_api0_accounts_getusername_requests_total{result="success_match"}`) + mo.accounts_getusername_requests_total.success_missing = mo.set.NewCounter(`atlas_api0_accounts_getusername_requests_total{result="success_missing"}`) + mo.accounts_getusername_requests_total.reject_bad_request = mo.set.NewCounter(`atlas_api0_accounts_getusername_requests_total{result="reject_bad_request"}`) + mo.accounts_getusername_requests_total.reject_player_not_found = mo.set.NewCounter(`atlas_api0_accounts_getusername_requests_total{result="reject_player_not_found"}`) + mo.accounts_getusername_requests_total.fail_storage_error_account = mo.set.NewCounter(`atlas_api0_accounts_getusername_requests_total{result="fail_storage_error_account"}`) + mo.accounts_getusername_requests_total.http_method_not_allowed = mo.set.NewCounter(`atlas_api0_accounts_getusername_requests_total{result="http_method_not_allowed"}`) + mo.client_mainmenupromos_requests_total.success = func(launcher_version string) *metrics.Counter { + if launcher_version == "" { + launcher_version = "unknown" + } + return mo.set.GetOrCreateCounter(`atlas_api0_client_mainmenupromos_requests_total{result="success",launcher_version="` + launcher_version + `"}`) + } + 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_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"}`) + mo.client_originauth_requests_total.reject_stryder_invalidgame = mo.set.NewCounter(`atlas_api0_client_originauth_requests_total{result="reject_stryder_invalidgame"}`) + mo.client_originauth_requests_total.reject_stryder_invalidtoken = mo.set.NewCounter(`atlas_api0_client_originauth_requests_total{result="reject_stryder_invalidtoken"}`) + mo.client_originauth_requests_total.reject_stryder_mpnotallowed = mo.set.NewCounter(`atlas_api0_client_originauth_requests_total{result="reject_stryder_mpnotallowed"}`) + mo.client_originauth_requests_total.reject_stryder_other = mo.set.NewCounter(`atlas_api0_client_originauth_requests_total{result="reject_stryder_other"}`) + mo.client_originauth_requests_total.fail_storage_error_account = mo.set.NewCounter(`atlas_api0_client_originauth_requests_total{result="fail_storage_error_account"}`) + 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_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"}`) + mo.client_originauth_origin_username_lookup_calls_total.notfound = mo.set.NewCounter(`atlas_api0_client_originauth_origin_username_lookup_calls_total{result="notfound"}`) + mo.client_originauth_origin_username_lookup_calls_total.fail_authtok_refresh = mo.set.NewCounter(`atlas_api0_client_originauth_origin_username_lookup_calls_total{result="fail_authtok_refresh"}`) + mo.client_originauth_origin_username_lookup_calls_total.fail_other_error = mo.set.NewCounter(`atlas_api0_client_originauth_origin_username_lookup_calls_total{result="fail_other_error"}`) + mo.client_authwithserver_requests_total.success = mo.set.NewCounter(`atlas_api0_client_authwithserver_requests_total{result="success"}`) + mo.client_authwithserver_requests_total.reject_bad_request = mo.set.NewCounter(`atlas_api0_client_authwithserver_requests_total{result="reject_bad_request"}`) + mo.client_authwithserver_requests_total.reject_versiongate = mo.set.NewCounter(`atlas_api0_client_authwithserver_requests_total{result="reject_versiongate"}`) + mo.client_authwithserver_requests_total.reject_player_not_found = mo.set.NewCounter(`atlas_api0_client_authwithserver_requests_total{result="reject_player_not_found"}`) + mo.client_authwithserver_requests_total.reject_masterserver_token = mo.set.NewCounter(`atlas_api0_client_authwithserver_requests_total{result="reject_masterserver_token"}`) + mo.client_authwithserver_requests_total.reject_password = mo.set.NewCounter(`atlas_api0_client_authwithserver_requests_total{result="reject_password"}`) + mo.client_authwithserver_requests_total.reject_gameserverauth = mo.set.NewCounter(`atlas_api0_client_authwithserver_requests_total{result="reject_gameserverauth"}`) + mo.client_authwithserver_requests_total.fail_gameserverauth = mo.set.NewCounter(`atlas_api0_client_authwithserver_requests_total{result="fail_gameserverauth"}`) + mo.client_authwithserver_requests_total.fail_storage_error_account = mo.set.NewCounter(`atlas_api0_client_authwithserver_requests_total{result="fail_storage_error_account"}`) + mo.client_authwithserver_requests_total.fail_storage_error_pdata = mo.set.NewCounter(`atlas_api0_client_authwithserver_requests_total{result="fail_storage_error_pdata"}`) + mo.client_authwithserver_requests_total.fail_other_error = mo.set.NewCounter(`atlas_api0_client_authwithserver_requests_total{result="fail_other_error"}`) + mo.client_authwithserver_requests_total.http_method_not_allowed = mo.set.NewCounter(`atlas_api0_client_authwithserver_requests_total{result="http_method_not_allowed"}`) + mo.client_authwithserver_gameserverauth_duration_seconds = mo.set.NewHistogram(`atlas_api0_client_authwithserver_gameserverauth_duration_seconds`) + mo.client_authwithself_requests_total.success = mo.set.NewCounter(`atlas_api0_client_authwithself_requests_total{result="success"}`) + mo.client_authwithself_requests_total.reject_bad_request = mo.set.NewCounter(`atlas_api0_client_authwithself_requests_total{result="reject_bad_request"}`) + mo.client_authwithself_requests_total.reject_versiongate = mo.set.NewCounter(`atlas_api0_client_authwithself_requests_total{result="reject_versiongate"}`) + mo.client_authwithself_requests_total.reject_player_not_found = mo.set.NewCounter(`atlas_api0_client_authwithself_requests_total{result="reject_player_not_found"}`) + mo.client_authwithself_requests_total.reject_masterserver_token = mo.set.NewCounter(`atlas_api0_client_authwithself_requests_total{result="reject_masterserver_token"}`) + mo.client_authwithself_requests_total.fail_storage_error_account = mo.set.NewCounter(`atlas_api0_client_authwithself_requests_total{result="fail_storage_error_account"}`) + mo.client_authwithself_requests_total.fail_storage_error_pdata = mo.set.NewCounter(`atlas_api0_client_authwithself_requests_total{result="fail_storage_error_pdata"}`) + mo.client_authwithself_requests_total.fail_other_error = mo.set.NewCounter(`atlas_api0_client_authwithself_requests_total{result="fail_other_error"}`) + mo.client_authwithself_requests_total.http_method_not_allowed = mo.set.NewCounter(`atlas_api0_client_authwithself_requests_total{result="http_method_not_allowed"}`) + mo.client_servers_requests_total.success = func(launcher_version string) *metrics.Counter { + if launcher_version == "" { + launcher_version = "unknown" + } + return mo.set.GetOrCreateCounter(`atlas_api0_client_servers_requests_total{result="success",launcher_version="` + launcher_version + `"}`) + } + 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_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 { + if action == "" { + panic("invalid action") + } + return mo.set.GetOrCreateCounter(`atlas_api0_server_upsert_requests_total{result="success_updated",action="` + action + `"}`) + } + mo.server_upsert_requests_total.success_verified = func(action string) *metrics.Counter { + if action == "" { + panic("invalid action") + } + return mo.set.GetOrCreateCounter(`atlas_api0_server_upsert_requests_total{result="success_verified",action="` + action + `"}`) + } + mo.server_upsert_requests_total.reject_versiongate = func(action string) *metrics.Counter { + if action == "" { + panic("invalid action") + } + return mo.set.GetOrCreateCounter(`atlas_api0_server_upsert_requests_total{result="reject_versiongate",action="` + action + `"}`) + } + mo.server_upsert_requests_total.reject_ipv6 = func(action string) *metrics.Counter { + if action == "" { + panic("invalid action") + } + return mo.set.GetOrCreateCounter(`atlas_api0_server_upsert_requests_total{result="reject_ipv6",action="` + action + `"}`) + } + mo.server_upsert_requests_total.reject_bad_request = func(action string) *metrics.Counter { + if action == "" { + panic("invalid action") + } + return mo.set.GetOrCreateCounter(`atlas_api0_server_upsert_requests_total{result="reject_bad_request",action="` + action + `"}`) + } + mo.server_upsert_requests_total.reject_unauthorized_ip = func(action string) *metrics.Counter { + if action == "" { + panic("invalid action") + } + return mo.set.GetOrCreateCounter(`atlas_api0_server_upsert_requests_total{result="reject_unauthorized_ip",action="` + action + `"}`) + } + mo.server_upsert_requests_total.reject_server_not_found = func(action string) *metrics.Counter { + if action == "" { + panic("invalid action") + } + return mo.set.GetOrCreateCounter(`atlas_api0_server_upsert_requests_total{result="reject_server_not_found",action="` + action + `"}`) + } + mo.server_upsert_requests_total.reject_duplicate_auth_addr = func(action string) *metrics.Counter { + if action == "" { + panic("invalid action") + } + return mo.set.GetOrCreateCounter(`atlas_api0_server_upsert_requests_total{result="reject_duplicate_auth_addr",action="` + action + `"}`) + } + mo.server_upsert_requests_total.reject_limits_exceeded = func(action string) *metrics.Counter { + if action == "" { + panic("invalid action") + } + return mo.set.GetOrCreateCounter(`atlas_api0_server_upsert_requests_total{result="reject_limits_exceeded",action="` + action + `"}`) + } + mo.server_upsert_requests_total.reject_verify_authtimeout = func(action string) *metrics.Counter { + if action == "" { + panic("invalid action") + } + return mo.set.GetOrCreateCounter(`atlas_api0_server_upsert_requests_total{result="reject_verify_authtimeout",action="` + action + `"}`) + } + mo.server_upsert_requests_total.reject_verify_authresp = func(action string) *metrics.Counter { + if action == "" { + panic("invalid action") + } + return mo.set.GetOrCreateCounter(`atlas_api0_server_upsert_requests_total{result="reject_verify_authresp",action="` + action + `"}`) + } + mo.server_upsert_requests_total.reject_verify_autherr = func(action string) *metrics.Counter { + if action == "" { + panic("invalid action") + } + return mo.set.GetOrCreateCounter(`atlas_api0_server_upsert_requests_total{result="reject_verify_autherr",action="` + action + `"}`) + } + mo.server_upsert_requests_total.reject_verify_udptimeout = func(action string) *metrics.Counter { + if action == "" { + panic("invalid action") + } + return mo.set.GetOrCreateCounter(`atlas_api0_server_upsert_requests_total{result="reject_verify_udptimeout",action="` + action + `"}`) + } + mo.server_upsert_requests_total.reject_verify_udperr = func(action string) *metrics.Counter { + if action == "" { + panic("invalid action") + } + return mo.set.GetOrCreateCounter(`atlas_api0_server_upsert_requests_total{result="reject_verify_udperr",action="` + action + `"}`) + } + mo.server_upsert_requests_total.fail_other_error = func(action string) *metrics.Counter { + if action == "" { + panic("invalid action") + } + return mo.set.GetOrCreateCounter(`atlas_api0_server_upsert_requests_total{result="fail_other_error",action="` + action + `"}`) + } + mo.server_upsert_requests_total.fail_serverlist_error = func(action string) *metrics.Counter { + if action == "" { + panic("invalid action") + } + return mo.set.GetOrCreateCounter(`atlas_api0_server_upsert_requests_total{result="fail_serverlist_error",action="` + action + `"}`) + } + mo.server_upsert_requests_total.http_method_not_allowed = func(action string) *metrics.Counter { + if action == "" { + panic("invalid action") + } + return mo.set.GetOrCreateCounter(`atlas_api0_server_upsert_requests_total{result="http_method_not_allowed",action="` + action + `"}`) + } + mo.server_upsert_modinfo_parse_errors_total = func(action string) *metrics.Counter { + if action == "" { + panic("invalid action") + } + return mo.set.GetOrCreateCounter(`atlas_api0_server_upsert_modinfo_parse_errors_total{action="` + action + `"}`) + } + for _, action := range []string{"add_server", "update_values", "heartbeat"} { + mo.server_upsert_requests_total.success_updated(action) + mo.server_upsert_requests_total.success_verified(action) + mo.server_upsert_requests_total.reject_versiongate(action) + mo.server_upsert_requests_total.reject_ipv6(action) + mo.server_upsert_requests_total.reject_bad_request(action) + mo.server_upsert_requests_total.reject_unauthorized_ip(action) + mo.server_upsert_requests_total.reject_server_not_found(action) + mo.server_upsert_requests_total.reject_duplicate_auth_addr(action) + mo.server_upsert_requests_total.reject_limits_exceeded(action) + mo.server_upsert_requests_total.reject_verify_authtimeout(action) + mo.server_upsert_requests_total.reject_verify_authresp(action) + mo.server_upsert_requests_total.reject_verify_autherr(action) + mo.server_upsert_requests_total.reject_verify_udptimeout(action) + mo.server_upsert_requests_total.reject_verify_udperr(action) + mo.server_upsert_requests_total.fail_other_error(action) + mo.server_upsert_requests_total.fail_serverlist_error(action) + mo.server_upsert_requests_total.http_method_not_allowed(action) + mo.server_upsert_modinfo_parse_errors_total(action) + } + mo.server_upsert_verify_time_seconds.success = mo.set.NewHistogram(`atlas_api0_server_upsert_verify_time_seconds{success="true"}`) + mo.server_upsert_verify_time_seconds.failure = mo.set.NewHistogram(`atlas_api0_server_upsert_verify_time_seconds{success="false"}`) + mo.server_remove_requests_total.success = mo.set.NewCounter(`atlas_api0_server_remove_requests_total{result="success"}`) + mo.server_remove_requests_total.reject_unauthorized_ip = mo.set.NewCounter(`atlas_api0_server_remove_requests_total{result="reject_unauthorized_ip"}`) + mo.server_remove_requests_total.reject_bad_request = mo.set.NewCounter(`atlas_api0_server_remove_requests_total{result="reject_bad_request"}`) + mo.server_remove_requests_total.reject_server_not_found = mo.set.NewCounter(`atlas_api0_server_remove_requests_total{result="reject_server_not_found"}`) + mo.server_remove_requests_total.fail_other_error = mo.set.NewCounter(`atlas_api0_server_remove_requests_total{result="fail_other_error"}`) + mo.server_remove_requests_total.http_method_not_allowed = mo.set.NewCounter(`atlas_api0_server_remove_requests_total{result="http_method_not_allowed"}`) + mo.player_pdata_requests_total.success = func(filter string) *metrics.Counter { + if filter == "" { + panic("invalid filter") + } + return mo.set.GetOrCreateCounter(`atlas_api0_player_pdata_requests_total{result="success",filter="` + filter + `"}`) + } + mo.player_pdata_requests_total.reject_bad_request = mo.set.NewCounter(`atlas_api0_player_pdata_requests_total{result="reject_bad_request"}`) + mo.player_pdata_requests_total.reject_player_not_found = mo.set.NewCounter(`atlas_api0_player_pdata_requests_total{result="reject_player_not_found"}`) + mo.player_pdata_requests_total.fail_storage_error_pdata = mo.set.NewCounter(`atlas_api0_player_pdata_requests_total{result="fail_storage_error_pdata"}`) + mo.player_pdata_requests_total.fail_pdata_invalid = mo.set.NewCounter(`atlas_api0_player_pdata_requests_total{result="fail_pdata_invalid"}`) + mo.player_pdata_requests_total.fail_other_error = mo.set.NewCounter(`atlas_api0_player_pdata_requests_total{result="fail_other_error"}`) + mo.player_pdata_requests_total.http_method_not_allowed = mo.set.NewCounter(`atlas_api0_player_pdata_requests_total{result="http_method_not_allowed"}`) + }) + + // ensure we initialized everything + var chk func(v reflect.Value, name string) + chk = func(v reflect.Value, name string) { + switch v.Kind() { + case reflect.Struct: + for i := 0; i < v.NumField(); i++ { + chk(v.Field(i), name+"."+v.Type().Field(i).Name) + } + case reflect.Pointer, reflect.Func: + if v.IsNil() { + panic(fmt.Errorf("check metrics: unexpected nil %q", name)) + } + default: + panic(fmt.Errorf("check metrics: unexpected kind %s", v.Kind())) + } + } + chk(reflect.ValueOf(h.metricsObj), "metricsObj") + + return &h.metricsObj +} diff --git a/pkg/api/api0/playerinfo.go b/pkg/api/api0/playerinfo.go index 70555cc..db0e42a 100644 --- a/pkg/api/api0/playerinfo.go +++ b/pkg/api/api0/playerinfo.go @@ -5,6 +5,7 @@ import ( "encoding/hex" "net/http" "strconv" + "strings" "time" "github.com/pg9182/atlas/pkg/pdata" @@ -42,20 +43,22 @@ func pdataFilterLoadout(path ...string) bool { func (h *Handler) handlePlayer(w http.ResponseWriter, r *http.Request) { var pdataFilter func(...string) bool - switch r.URL.Path { - case "/player/pdata": + var pdataFilterName string + switch pdataFilterName := strings.TrimPrefix(r.URL.Path, "/player/"); pdataFilterName { + case "pdata": pdataFilter = nil - case "/player/info": + case "info": pdataFilter = pdataFilterInfo - case "/player/stats": + case "stats": pdataFilter = pdataFilterStats - case "/player/loadout": + case "loadout": pdataFilter = pdataFilterLoadout default: http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) return } if r.Method != http.MethodOptions && r.Method != http.MethodGet && r.Method != http.MethodHead { + h.m().player_pdata_requests_total.http_method_not_allowed.Inc() http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) return } @@ -80,12 +83,14 @@ func (h *Handler) handlePlayer(w http.ResponseWriter, r *http.Request) { uidQ := r.URL.Query().Get("id") if uidQ == "" { + h.m().player_pdata_requests_total.reject_bad_request.Inc() respFail(w, r, http.StatusBadRequest, ErrorCode_BAD_REQUEST.MessageObjf("id param is required")) return } uid, err := strconv.ParseUint(uidQ, 10, 64) if err != nil { + h.m().player_pdata_requests_total.reject_bad_request.Inc() respFail(w, r, http.StatusNotFound, ErrorCode_PLAYER_NOT_FOUND.MessageObj()) return } @@ -98,13 +103,16 @@ func (h *Handler) handlePlayer(w http.ResponseWriter, r *http.Request) { Err(err). Uint64("uid", uid). Msgf("failed to read pdata hash from storage") + h.m().player_pdata_requests_total.fail_storage_error_pdata.Inc() w.WriteHeader(http.StatusInternalServerError) return } if !exists { + h.m().player_pdata_requests_total.reject_player_not_found.Inc() w.WriteHeader(http.StatusNotFound) return } + h.m().player_pdata_requests_total.success(pdataFilterName).Inc() w.Header().Set("ETag", `W/"`+hex.EncodeToString(hash[:])+`"`) w.WriteHeader(http.StatusOK) return @@ -116,10 +124,12 @@ func (h *Handler) handlePlayer(w http.ResponseWriter, r *http.Request) { Err(err). Uint64("uid", uid). Msgf("failed to read pdata from storage") + h.m().player_pdata_requests_total.fail_storage_error_pdata.Inc() respFail(w, r, http.StatusInternalServerError, ErrorCode_INTERNAL_SERVER_ERROR.MessageObj()) return } if !exists { + h.m().player_pdata_requests_total.reject_player_not_found.Inc() respFail(w, r, http.StatusNotFound, ErrorCode_PLAYER_NOT_FOUND.MessageObj()) return } @@ -134,6 +144,7 @@ func (h *Handler) handlePlayer(w http.ResponseWriter, r *http.Request) { Uint64("uid", uid). Str("pdata_sha256", hex.EncodeToString(hash[:])). Msgf("failed to parse pdata from storage") + h.m().player_pdata_requests_total.fail_pdata_invalid.Inc() respFail(w, r, http.StatusInternalServerError, ErrorCode_INTERNAL_SERVER_ERROR.MessageObjf("failed to parse stored pdata")) return } @@ -145,11 +156,13 @@ func (h *Handler) handlePlayer(w http.ResponseWriter, r *http.Request) { Uint64("uid", uid). Str("pdata_sha256", hex.EncodeToString(hash[:])). Msgf("failed to encode pdata as json") + h.m().player_pdata_requests_total.fail_other_error.Inc() respFail(w, r, http.StatusInternalServerError, ErrorCode_INTERNAL_SERVER_ERROR.MessageObjf("failed to encode pdata as json")) return } jbuf = append(jbuf, '\n') + h.m().player_pdata_requests_total.success(pdataFilterName).Inc() w.Header().Set("Content-Type", "application/json; charset=utf-8") respMaybeCompress(w, r, http.StatusOK, jbuf) } diff --git a/pkg/api/api0/server.go b/pkg/api/api0/server.go index 14edc35..1c27f24 100644 --- a/pkg/api/api0/server.go +++ b/pkg/api/api0/server.go @@ -8,6 +8,7 @@ import ( "net/http" "net/netip" "strconv" + "strings" "time" "github.com/pg9182/atlas/pkg/a2s" @@ -20,15 +21,16 @@ func (h *Handler) handleServerUpsert(w http.ResponseWriter, r *http.Request) { // - https://github.com/R2Northstar/NorthstarLauncher/commit/753dda6231bbb2adf585bbc916c0b220e816fcdc // - https://github.com/R2Northstar/NorthstarLauncher/blob/v1.9.7/NorthstarDLL/masterserver.cpp + var action string var isCreate, canCreate, isUpdate, canUpdate bool - switch r.URL.Path { - case "/server/add_server": + switch action = strings.TrimPrefix(r.URL.Path, "/server/"); action { + case "add_server": isCreate = true canCreate = true - case "/server/update_values": + case "update_values": canCreate = true fallthrough - case "/server/heartbeat": + case "heartbeat": isUpdate = true canUpdate = true default: @@ -36,6 +38,7 @@ func (h *Handler) handleServerUpsert(w http.ResponseWriter, r *http.Request) { } if r.Method != http.MethodOptions && r.Method != http.MethodPost { + h.m().server_upsert_requests_total.http_method_not_allowed(action).Inc() http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) return } @@ -51,6 +54,7 @@ func (h *Handler) handleServerUpsert(w http.ResponseWriter, r *http.Request) { } if !h.checkLauncherVersion(r) { + h.m().server_upsert_requests_total.reject_versiongate(action).Inc() respFail(w, r, http.StatusBadRequest, ErrorCode_UNSUPPORTED_VERSION.MessageObj()) return } @@ -60,12 +64,14 @@ func (h *Handler) handleServerUpsert(w http.ResponseWriter, r *http.Request) { hlog.FromRequest(r).Error(). Err(err). Msgf("failed to parse remote ip %q", r.RemoteAddr) + h.m().server_upsert_requests_total.fail_other_error(action).Inc() respFail(w, r, http.StatusInternalServerError, ErrorCode_INTERNAL_SERVER_ERROR.MessageObj()) return } if !h.AllowGameServerIPv6 { if raddr.Addr().Is6() { + h.m().server_upsert_requests_total.reject_ipv6(action).Inc() respFail(w, r, http.StatusBadRequest, ErrorCode_BAD_REQUEST.MessageObjf("ipv6 is not currently supported (ip %s)", raddr.Addr())) return } @@ -101,6 +107,7 @@ func (h *Handler) handleServerUpsert(w http.ResponseWriter, r *http.Request) { if canUpdate { if v := r.URL.Query().Get("id"); v == "" { if isUpdate { + h.m().server_upsert_requests_total.reject_bad_request(action).Inc() respFail(w, r, http.StatusBadRequest, ErrorCode_BAD_REQUEST.MessageObjf("port param is required")) return } @@ -112,10 +119,12 @@ func (h *Handler) handleServerUpsert(w http.ResponseWriter, r *http.Request) { if canCreate { if v := r.URL.Query().Get("port"); v == "" { if isCreate { + h.m().server_upsert_requests_total.reject_bad_request(action).Inc() respFail(w, r, http.StatusBadRequest, ErrorCode_BAD_REQUEST.MessageObjf("port param is required")) return } } else if n, err := strconv.ParseUint(v, 10, 16); err != nil { + h.m().server_upsert_requests_total.reject_bad_request(action).Inc() respFail(w, r, http.StatusBadRequest, ErrorCode_BAD_REQUEST.MessageObjf("port param is invalid: %v", err)) return } else { @@ -124,10 +133,12 @@ func (h *Handler) handleServerUpsert(w http.ResponseWriter, r *http.Request) { if v := r.URL.Query().Get("authPort"); v == "" { if isCreate { + h.m().server_upsert_requests_total.reject_bad_request(action).Inc() respFail(w, r, http.StatusBadRequest, ErrorCode_BAD_REQUEST.MessageObjf("authPort param is required")) return } } else if n, err := strconv.ParseUint(v, 10, 16); err != nil { + h.m().server_upsert_requests_total.reject_bad_request(action).Inc() respFail(w, r, http.StatusBadRequest, ErrorCode_BAD_REQUEST.MessageObjf("authPort param is invalid: %v", err)) return } else { @@ -136,6 +147,7 @@ func (h *Handler) handleServerUpsert(w http.ResponseWriter, r *http.Request) { if v := r.URL.Query().Get("password"); len(v) > 128 { if isCreate { + h.m().server_upsert_requests_total.reject_bad_request(action).Inc() respFail(w, r, http.StatusBadRequest, ErrorCode_BAD_REQUEST.MessageObjf("password is too long")) return } @@ -147,6 +159,7 @@ func (h *Handler) handleServerUpsert(w http.ResponseWriter, r *http.Request) { if canCreate || canUpdate { if v := r.URL.Query().Get("name"); v == "" { if isCreate { + h.m().server_upsert_requests_total.reject_bad_request(action).Inc() respFail(w, r, http.StatusBadRequest, ErrorCode_BAD_REQUEST.MessageObjf("name param must not be empty")) return } @@ -266,6 +279,7 @@ func (h *Handler) handleServerUpsert(w http.ResponseWriter, r *http.Request) { } } if modInfoErr != nil { + h.m().server_upsert_modinfo_parse_errors_total(action).Inc() hlog.FromRequest(r).Warn(). Err(err). Msgf("failed to parse modinfo") @@ -275,42 +289,54 @@ func (h *Handler) handleServerUpsert(w http.ResponseWriter, r *http.Request) { nsrv, err := h.ServerList.ServerHybridUpdatePut(u, s, l) if err != nil { if errors.Is(err, ErrServerListUpdateWrongIP) { + h.m().server_upsert_requests_total.reject_unauthorized_ip(action).Inc() respFail(w, r, http.StatusForbidden, ErrorCode_UNAUTHORIZED_GAMESERVER.MessageObjf("%v", err)) return } if errors.Is(err, ErrServerListUpdateServerDead) { + h.m().server_upsert_requests_total.reject_server_not_found(action).Inc() respFail(w, r, http.StatusForbidden, ErrorCode_UNAUTHORIZED_GAMESERVER.MessageObjf("no such server")) return } if errors.Is(err, ErrServerListDuplicateAuthAddr) { + h.m().server_upsert_requests_total.reject_duplicate_auth_addr(action).Inc() respFail(w, r, http.StatusForbidden, ErrorCode_DUPLICATE_SERVER.MessageObjf("%v", err)) return } if errors.Is(err, ErrServerListLimitExceeded) { + h.m().server_upsert_requests_total.reject_limits_exceeded(action).Inc() respFail(w, r, http.StatusInternalServerError, ErrorCode_INTERNAL_SERVER_ERROR.MessageObjf("%v", err)) return } hlog.FromRequest(r).Error(). Err(err). Msgf("failed to update server list") + h.m().server_upsert_requests_total.fail_serverlist_error(action).Inc() respFail(w, r, http.StatusInternalServerError, ErrorCode_INTERNAL_SERVER_ERROR.MessageObj()) return } if !nsrv.VerificationDeadline.IsZero() { + verifyStart := time.Now() + ctx, cancel := context.WithDeadline(r.Context(), nsrv.VerificationDeadline) defer cancel() if err := api0gameserver.Verify(ctx, s.AuthAddr()); err != nil { - if errors.Is(err, context.DeadlineExceeded) { - err = fmt.Errorf("request timed out") - } var code ErrorCode - if errors.Is(err, api0gameserver.ErrInvalidResponse) { + switch { + case errors.Is(err, context.DeadlineExceeded): + err = fmt.Errorf("request timed out") + code = ErrorCode_NO_GAMESERVER_RESPONSE + h.m().server_upsert_requests_total.reject_verify_authtimeout(action).Inc() + case errors.Is(err, api0gameserver.ErrInvalidResponse): code = ErrorCode_BAD_GAMESERVER_RESPONSE - } else { + h.m().server_upsert_requests_total.reject_verify_authresp(action).Inc() + default: code = ErrorCode_NO_GAMESERVER_RESPONSE + h.m().server_upsert_requests_total.reject_verify_autherr(action).Inc() } + h.m().server_upsert_verify_time_seconds.failure.UpdateDuration(verifyStart) respFail(w, r, http.StatusBadGateway, code.MessageObjf("failed to connect to auth port: %v", err)) return } @@ -319,20 +345,28 @@ func (h *Handler) handleServerUpsert(w http.ResponseWriter, r *http.Request) { var code ErrorCode switch { case errors.Is(err, a2s.ErrTimeout): + h.m().server_upsert_requests_total.reject_verify_udptimeout(action).Inc() code = ErrorCode_NO_GAMESERVER_RESPONSE default: + h.m().server_upsert_requests_total.reject_verify_udperr(action).Inc() code = ErrorCode_BAD_GAMESERVER_RESPONSE } + h.m().server_upsert_verify_time_seconds.failure.UpdateDuration(verifyStart) respFail(w, r, http.StatusBadGateway, code.MessageObjf("failed to connect to game port: %v", err)) return } + h.m().server_upsert_verify_time_seconds.success.UpdateDuration(verifyStart) if !h.ServerList.VerifyServer(nsrv.ID) { + h.m().server_upsert_requests_total.reject_verify_udptimeout(action).Inc() respFail(w, r, http.StatusBadGateway, ErrorCode_NO_GAMESERVER_RESPONSE.MessageObjf("verification timed out")) return } - } + h.m().server_upsert_requests_total.success_verified(action).Inc() + } else { + h.m().server_upsert_requests_total.success_updated(action).Inc() + } respJSON(w, r, http.StatusOK, map[string]any{ "success": true, "id": nsrv.ID, @@ -342,6 +376,7 @@ func (h *Handler) handleServerUpsert(w http.ResponseWriter, r *http.Request) { func (h *Handler) handleServerRemove(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodOptions && r.Method != http.MethodDelete { + h.m().server_remove_requests_total.http_method_not_allowed.Inc() http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) return } @@ -361,12 +396,14 @@ func (h *Handler) handleServerRemove(w http.ResponseWriter, r *http.Request) { hlog.FromRequest(r).Error(). Err(err). Msgf("failed to parse remote ip %q", r.RemoteAddr) + h.m().server_remove_requests_total.fail_other_error.Inc() respFail(w, r, http.StatusInternalServerError, ErrorCode_INTERNAL_SERVER_ERROR.MessageObj()) return } var id string if v := r.URL.Query().Get("id"); v == "" { + h.m().server_remove_requests_total.reject_bad_request.Inc() respFail(w, r, http.StatusBadRequest, ErrorCode_BAD_REQUEST.MessageObjf("id param is required")) return } else { @@ -375,15 +412,18 @@ func (h *Handler) handleServerRemove(w http.ResponseWriter, r *http.Request) { srv := h.ServerList.GetServerByID(id) if srv == nil { + h.m().server_remove_requests_total.reject_server_not_found.Inc() respFail(w, r, http.StatusForbidden, ErrorCode_UNAUTHORIZED_GAMESERVER.MessageObjf("no such game server")) return } if srv.Addr.Addr() != raddr.Addr() { + h.m().server_remove_requests_total.reject_unauthorized_ip.Inc() respFail(w, r, http.StatusForbidden, ErrorCode_UNAUTHORIZED_GAMESERVER.MessageObj()) return } h.ServerList.DeleteServerByID(id) + h.m().server_remove_requests_total.success.Inc() respJSON(w, r, http.StatusOK, map[string]any{ "success": true, }) diff --git a/pkg/api/api0/serverlist.go b/pkg/api/api0/serverlist.go index 2587f8b..73837ba 100644 --- a/pkg/api/api0/serverlist.go +++ b/pkg/api/api0/serverlist.go @@ -5,6 +5,7 @@ import ( "compress/gzip" "errors" "fmt" + "io" "net/netip" "sort" "strconv" |