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

+39 -9
View File
@@ -12,6 +12,7 @@ import (
"music-server/internal/db/repository"
"music-server/internal/logging"
"music-server/internal/metrics"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
@@ -21,6 +22,24 @@ import (
// maxPickAttempts bounds how many broken songs are skipped before giving up.
const maxPickAttempts = 100
// PickMode is how a random song was picked. It is stored with queued songs and used as
// the mode label of the song metrics.
type PickMode string
const (
PickRandom PickMode = "random"
PickLow PickMode = "low"
PickClassic PickMode = "classic"
)
func init() {
// Start every mode at 0, so the series exist before the first song.
for _, mode := range []PickMode{PickRandom, PickLow, PickClassic} {
metrics.SongsRequested.WithLabelValues(string(mode))
metrics.SongsPlayed.WithLabelValues(string(mode))
}
}
type SongInfo struct {
SoundtrackUuid uuid.UUID `json:"SoundtrackUuid"`
Soundtrack string `json:"Soundtrack"`
@@ -145,7 +164,11 @@ func (m *MusicService) markPlayed(ctx context.Context, songs []repository.ListQu
if err := m.q.AddSoundtrackPlayedByUUID(ctx, song.SoundtrackUuid); err != nil {
return err
}
return m.q.AddSongPlayedByUUID(ctx, song.SongUuid)
if err := m.q.AddSongPlayedByUUID(ctx, song.SongUuid); err != nil {
return err
}
metrics.SongsPlayed.WithLabelValues(song.Mode).Inc()
return nil
}
func (m *MusicService) loadQueue(ctx context.Context, queueID uuid.UUID) (repository.Queue, []repository.ListQueueSongsRow, error) {
@@ -167,7 +190,7 @@ func (m *MusicService) RandomSong(ctx context.Context, queueID uuid.UUID) (strin
for i, s := range soundtracks {
candidates[i] = weightedSoundtrack{soundtrack: s, weight: 1}
}
return m.pickAndRemember(ctx, queueID, candidates)
return m.pickAndRemember(ctx, queueID, candidates, PickRandom)
}
// RandomSongLowChance picks a random song, favouring soundtracks played less than average.
@@ -176,23 +199,30 @@ func (m *MusicService) RandomSongLowChance(ctx context.Context, queueID uuid.UUI
if err != nil {
return "", err
}
return m.pickAndRemember(ctx, queueID, lowChanceWeights(soundtracks))
return m.pickAndRemember(ctx, queueID, lowChanceWeights(soundtracks), PickLow)
}
func (m *MusicService) pickAndRemember(ctx context.Context, queueID uuid.UUID, candidates []weightedSoundtrack) (string, error) {
func (m *MusicService) pickAndRemember(
ctx context.Context, queueID uuid.UUID, candidates []weightedSoundtrack, mode PickMode,
) (string, error) {
song, err := m.pickSong(ctx, candidates)
if err != nil {
return "", err
}
return m.remember(ctx, queueID, song)
return m.remember(ctx, queueID, song, mode)
}
// remember stores the song as the queue's last fetched song, for AddLatestToQueue.
func (m *MusicService) remember(ctx context.Context, queueID uuid.UUID, song repository.Song) (string, error) {
err := m.q.SetQueueLastFetched(ctx, repository.SetQueueLastFetchedParams{Uuid: queueID, LastFetchedSongUuid: song.Uuid})
// remember stores the song and how it was picked as the queue's last fetched song,
// for AddLatestToQueue.
func (m *MusicService) remember(ctx context.Context, queueID uuid.UUID, song repository.Song, mode PickMode) (string, error) {
modeName := string(mode)
err := m.q.SetQueueLastFetched(ctx, repository.SetQueueLastFetchedParams{
Uuid: queueID, LastFetchedSongUuid: song.Uuid, LastFetchedMode: &modeName,
})
if err != nil {
return "", err
}
metrics.SongsRequested.WithLabelValues(modeName).Inc()
return song.Path, nil
}
@@ -212,7 +242,7 @@ func (m *MusicService) RandomSongClassic(ctx context.Context, queueID uuid.UUID)
}
continue
}
return m.remember(ctx, queueID, row.Song)
return m.remember(ctx, queueID, row.Song, PickClassic)
}
return "", ErrNoSongs
}