diff --git a/docs/examples/grafana/grafana-dashboard.json b/docs/examples/grafana/grafana-dashboard.json new file mode 100644 index 0000000..287479e --- /dev/null +++ b/docs/examples/grafana/grafana-dashboard.json @@ -0,0 +1,920 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "id": 0, + "links": [], + "liveNow": true, + "panels": [ + { + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 18, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "
\n
\n \"Soulsolid\n
\n
\n

Soulsolid Monitoring Dashboard

\n

Monitoring library metrics and job processing health for your music library.

\n
\n
\n Repository: github.com/contre95/soulsolid
\n Version: ${IMAGE_TAG:-dev} (set via IMAGE_TAG environment variable)
\n Metrics Updated: When `calculate_metrics` job runs\n
\n
\n
\n
\n
\n
", + "mode": "markdown" + }, + "pluginVersion": "12.3.3", + "title": "", + "transparent": true, + "type": "text" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "fieldMinMax": true, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue", + "value": 0 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 12, + "y": 0 + }, + "id": 1, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "center", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "last" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "text": {}, + "textMode": "value_and_name", + "wideLayout": true + }, + "pluginVersion": "12.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "soulsolid_library_tracks_total", + "legendFormat": "Tracks", + "range": true, + "refId": "A" + } + ], + "title": "", + "transparent": true, + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "fixed" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "fieldMinMax": false, + "mappings": [] + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "flac" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "orange", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "mp3" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "blue", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 4, + "x": 16, + "y": 0 + }, + "id": 13, + "options": { + "displayLabels": [ + "percent" + ], + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "pieType": "pie", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "sort": "desc", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "desc" + } + }, + "pluginVersion": "12.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "builder", + "expr": "soulsolid_library_format_count", + "legendFormat": "{{format}}", + "range": true, + "refId": "A" + } + ], + "title": "Audio Formats", + "transparent": true, + "type": "piechart" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "fixed" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "mappings": [] + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "No lyrics" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#bebebe", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Lyrics" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#f53399", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 4, + "x": 20, + "y": 0 + }, + "id": 19, + "options": { + "displayLabels": [ + "percent" + ], + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "pieType": "pie", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "sort": "desc", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "soulsolid_library_lyrics_missing", + "legendFormat": "No lyrics", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "soulsolid_library_lyrics_present", + "hide": false, + "legendFormat": "Lyrics", + "range": true, + "refId": "B" + } + ], + "title": "Lyrics Presence", + "transparent": true, + "type": "piechart" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "fieldMinMax": true, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "light-blue", + "value": 0 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 4, + "x": 12, + "y": 4 + }, + "id": 21, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "center", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "builder", + "expr": "soulsolid_library_albums_total", + "legendFormat": "Albums", + "range": true, + "refId": "A" + } + ], + "title": "", + "transparent": true, + "type": "stat" + }, + { + "datasource": { + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 12, + "x": 0, + "y": 5 + }, + "id": 23, + "options": { + "colorMode": "none", + "graphMode": "none", + "justifyMode": "center", + "orientation": "horizontal", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "name", + "wideLayout": true + }, + "pluginVersion": "12.3.3", + "targets": [ + { + "editorMode": "builder", + "expr": "soulsolid_build_info", + "legendFormat": "{{version}}", + "range": true, + "refId": "A" + } + ], + "title": "", + "transparent": true, + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "fieldMinMax": true, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "light-blue", + "value": 0 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 4, + "x": 12, + "y": 7 + }, + "id": 22, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "center", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": true, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "builder", + "expr": "soulsolid_library_artists_total", + "legendFormat": "Artists", + "range": true, + "refId": "A" + } + ], + "title": "", + "transparent": true, + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisGridShow": true, + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "log": 2, + "type": "log" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Artist" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "super-light-blue", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Tracks" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "blue", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Album" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "light-blue", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 16, + "x": 0, + "y": 9 + }, + "id": 6, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "builder", + "expr": "soulsolid_library_tracks_total", + "hide": false, + "legendFormat": "Tracks", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "builder", + "expr": "soulsolid_library_artists_total", + "hide": false, + "legendFormat": "Artist", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "builder", + "expr": "soulsolid_library_albums_total", + "hide": false, + "legendFormat": "Album", + "range": true, + "refId": "C" + } + ], + "title": "", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Missing Lyrics" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#d8a4c0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Complete" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#88d590", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Missing Year" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "yellow", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Missing Genre" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "red", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 16, + "y": 9 + }, + "id": 9, + "options": { + "displayMode": "gradient", + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "maxVizHeight": 300, + "minVizHeight": 7, + "minVizWidth": 8, + "namePlacement": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showUnfilled": true, + "sizing": "auto", + "valueMode": "color" + }, + "pluginVersion": "12.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "builder", + "expr": "irate(soulsolid_library_metadata_complete[$__interval])", + "legendFormat": "Complete", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "builder", + "expr": "soulsolid_library_metadata_missing_genre", + "legendFormat": "Missing Genre", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "builder", + "expr": "soulsolid_library_metadata_missing_year", + "legendFormat": "Missing Year", + "range": true, + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "builder", + "expr": "soulsolid_library_metadata_missing_lyrics", + "legendFormat": "Missing Lyrics", + "range": true, + "refId": "D" + } + ], + "title": "Metadata completeness", + "transparent": true, + "type": "bargauge" + } + ], + "preload": true, + "refresh": "30s", + "schemaVersion": 42, + "tags": [ + "soulsolid", + "music", + "library", + "monitoring" + ], + "templating": { + "list": [ + { + "current": { + "text": "PBFA97CFB590B2093", + "value": "PBFA97CFB590B2093" + }, + "description": "", + "hide": 2, + "label": "DS_PROMETHEUS", + "name": "DS_PROMETHEUS", + "query": "PBFA97CFB590B2093", + "skipUrlSync": true, + "type": "constant" + } + ] + }, + "time": { + "from": "now-15m", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Soulsolid", + "uid": "016dba3a-1286-409b-997e-1aa27fb7c14a", + "version": 30, + "weekStart": "monday" +} diff --git a/go.mod b/go.mod index f4ebb7d..67a1883 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/gosimple/unidecode v1.0.1 github.com/mattn/go-sqlite3 v1.14.32 github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 + github.com/prometheus/client_golang v1.23.2 golang.org/x/image v0.32.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -25,6 +26,8 @@ require ( require ( github.com/andybalholm/brotli v1.1.0 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect github.com/charmbracelet/lipgloss v1.1.0 // indirect github.com/charmbracelet/x/ansi v0.8.0 // indirect @@ -36,20 +39,27 @@ require ( github.com/go-playground/universal-translator v0.18.1 // indirect github.com/gofiber/template v1.8.3 // indirect github.com/gofiber/utils v1.1.0 // indirect - github.com/klauspost/compress v1.17.9 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/kr/text v0.2.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/muesli/termenv v0.16.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/procfs v0.16.1 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.51.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect golang.org/x/crypto v0.42.0 // indirect golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect golang.org/x/sys v0.36.0 // indirect golang.org/x/text v0.30.0 // indirect + google.golang.org/protobuf v1.36.8 // indirect ) diff --git a/go.sum b/go.sum index 8f012d5..b2483bb 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,12 @@ github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1 github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bogem/id3v2/v2 v2.1.4 h1:CEwe+lS2p6dd9UZRlPc1zbFNIha2mb2qzT1cCEoNWoI= github.com/bogem/id3v2/v2 v2.1.4/go.mod h1:l+gR8MZ6rc9ryPTPkX77smS5Me/36gxkMgDayZ9G1vY= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= @@ -16,6 +20,7 @@ github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0G github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8 h1:OtSeLS5y0Uy01jaKK4mA/WVIYtpzVm63vLVAPzJXigg= @@ -50,12 +55,20 @@ github.com/gofiber/template/html/v2 v2.1.3 h1:n1LYBtmr9C0V/k/3qBblXyMxV5B0o/gpb6 github.com/gofiber/template/html/v2 v2.1.3/go.mod h1:U5Fxgc5KpyujU9OqKzy6Kn6Qup6Tm7zdsISR+VpnHRE= github.com/gofiber/utils v1.1.0 h1:vdEBpn7AzIUJRhe+CiTOJdUcTg4Q9RK+pEa0KPbLdrM= github.com/gofiber/utils v1.1.0/go.mod h1:poZpsnhBykfnY1Mc0KeEa6mSHrS3dV0+oBWyeQmb2e0= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o= github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc= -github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= -github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= @@ -71,15 +84,27 @@ github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuE github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= @@ -89,6 +114,10 @@ github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7Fw github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= @@ -124,7 +153,10 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= +google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/server.log b/server.log new file mode 100644 index 0000000..74ad4b4 --- /dev/null +++ b/server.log @@ -0,0 +1,8 @@ +2026/02/16 00:24:02 INFO Required directories created/verified library=./music downloads=./downloads +12:24AM INFO Soulsolid: Logger initialized +12:24AM INFO Soulsolid: Loading downloader plugins +12:24AM INFO Soulsolid: Building plugin from git repository name=deezer url=https://git.sr.ht/~canuslector/soulsolid-deemix-plugin +12:24AM WARN Soulsolid: Failed to check user options error="ARL token is invalid or expired (no user ID returned)" +12:24AM INFO Soulsolid: Successfully loaded plugin name=deezer downloader_name=Deezer total_downloaders=1 +12:24AM INFO Soulsolid: Plugin loading completed total_downloaders=1 +12:24AM INFO Soulsolid: Starting server port=3536 diff --git a/soulsolid-test b/soulsolid-test new file mode 100755 index 0000000..361f0f7 Binary files /dev/null and b/soulsolid-test differ diff --git a/src/features/config/config.go b/src/features/config/config.go index a99240d..9e5eeb4 100644 --- a/src/features/config/config.go +++ b/src/features/config/config.go @@ -51,8 +51,9 @@ type Database struct { // Server hold the configuration for the Fiber server Config type Server struct { - PrintRoutes bool `yaml:"show_routes"` - Port uint32 `yaml:"port"` + PrintRoutes bool `yaml:"show_routes"` + Port uint32 `yaml:"port"` + PrometheusEnabled bool `yaml:"prometheus_enabled"` } // Logger holds the configuration for the app logging diff --git a/src/features/config/default.go b/src/features/config/default.go index ea41116..ef4b27c 100644 --- a/src/features/config/default.go +++ b/src/features/config/default.go @@ -26,8 +26,9 @@ var defaultConfig = Config{ }, }, Server: Server{ - PrintRoutes: false, - Port: 3535, + PrintRoutes: false, + Port: 3535, + PrometheusEnabled: false, }, Database: Database{ Path: "./library.db", diff --git a/src/features/hosting/server.go b/src/features/hosting/server.go index 6e2bcab..febfe84 100644 --- a/src/features/hosting/server.go +++ b/src/features/hosting/server.go @@ -20,7 +20,10 @@ import ( "github.com/contre95/soulsolid/src/features/ui" "github.com/contre95/soulsolid/src/music" "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/adaptor" "github.com/gofiber/template/html/v2" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" ) // Server is the HTTP server for the application. @@ -140,6 +143,12 @@ func NewServer(cfg *config.Manager, importingService *importing.Service, library lyrics.RegisterRoutes(app, lyricsHandler) analyze.RegisterRoutes(app, analyzeHandler) + if cfg.Get().Server.PrometheusEnabled { + collector := metrics.NewPrometheusCollector(metricsService.GetLibraryMetrics(), jobService) + prometheus.MustRegister(collector) + app.Get("/metrics", adaptor.HTTPHandler(promhttp.Handler())) + } + return &Server{app: app, port: cfg.Get().Server.Port} } diff --git a/src/features/metrics/prometheus_collector.go b/src/features/metrics/prometheus_collector.go new file mode 100644 index 0000000..f13a2fa --- /dev/null +++ b/src/features/metrics/prometheus_collector.go @@ -0,0 +1,280 @@ +package metrics + +import ( + "context" + "log/slog" + "os" + + "github.com/contre95/soulsolid/src/features/jobs" + "github.com/prometheus/client_golang/prometheus" +) + +// PrometheusCollector implements prometheus.Collector for soulsolid metrics. +type PrometheusCollector struct { + libraryMetrics LibraryMetrics + jobService jobs.JobService + + // Library metrics + tracksTotal prometheus.Gauge + artistsTotal prometheus.Gauge + albumsTotal prometheus.Gauge + genreCount *prometheus.GaugeVec + formatCount *prometheus.GaugeVec + yearCount *prometheus.GaugeVec + metadataComplete prometheus.Gauge + metadataMissingGenre prometheus.Gauge + metadataMissingYear prometheus.Gauge + metadataMissingLyrics prometheus.Gauge + lyricsPresent prometheus.Gauge + lyricsMissing prometheus.Gauge + + // Job metrics + jobsTotal *prometheus.GaugeVec + + // Build info + buildInfo *prometheus.GaugeVec +} + +// NewPrometheusCollector creates a new PrometheusCollector. +func NewPrometheusCollector(libraryMetrics LibraryMetrics, jobService jobs.JobService) *PrometheusCollector { + const namespace = "soulsolid" + + return &PrometheusCollector{ + libraryMetrics: libraryMetrics, + jobService: jobService, + + tracksTotal: prometheus.NewGauge(prometheus.GaugeOpts{ + Namespace: namespace, + Name: "library_tracks_total", + Help: "Total number of tracks in library", + }), + artistsTotal: prometheus.NewGauge(prometheus.GaugeOpts{ + Namespace: namespace, + Name: "library_artists_total", + Help: "Total number of artists in library", + }), + albumsTotal: prometheus.NewGauge(prometheus.GaugeOpts{ + Namespace: namespace, + Name: "library_albums_total", + Help: "Total number of albums in library", + }), + genreCount: prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: namespace, + Name: "library_genre_count", + Help: "Number of tracks per genre", + }, []string{"genre"}), + formatCount: prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: namespace, + Name: "library_format_count", + Help: "Number of tracks per audio format", + }, []string{"format"}), + yearCount: prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: namespace, + Name: "library_year_count", + Help: "Number of tracks per release year", + }, []string{"year"}), + metadataComplete: prometheus.NewGauge(prometheus.GaugeOpts{ + Namespace: namespace, + Name: "library_metadata_complete", + Help: "Number of tracks with complete metadata (title, artist, album, genre, year)", + }), + metadataMissingGenre: prometheus.NewGauge(prometheus.GaugeOpts{ + Namespace: namespace, + Name: "library_metadata_missing_genre", + Help: "Number of tracks missing genre", + }), + metadataMissingYear: prometheus.NewGauge(prometheus.GaugeOpts{ + Namespace: namespace, + Name: "library_metadata_missing_year", + Help: "Number of tracks missing year", + }), + metadataMissingLyrics: prometheus.NewGauge(prometheus.GaugeOpts{ + Namespace: namespace, + Name: "library_metadata_missing_lyrics", + Help: "Number of tracks missing lyrics", + }), + lyricsPresent: prometheus.NewGauge(prometheus.GaugeOpts{ + Namespace: namespace, + Name: "library_lyrics_present", + Help: "Number of tracks with lyrics", + }), + lyricsMissing: prometheus.NewGauge(prometheus.GaugeOpts{ + Namespace: namespace, + Name: "library_lyrics_missing", + Help: "Number of tracks without lyrics", + }), + jobsTotal: prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: namespace, + Name: "jobs_total", + Help: "Number of jobs by status", + }, []string{"status"}), + buildInfo: prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: namespace, + Name: "build_info", + Help: "Build information about the Soulsolid application", + }, []string{"version", "commit", "branch"}), + } +} + +// Describe sends all metric descriptors to the channel. +func (c *PrometheusCollector) Describe(ch chan<- *prometheus.Desc) { + c.tracksTotal.Describe(ch) + c.artistsTotal.Describe(ch) + c.albumsTotal.Describe(ch) + c.genreCount.Describe(ch) + c.formatCount.Describe(ch) + c.yearCount.Describe(ch) + c.metadataComplete.Describe(ch) + c.metadataMissingGenre.Describe(ch) + c.metadataMissingYear.Describe(ch) + c.metadataMissingLyrics.Describe(ch) + c.lyricsPresent.Describe(ch) + c.lyricsMissing.Describe(ch) + c.jobsTotal.Describe(ch) + c.buildInfo.Describe(ch) +} + +// Collect fetches metrics from the library and job service and sends them to the channel. +func (c *PrometheusCollector) Collect(ch chan<- prometheus.Metric) { + ctx := context.Background() + + // Collect library metrics + if totalTracks, err := c.libraryMetrics.GetTotalTracks(ctx); err != nil { + slog.Warn("Failed to get track count for Prometheus", "error", err) + } else { + c.tracksTotal.Set(float64(totalTracks)) + } + + if totalArtists, err := c.libraryMetrics.GetTotalArtists(ctx); err != nil { + slog.Warn("Failed to get artist count for Prometheus", "error", err) + } else { + c.artistsTotal.Set(float64(totalArtists)) + } + + if totalAlbums, err := c.libraryMetrics.GetTotalAlbums(ctx); err != nil { + slog.Warn("Failed to get album count for Prometheus", "error", err) + } else { + c.albumsTotal.Set(float64(totalAlbums)) + } + + // Collect genre distribution + if genreMetrics, err := c.libraryMetrics.GetStoredMetrics(ctx, "genre_counts"); err != nil { + slog.Warn("Failed to get genre metrics for Prometheus", "error", err) + } else { + c.genreCount.Reset() + for _, metric := range genreMetrics { + c.genreCount.WithLabelValues(metric.Key).Set(float64(metric.Value)) + } + } + + // Collect format distribution + if formatMetrics, err := c.libraryMetrics.GetStoredMetrics(ctx, "format_distribution"); err != nil { + slog.Warn("Failed to get format metrics for Prometheus", "error", err) + } else { + c.formatCount.Reset() + for _, metric := range formatMetrics { + c.formatCount.WithLabelValues(metric.Key).Set(float64(metric.Value)) + } + } + + // Collect year distribution + if yearMetrics, err := c.libraryMetrics.GetStoredMetrics(ctx, "year_distribution"); err != nil { + slog.Warn("Failed to get year metrics for Prometheus", "error", err) + } else { + c.yearCount.Reset() + for _, metric := range yearMetrics { + c.yearCount.WithLabelValues(metric.Key).Set(float64(metric.Value)) + } + } + + // Collect metadata completeness + if metadataMetrics, err := c.libraryMetrics.GetStoredMetrics(ctx, "metadata_completeness"); err != nil { + slog.Warn("Failed to get metadata completeness metrics for Prometheus", "error", err) + } else { + // Reset individual gauges + c.metadataComplete.Set(0) + c.metadataMissingGenre.Set(0) + c.metadataMissingYear.Set(0) + c.metadataMissingLyrics.Set(0) + + for _, metric := range metadataMetrics { + switch metric.Key { + case "complete": + c.metadataComplete.Set(float64(metric.Value)) + case "missing_genre": + c.metadataMissingGenre.Set(float64(metric.Value)) + case "missing_year": + c.metadataMissingYear.Set(float64(metric.Value)) + case "missing_lyrics": + c.metadataMissingLyrics.Set(float64(metric.Value)) + } + } + } + + // Collect lyrics stats + if lyricsMetrics, err := c.libraryMetrics.GetStoredMetrics(ctx, "lyrics_stats"); err != nil { + slog.Warn("Failed to get lyrics metrics for Prometheus", "error", err) + } else { + c.lyricsPresent.Set(0) + c.lyricsMissing.Set(0) + + for _, metric := range lyricsMetrics { + switch metric.Key { + case "has_lyrics": + c.lyricsPresent.Set(float64(metric.Value)) + case "no_lyrics": + c.lyricsMissing.Set(float64(metric.Value)) + } + } + } + + // Collect job counts + jobCounts := map[string]int{ + "pending": 0, + "running": 0, + "completed": 0, + "failed": 0, + "cancelled": 0, + } + + for _, job := range c.jobService.GetJobs() { + jobCounts[string(job.Status)]++ + } + + c.jobsTotal.Reset() + for status, count := range jobCounts { + c.jobsTotal.WithLabelValues(status).Set(float64(count)) + } + + // Set build info + c.buildInfo.Reset() + version := os.Getenv("IMAGE_TAG") + if version == "" { + version = "dev" + } + commit := os.Getenv("GIT_COMMIT") + if commit == "" { + commit = "unknown" + } + branch := os.Getenv("GIT_BRANCH") + if branch == "" { + branch = "unknown" + } + c.buildInfo.WithLabelValues(version, commit, branch).Set(1) + + // Send metrics + c.tracksTotal.Collect(ch) + c.artistsTotal.Collect(ch) + c.albumsTotal.Collect(ch) + c.genreCount.Collect(ch) + c.formatCount.Collect(ch) + c.yearCount.Collect(ch) + c.metadataComplete.Collect(ch) + c.metadataMissingGenre.Collect(ch) + c.metadataMissingYear.Collect(ch) + c.metadataMissingLyrics.Collect(ch) + c.lyricsPresent.Collect(ch) + c.lyricsMissing.Collect(ch) + c.jobsTotal.Collect(ch) + c.buildInfo.Collect(ch) +} diff --git a/src/features/metrics/service.go b/src/features/metrics/service.go index 9fa5776..13de46c 100644 --- a/src/features/metrics/service.go +++ b/src/features/metrics/service.go @@ -21,6 +21,11 @@ func NewService(metrics LibraryMetrics, cfgManager *config.Manager) *Service { } } +// GetLibraryMetrics returns the underlying LibraryMetrics implementation. +func (s *Service) GetLibraryMetrics() LibraryMetrics { + return s.metrics +} + // Metric represents a single metric data point. type Metric struct { Type string `json:"type"`