aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--go.mod3
-rw-r--r--go.sum6
-rw-r--r--pkg/api/api0/accounts.go40
-rw-r--r--pkg/api/api0/api.go31
-rw-r--r--pkg/api/api0/client.go77
-rw-r--r--pkg/api/api0/metrics.go430
-rw-r--r--pkg/api/api0/playerinfo.go23
-rw-r--r--pkg/api/api0/server.go60
-rw-r--r--pkg/api/api0/serverlist.go1
9 files changed, 655 insertions, 16 deletions
diff --git a/go.mod b/go.mod
index 83bbafa..d3950fb 100644
--- a/go.mod
+++ b/go.mod
@@ -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
)
diff --git a/go.sum b/go.sum
index d014445..d83ba03 100644
--- a/go.sum
+++ b/go.sum
@@ -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"