(3/3) Use Zap for structured logging rather than logrus. (#115)

* Refactor from logrus to zap, with structured logging

Signed-off-by: Russell Troxel <russell.troxel@segment.com>

* Print config errors to stderr when before initLogger

Signed-off-by: Russell Troxel <russell.troxel@segment.com>

---------

Signed-off-by: Russell Troxel <russell.troxel@segment.com>
This commit is contained in:
Russell Troxel 2023-03-18 08:16:02 -07:00 committed by GitHub
parent 78c9991d1e
commit 3cd02da157
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 178 additions and 68 deletions

7
go.mod
View File

@ -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

18
go.sum
View File

@ -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=

View File

@ -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 {

View File

@ -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
}

View File

@ -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)

View File

@ -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
}

View File

@ -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,
)
}

View File

@ -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)

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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,
)
}

View File

@ -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)
})
}

View File

@ -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",
}
}

View File

@ -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{