mirror of
https://github.com/navidrome/navidrome.git
synced 2025-03-31 11:07:36 +03:00
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:
parent
4f8cd5307c
commit
6c11649b06
10 changed files with 103 additions and 29 deletions
|
@ -14,4 +14,5 @@ tmp
|
|||
dist
|
||||
binaries
|
||||
cache
|
||||
music
|
||||
music
|
||||
!Dockerfile
|
4
Makefile
4
Makefile
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -90,6 +90,7 @@ const (
|
|||
InsightsIDKey = "InsightsID"
|
||||
InsightsEndpoint = "https://insights.navidrome.org/collect"
|
||||
InsightsUpdateInterval = 24 * time.Hour
|
||||
InsightsInitialDelay = 30 * time.Minute
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"`
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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.
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue