diff --git a/go.mod b/go.mod index 2aed2a7..d756ad8 100644 --- a/go.mod +++ b/go.mod @@ -10,10 +10,11 @@ require ( github.com/knadh/koanf/providers/posflag v0.1.0 github.com/knadh/koanf/v2 v2.0.0 github.com/prometheus/client_golang v1.14.0 - github.com/sirupsen/logrus v1.9.0 github.com/spf13/cobra v1.6.1 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.8.2 + go.uber.org/zap v1.24.0 + golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 ) require ( @@ -34,7 +35,9 @@ require ( github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/common v0.37.0 // indirect github.com/prometheus/procfs v0.8.0 // indirect - golang.org/x/sys v0.0.0-20220908164124-27713097b956 // indirect + go.uber.org/atomic v1.7.0 // indirect + go.uber.org/multierr v1.6.0 // indirect + golang.org/x/sys v0.1.0 // indirect golang.org/x/text v0.3.8 // indirect google.golang.org/protobuf v1.28.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 237187d..0ccfb09 100644 --- a/go.sum +++ b/go.sum @@ -38,6 +38,7 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -190,6 +191,7 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -224,8 +226,6 @@ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= -github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -237,7 +237,6 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= @@ -253,6 +252,13 @@ go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= +go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -272,6 +278,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 h1:pVgRXcIictcr+lBQIFeiwuwtDIs4eL21OuM9nyAADmo= +golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -384,12 +392,12 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220908164124-27713097b956 h1:XeJjHH1KiLpKGb6lvMiksZ9l0fVUh+AmGcm0nOMEBOY= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 h1:Q5284mrmYTpACcm+eAKjKJH48BBwSyfJqmmGDTtT8Vc= diff --git a/internal/client/client.go b/internal/client/client.go index 8aa22dc..b57f89a 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -8,16 +8,16 @@ import ( "net/url" "github.com/onedr0p/exportarr/internal/config" - log "github.com/sirupsen/logrus" + "go.uber.org/zap" ) -// Client struct is a Radarr client to request an instance of a Radarr +// Client struct is an *Arr client. type Client struct { httpClient http.Client URL url.URL } -// NewClient method initializes a new Radarr client. +// NewClient method initializes a new *Arr client. func NewClient(config *config.Config) (*Client, error) { baseURL, err := url.Parse(config.URL) @@ -72,8 +72,8 @@ func (c *Client) DoRequest(endpoint string, target interface{}, queryParams ...m } url := c.URL.JoinPath(endpoint) url.RawQuery = values.Encode() - - log.Infof("Sending HTTP request to %s", url.String()) + zap.S().Infow("Sending HTTP request", + "url", url) req, err := http.NewRequest("GET", url.String(), nil) if err != nil { diff --git a/internal/collector/lidarr/music.go b/internal/collector/lidarr/music.go index cdbb933..8ce7fdf 100644 --- a/internal/collector/lidarr/music.go +++ b/internal/collector/lidarr/music.go @@ -7,7 +7,7 @@ import ( "github.com/onedr0p/exportarr/internal/config" "github.com/onedr0p/exportarr/internal/model" "github.com/prometheus/client_golang/prometheus" - log "github.com/sirupsen/logrus" + "go.uber.org/zap" ) type lidarrCollector struct { @@ -127,9 +127,10 @@ func (collector *lidarrCollector) Describe(ch chan<- *prometheus.Desc) { } func (collector *lidarrCollector) Collect(ch chan<- prometheus.Metric) { + log := zap.S().With("collector", "lidarr") c, err := client.NewClient(collector.config) if err != nil { - log.Errorf("Error creating client: %s", err) + log.Errorf("Error creating client", "error", err) ch <- prometheus.NewInvalidMetric(collector.errorMetric, err) return } @@ -147,7 +148,7 @@ func (collector *lidarrCollector) Collect(ch chan<- prometheus.Metric) { artists := model.Artist{} if err := c.DoRequest("artist", &artists); err != nil { - log.Errorf("Error creating client: %s", err) + log.Errorw("Error creating client", "error", err) ch <- prometheus.NewInvalidMetric(collector.errorMetric, err) return } @@ -169,7 +170,7 @@ func (collector *lidarrCollector) Collect(ch chan<- prometheus.Metric) { songFile := model.SongFile{} params := map[string]string{"artistid": fmt.Sprintf("%d", s.Id)} if err := c.DoRequest("trackfile", &songFile, params); err != nil { - log.Errorf("Error getting trackfile: %s", err) + log.Errorw("Error getting trackfile", "error", err) ch <- prometheus.NewInvalidMetric(collector.errorMetric, err) return } @@ -182,7 +183,7 @@ func (collector *lidarrCollector) Collect(ch chan<- prometheus.Metric) { album := model.Album{} params = map[string]string{"artistid": fmt.Sprintf("%d", s.Id)} if err := c.DoRequest("album", &album, params); err != nil { - log.Errorf("Error getting album: %s", err) + log.Errorw("Error getting album", "error", err) ch <- prometheus.NewInvalidMetric(collector.errorMetric, err) return } @@ -199,7 +200,7 @@ func (collector *lidarrCollector) Collect(ch chan<- prometheus.Metric) { songMissing := model.Missing{} if err := c.DoRequest("wanted/missing", &songMissing); err != nil { - log.Errorf("Error getting missing: %s", err) + log.Errorw("Error getting missing", "error", err) ch <- prometheus.NewInvalidMetric(collector.errorMetric, err) return } diff --git a/internal/collector/prowlarr/stats.go b/internal/collector/prowlarr/stats.go index ada3fe8..6f45a80 100644 --- a/internal/collector/prowlarr/stats.go +++ b/internal/collector/prowlarr/stats.go @@ -8,7 +8,7 @@ import ( "github.com/onedr0p/exportarr/internal/config" "github.com/onedr0p/exportarr/internal/model" "github.com/prometheus/client_golang/prometheus" - log "github.com/sirupsen/logrus" + "go.uber.org/zap" ) type indexerStatCache struct { @@ -245,6 +245,7 @@ func (collector *prowlarrCollector) Describe(ch chan<- *prometheus.Desc) { func (collector *prowlarrCollector) Collect(ch chan<- prometheus.Metric) { total := time.Now() + log := zap.S().With("collector", "prowlarr") c, err := client.NewClient(collector.config) if err != nil { log.Errorf("Error creating client: %s", err) diff --git a/internal/collector/radarr/movie.go b/internal/collector/radarr/movie.go index df94b1e..0913ca2 100644 --- a/internal/collector/radarr/movie.go +++ b/internal/collector/radarr/movie.go @@ -5,7 +5,7 @@ import ( "github.com/onedr0p/exportarr/internal/config" "github.com/onedr0p/exportarr/internal/model" "github.com/prometheus/client_golang/prometheus" - log "github.com/sirupsen/logrus" + "go.uber.org/zap" ) type radarrCollector struct { @@ -93,9 +93,10 @@ func (collector *radarrCollector) Describe(ch chan<- *prometheus.Desc) { } func (collector *radarrCollector) Collect(ch chan<- prometheus.Metric) { + log := zap.S().With("collector", "radarr") c, err := client.NewClient(collector.config) if err != nil { - log.Errorf("Error creating client: %s", err) + log.Errorw("Error creating client", "error", err) ch <- prometheus.NewInvalidMetric(collector.errorMetric, err) return } @@ -111,7 +112,7 @@ func (collector *radarrCollector) Collect(ch chan<- prometheus.Metric) { movies := model.Movie{} // https://radarr.video/docs/api/#/Movie/get_api_v3_movie if err := c.DoRequest("movie", &movies); err != nil { - log.Errorf("Error getting movies: %s", err) + log.Errorw("Error getting movies", "error", err) ch <- prometheus.NewInvalidMetric(collector.errorMetric, err) return } diff --git a/internal/collector/readarr/author.go b/internal/collector/readarr/author.go index ccef8fb..6a62c12 100644 --- a/internal/collector/readarr/author.go +++ b/internal/collector/readarr/author.go @@ -7,7 +7,7 @@ import ( "github.com/onedr0p/exportarr/internal/config" "github.com/onedr0p/exportarr/internal/model" "github.com/prometheus/client_golang/prometheus" - log "github.com/sirupsen/logrus" + "go.uber.org/zap" ) type readarrCollector struct { @@ -119,9 +119,10 @@ func (c *readarrCollector) Describe(ch chan<- *prometheus.Desc) { func (collector *readarrCollector) Collect(ch chan<- prometheus.Metric) { total := time.Now() + log := zap.S().With("collector", "readarr") c, err := client.NewClient(collector.config) if err != nil { - log.Errorf("Error creating client: %s", err) + log.Errorw("Error creating client", "error", err) ch <- prometheus.NewInvalidMetric(collector.errorMetric, err) return } @@ -141,7 +142,7 @@ func (collector *readarrCollector) Collect(ch chan<- prometheus.Metric) { authors := model.Author{} if err := c.DoRequest("author", &authors); err != nil { - log.Errorf("Error getting authors: %s", err) + log.Errorw("Error getting authors", "error", err) ch <- prometheus.NewInvalidMetric(collector.errorMetric, err) return } @@ -164,12 +165,15 @@ func (collector *readarrCollector) Collect(ch chan<- prometheus.Metric) { } b := time.Since(tauthor) tauthors = append(tauthors, b) - log.Debugf("TIME :: author %s took %s", a.AuthorName, b) + log.Debugw("author metrics retrieved", + "author", a.AuthorName, + "duration", b) } books := model.Book{} if err := c.DoRequest("book", &books); err != nil { - log.Errorf("Error getting books: %s", err) + log.Errorw("Error getting books", + "error", err) ch <- prometheus.NewInvalidMetric(collector.errorMetric, err) return } @@ -199,8 +203,8 @@ func (collector *readarrCollector) Collect(ch chan<- prometheus.Metric) { ch <- prometheus.MustNewConstMetric(collector.bookUnmonitoredMetric, prometheus.GaugeValue, float64(booksUnmonitored)) ch <- prometheus.MustNewConstMetric(collector.bookMissingMetric, prometheus.GaugeValue, float64(booksMissing)) - log.Debugf("TIME :: total took %s with author timings as %s", - time.Since(total), - tauthors, + log.Debugf("collector cycle completed", + "duration", time.Since(total), + "author_durations", tauthors, ) } diff --git a/internal/collector/shared/health.go b/internal/collector/shared/health.go index 1707eb9..792df8e 100644 --- a/internal/collector/shared/health.go +++ b/internal/collector/shared/health.go @@ -7,7 +7,7 @@ import ( "github.com/onedr0p/exportarr/internal/config" "github.com/onedr0p/exportarr/internal/model" "github.com/prometheus/client_golang/prometheus" - log "github.com/sirupsen/logrus" + "go.uber.org/zap" ) type systemHealthCollector struct { @@ -39,6 +39,7 @@ func (collector *systemHealthCollector) Describe(ch chan<- *prometheus.Desc) { } func (collector *systemHealthCollector) Collect(ch chan<- prometheus.Metric) { + log := zap.S().With("collector", "systemHealth") c, err := client.NewClient(collector.config) if err != nil { log.Errorf("Error creating client: %s", err) diff --git a/internal/collector/shared/history.go b/internal/collector/shared/history.go index bd81398..8bbd942 100644 --- a/internal/collector/shared/history.go +++ b/internal/collector/shared/history.go @@ -7,7 +7,7 @@ import ( "github.com/onedr0p/exportarr/internal/config" "github.com/onedr0p/exportarr/internal/model" "github.com/prometheus/client_golang/prometheus" - log "github.com/sirupsen/logrus" + "go.uber.org/zap" ) type historyCollector struct { @@ -39,15 +39,18 @@ func (collector *historyCollector) Describe(ch chan<- *prometheus.Desc) { } func (collector *historyCollector) Collect(ch chan<- prometheus.Metric) { + log := zap.S().With("collector", "history") c, err := client.NewClient(collector.config) if err != nil { - log.Errorf("Error creating client: %s", err) + log.Errorw("Error creating client", + "error", err) ch <- prometheus.NewInvalidMetric(collector.errorMetric, err) return } history := model.History{} if err := c.DoRequest("history", &history); err != nil { - log.Errorf("Error getting history: %s", err) + log.Errorw("Error getting history", + "error", err) ch <- prometheus.NewInvalidMetric(collector.errorMetric, err) return } diff --git a/internal/collector/shared/queue.go b/internal/collector/shared/queue.go index 0b1783c..29b37b6 100644 --- a/internal/collector/shared/queue.go +++ b/internal/collector/shared/queue.go @@ -7,7 +7,7 @@ import ( "github.com/onedr0p/exportarr/internal/config" "github.com/onedr0p/exportarr/internal/model" "github.com/prometheus/client_golang/prometheus" - log "github.com/sirupsen/logrus" + "go.uber.org/zap" ) type queueCollector struct { @@ -39,9 +39,11 @@ func (collector *queueCollector) Describe(ch chan<- *prometheus.Desc) { } func (collector *queueCollector) Collect(ch chan<- prometheus.Metric) { + log := zap.S().With("collector", "queue") c, err := client.NewClient(collector.config) if err != nil { - log.Errorf("Error creating client: %s", err) + log.Errorw("Error creating client", + "error", err) ch <- prometheus.NewInvalidMetric(collector.errorMetric, err) return } @@ -57,7 +59,8 @@ func (collector *queueCollector) Collect(ch chan<- prometheus.Metric) { queue := model.Queue{} if err := c.DoRequest("queue", &queue, params); err != nil { - log.Errorf("Error getting queue: %s", err) + log.Errorw("Error getting queue", + "error", err) ch <- prometheus.NewInvalidMetric(collector.errorMetric, err) return } @@ -70,7 +73,9 @@ func (collector *queueCollector) Collect(ch chan<- prometheus.Metric) { for page := 2; page <= totalPages; page++ { params["page"] = fmt.Sprintf("%d", page) if err := c.DoRequest("queue", &queue, params); err != nil { - log.Errorf("Error getting queue (page %d): %s", page, err) + log.Errorw("Error getting queue page", + "page", page, + "error", err) ch <- prometheus.NewInvalidMetric(collector.errorMetric, err) return } diff --git a/internal/collector/shared/rootfolder.go b/internal/collector/shared/rootfolder.go index c93a5bf..485e4d3 100644 --- a/internal/collector/shared/rootfolder.go +++ b/internal/collector/shared/rootfolder.go @@ -7,7 +7,7 @@ import ( "github.com/onedr0p/exportarr/internal/config" "github.com/onedr0p/exportarr/internal/model" "github.com/prometheus/client_golang/prometheus" - log "github.com/sirupsen/logrus" + "go.uber.org/zap" ) type rootFolderCollector struct { @@ -39,15 +39,18 @@ func (collector *rootFolderCollector) Describe(ch chan<- *prometheus.Desc) { } func (collector *rootFolderCollector) Collect(ch chan<- prometheus.Metric) { + log := zap.S().With("collector", "rootfolder") c, err := client.NewClient(collector.config) if err != nil { - log.Errorf("Error creating client: %s", err) + log.Errorw("Error creating client", + "error", err) ch <- prometheus.NewInvalidMetric(collector.errorMetric, err) return } rootFolders := model.RootFolder{} if err := c.DoRequest("rootfolder", &rootFolders); err != nil { - log.Errorf("Error getting rootfolder: %s", err) + log.Errorw("Error getting rootfolder", + "error", err) ch <- prometheus.NewInvalidMetric(collector.errorMetric, err) return } diff --git a/internal/collector/shared/status.go b/internal/collector/shared/status.go index e080880..5b46859 100644 --- a/internal/collector/shared/status.go +++ b/internal/collector/shared/status.go @@ -7,7 +7,7 @@ import ( "github.com/onedr0p/exportarr/internal/config" "github.com/onedr0p/exportarr/internal/model" "github.com/prometheus/client_golang/prometheus" - log "github.com/sirupsen/logrus" + "go.uber.org/zap" ) type systemStatusCollector struct { @@ -40,9 +40,11 @@ func (collector *systemStatusCollector) Describe(ch chan<- *prometheus.Desc) { } func (collector *systemStatusCollector) Collect(ch chan<- prometheus.Metric) { + log := zap.S().With("collector", "system_status") c, err := client.NewClient(collector.config) if err != nil { - log.Errorf("Error creating client: %s", err) + log.Errorw("Error creating client", + "error", err) ch <- prometheus.NewInvalidMetric(collector.errorMetric, err) return } diff --git a/internal/collector/sonarr/series.go b/internal/collector/sonarr/series.go index 42dba89..896ee62 100644 --- a/internal/collector/sonarr/series.go +++ b/internal/collector/sonarr/series.go @@ -8,7 +8,7 @@ import ( "github.com/onedr0p/exportarr/internal/config" "github.com/onedr0p/exportarr/internal/model" "github.com/prometheus/client_golang/prometheus" - log "github.com/sirupsen/logrus" + "go.uber.org/zap" ) type sonarrCollector struct { @@ -153,9 +153,11 @@ func (collector *sonarrCollector) Describe(ch chan<- *prometheus.Desc) { func (collector *sonarrCollector) Collect(ch chan<- prometheus.Metric) { total := time.Now() + log := zap.S().With("collector", "sonarr") c, err := client.NewClient(collector.config) if err != nil { - log.Errorf("Error creating client: %s", err) + log.Errorw("Error creating client", + "error", err) ch <- prometheus.NewInvalidMetric(collector.errorMetric, err) return } @@ -178,7 +180,8 @@ func (collector *sonarrCollector) Collect(ch chan<- prometheus.Metric) { cseries := []time.Duration{} series := model.Series{} if err := c.DoRequest("series", &series); err != nil { - log.Errorf("Error getting series: %s", err) + log.Errorw("Error getting series", + "error", err) ch <- prometheus.NewInvalidMetric(collector.errorMetric, err) return } @@ -218,7 +221,8 @@ func (collector *sonarrCollector) Collect(ch chan<- prometheus.Metric) { episodeFile := model.EpisodeFile{} params := map[string]string{"seriesId": fmt.Sprintf("%d", s.Id)} if err := c.DoRequest("episodefile", &episodeFile, params); err != nil { - log.Errorf("Error getting episodefile: %s", err) + log.Errorw("Error getting episodefile", + "error", err) ch <- prometheus.NewInvalidMetric(collector.errorMetric, err) return } @@ -230,7 +234,8 @@ func (collector *sonarrCollector) Collect(ch chan<- prometheus.Metric) { episode := model.Episode{} if err := c.DoRequest("episode", &episode, params); err != nil { - log.Errorf("Error getting episode: %s", err) + log.Errorw("Error getting episode", + "error", err) ch <- prometheus.NewInvalidMetric(collector.errorMetric, err) return } @@ -241,17 +246,21 @@ func (collector *sonarrCollector) Collect(ch chan<- prometheus.Metric) { episodesMonitored++ } } - log.Debugf("TIME :: Extra options took %s", time.Since(textra)) + log.Debugw("Extra options completed", + "duration", time.Since(textra)) } e := time.Since(tseries) cseries = append(cseries, e) - log.Debugf("TIME :: series %d took %s", s.Id, e) + log.Debugw("series completed", + "series_id", s.Id, + "duration", e) } episodesMissing := model.Missing{} params := map[string]string{"sortKey": "airDateUtc"} if err := c.DoRequest("wanted/missing", &episodesMissing, params); err != nil { - log.Errorf("Error getting missing: %s", err) + log.Errorw("Error getting missing", + "error", err) ch <- prometheus.NewInvalidMetric(collector.errorMetric, err) return } @@ -281,9 +290,9 @@ func (collector *sonarrCollector) Collect(ch chan<- prometheus.Metric) { } } } - log.Debugf("TIME :: total took %s with series timings as %s", - time.Since(total), - cseries, + log.Debugw("Sonarr cycle completed", + "duration", time.Since(total), + "series_durations", cseries, ) } diff --git a/internal/commands/root.go b/internal/commands/root.go index 4ef43e8..9e8f98d 100644 --- a/internal/commands/root.go +++ b/internal/commands/root.go @@ -11,8 +11,9 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" - log "github.com/sirupsen/logrus" "github.com/spf13/cobra" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" "github.com/onedr0p/exportarr/internal/config" "github.com/onedr0p/exportarr/internal/handlers" @@ -37,9 +38,11 @@ func Execute() error { } func init() { - cobra.OnInitialize(initConfig) + cobra.OnInitialize(initConfig, initLogger) + cobra.OnFinalize(finalizeLogger) rootCmd.PersistentFlags().StringP("log-level", "l", "info", "Log level (debug, info, warn, error, fatal, panic)") + rootCmd.PersistentFlags().String("log-format", "console", "Log format (console, json)") rootCmd.PersistentFlags().StringP("config", "c", "", "*arr config.xml file for parsing authentication information") rootCmd.PersistentFlags().StringP("url", "u", "", "URL to *arr instance") rootCmd.PersistentFlags().StringP("api-key", "k", "", "API Key for *arr instance") @@ -58,18 +61,42 @@ func initConfig() { var err error conf, err = config.LoadConfig(rootCmd.PersistentFlags()) if err != nil { - log.Fatal(err) + fmt.Fprintln(os.Stderr, err) + os.Exit(1) } if err := conf.Validate(); err != nil { - log.Fatal(err) + fmt.Fprintln(os.Stderr, err) + os.Exit(1) } - level, err := log.ParseLevel(conf.LogLevel) +} + +func initLogger() { + atom := zap.NewAtomicLevel() + + encoder := zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig()) + if conf.LogFormat == "json" { + encoder = zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()) + } + + logger := zap.New(zapcore.NewCore(encoder, zapcore.Lock(os.Stdout), atom)) + // Create a logger with a default level first to ensure config failures are loggable. + atom.SetLevel(zapcore.InfoLevel) + zap.ReplaceGlobals(logger) + + lvl, err := zapcore.ParseLevel(conf.LogLevel) if err != nil { - log.Errorf("Invalid log level %s, using default level: info", conf.LogLevel) - level = log.InfoLevel + zap.S().Errorf("Invalid log level %s, using default level: info", conf.LogLevel) + lvl = zapcore.InfoLevel } - log.SetLevel(level) + atom.SetLevel(lvl) + + zap.S().Debug("Logger initialized") +} + +func finalizeLogger() { + // Flushes buffered log messages + zap.S().Sync() } type registerFunc func(registry *prometheus.Registry) @@ -83,13 +110,15 @@ func serveHttp(fn registerFunc) { signal.Notify(sigchan, os.Interrupt) signal.Notify(sigchan, syscall.SIGTERM) sig := <-sigchan - log.Infof("Received signal %s, shutting down", sig) + zap.S().Infof( + "Shutting down due to signal: %s", sig) ctx, cancel := context.WithTimeout(context.Background(), GRACEFUL_TIMEOUT) defer cancel() if err := srv.Shutdown(ctx); err != nil { - log.Fatalf("Server shutdown failed: %v", err) + zap.S().Fatalw("Server shutdown failed", + "error", err) } close(idleConnsClosed) }() @@ -102,12 +131,15 @@ func serveHttp(fn registerFunc) { http.HandleFunc("/healthz", handlers.HealthzHandler) http.Handle("/metrics", handler) - log.Infof("Listening on %s:%d", conf.Interface, conf.Port) + zap.S().Infow("Starting HTTP Server", + "interface", conf.Interface, + "port", conf.Port) srv.Addr = fmt.Sprintf("%s:%d", conf.Interface, conf.Port) srv.Handler = logRequest(http.DefaultServeMux) if err := srv.ListenAndServe(); err != http.ErrServerClosed { - log.Fatalf("Failed to Start HTTP Server: %v", err) + zap.S().Fatalw("Failed to Start HTTP Server", + "error", err) } <-idleConnsClosed } @@ -115,7 +147,10 @@ func serveHttp(fn registerFunc) { // Log internal request to stdout func logRequest(handler http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - log.Debugf("%s %s %s\n", r.RemoteAddr, r.Method, r.URL) + zap.S().Debugw("Request Received", + "remote_addr", r.RemoteAddr, + "method", r.Method, + "url", r.URL) handler.ServeHTTP(w, r) }) } diff --git a/internal/config/config.go b/internal/config/config.go index 116d4fe..c9f63d5 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -13,11 +13,14 @@ import ( "github.com/knadh/koanf/providers/posflag" "github.com/knadh/koanf/v2" flag "github.com/spf13/pflag" + "go.uber.org/zap/zapcore" + "golang.org/x/exp/slices" ) type Config struct { Arr string `koanf:"arr"` - LogLevel string `koanf:"log-level"` + LogLevel string `koanf:"log-level" validate:"ValidateLogLevel"` + LogFormat string `koanf:"log-format" validate:"in:console,json"` URL string `koanf:"url" validate:"required|url"` ApiKey string `koanf:"api-key" validate:"required|regex:([a-z0-9]{32})"` ApiKeyFile string `koanf:"api-key-file"` @@ -60,6 +63,7 @@ func LoadConfig(flags *flag.FlagSet) (*Config, error) { // Defaults err := k.Load(confmap.Provider(map[string]interface{}{ "log-level": "info", + "log-format": "console", "api-version": "v3", "port": "8081", "interface": "0.0.0.0", @@ -109,6 +113,16 @@ func LoadConfig(flags *flag.FlagSet) (*Config, error) { return &out, nil } +// ValidateLogLevel validates that the log level is one of the valid log levels +// gookit/Validate is pretty opinionated, and requires that this is not a pointer method. +func (c Config) ValidateLogLevel(val string) bool { + validLogLevels := []string{} + for i := zapcore.DebugLevel; i < zapcore.InvalidLevel; i++ { + validLogLevels = append(validLogLevels, i.String()) + } + return slices.Contains(validLogLevels, val) + +} func (c *Config) Validate() error { v := validate.Struct(c) if !v.Validate() { @@ -125,3 +139,10 @@ func (c *Config) Validate() error { } return nil } + +func (c *Config) Messages() map[string]string { + return validate.MS{ + "ApiKey.regex": "API Key must be a 32 character hex string", + "LogLevel.validateLogLevel": "Log Level must be one of: debug, info, warn, error, dpanic, panic, fatal", + } +} diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 0371195..dd69dff 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -30,6 +30,7 @@ func TestLoadConfig_Defaults(t *testing.T) { config, err := LoadConfig(&pflag.FlagSet{}) require.NoError(err) require.Equal("info", config.LogLevel) + require.Equal("console", config.LogFormat) require.Equal("v3", config.ApiVersion) require.Equal(8081, config.Port) require.Equal("0.0.0.0", config.Interface) @@ -251,6 +252,18 @@ func TestValidate(t *testing.T) { }, shouldError: true, }, + { + name: "bad-log-level", + config: &Config{ + LogLevel: "asdf", + URL: "http://localhost", + ApiKey: "abcdef0123456789abcdef0123456789", + ApiVersion: "v3", + Port: 1234, + Interface: "0.0.0.0", + }, + shouldError: true, + }, { name: "password-needs-username", config: &Config{