fix(prometheus): report subsonic error code (#4282)

* fix(prometheus): report subsonic error code

* address feedback
This commit is contained in:
Kendall Garner
2025-06-30 15:54:02 +00:00
committed by GitHub
parent a559414ffa
commit f9c7cc5348
4 changed files with 46 additions and 5 deletions
+3 -3
View File
@@ -20,7 +20,7 @@ import (
type Metrics interface { type Metrics interface {
WriteInitialMetrics(ctx context.Context) WriteInitialMetrics(ctx context.Context)
WriteAfterScanMetrics(ctx context.Context, success bool) WriteAfterScanMetrics(ctx context.Context, success bool)
RecordRequest(ctx context.Context, endpoint, method, client string, status int, elapsed int64) RecordRequest(ctx context.Context, endpoint, method, client string, status int32, elapsed int64)
RecordPluginRequest(ctx context.Context, plugin, method string, ok bool, elapsed int64) RecordPluginRequest(ctx context.Context, plugin, method string, ok bool, elapsed int64)
GetHandler() http.Handler GetHandler() http.Handler
} }
@@ -56,7 +56,7 @@ func (m *metrics) WriteAfterScanMetrics(ctx context.Context, success bool) {
getPrometheusMetrics().mediaScansCounter.With(scanLabels).Inc() getPrometheusMetrics().mediaScansCounter.With(scanLabels).Inc()
} }
func (m *metrics) RecordRequest(_ context.Context, endpoint, method, client string, status int, elapsed int64) { func (m *metrics) RecordRequest(_ context.Context, endpoint, method, client string, status int32, elapsed int64) {
httpLabel := prometheus.Labels{ httpLabel := prometheus.Labels{
"endpoint": endpoint, "endpoint": endpoint,
"method": method, "method": method,
@@ -233,7 +233,7 @@ func (n noopMetrics) WriteInitialMetrics(context.Context) {}
func (n noopMetrics) WriteAfterScanMetrics(context.Context, bool) {} func (n noopMetrics) WriteAfterScanMetrics(context.Context, bool) {}
func (n noopMetrics) RecordRequest(context.Context, string, string, string, int, int64) {} func (n noopMetrics) RecordRequest(context.Context, string, string, string, int32, int64) {}
func (n noopMetrics) RecordPluginRequest(context.Context, string, string, bool, int64) {} func (n noopMetrics) RecordPluginRequest(context.Context, string, string, bool, int64) {}
+12
View File
@@ -333,6 +333,7 @@ func sendResponse(w http.ResponseWriter, r *http.Request, payload *responses.Sub
sendError(w, r, err) sendError(w, r, err)
return return
} }
if payload.Status == responses.StatusOK { if payload.Status == responses.StatusOK {
if log.IsGreaterOrEqualTo(log.LevelTrace) { if log.IsGreaterOrEqualTo(log.LevelTrace) {
log.Debug(r.Context(), "API: Successful response", "endpoint", r.URL.Path, "status", "OK", "body", string(response)) log.Debug(r.Context(), "API: Successful response", "endpoint", r.URL.Path, "status", "OK", "body", string(response))
@@ -342,6 +343,17 @@ func sendResponse(w http.ResponseWriter, r *http.Request, payload *responses.Sub
} else { } else {
log.Warn(r.Context(), "API: Failed response", "endpoint", r.URL.Path, "error", payload.Error.Code, "message", payload.Error.Message) log.Warn(r.Context(), "API: Failed response", "endpoint", r.URL.Path, "error", payload.Error.Code, "message", payload.Error.Message)
} }
statusPointer, ok := r.Context().Value(subsonicErrorPointer).(*int32)
if ok && statusPointer != nil {
if payload.Status == responses.StatusOK {
*statusPointer = 0
} else {
*statusPointer = payload.Error.Code
}
}
if _, err := w.Write(response); err != nil { if _, err := w.Write(response); err != nil {
log.Error(r, "Error sending response to client", "endpoint", r.URL.Path, "payload", string(response), err) log.Error(r, "Error sending response to client", "endpoint", r.URL.Path, "payload", string(response), err)
} }
+15
View File
@@ -12,6 +12,7 @@ import (
"github.com/navidrome/navidrome/utils/gg" "github.com/navidrome/navidrome/utils/gg"
. "github.com/onsi/ginkgo/v2" . "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
"golang.org/x/net/context"
) )
var _ = Describe("sendResponse", func() { var _ = Describe("sendResponse", func() {
@@ -109,4 +110,18 @@ var _ = Describe("sendResponse", func() {
}) })
}) })
It("updates status pointer when an error occurs", func() {
pointer := int32(0)
ctx := context.WithValue(r.Context(), subsonicErrorPointer, &pointer)
r = r.WithContext(ctx)
payload.Status = responses.StatusFailed
payload.Error = &responses.Error{Code: responses.ErrorDataNotFound}
sendResponse(w, r, payload)
Expect(w.Code).To(Equal(http.StatusOK))
Expect(pointer).To(Equal(responses.ErrorDataNotFound))
})
}) })
+16 -2
View File
@@ -226,21 +226,35 @@ func playerIDCookieName(userName string) string {
return cookieName return cookieName
} }
const subsonicErrorPointer = "subsonicErrorPointer"
func recordStats(metrics metrics.Metrics) func(next http.Handler) http.Handler { func recordStats(metrics metrics.Metrics) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) { fn := func(w http.ResponseWriter, r *http.Request) {
ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor) ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor)
status := int32(-1)
contextWithStatus := context.WithValue(r.Context(), subsonicErrorPointer, &status)
start := time.Now() start := time.Now()
defer func() { defer func() {
elapsed := time.Since(start).Milliseconds()
// We want to get the client name (even if not present for certain endpoints) // We want to get the client name (even if not present for certain endpoints)
p := req.Params(r) p := req.Params(r)
client, _ := p.String("c") client, _ := p.String("c")
metrics.RecordRequest(r.Context(), strings.Replace(r.URL.Path, ".view", "", 1), r.Method, client, ww.Status(), time.Since(start).Milliseconds()) // If there is no Subsonic status (e.g., HTTP 501 not implemented), fallback to HTTP
if status == -1 {
status = int32(ww.Status())
}
shortPath := strings.Replace(r.URL.Path, ".view", "", 1)
metrics.RecordRequest(r.Context(), shortPath, r.Method, client, status, elapsed)
}() }()
next.ServeHTTP(ww, r) next.ServeHTTP(ww, r.WithContext(contextWithStatus))
} }
return http.HandlerFunc(fn) return http.HandlerFunc(fn)
} }