mirror of
https://github.com/navidrome/navidrome.git
synced 2025-04-01 19:47:37 +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
|
dist
|
||||||
binaries
|
binaries
|
||||||
cache
|
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
|
.PHONY: setup
|
||||||
|
|
||||||
dev: check_env ##@Development Start Navidrome in development mode, with hot-reload for both frontend and backend
|
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
|
.PHONY: dev
|
||||||
|
|
||||||
server: check_go_env buildjs ##@Development Start the backend in development mode
|
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
|
.PHONY: server
|
||||||
|
|
||||||
watch: ##@Development Start Go tests in watch mode (re-run when code changes)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
log.Info(ctx, "Starting Insight Collector")
|
log.Info(ctx, "Starting Insight Collector")
|
||||||
|
time.Sleep(conf.Server.DevInsightsInitialDelay)
|
||||||
ic := CreateInsights()
|
ic := CreateInsights()
|
||||||
ic.Run(ctx)
|
ic.Run(ctx)
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -113,6 +113,8 @@ type configOptions struct {
|
||||||
DevArtworkThrottleBacklogTimeout time.Duration
|
DevArtworkThrottleBacklogTimeout time.Duration
|
||||||
DevArtistInfoTimeToLive time.Duration
|
DevArtistInfoTimeToLive time.Duration
|
||||||
DevAlbumInfoTimeToLive time.Duration
|
DevAlbumInfoTimeToLive time.Duration
|
||||||
|
DevInsightsInitialDelay time.Duration
|
||||||
|
DevEnablePlayerInsights bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type scannerOptions struct {
|
type scannerOptions struct {
|
||||||
|
@ -470,6 +472,8 @@ func init() {
|
||||||
viper.SetDefault("devartworkthrottlebacklogtimeout", consts.RequestThrottleBacklogTimeout)
|
viper.SetDefault("devartworkthrottlebacklogtimeout", consts.RequestThrottleBacklogTimeout)
|
||||||
viper.SetDefault("devartistinfotimetolive", consts.ArtistInfoTimeToLive)
|
viper.SetDefault("devartistinfotimetolive", consts.ArtistInfoTimeToLive)
|
||||||
viper.SetDefault("devalbuminfotimetolive", consts.AlbumInfoTimeToLive)
|
viper.SetDefault("devalbuminfotimetolive", consts.AlbumInfoTimeToLive)
|
||||||
|
viper.SetDefault("devinsightsinitialdelay", consts.InsightsInitialDelay)
|
||||||
|
viper.SetDefault("devenableplayerinsights", true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func InitConfig(cfgFile string) {
|
func InitConfig(cfgFile string) {
|
||||||
|
|
|
@ -90,6 +90,7 @@ const (
|
||||||
InsightsIDKey = "InsightsID"
|
InsightsIDKey = "InsightsID"
|
||||||
InsightsEndpoint = "https://insights.navidrome.org/collect"
|
InsightsEndpoint = "https://insights.navidrome.org/collect"
|
||||||
InsightsUpdateInterval = 24 * time.Hour
|
InsightsUpdateInterval = 24 * time.Hour
|
||||||
|
InsightsInitialDelay = 30 * time.Minute
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
|
@ -16,6 +16,7 @@ import (
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/navidrome/navidrome/conf"
|
"github.com/navidrome/navidrome/conf"
|
||||||
"github.com/navidrome/navidrome/consts"
|
"github.com/navidrome/navidrome/consts"
|
||||||
|
"github.com/navidrome/navidrome/core/auth"
|
||||||
"github.com/navidrome/navidrome/core/metrics/insights"
|
"github.com/navidrome/navidrome/core/metrics/insights"
|
||||||
"github.com/navidrome/navidrome/log"
|
"github.com/navidrome/navidrome/log"
|
||||||
"github.com/navidrome/navidrome/model"
|
"github.com/navidrome/navidrome/model"
|
||||||
|
@ -54,6 +55,7 @@ func GetInstance(ds model.DataStore) Insights {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *insightsCollector) Run(ctx context.Context) {
|
func (c *insightsCollector) Run(ctx context.Context) {
|
||||||
|
ctx = auth.WithAdminUser(ctx, c.ds)
|
||||||
for {
|
for {
|
||||||
c.sendInsights(ctx)
|
c.sendInsights(ctx)
|
||||||
select {
|
select {
|
||||||
|
@ -199,15 +201,45 @@ func (c *insightsCollector) collect(ctx context.Context) []byte {
|
||||||
data.Uptime = time.Since(consts.ServerStart).Milliseconds() / 1000
|
data.Uptime = time.Since(consts.ServerStart).Milliseconds() / 1000
|
||||||
|
|
||||||
// Library info
|
// Library info
|
||||||
data.Library.Tracks, _ = c.ds.MediaFile(ctx).CountAll()
|
var err error
|
||||||
data.Library.Albums, _ = c.ds.Album(ctx).CountAll()
|
data.Library.Tracks, err = c.ds.MediaFile(ctx).CountAll()
|
||||||
data.Library.Artists, _ = c.ds.Artist(ctx).CountAll()
|
if err != nil {
|
||||||
data.Library.Playlists, _ = c.ds.Playlist(ctx).Count()
|
log.Trace(ctx, "Error reading tracks count", err)
|
||||||
data.Library.Shares, _ = c.ds.Share(ctx).CountAll()
|
}
|
||||||
data.Library.Radios, _ = c.ds.Radio(ctx).Count()
|
data.Library.Albums, err = c.ds.Album(ctx).CountAll()
|
||||||
data.Library.ActiveUsers, _ = c.ds.User(ctx).CountAll(model.QueryOptions{
|
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)},
|
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
|
// Memory info
|
||||||
var m runtime.MemStats
|
var m runtime.MemStats
|
||||||
|
|
|
@ -29,13 +29,14 @@ type Data struct {
|
||||||
Backup *FSInfo `json:"backup,omitempty"`
|
Backup *FSInfo `json:"backup,omitempty"`
|
||||||
} `json:"fs"`
|
} `json:"fs"`
|
||||||
Library struct {
|
Library struct {
|
||||||
Tracks int64 `json:"tracks"`
|
Tracks int64 `json:"tracks"`
|
||||||
Albums int64 `json:"albums"`
|
Albums int64 `json:"albums"`
|
||||||
Artists int64 `json:"artists"`
|
Artists int64 `json:"artists"`
|
||||||
Playlists int64 `json:"playlists"`
|
Playlists int64 `json:"playlists"`
|
||||||
Shares int64 `json:"shares"`
|
Shares int64 `json:"shares"`
|
||||||
Radios int64 `json:"radios"`
|
Radios int64 `json:"radios"`
|
||||||
ActiveUsers int64 `json:"activeUsers"`
|
ActiveUsers int64 `json:"activeUsers"`
|
||||||
|
ActivePlayers map[string]int64 `json:"activePlayers"`
|
||||||
} `json:"library"`
|
} `json:"library"`
|
||||||
Config struct {
|
Config struct {
|
||||||
LogLevel string `json:"logLevel,omitempty"`
|
LogLevel string `json:"logLevel,omitempty"`
|
||||||
|
|
|
@ -41,22 +41,29 @@ type MountInfo struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
var fsTypeMap = map[int64]string{
|
var fsTypeMap = map[int64]string{
|
||||||
// Add filesystem type mappings
|
0x5346414f: "afs",
|
||||||
|
0x61756673: "aufs",
|
||||||
0x9123683E: "btrfs",
|
0x9123683E: "btrfs",
|
||||||
0x0000EF53: "ext2/ext3/ext4",
|
|
||||||
0x00006969: "nfs",
|
|
||||||
0x58465342: "xfs",
|
|
||||||
0x2FC12FC1: "zfs",
|
|
||||||
0x01021994: "tmpfs",
|
|
||||||
0x28cd3d45: "cramfs",
|
0x28cd3d45: "cramfs",
|
||||||
0x64626720: "debugfs",
|
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",
|
0x73717368: "squashfs",
|
||||||
0x62656572: "sysfs",
|
0x62656572: "sysfs",
|
||||||
0x9fa0: "proc",
|
0x01021994: "tmpfs",
|
||||||
0x61756673: "aufs",
|
0x01021997: "v9fs",
|
||||||
0x794c7630: "overlayfs",
|
0x4d44: "vfat",
|
||||||
0x6a656a63: "fakeowner", // FS inside a container
|
0x58465342: "xfs",
|
||||||
// Include other filesystem types as needed
|
0x2FC12FC1: "zfs",
|
||||||
}
|
}
|
||||||
|
|
||||||
func getFilesystemType(path string) (string, error) {
|
func getFilesystemType(path string) (string, error) {
|
||||||
|
|
|
@ -26,5 +26,7 @@ type PlayerRepository interface {
|
||||||
Get(id string) (*Player, error)
|
Get(id string) (*Player, error)
|
||||||
FindMatch(userId, client, userAgent string) (*Player, error)
|
FindMatch(userId, client, userAgent string) (*Player, error)
|
||||||
Put(p *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.
|
// 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})
|
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) {
|
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) {
|
func (r *playerRepository) Read(id string) (interface{}, error) {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue