Expose Prometheus metrics on a separate port
CI / integration (push) Failing after 14s
CI / docker (push) Failing after 20s
CI / test (push) Successful in 1m10s
CI / publish (push) Skipped

Metrics are served on METRICS_PORT (default 2112, "off" to disable), never on
the public port. Besides the Go runtime and process metrics this adds:

- http_requests_total and http_request_duration_seconds per route pattern,
  from an Echo middleware.
- musicserver_songs_requested_total and musicserver_songs_played_total by
  pick mode (random, low, classic). The mode is now stored with each queued
  song (migration 000006, not released yet).
- Library size, sync duration, last successful sync, broken songs from the
  last full sync, and the database connection pool.

Refs #35, #36, #37

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
SansanandClaude Opus 5.5 committed 2026-10-11 21:57:58 +02:00
1 parent c62b65d7cd
commit f79f51ea4c
28 files changed
+864 -70

No files matched your search

+30
View File
@@ -5,6 +5,7 @@ import (
"encoding/json"
"net"
"net/http"
"net/http/httptest"
"path/filepath"
"sync/atomic"
"testing"
@@ -223,3 +224,32 @@ func TestCORSWithoutAllowedOrigins(t *testing.T) {
assert.Equal(t, http.StatusOK, rec.Code, "the request itself works; the browser blocks reading it")
assert.Empty(t, rec.Header().Get("Access-Control-Allow-Origin"))
}
func TestMetricsAreNotServedOnTheMainPort(t *testing.T) {
e := newTestEnv(t, func(c *config.Config) { c.MetricsPort = 2112 })
require.NotNil(t, e.server.metrics)
for _, path := range []string{"/metrics", "/api/v1/metrics"} {
rec := e.do(http.MethodGet, path)
assert.Equal(t, http.StatusNotFound, rec.Code, path)
assert.NotContains(t, rec.Body.String(), "go_goroutines", path)
}
}
func TestMetricsOff(t *testing.T) {
assert.Nil(t, newTestEnv(t).server.metrics, "MetricsPort 0 means no metrics server")
}
func TestRequestMetricsUseRoutePatterns(t *testing.T) {
e := newTestEnv(t, func(c *config.Config) { c.MetricsPort = 2112 })
song := e.addSong("Game A", "Song", "x")
e.do(http.MethodGet, "/api/v1/songs/"+song.Uuid.String())
e.do(http.MethodGet, "/no/such/page?secret=1")
rec := httptest.NewRecorder()
e.server.metrics.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/metrics", nil))
body := rec.Body.String()
assert.Contains(t, body, `http_requests_total{code="200",method="GET",route="/api/v1/songs/:uuid"}`)
assert.NotContains(t, body, song.Uuid.String(), "IDs never become labels")
assert.NotContains(t, body, "secret", "query strings never become labels")
assert.NotContains(t, body, "/no/such/page", "unknown URLs count under the frontend's /* route")
}