Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: collect library sizes #165

Merged
merged 13 commits into from
Nov 19, 2023
2 changes: 1 addition & 1 deletion .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ on:
push:
branches:
- master
- metrics
- plexlib
paths-ignore:
- 'assets/**'

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ on:
push:
branches-ignore:
- master
- metrics
- plexlib
pull_request_target:

jobs:
Expand Down
28 changes: 14 additions & 14 deletions cmd/qplex/qplex.go
Original file line number Diff line number Diff line change
Expand Up @@ -269,21 +269,21 @@ func getSessions(cmd *cobra.Command, _ []string) {
return
}

if len(sessions.Metadata) > 0 {
if len(sessions) > 0 {
fmt.Printf("%-10s %-40s %-8s %-10s %-10s %s\n", "USER", "TITLE", "LOCATION", "VIDEO MODE", "STATE", "PROGRESS")
for _, session := range sessions.Metadata {
video := session.TranscodeSession.VideoDecision
if video == "" {
video = "direct"
}
fmt.Printf("%-10s %-40s %-8s %-10s %-10s %.2f%%\n",
session.User.Title,
session.GetTitle(),
session.Session.Location,
video,
session.Player.State,
100.0*session.GetProgress(),
)
}
for _, session := range sessions {
video := session.TranscodeSession.VideoDecision
if video == "" {
video = "direct"
}
fmt.Printf("%-10s %-40s %-8s %-10s %-10s %.2f%%\n",
session.User.Title,
session.GetTitle(),
session.Session.Location,
video,
session.Player.State,
100.0*session.GetProgress(),
)
}
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ go 1.21
require (
github.com/clambin/go-common/httpclient v0.6.0
github.com/clambin/go-common/set v0.3.0
github.com/clambin/mediaclients v0.2.0
github.com/clambin/mediaclients v0.3.0
github.com/prometheus/client_golang v1.17.0
github.com/spf13/cobra v1.8.0
github.com/spf13/viper v1.17.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ github.com/clambin/go-common/httpclient v0.6.0 h1:zSt9OV8ekXB/LwFwazEO4x8z+9p968
github.com/clambin/go-common/httpclient v0.6.0/go.mod h1:77y+6LzAKdenkU+vwQFdNKyxs2HhUXEiqYwzS7Q9FKI=
github.com/clambin/go-common/set v0.3.0 h1:JvLbPbmFEjhqk4wsgqa/swxD0wGOEDXLOjTrsgZLR3o=
github.com/clambin/go-common/set v0.3.0/go.mod h1:45ZNvBGpHWw2vtfgG6pzqEJpTUUNfgUluJ1/jmM0uWI=
github.com/clambin/mediaclients v0.2.0 h1:sFa/NB5Fx3hWTzdB/DY5RBKKZCWPDskX/1lIQNx/HZ8=
github.com/clambin/mediaclients v0.2.0/go.mod h1:742/OB6kzUKwUtgvbviFrtmRXOUUs9KGWnQpDaT81FA=
github.com/clambin/mediaclients v0.3.0 h1:TJS0lN5VPVVjJ/jmtaLcVrDdukHPsfTp7y1dT61xvnc=
github.com/clambin/mediaclients v0.3.0/go.mod h1:TJPhx9pRVM6Vcyx9DxA7BJGM6Ttx6Z8lmHRhUA8oK1c=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
Expand Down
178 changes: 178 additions & 0 deletions internal/collectors/plex/libraries.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
package plex

import (
"context"
"fmt"
"github.com/clambin/mediaclients/plex"
"github.com/prometheus/client_golang/prometheus"
"log/slog"
"sync"
"time"
)

var (
libraryBytesMetric = prometheus.NewDesc(
prometheus.BuildFQName("mediamon", "plex", "library_bytes"),
"Library size in bytes",
[]string{"url", "library"},
nil,
)
libraryCountMetric = prometheus.NewDesc(
prometheus.BuildFQName("mediamon", "plex", "library_count"),
"Library size in number of entries",
[]string{"url", "library"},
nil,
)
)

type libraryCollector struct {
libraryGetter
url string
logger *slog.Logger
cache *map[string][]libraryEntry
age time.Time
lock sync.Mutex
}

type libraryGetter interface {
GetLibraries(ctx context.Context) ([]plex.Library, error)
GetMovies(ctx context.Context, key string) ([]plex.Movie, error)
GetShows(ctx context.Context, key string) ([]plex.Show, error)
GetSeasons(ctx context.Context, key string) ([]plex.Season, error)
GetEpisodes(ctx context.Context, key string) ([]plex.Episode, error)
}

func (c *libraryCollector) Describe(ch chan<- *prometheus.Desc) {
ch <- libraryBytesMetric
ch <- libraryCountMetric
}

func (c *libraryCollector) Collect(ch chan<- prometheus.Metric) {
sizes, err := c.reportLibraries()
if err != nil {
c.logger.Error("failed to collect plex library stats", "err", err)
return
}

for library, entries := range sizes {
ch <- prometheus.MustNewConstMetric(libraryCountMetric, prometheus.GaugeValue, float64(len(entries)), c.url, library)
var size int64
for _, entry := range entries {
size += entry.size
}
ch <- prometheus.MustNewConstMetric(libraryBytesMetric, prometheus.GaugeValue, float64(size), c.url, library)
}
}

type libraryEntry struct {
title string
size int64
}

const libraryRefreshInterval = time.Hour

func (c *libraryCollector) reportLibraries() (map[string][]libraryEntry, error) {
c.lock.Lock()
defer c.lock.Unlock()

if c.cache != nil && time.Since(c.age) < libraryRefreshInterval {
return *c.cache, nil
}

sizes, err := c.getLibraries()
if err == nil {
c.cache = &sizes
c.age = time.Now()
}
return sizes, err
}

func (c *libraryCollector) getLibraries() (map[string][]libraryEntry, error) {
ctx := context.Background()
libraries, err := c.libraryGetter.GetLibraries(ctx)
if err != nil {
return nil, fmt.Errorf("GetLibraries: %w", err)
}

result := make(map[string][]libraryEntry)
var sizes []libraryEntry
for index := range libraries {
switch libraries[index].Type {
case "movie":
sizes, err = c.getMovieTotals(ctx, libraries[index].Key)
case "show":
sizes, err = c.getShowTotals(ctx, libraries[index].Key)
}
if err != nil {
return nil, fmt.Errorf("getTotals (%s): %w", libraries[index].Type, err)
}
result[libraries[index].Title] = sizes
}
return result, nil
}

func (c *libraryCollector) getMovieTotals(ctx context.Context, key string) ([]libraryEntry, error) {
movies, err := c.GetMovies(ctx, key)
if err != nil {
return nil, fmt.Errorf("GetMovies: %w", err)
}
entries := make([]libraryEntry, len(movies))
for index := range movies {
entries[index] = libraryEntry{
title: movies[index].Title,
size: getMediaSize(movies[index].Media),
}
}
return entries, nil
}

func (c *libraryCollector) getShowTotals(ctx context.Context, key string) ([]libraryEntry, error) {
shows, err := c.GetShows(ctx, key)
if err != nil {
return nil, fmt.Errorf("GetShows: %w", err)
}

entries := make([]libraryEntry, 0, len(shows))
for index := range shows {
size, err := c.getShowTotal(ctx, shows[index].RatingKey)
if err != nil {
return nil, fmt.Errorf("getShowTotal: %w", err)
}
if size > 0 {
entries = append(entries, libraryEntry{
title: shows[index].Title,
size: size,
})
}
}
return entries, nil
}

func (c *libraryCollector) getShowTotal(ctx context.Context, key string) (int64, error) {
seasons, err := c.GetSeasons(ctx, key)
if err != nil {
return 0, fmt.Errorf("GetSeasons: %w", err)
}
var size int64
for index := range seasons {
episodes, err := c.GetEpisodes(ctx, seasons[index].RatingKey)
if err != nil {
return 0, fmt.Errorf("GetEpisodes: %w", err)
}
for index2 := range episodes {
size += getMediaSize(episodes[index2].Media)
}
}
return size, nil
}

func getMediaSize(medias []plex.Media) int64 {
for _, media := range medias {
for _, part := range media.Part {
if part.Size > 0 {
return part.Size
}
}
}
return 0
}
Loading
Loading