fix(insights): fix issues and improve reports (#3558)

* fix(insights): show error whn reading library counts

Signed-off-by: Deluan <deluan@navidrome.org>

* fix(insights): wait 30 mins before send first report

Signed-off-by: Deluan <deluan@navidrome.org>

* fix(insights): send number of active players, grouped by client type

Signed-off-by: Deluan <deluan@navidrome.org>

* fix(insights): disable reports when running in dev mode

Signed-off-by: Deluan <deluan@navidrome.org>

* fix(insights): add Dockerfile to the docker build, to avoid `vcs.modified=true`

Signed-off-by: Deluan <deluan@navidrome.org>

* fix(insights): add more linux fs types

Signed-off-by: Deluan <deluan@navidrome.org>

* fix(insights): need admin permissions to retrieve library counts

Signed-off-by: Deluan <deluan@navidrome.org>

* fix(insights): dev flag to disable player insights

Signed-off-by: Deluan <deluan@navidrome.org>

---------

Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
Deluan Quintão 2024-12-18 20:37:35 -05:00 committed by GitHub
parent 4f8cd5307c
commit 6c11649b06
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 103 additions and 29 deletions

View file

@ -14,4 +14,5 @@ tmp
dist
binaries
cache
music
music
!Dockerfile

View file

@ -25,11 +25,11 @@ setup: check_env download-deps setup-git ##@1_Run_First Install dependencies and
.PHONY: setup
dev: check_env ##@Development Start Navidrome in development mode, with hot-reload for both frontend and backend
npx foreman -j Procfile.dev -p 4533 start
ND_ENABLEINSIGHTSCOLLECTOR="false" npx foreman -j Procfile.dev -p 4533 start
.PHONY: dev
server: check_go_env buildjs ##@Development Start the backend in development mode
@go run github.com/cespare/reflex@latest -d none -c reflex.conf
@ND_ENABLEINSIGHTSCOLLECTOR="false" go run github.com/cespare/reflex@latest -d none -c reflex.conf
.PHONY: server
watch: ##@Development Start Go tests in watch mode (re-run when code changes)

View file

@ -208,6 +208,7 @@ func startInsightsCollector(ctx context.Context) func() error {
return nil
}
log.Info(ctx, "Starting Insight Collector")
time.Sleep(conf.Server.DevInsightsInitialDelay)
ic := CreateInsights()
ic.Run(ctx)
return nil

View file

@ -113,6 +113,8 @@ type configOptions struct {
DevArtworkThrottleBacklogTimeout time.Duration
DevArtistInfoTimeToLive time.Duration
DevAlbumInfoTimeToLive time.Duration
DevInsightsInitialDelay time.Duration
DevEnablePlayerInsights bool
}
type scannerOptions struct {
@ -470,6 +472,8 @@ func init() {
viper.SetDefault("devartworkthrottlebacklogtimeout", consts.RequestThrottleBacklogTimeout)
viper.SetDefault("devartistinfotimetolive", consts.ArtistInfoTimeToLive)
viper.SetDefault("devalbuminfotimetolive", consts.AlbumInfoTimeToLive)
viper.SetDefault("devinsightsinitialdelay", consts.InsightsInitialDelay)
viper.SetDefault("devenableplayerinsights", true)
}
func InitConfig(cfgFile string) {

View file

@ -90,6 +90,7 @@ const (
InsightsIDKey = "InsightsID"
InsightsEndpoint = "https://insights.navidrome.org/collect"
InsightsUpdateInterval = 24 * time.Hour
InsightsInitialDelay = 30 * time.Minute
)
var (

View file

@ -16,6 +16,7 @@ import (
"github.com/google/uuid"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/consts"
"github.com/navidrome/navidrome/core/auth"
"github.com/navidrome/navidrome/core/metrics/insights"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
@ -54,6 +55,7 @@ func GetInstance(ds model.DataStore) Insights {
}
func (c *insightsCollector) Run(ctx context.Context) {
ctx = auth.WithAdminUser(ctx, c.ds)
for {
c.sendInsights(ctx)
select {
@ -199,15 +201,45 @@ func (c *insightsCollector) collect(ctx context.Context) []byte {
data.Uptime = time.Since(consts.ServerStart).Milliseconds() / 1000
// Library info
data.Library.Tracks, _ = c.ds.MediaFile(ctx).CountAll()
data.Library.Albums, _ = c.ds.Album(ctx).CountAll()
data.Library.Artists, _ = c.ds.Artist(ctx).CountAll()
data.Library.Playlists, _ = c.ds.Playlist(ctx).Count()
data.Library.Shares, _ = c.ds.Share(ctx).CountAll()
data.Library.Radios, _ = c.ds.Radio(ctx).Count()
data.Library.ActiveUsers, _ = c.ds.User(ctx).CountAll(model.QueryOptions{
var err error
data.Library.Tracks, err = c.ds.MediaFile(ctx).CountAll()
if err != nil {
log.Trace(ctx, "Error reading tracks count", err)
}
data.Library.Albums, err = c.ds.Album(ctx).CountAll()
if err != nil {
log.Trace(ctx, "Error reading albums count", err)
}
data.Library.Artists, err = c.ds.Artist(ctx).CountAll()
if err != nil {
log.Trace(ctx, "Error reading artists count", err)
}
data.Library.Playlists, err = c.ds.Playlist(ctx).CountAll()
if err != nil {
log.Trace(ctx, "Error reading playlists count", err)
}
data.Library.Shares, err = c.ds.Share(ctx).CountAll()
if err != nil {
log.Trace(ctx, "Error reading shares count", err)
}
data.Library.Radios, err = c.ds.Radio(ctx).Count()
if err != nil {
log.Trace(ctx, "Error reading radios count", err)
}
data.Library.ActiveUsers, err = c.ds.User(ctx).CountAll(model.QueryOptions{
Filters: squirrel.Gt{"last_access_at": time.Now().Add(-7 * 24 * time.Hour)},
})
if err != nil {
log.Trace(ctx, "Error reading active users count", err)
}
if conf.Server.DevEnablePlayerInsights {
data.Library.ActivePlayers, err = c.ds.Player(ctx).CountByClient(model.QueryOptions{
Filters: squirrel.Gt{"last_seen": time.Now().Add(-7 * 24 * time.Hour)},
})
if err != nil {
log.Trace(ctx, "Error reading active players count", err)
}
}
// Memory info
var m runtime.MemStats

View file

@ -29,13 +29,14 @@ type Data struct {
Backup *FSInfo `json:"backup,omitempty"`
} `json:"fs"`
Library struct {
Tracks int64 `json:"tracks"`
Albums int64 `json:"albums"`
Artists int64 `json:"artists"`
Playlists int64 `json:"playlists"`
Shares int64 `json:"shares"`
Radios int64 `json:"radios"`
ActiveUsers int64 `json:"activeUsers"`
Tracks int64 `json:"tracks"`
Albums int64 `json:"albums"`
Artists int64 `json:"artists"`
Playlists int64 `json:"playlists"`
Shares int64 `json:"shares"`
Radios int64 `json:"radios"`
ActiveUsers int64 `json:"activeUsers"`
ActivePlayers map[string]int64 `json:"activePlayers"`
} `json:"library"`
Config struct {
LogLevel string `json:"logLevel,omitempty"`

View file

@ -41,22 +41,29 @@ type MountInfo struct {
}
var fsTypeMap = map[int64]string{
// Add filesystem type mappings
0x5346414f: "afs",
0x61756673: "aufs",
0x9123683E: "btrfs",
0x0000EF53: "ext2/ext3/ext4",
0x00006969: "nfs",
0x58465342: "xfs",
0x2FC12FC1: "zfs",
0x01021994: "tmpfs",
0x28cd3d45: "cramfs",
0x64626720: "debugfs",
0x0000EF53: "ext2/ext3/ext4",
0x6a656a63: "fakeowner", // FS inside a container
0x65735546: "fuse",
0x4244: "hfs",
0x9660: "iso9660",
0x3153464a: "jfs",
0x00006969: "nfs",
0x794c7630: "overlayfs",
0x9fa0: "proc",
0x517b: "smb",
0xfe534d42: "smb2",
0x73717368: "squashfs",
0x62656572: "sysfs",
0x9fa0: "proc",
0x61756673: "aufs",
0x794c7630: "overlayfs",
0x6a656a63: "fakeowner", // FS inside a container
// Include other filesystem types as needed
0x01021994: "tmpfs",
0x01021997: "v9fs",
0x4d44: "vfat",
0x58465342: "xfs",
0x2FC12FC1: "zfs",
}
func getFilesystemType(path string) (string, error) {

View file

@ -26,5 +26,7 @@ type PlayerRepository interface {
Get(id string) (*Player, error)
FindMatch(userId, client, userAgent string) (*Player, error)
Put(p *Player) error
CountAll(...QueryOptions) (int64, error)
CountByClient(...QueryOptions) (map[string]int64, error)
// TODO: Add CountAll method. Useful at least for metrics.
}

View file

@ -74,8 +74,33 @@ func (r *playerRepository) addRestriction(sql ...Sqlizer) Sqlizer {
return append(s, Eq{"user_id": u.ID})
}
func (r *playerRepository) CountByClient(options ...model.QueryOptions) (map[string]int64, error) {
sel := r.newSelect(options...).
Columns(
"case when client = 'NavidromeUI' then name else client end as player",
"count(*) as count",
).GroupBy("client")
var res []struct {
Player string
Count int64
}
err := r.queryAll(sel, &res)
if err != nil {
return nil, err
}
counts := make(map[string]int64, len(res))
for _, c := range res {
counts[c.Player] = c.Count
}
return counts, nil
}
func (r *playerRepository) CountAll(options ...model.QueryOptions) (int64, error) {
return r.count(r.newRestSelect(), options...)
}
func (r *playerRepository) Count(options ...rest.QueryOptions) (int64, error) {
return r.count(r.newRestSelect(), r.parseRestOptions(r.ctx, options...))
return r.CountAll(r.parseRestOptions(r.ctx, options...))
}
func (r *playerRepository) Read(id string) (interface{}, error) {