From 367f6031dfefeba87b88be55f5780267cb143975 Mon Sep 17 00:00:00 2001 From: ShawnHardwick Date: Tue, 12 Aug 2025 00:14:47 -0400 Subject: [PATCH] Track total count of episodes with cutoff unmet in Sonarr (#375) * Add sonarr_episode_cutoff_unmet_total metric * Fix sonarr test fixture by adding missing json file * Add radarr_movie_cutoff_unmet_total metric * Remove compiled file * Fix expected_metrics.txt for radarr using wrong HELP string * Add missing end-of-line to test ficture JSON files * Fix redeclare of CutoffUnmet type * Fix EOL for radarr model * Fix test fixture data for sonarr and radarr to match expectations --- internal/arr/collector/radarr.go | 20 +++++++++++++++++++ internal/arr/collector/sonarr.go | 19 ++++++++++++++++++ internal/arr/model/radarr.go | 6 ++++++ internal/arr/model/sonarr.go | 6 ++++++ .../test_fixtures/radarr/expected_metrics.txt | 3 +++ .../radarr/v3_wanted_cutoff.json | 3 +++ .../test_fixtures/sonarr/expected_metrics.txt | 3 +++ .../sonarr/expected_metrics_extended.txt | 3 +++ .../sonarr/v3_wanted_cutoff.json | 3 +++ .../sonarr/v3_wanted_missing.json | 2 +- 10 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 internal/arr/test_fixtures/radarr/v3_wanted_cutoff.json create mode 100644 internal/arr/test_fixtures/sonarr/v3_wanted_cutoff.json diff --git a/internal/arr/collector/radarr.go b/internal/arr/collector/radarr.go index 051d0ad..914c506 100644 --- a/internal/arr/collector/radarr.go +++ b/internal/arr/collector/radarr.go @@ -19,6 +19,7 @@ type radarrCollector struct { movieUnmonitoredMetric *prometheus.Desc // Total number of unmonitored movies movieWantedMetric *prometheus.Desc // Total number of wanted movies movieMissingMetric *prometheus.Desc // Total number of missing movies + movieCutoffUnmetMetric *prometheus.Desc // Total number of movies with cutoff unmet movieQualitiesMetric *prometheus.Desc // Total number of movies by quality movieFileSizeMetric *prometheus.Desc // Total fizesize of all movies in bytes errorMetric *prometheus.Desc // Error Description for use with InvalidMetric @@ -70,6 +71,12 @@ func NewRadarrCollector(c *config.ArrConfig) *radarrCollector { nil, prometheus.Labels{"url": c.URL}, ), + movieCutoffUnmetMetric: prometheus.NewDesc( + "radarr_movie_cutoff_unmet_total", + "Total number of movies with cutoff unmet", + nil, + prometheus.Labels{"url": c.URL}, + ), movieFileSizeMetric: prometheus.NewDesc( "radarr_movie_filesize_total", "Total filesize of all movies", @@ -105,6 +112,7 @@ func (collector *radarrCollector) Describe(ch chan<- *prometheus.Desc) { ch <- collector.movieUnmonitoredMetric ch <- collector.movieWantedMetric ch <- collector.movieMissingMetric + ch <- collector.movieCutoffUnmetMetric ch <- collector.movieFileSizeMetric ch <- collector.movieQualitiesMetric ch <- collector.movieTagsMetric @@ -202,6 +210,17 @@ func (collector *radarrCollector) Collect(ch chan<- prometheus.Metric) { } } + moviesCutoffUnmet := model.CutoffUnmetMovies{} + moviesCutoffUnmetParams := client.QueryParams{} + params.Add("sortKey", "airDateUtc") + + if err := c.DoRequest("wanted/cutoff", &moviesCutoffUnmet, moviesCutoffUnmetParams); err != nil { + log.Errorw("Error getting cutoff unmet", + "error", err) + ch <- prometheus.NewInvalidMetric(collector.errorMetric, err) + return + } + ch <- prometheus.MustNewConstMetric(collector.movieEdition, prometheus.GaugeValue, float64(editions)) ch <- prometheus.MustNewConstMetric(collector.movieMetric, prometheus.GaugeValue, float64(len(movies))) ch <- prometheus.MustNewConstMetric(collector.movieDownloadedMetric, prometheus.GaugeValue, float64(downloaded)) @@ -209,6 +228,7 @@ func (collector *radarrCollector) Collect(ch chan<- prometheus.Metric) { ch <- prometheus.MustNewConstMetric(collector.movieUnmonitoredMetric, prometheus.GaugeValue, float64(unmonitored)) ch <- prometheus.MustNewConstMetric(collector.movieWantedMetric, prometheus.GaugeValue, float64(wanted)) ch <- prometheus.MustNewConstMetric(collector.movieMissingMetric, prometheus.GaugeValue, float64(missing)) + ch <- prometheus.MustNewConstMetric(collector.movieCutoffUnmetMetric, prometheus.GaugeValue, float64(moviesCutoffUnmet.TotalRecords)) ch <- prometheus.MustNewConstMetric(collector.movieFileSizeMetric, prometheus.GaugeValue, float64(fileSize)) if len(qualities) > 0 { diff --git a/internal/arr/collector/sonarr.go b/internal/arr/collector/sonarr.go index 258980c..0e38102 100644 --- a/internal/arr/collector/sonarr.go +++ b/internal/arr/collector/sonarr.go @@ -28,6 +28,7 @@ type sonarrCollector struct { episodeUnmonitoredMetric *prometheus.Desc // Total number of unmonitored episodes episodeDownloadedMetric *prometheus.Desc // Total number of downloaded episodes episodeMissingMetric *prometheus.Desc // Total number of missing episodes + episodeCutoffUnmetMetric *prometheus.Desc // Total number of episodes with cutoff unmet episodeQualitiesMetric *prometheus.Desc // Total number of episodes by quality errorMetric *prometheus.Desc // Error Description for use with InvalidMetric } @@ -119,6 +120,12 @@ func NewSonarrCollector(conf *config.ArrConfig) *sonarrCollector { nil, prometheus.Labels{"url": conf.URL}, ), + episodeCutoffUnmetMetric: prometheus.NewDesc( + "sonarr_episode_cutoff_unmet_total", + "Total number of episodes with cutoff unmet", + nil, + prometheus.Labels{"url": conf.URL}, + ), episodeQualitiesMetric: prometheus.NewDesc( "sonarr_episode_quality_total", "Total number of downloaded episodes by quality", @@ -149,6 +156,7 @@ func (collector *sonarrCollector) Describe(ch chan<- *prometheus.Desc) { ch <- collector.episodeUnmonitoredMetric ch <- collector.episodeDownloadedMetric ch <- collector.episodeMissingMetric + ch <- collector.episodeCutoffUnmetMetric ch <- collector.episodeQualitiesMetric } @@ -287,6 +295,16 @@ func (collector *sonarrCollector) Collect(ch chan<- prometheus.Metric) { return } + episodesCutoffUnmet := model.CutoffUnmet{} + + // Cutoff unmet endpoint uses the same params as missing + if err := c.DoRequest("wanted/cutoff", &episodesCutoffUnmet, params); err != nil { + log.Errorw("Error getting cutoff unmet", + "error", err) + ch <- prometheus.NewInvalidMetric(collector.errorMetric, err) + return + } + ch <- prometheus.MustNewConstMetric(collector.seriesMetric, prometheus.GaugeValue, float64(len(series))) ch <- prometheus.MustNewConstMetric(collector.seriesDownloadedMetric, prometheus.GaugeValue, float64(seriesDownloaded)) ch <- prometheus.MustNewConstMetric(collector.seriesMonitoredMetric, prometheus.GaugeValue, float64(seriesMonitored)) @@ -299,6 +317,7 @@ func (collector *sonarrCollector) Collect(ch chan<- prometheus.Metric) { ch <- prometheus.MustNewConstMetric(collector.episodeMetric, prometheus.GaugeValue, float64(episodes)) ch <- prometheus.MustNewConstMetric(collector.episodeDownloadedMetric, prometheus.GaugeValue, float64(episodesDownloaded)) ch <- prometheus.MustNewConstMetric(collector.episodeMissingMetric, prometheus.GaugeValue, float64(episodesMissing.TotalRecords)) + ch <- prometheus.MustNewConstMetric(collector.episodeCutoffUnmetMetric, prometheus.GaugeValue, float64(episodesCutoffUnmet.TotalRecords)) if collector.config.EnableAdditionalMetrics { ch <- prometheus.MustNewConstMetric(collector.episodeMonitoredMetric, prometheus.GaugeValue, float64(episodesMonitored)) diff --git a/internal/arr/model/radarr.go b/internal/arr/model/radarr.go index f26fb86..d5d4f57 100644 --- a/internal/arr/model/radarr.go +++ b/internal/arr/model/radarr.go @@ -23,3 +23,9 @@ type TagMovies []struct { Label string `json:"label"` MovieIds []int `json:"movieIds"` } + +// CutoffUnmetMovies - Stores struct of JSON response +// https://radarr.video/docs/api/#/Cutoff/get_api_v3_wanted_cutoff +type CutoffUnmetMovies struct { + TotalRecords int `json:"totalRecords"` +} diff --git a/internal/arr/model/sonarr.go b/internal/arr/model/sonarr.go index dfe2448..faf166c 100644 --- a/internal/arr/model/sonarr.go +++ b/internal/arr/model/sonarr.go @@ -42,6 +42,12 @@ type Missing struct { TotalRecords int `json:"totalRecords"` } +// CutoffUnmet - Stores struct of JSON response +// https://sonarr.tv/docs/api/#/Cutoff/get_api_v3_wanted_cutoff +type CutoffUnmet struct { + TotalRecords int `json:"totalRecords"` +} + // EpisodeFile - Stores struct of JSON response // https://github.com/Sonarr/Sonarr/wiki/EpisodeFile type EpisodeFile []struct { diff --git a/internal/arr/test_fixtures/radarr/expected_metrics.txt b/internal/arr/test_fixtures/radarr/expected_metrics.txt index 3b369e5..b02855f 100644 --- a/internal/arr/test_fixtures/radarr/expected_metrics.txt +++ b/internal/arr/test_fixtures/radarr/expected_metrics.txt @@ -10,6 +10,9 @@ radarr_movie_filesize_total{url="SOMEURL"} 1.47062956689e+11 # HELP radarr_movie_missing_total Total number of missing movies # TYPE radarr_movie_missing_total gauge radarr_movie_missing_total{url="SOMEURL"} 2 +# HELP radarr_movie_cutoff_unmet_total Total number of movies with cutoff unmet +# TYPE radarr_movie_cutoff_unmet_total gauge +radarr_movie_cutoff_unmet_total{url="SOMEURL"} 1179 # HELP radarr_movie_monitored_total Total number of monitored movies # TYPE radarr_movie_monitored_total gauge radarr_movie_monitored_total{url="SOMEURL"} 7 diff --git a/internal/arr/test_fixtures/radarr/v3_wanted_cutoff.json b/internal/arr/test_fixtures/radarr/v3_wanted_cutoff.json new file mode 100644 index 0000000..c6d85b5 --- /dev/null +++ b/internal/arr/test_fixtures/radarr/v3_wanted_cutoff.json @@ -0,0 +1,3 @@ +{ + "totalRecords": 1179 +} diff --git a/internal/arr/test_fixtures/sonarr/expected_metrics.txt b/internal/arr/test_fixtures/sonarr/expected_metrics.txt index 15e9646..c716ff0 100644 --- a/internal/arr/test_fixtures/sonarr/expected_metrics.txt +++ b/internal/arr/test_fixtures/sonarr/expected_metrics.txt @@ -4,6 +4,9 @@ sonarr_episode_downloaded_total{url="SOMEURL"} 285 # HELP sonarr_episode_missing_total Total number of missing episodes # TYPE sonarr_episode_missing_total gauge sonarr_episode_missing_total{url="SOMEURL"} 1179 +# HELP sonarr_episode_cutoff_unmet_total Total number of episodes with cutoff unmet +# TYPE sonarr_episode_cutoff_unmet_total gauge +sonarr_episode_cutoff_unmet_total{url="SOMEURL"} 1179 # HELP sonarr_episode_total Total number of episodes # TYPE sonarr_episode_total gauge sonarr_episode_total{url="SOMEURL"} 675 diff --git a/internal/arr/test_fixtures/sonarr/expected_metrics_extended.txt b/internal/arr/test_fixtures/sonarr/expected_metrics_extended.txt index 712196e..e9382e4 100644 --- a/internal/arr/test_fixtures/sonarr/expected_metrics_extended.txt +++ b/internal/arr/test_fixtures/sonarr/expected_metrics_extended.txt @@ -4,6 +4,9 @@ sonarr_episode_downloaded_total{url="SOMEURL"} 285 # HELP sonarr_episode_missing_total Total number of missing episodes # TYPE sonarr_episode_missing_total gauge sonarr_episode_missing_total{url="SOMEURL"} 1179 +# HELP sonarr_episode_cutoff_unmet_total Total number of episodes with cutoff unmet +# TYPE sonarr_episode_cutoff_unmet_total gauge +sonarr_episode_cutoff_unmet_total{url="SOMEURL"} 1179 # HELP sonarr_episode_monitored_total Total number of monitored episodes # TYPE sonarr_episode_monitored_total gauge sonarr_episode_monitored_total{url="SOMEURL"} 12 diff --git a/internal/arr/test_fixtures/sonarr/v3_wanted_cutoff.json b/internal/arr/test_fixtures/sonarr/v3_wanted_cutoff.json new file mode 100644 index 0000000..e9a8884 --- /dev/null +++ b/internal/arr/test_fixtures/sonarr/v3_wanted_cutoff.json @@ -0,0 +1,3 @@ +{ + "totalRecords": 1179 +} \ No newline at end of file diff --git a/internal/arr/test_fixtures/sonarr/v3_wanted_missing.json b/internal/arr/test_fixtures/sonarr/v3_wanted_missing.json index 23c8a98..e98ea11 100644 --- a/internal/arr/test_fixtures/sonarr/v3_wanted_missing.json +++ b/internal/arr/test_fixtures/sonarr/v3_wanted_missing.json @@ -1,3 +1,3 @@ { "totalRecords": 1179 -} \ No newline at end of file +}