diff --git a/cmd/exportarr/main.go b/cmd/exportarr/main.go index 5b51bdf..ee4608e 100644 --- a/cmd/exportarr/main.go +++ b/cmd/exportarr/main.go @@ -1,385 +1,7 @@ package main -import ( - "fmt" - "net/http" - "os" - "strings" - - lidarrCollector "github.com/onedr0p/exportarr/internal/collector/lidarr" - prowlarrCollector "github.com/onedr0p/exportarr/internal/collector/prowlarr" - radarrCollector "github.com/onedr0p/exportarr/internal/collector/radarr" - readarrCollector "github.com/onedr0p/exportarr/internal/collector/readarr" - sharedCollector "github.com/onedr0p/exportarr/internal/collector/shared" - sonarrCollector "github.com/onedr0p/exportarr/internal/collector/sonarr" - "github.com/onedr0p/exportarr/internal/model" - - "github.com/onedr0p/exportarr/internal/handlers" - "github.com/onedr0p/exportarr/internal/utils" - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promhttp" - log "github.com/sirupsen/logrus" - "github.com/urfave/cli/v2" -) +import "github.com/onedr0p/exportarr/internal/commands" func main() { - app := cli.NewApp() - app.Name = "Exportarr" - app.Usage = "AIO Prometheus Exporter for Sonarr, Radarr or Lidarr" - app.EnableBashCompletion = true - app.HideVersion = true - app.Authors = []*cli.Author{ - &cli.Author{ - Name: "onedr0p", - Email: "onedr0p@users.noreply.github.com", - }, - &cli.Author{ - Name: "kinduff", - Email: "313nyk550@relay.firefox.com", - }, - } - // Global flags - app.Flags = []cli.Flag{ - &cli.StringFlag{ - Name: "log-level", - Aliases: []string{"l"}, - Usage: "Set the default Log Level", - Value: "INFO", - Required: false, - EnvVars: []string{"LOG_LEVEL"}, - }, - } - app.Before = func(config *cli.Context) error { - switch strings.ToUpper(config.String("log-level")) { - case "TRACE": - log.SetLevel(log.TraceLevel) - case "DEBUG": - log.SetLevel(log.DebugLevel) - case "INFO": - log.SetLevel(log.InfoLevel) - case "WARN": - log.SetLevel(log.WarnLevel) - default: - log.SetLevel(log.TraceLevel) - } - log.SetFormatter(&log.TextFormatter{}) - log.SetOutput(os.Stdout) - return nil - } - app.Commands = []*cli.Command{ - { - Name: "lidarr", - Aliases: []string{"l"}, - Usage: "Prometheus Exporter for Lidarr", - Description: strings.Title("Lidarr Exporter"), - Flags: flags("lidarr"), - Action: lidarr, - Before: validation, - }, - { - Name: "radarr", - Aliases: []string{"r"}, - Usage: "Prometheus Exporter for Radarr", - Description: strings.Title("Radarr Exporter"), - Flags: flags("radarr"), - Action: radarr, - Before: validation, - }, - { - Name: "sonarr", - Aliases: []string{"s"}, - Usage: "Prometheus Exporter for Sonarr", - Description: strings.Title("Sonarr Exporter"), - Flags: flags("sonarr"), - Action: sonarr, - Before: validation, - }, - { - Name: "prowlarr", - Aliases: []string{"p"}, - Usage: "Prometheus Exporter for Prowlarr", - Description: strings.Title("Prowlarr Exporter"), - Flags: flags("prowlarr"), - Action: prowlarr, - Before: validation, - }, - { - Name: "readarr", - Aliases: []string{"b"}, // b for book - Usage: "Prometheus Exporter for Readarr", - Description: strings.Title("Readarr Exporter"), - Flags: flags("readarr"), - Action: readarr, - Before: validation, - }, - } - - err := app.Run(os.Args) - if err != nil { - log.Fatal(err) - } -} - -func lidarr(config *cli.Context) (err error) { - registry := prometheus.NewRegistry() - - var configFile *model.Config - if config.String("config") != "" { - configFile, _ = utils.GetArrConfigFromFile(config.String("config")) - } else { - configFile = model.NewConfig() - } - configFile.ApiVersion = "v1" - - registry.MustRegister( - lidarrCollector.NewLidarrCollector(config, configFile), - sharedCollector.NewQueueCollector(config, configFile), - sharedCollector.NewHistoryCollector(config, configFile), - sharedCollector.NewRootFolderCollector(config, configFile), - sharedCollector.NewSystemStatusCollector(config, configFile), - sharedCollector.NewSystemHealthCollector(config, configFile), - ) - return serveHttp(config, registry) -} - -func radarr(config *cli.Context) (err error) { - registry := prometheus.NewRegistry() - - var configFile *model.Config - if config.String("config") != "" { - configFile, _ = utils.GetArrConfigFromFile(config.String("config")) - } else { - configFile = model.NewConfig() - } - - registry.MustRegister( - radarrCollector.NewRadarrCollector(config, configFile), - sharedCollector.NewQueueCollector(config, configFile), - sharedCollector.NewHistoryCollector(config, configFile), - sharedCollector.NewRootFolderCollector(config, configFile), - sharedCollector.NewSystemStatusCollector(config, configFile), - sharedCollector.NewSystemHealthCollector(config, configFile), - ) - return serveHttp(config, registry) -} - -func sonarr(config *cli.Context) (err error) { - registry := prometheus.NewRegistry() - - var configFile *model.Config - if config.String("config") != "" { - configFile, _ = utils.GetArrConfigFromFile(config.String("config")) - } else { - configFile = model.NewConfig() - } - - registry.MustRegister( - sonarrCollector.NewSonarrCollector(config, configFile), - sharedCollector.NewQueueCollector(config, configFile), - sharedCollector.NewHistoryCollector(config, configFile), - sharedCollector.NewRootFolderCollector(config, configFile), - sharedCollector.NewSystemStatusCollector(config, configFile), - sharedCollector.NewSystemHealthCollector(config, configFile), - ) - return serveHttp(config, registry) -} - -func prowlarr(config *cli.Context) (err error) { - registry := prometheus.NewRegistry() - - var configFile *model.Config - if config.String("config") != "" { - configFile, _ = utils.GetArrConfigFromFile(config.String("config")) - } else { - configFile = model.NewConfig() - } - configFile.ApiVersion = "v1" - - registry.MustRegister( - prowlarrCollector.NewProwlarrCollector(config, configFile), - sharedCollector.NewHistoryCollector(config, configFile), - sharedCollector.NewSystemStatusCollector(config, configFile), - sharedCollector.NewSystemHealthCollector(config, configFile), - ) - return serveHttp(config, registry) -} - -func readarr(config *cli.Context) (err error) { - registry := prometheus.NewRegistry() - - var configFile *model.Config - if config.String("config") != "" { - configFile, _ = utils.GetArrConfigFromFile(config.String("config")) - } else { - configFile = model.NewConfig() - } - configFile.ApiVersion = "v1" - - registry.MustRegister( - readarrCollector.NewReadarrCollector(config, configFile), - sharedCollector.NewQueueCollector(config, configFile), - sharedCollector.NewHistoryCollector(config, configFile), - sharedCollector.NewRootFolderCollector(config, configFile), - sharedCollector.NewSystemStatusCollector(config, configFile), - sharedCollector.NewSystemHealthCollector(config, configFile), - ) - return serveHttp(config, registry) -} - -func serveHttp(config *cli.Context, registry *prometheus.Registry) error { - // Set up the handlers - handler := promhttp.HandlerFor(registry, promhttp.HandlerOpts{}) - http.HandleFunc("/", handlers.IndexHandler) - http.HandleFunc("/healthz", handlers.HealthzHandler) - http.Handle("/metrics", handler) - // Serve up the metrics - log.Infof("Listening on %s:%d", config.String("interface"), config.Int("port")) - httpErr := http.ListenAndServe( - fmt.Sprintf("%s:%d", config.String("interface"), config.Int("port")), - logRequest(http.DefaultServeMux), - ) - if httpErr != nil { - return httpErr - } - return nil -} - -// 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) - handler.ServeHTTP(w, r) - }) -} - -// Validation used for all services -func validation(config *cli.Context) error { - // Data validations - if config.String("url") != "" && !utils.IsValidUrl(config.String("url")) { - return cli.Exit(fmt.Sprintf("%s is not a valid URL", config.String("url")), 1) - } - if config.String("api-key") != "" && !utils.IsValidApikey(config.String("api-key")) { - return cli.Exit(fmt.Sprintf("%s is not a valid API Key", config.String("api-key")), 1) - } - if config.String("api-key-file") != "" { - data, err := os.ReadFile(config.String("api-key-file")) - if err != nil { - return cli.Exit(fmt.Sprintf("unable to read API Key file %s", config.String("api-key-file")), 1) - } - if !utils.IsValidApikey(string(data)) { - return cli.Exit(fmt.Sprintf("%s is not a valid API Key", string(data)), 1) - } - } - if config.String("config") != "" && - !utils.IsFileThere(config.String("config")) { - return cli.Exit(fmt.Sprintf("%s config file does not exist", config.String("config")), 1) - } - - // Logical validations - if config.String("api-key") != "" && config.String("api-key-file") != "" { - return cli.Exit("either api-key or api-key-file can be set, not both of them", 1) - } - apiKeyIsSet := config.String("api-key") != "" || config.String("api-key-file") != "" - if config.String("url") != "" && apiKeyIsSet && config.String("config") != "" { - return cli.Exit("url and api-key or config must be set, not all of them", 1) - } - if config.String("url") == "" && !apiKeyIsSet && config.String("config") == "" { - return cli.Exit("url and api-key or config must be set, not none of them", 1) - } - if config.Bool("form-auth") && (config.String("auth-username") == "" || config.String("auth-password") == "") { - return cli.Exit("username and password must be set if form-auth is set", 1) - } - return nil -} - -// Flags used for all services -func flags(arr string) []cli.Flag { - flags := []cli.Flag{ - &cli.StringFlag{ - Name: "url", - Aliases: []string{"u"}, - Usage: fmt.Sprintf("%s's full URL", arr), - Required: true, - EnvVars: []string{"URL"}, - }, - &cli.StringFlag{ - Name: "api-key", - Aliases: []string{"a"}, - Usage: fmt.Sprintf("%s's API Key", arr), - Required: false, - EnvVars: []string{"APIKEY"}, - FilePath: "/etc/exportarr/apikey", - }, - &cli.StringFlag{ - Name: "api-key-file", - Usage: fmt.Sprintf("%s's API Key file location", arr), - Required: false, - EnvVars: []string{"APIKEY_FILE"}, - }, - &cli.StringFlag{ - Name: "config", - Aliases: []string{"c"}, - Usage: fmt.Sprintf("Path to %s's config.xml", arr), - Required: false, - EnvVars: []string{"CONFIG"}, - }, - &cli.IntFlag{ - Name: "port", - Aliases: []string{"p"}, - Usage: "Port the exporter will listen on", - Required: true, - EnvVars: []string{"PORT"}, - }, - &cli.StringFlag{ - Name: "interface", - Aliases: []string{"i"}, - Usage: "IP the exporter will listen on", - Value: "0.0.0.0", - Required: false, - EnvVars: []string{"INTERFACE"}, - }, - &cli.BoolFlag{ - Name: "disable-ssl-verify", - Usage: "Disable SSL Verifications (use with caution)", - Value: false, - Required: false, - EnvVars: []string{"DISABLE_SSL_VERIFY"}, - }, - &cli.StringFlag{ - Name: "auth-username", - Aliases: []string{"basic-auth-username"}, - Usage: "Provide the username for basic or form auth", - Required: false, - EnvVars: []string{"AUTH_USERNAME", "BASIC_AUTH_USERNAME"}, - }, - &cli.StringFlag{ - Name: "auth-password", - Aliases: []string{"basic-auth-password"}, - Usage: "Provide the password for basic or form auth", - Required: false, - EnvVars: []string{"AUTH_PASSWORD", "BASIC_AUTH_PASSWORD"}, - }, - &cli.BoolFlag{ - Name: "form-auth", - Usage: "Use form authentication rather than basic auth", - Value: false, - Required: false, - EnvVars: []string{"FORM_AUTH"}, - }, - &cli.BoolFlag{ - Name: "enable-unknown-queue-items", - Usage: "Enable gathering of unknown queue items in Queue metrics", - Value: false, - Required: false, - EnvVars: []string{"ENABLE_UNKNOWN_QUEUE_ITEMS"}, - }, - &cli.BoolFlag{ - Name: "enable-additional-metrics", - Usage: "Enable gathering of additional metrics (will slow down metrics gathering)", - Value: false, - Required: false, - EnvVars: []string{"ENABLE_ADDITIONAL_METRICS"}, - }, - } - return flags + commands.Execute() } diff --git a/go.mod b/go.mod index b9cc6c4..9d78954 100644 --- a/go.mod +++ b/go.mod @@ -3,26 +3,42 @@ module github.com/onedr0p/exportarr go 1.19 require ( + github.com/go-ozzo/ozzo-validation v3.6.0+incompatible + github.com/go-ozzo/ozzo-validation/v4 v4.3.0 + github.com/knadh/koanf/providers/confmap v0.1.0 + github.com/knadh/koanf/providers/env v0.1.0 + github.com/knadh/koanf/providers/file v0.1.0 + 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 github.com/urfave/cli/v2 v2.25.0 ) require ( + github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/golang/protobuf v1.5.2 // indirect + github.com/inconshreveable/mousetrap v1.0.1 // indirect + github.com/knadh/koanf/maps v0.1.1 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect 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 github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect - golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect + golang.org/x/sys v0.0.0-20220908164124-27713097b956 // 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 4532460..6c25b16 100644 --- a/go.sum +++ b/go.sum @@ -38,6 +38,8 @@ 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/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496 h1:zV3ejI06GQ59hwDQAvmK1qxOQGB3WuVTRoY0okPTAv0= +github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= 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= @@ -60,6 +62,8 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -71,6 +75,10 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-ozzo/ozzo-validation v3.6.0+incompatible h1:msy24VGS42fKO9K1vLz82/GeYW1cILu7Nuuj1N3BBkE= +github.com/go-ozzo/ozzo-validation v3.6.0+incompatible/go.mod h1:gsEKFIVnabGBt6mXmxK0MoFy+cZoTJY6mu5Ll3LVLBU= +github.com/go-ozzo/ozzo-validation/v4 v4.3.0 h1:byhDUpfEwjsVQb1vBunvIjh2BHQ9ead57VkAEY4V+Es= +github.com/go-ozzo/ozzo-validation/v4 v4.3.0/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -129,6 +137,8 @@ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5m github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= +github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -139,6 +149,18 @@ github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/X github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs= +github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= +github.com/knadh/koanf/providers/confmap v0.1.0 h1:gOkxhHkemwG4LezxxN8DMOFopOPghxRVp7JbIvdvqzU= +github.com/knadh/koanf/providers/confmap v0.1.0/go.mod h1:2uLhxQzJnyHKfxG927awZC7+fyHFdQkd697K4MdLnIU= +github.com/knadh/koanf/providers/env v0.1.0 h1:LqKteXqfOWyx5Ab9VfGHmjY9BvRXi+clwyZozgVRiKg= +github.com/knadh/koanf/providers/env v0.1.0/go.mod h1:RE8K9GbACJkeEnkl8L/Qcj8p4ZyPXZIQ191HJi44ZaQ= +github.com/knadh/koanf/providers/file v0.1.0 h1:fs6U7nrV58d3CFAFh8VTde8TM262ObYf3ODrc//Lp+c= +github.com/knadh/koanf/providers/file v0.1.0/go.mod h1:rjJ/nHQl64iYCtAW2QQnF0eSmDEX/YZ/eNFj5yR6BvA= +github.com/knadh/koanf/providers/posflag v0.1.0 h1:mKJlLrKPcAP7Ootf4pBZWJ6J+4wHYujwipe7Ie3qW6U= +github.com/knadh/koanf/providers/posflag v0.1.0/go.mod h1:SYg03v/t8ISBNrMBRMlojH8OsKowbkXV7giIbBVgbz0= +github.com/knadh/koanf/v2 v2.0.0 h1:XPQ5ilNnwnNaHrfQ1YpTVhUAjcGHnEKA+lRpipQv02Y= +github.com/knadh/koanf/v2 v2.0.0/go.mod h1:ZeiIlIDXTE7w1lMT6UVcNiRAS2/rCeLn/GdLNvY1Dus= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= @@ -149,6 +171,12 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= @@ -195,6 +223,10 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd 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= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= @@ -340,8 +372,9 @@ golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/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/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/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/internal/client/client.go b/internal/client/client.go index b59b893..8aa22dc 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -6,11 +6,9 @@ import ( "fmt" "net/http" "net/url" - "os" - "github.com/onedr0p/exportarr/internal/model" + "github.com/onedr0p/exportarr/internal/config" log "github.com/sirupsen/logrus" - "github.com/urfave/cli/v2" ) // Client struct is a Radarr client to request an instance of a Radarr @@ -20,64 +18,36 @@ type Client struct { } // NewClient method initializes a new Radarr client. -func NewClient(c *cli.Context, cf *model.Config) (*Client, error) { - var apiKey string - var baseURL *url.URL +func NewClient(config *config.Config) (*Client, error) { - apiVersion := cf.ApiVersion - - if c.String("config") != "" { - var err error - baseURL, err = baseURL.Parse(c.String("url") + ":" + cf.Port) - if err != nil { - return nil, fmt.Errorf("Couldn't parse URL: %w", err) - } - baseURL = baseURL.JoinPath(cf.UrlBase) - apiKey = cf.ApiKey - - } else { - // Otherwise use the value provided in the api-key flag - var err error - baseURL, err = baseURL.Parse(c.String("url")) - if err != nil { - return nil, fmt.Errorf("Couldn't parse URL: %w", err) - } - - if c.String("api-key") != "" { - apiKey = c.String("api-key") - } else if c.String("api-key-file") != "" { - data, err := os.ReadFile(c.String("api-key-file")) - if err != nil { - return nil, fmt.Errorf("Couldn't Read API Key file %w", err) - } - - apiKey = string(data) - } + baseURL, err := url.Parse(config.URL) + if err != nil { + return nil, fmt.Errorf("Failed to parse URL(%s): %w", config.URL, err) } baseTransport := http.DefaultTransport - if c.Bool("disable-ssl-verify") { + if config.DisableSSLVerify { baseTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} } var auth Authenticator - if c.Bool("form-auth") { + if config.UseFormAuth() { auth = &FormAuth{ - Username: c.String("auth-username"), - Password: c.String("auth-password"), - ApiKey: apiKey, + Username: config.AuthUsername, + Password: config.AuthPassword, + ApiKey: config.ApiKey, AuthBaseURL: baseURL, Transport: baseTransport, } - } else if c.String("username") != "" && c.String("password") != "" { + } else if config.UseBasicAuth() { auth = &BasicAuth{ - Username: c.String("auth-username"), - Password: c.String("auth-password"), - ApiKey: apiKey, + Username: config.AuthUsername, + Password: config.AuthPassword, + ApiKey: config.ApiKey, } } else { auth = &ApiKeyAuth{ - ApiKey: apiKey, + ApiKey: config.ApiKey, } } @@ -88,7 +58,7 @@ func NewClient(c *cli.Context, cf *model.Config) (*Client, error) { }, Transport: NewArrTransport(auth, baseTransport), }, - URL: *baseURL.JoinPath("api", apiVersion), + URL: *baseURL.JoinPath("api", config.ApiVersion), }, nil } diff --git a/internal/client/client_test.go b/internal/client/client_test.go index cd0feae..df6bdbf 100644 --- a/internal/client/client_test.go +++ b/internal/client/client_test.go @@ -4,73 +4,30 @@ import ( "fmt" "net/http" "net/http/httptest" - "os" "testing" "github.com/stretchr/testify/require" - "github.com/urfave/cli/v2" - "github.com/onedr0p/exportarr/internal/model" + "github.com/onedr0p/exportarr/internal/config" ) -func testContext(args map[string]string) *cli.Context { - var ret cli.Context - - osArgs := os.Args[0:1] - for k, v := range args { - osArgs = append(osArgs, fmt.Sprintf("--%s=%s", k, v)) - } - app := cli.NewApp() - app.Flags = []cli.Flag{ - &cli.StringFlag{Name: "url"}, - &cli.StringFlag{Name: "api-key"}, - &cli.StringFlag{Name: "api-key-file"}, - &cli.StringFlag{Name: "basic-auth-username"}, - &cli.StringFlag{Name: "basic-auth-password"}, - &cli.StringFlag{Name: "config"}, - &cli.StringFlag{Name: "disable-ssl-verify"}, +func TestNewClient(t *testing.T) { + require := require.New(t) + c := &config.Config{ + URL: "http://localhost:7878", + ApiKey: "abcdef0123456789abcdef0123456789", + ApiVersion: "v3", } - app.Action = func(c *cli.Context) error { - // copy out context - ret = *c - return nil - } - err := app.Run(osArgs) - if err != nil { - panic(err) - } - return &ret -} -func TestNewClient_Flags(t *testing.T) { - require := require.New(t) - c := testContext(map[string]string{ - "url": "http://localhost:7878", - "api-key": "abcdef0123456789abcdef0123456789", - }) - cf := model.NewConfig() - client, err := NewClient(c, cf) + client, err := NewClient(c) + _, ok := client.httpClient.Transport.(*ArrTransport).auth.(*ApiKeyAuth) + require.True(ok, "NewClient should return a client with an ApiKeyAuth authenticator") require.Nil(err, "NewClient should not return an error") require.NotNil(client, "NewClient should return a client") require.Equal(client.URL.String(), "http://localhost:7878/api/v3", "NewClient should return a client with the correct URL") } -func TestNewClient_File(t *testing.T) { - require := require.New(t) - c := testContext(map[string]string{ - "config": "testdata/config.json", - "url": "http://localhost", - "api-key": "abcdef0123456789abcdef0123456789", - }) - cf := model.NewConfig() - cf.Port = "7878" - cf.UrlBase = "/radarr" - - client, err := NewClient(c, cf) - require.Nil(err, "NewClient should not return an error") - require.NotNil(client, "NewClient should return a client") - require.Equal(client.URL.String(), "http://localhost:7878/radarr/api/v3", "NewClient should return a client with the correct URL") -} +// Need tests for FormAuth & BasicAuth func TestDoRequest(t *testing.T) { parameters := []struct { @@ -103,17 +60,17 @@ func TestDoRequest(t *testing.T) { })) defer ts.Close() - c := testContext(map[string]string{ - "url": ts.URL, - }) - cf := model.NewConfig() + c := &config.Config{ + URL: ts.URL, + ApiVersion: "v3", + } target := struct { Test string `json:"test"` }{} expected := target expected.Test = "asdf2" - client, err := NewClient(c, cf) + client, err := NewClient(c) require.Nil(err, "NewClient should not return an error") require.NotNil(client, "NewClient should return a client") err = client.DoRequest(param.endpoint, &target, param.queryParams) diff --git a/internal/collector/lidarr/music.go b/internal/collector/lidarr/music.go index a0095fa..cdbb933 100644 --- a/internal/collector/lidarr/music.go +++ b/internal/collector/lidarr/music.go @@ -4,15 +4,14 @@ import ( "fmt" "github.com/onedr0p/exportarr/internal/client" + "github.com/onedr0p/exportarr/internal/config" "github.com/onedr0p/exportarr/internal/model" "github.com/prometheus/client_golang/prometheus" log "github.com/sirupsen/logrus" - "github.com/urfave/cli/v2" ) type lidarrCollector struct { - config *cli.Context // App configuration - configFile *model.Config // *arr configuration from config.xml + config *config.Config // App configuration artistsMetric *prometheus.Desc // Total number of artists artistsMonitoredMetric *prometheus.Desc // Total number of monitored artists artistGenresMetric *prometheus.Desc // Total number of artists by genre @@ -28,87 +27,86 @@ type lidarrCollector struct { errorMetric *prometheus.Desc // Error Description for use with InvalidMetric } -func NewLidarrCollector(c *cli.Context, cf *model.Config) *lidarrCollector { +func NewLidarrCollector(c *config.Config) *lidarrCollector { return &lidarrCollector{ - config: c, - configFile: cf, + config: c, artistsMetric: prometheus.NewDesc( "lidarr_artists_total", "Total number of artists", nil, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": c.URLLabel()}, ), artistsMonitoredMetric: prometheus.NewDesc( "lidarr_artists_monitored_total", "Total number of monitored artists", nil, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": c.URLLabel()}, ), artistGenresMetric: prometheus.NewDesc( "lidarr_artists_genres_total", "Total number of artists by genre", []string{"genre"}, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": c.URLLabel()}, ), artistsFileSizeMetric: prometheus.NewDesc( "lidarr_artists_filesize_bytes", "Total fizesize of all artists in bytes", nil, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": c.URLLabel()}, ), albumsMetric: prometheus.NewDesc( "lidarr_albums_total", "Total number of albums", nil, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": c.URLLabel()}, ), albumsMonitoredMetric: prometheus.NewDesc( "lidarr_albums_monitored_total", "Total number of albums", nil, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": c.URLLabel()}, ), albumsGenresMetric: prometheus.NewDesc( "lidarr_albums_genres_total", "Total number of albums by genre", []string{"genre"}, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": c.URLLabel()}, ), songsMetric: prometheus.NewDesc( "lidarr_songs_total", "Total number of songs", nil, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": c.URLLabel()}, ), songsMonitoredMetric: prometheus.NewDesc( "lidarr_songs_monitored_total", "Total number of monitored songs", nil, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": c.URLLabel()}, ), songsDownloadedMetric: prometheus.NewDesc( "lidarr_songs_downloaded_total", "Total number of downloaded songs", nil, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": c.URLLabel()}, ), songsMissingMetric: prometheus.NewDesc( "lidarr_songs_missing_total", "Total number of missing songs", nil, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": c.URLLabel()}, ), songsQualitiesMetric: prometheus.NewDesc( "lidarr_songs_quality_total", "Total number of downloaded songs by quality", []string{"quality"}, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": c.URLLabel()}, ), errorMetric: prometheus.NewDesc( "lidarr_collector_error", "Error while collecting metrics", nil, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": c.URLLabel()}, ), } } @@ -129,7 +127,7 @@ func (collector *lidarrCollector) Describe(ch chan<- *prometheus.Desc) { } func (collector *lidarrCollector) Collect(ch chan<- prometheus.Metric) { - c, err := client.NewClient(collector.config, collector.configFile) + c, err := client.NewClient(collector.config) if err != nil { log.Errorf("Error creating client: %s", err) ch <- prometheus.NewInvalidMetric(collector.errorMetric, err) @@ -167,7 +165,7 @@ func (collector *lidarrCollector) Collect(ch chan<- prometheus.Metric) { artistGenres[genre]++ } - if collector.config.Bool("enable-additional-metrics") { + if collector.config.EnableAdditionalMetrics { songFile := model.SongFile{} params := map[string]string{"artistid": fmt.Sprintf("%d", s.Id)} if err := c.DoRequest("trackfile", &songFile, params); err != nil { @@ -220,7 +218,7 @@ func (collector *lidarrCollector) Collect(ch chan<- prometheus.Metric) { } } - if collector.config.Bool("enable-additional-metrics") { + if collector.config.EnableAdditionalMetrics { ch <- prometheus.MustNewConstMetric(collector.albumsMonitoredMetric, prometheus.GaugeValue, float64(albumsMonitored)) if len(songsQualities) > 0 { diff --git a/internal/collector/prowlarr/stats.go b/internal/collector/prowlarr/stats.go index 3d3b13e..ada3fe8 100644 --- a/internal/collector/prowlarr/stats.go +++ b/internal/collector/prowlarr/stats.go @@ -5,10 +5,10 @@ import ( "time" "github.com/onedr0p/exportarr/internal/client" + "github.com/onedr0p/exportarr/internal/config" "github.com/onedr0p/exportarr/internal/model" "github.com/prometheus/client_golang/prometheus" log "github.com/sirupsen/logrus" - "github.com/urfave/cli/v2" ) type indexerStatCache struct { @@ -92,8 +92,7 @@ func (u *userAgentStatCache) UpdateKey(key string, value model.UserAgentStats) m } type prowlarrCollector struct { - config *cli.Context // App configuration - configFile *model.Config // *arr configuration from config.xml + config *config.Config // App configuration indexerStatCache indexerStatCache // Cache of indexer stats userAgentStatCache userAgentStatCache // Cache of user agent stats lastStatUpdate time.Time // Last time stat caches were updated @@ -116,9 +115,9 @@ type prowlarrCollector struct { } -func NewProwlarrCollector(c *cli.Context, cf *model.Config) *prowlarrCollector { +func NewProwlarrCollector(c *config.Config) *prowlarrCollector { var lastStatUpdate time.Time - if c.Bool("enable-additional-metrics") { + if c.EnableAdditionalMetrics { // If additional metrics are enabled, backfill the cache. lastStatUpdate = time.Time{} } else { @@ -126,7 +125,6 @@ func NewProwlarrCollector(c *cli.Context, cf *model.Config) *prowlarrCollector { } return &prowlarrCollector{ config: c, - configFile: cf, indexerStatCache: NewIndexerStatCache(), userAgentStatCache: NewUserAgentCache(), lastStatUpdate: lastStatUpdate, @@ -134,97 +132,97 @@ func NewProwlarrCollector(c *cli.Context, cf *model.Config) *prowlarrCollector { "prowlarr_indexer_total", "Total number of configured indexers", nil, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": c.URLLabel()}, ), indexerEnabledMetric: prometheus.NewDesc( "prowlarr_indexer_enabled_total", "Total number of enabled indexers", nil, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": c.URLLabel()}, ), indexerAverageResponseTimeMetric: prometheus.NewDesc( "prowlarr_indexer_average_response_time_ms", "Average response time of indexers in ms", []string{"indexer"}, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": c.URLLabel()}, ), indexerQueriesMetric: prometheus.NewDesc( "prowlarr_indexer_queries_total", "Total number of queries", []string{"indexer"}, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": c.URLLabel()}, ), indexerGrabsMetric: prometheus.NewDesc( "prowlarr_indexer_grabs_total", "Total number of grabs", []string{"indexer"}, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": c.URLLabel()}, ), indexerRssQueriesMetric: prometheus.NewDesc( "prowlarr_indexer_rss_queries_total", "Total number of rss queries", []string{"indexer"}, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": c.URLLabel()}, ), indexerAuthQueriesMetric: prometheus.NewDesc( "prowlarr_indexer_auth_queries_total", "Total number of auth queries", []string{"indexer"}, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": c.URLLabel()}, ), indexerFailedQueriesMetric: prometheus.NewDesc( "prowlarr_indexer_failed_queries_total", "Total number of failed queries", []string{"indexer"}, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": c.URLLabel()}, ), indexerFailedGrabsMetric: prometheus.NewDesc( "prowlarr_indexer_failed_grabs_total", "Total number of failed grabs", []string{"indexer"}, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": c.URLLabel()}, ), indexerFailedRssQueriesMetric: prometheus.NewDesc( "prowlarr_indexer_failed_rss_queries_total", "Total number of failed rss queries", []string{"indexer"}, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": c.URLLabel()}, ), indexerFailedAuthQueriesMetric: prometheus.NewDesc( "prowlarr_indexer_failed_auth_queries_total", "Total number of failed auth queries", []string{"indexer"}, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": c.URLLabel()}, ), indexerVipExpirationMetric: prometheus.NewDesc( "prowlarr_indexer_vip_expires_in_seconds", "VIP expiration date", []string{"indexer"}, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": c.URLLabel()}, ), userAgentMetric: prometheus.NewDesc( "prowlarr_user_agent_total", "Total number of active user agents", nil, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": c.URLLabel()}, ), userAgentQueriesMetric: prometheus.NewDesc( "prowlarr_user_agent_queries_total", "Total number of queries", []string{"user_agent"}, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": c.URLLabel()}, ), userAgentGrabsMetric: prometheus.NewDesc( "prowlarr_user_agent_grabs_total", "Total number of grabs", []string{"user_agent"}, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": c.URLLabel()}, ), errorMetric: prometheus.NewDesc( "prowlarr_collector_error", "Error while collecting metrics", nil, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": c.URLLabel()}, ), } } @@ -247,7 +245,7 @@ func (collector *prowlarrCollector) Describe(ch chan<- *prometheus.Desc) { func (collector *prowlarrCollector) Collect(ch chan<- prometheus.Metric) { total := time.Now() - c, err := client.NewClient(collector.config, collector.configFile) + c, err := client.NewClient(collector.config) if err != nil { log.Errorf("Error creating client: %s", err) ch <- prometheus.NewInvalidMetric(collector.errorMetric, err) diff --git a/internal/collector/radarr/movie.go b/internal/collector/radarr/movie.go index 9c4ac16..df94b1e 100644 --- a/internal/collector/radarr/movie.go +++ b/internal/collector/radarr/movie.go @@ -2,15 +2,14 @@ package collector import ( "github.com/onedr0p/exportarr/internal/client" + "github.com/onedr0p/exportarr/internal/config" "github.com/onedr0p/exportarr/internal/model" "github.com/prometheus/client_golang/prometheus" log "github.com/sirupsen/logrus" - "github.com/urfave/cli/v2" ) type radarrCollector struct { - config *cli.Context // App configuration - configFile *model.Config // *arr configuration from config.xml + config *config.Config // App configuration movieMetric *prometheus.Desc // Total number of movies movieDownloadedMetric *prometheus.Desc // Total number of downloaded movies movieMonitoredMetric *prometheus.Desc // Total number of monitored movies @@ -22,63 +21,62 @@ type radarrCollector struct { errorMetric *prometheus.Desc // Error Description for use with InvalidMetric } -func NewRadarrCollector(c *cli.Context, cf *model.Config) *radarrCollector { +func NewRadarrCollector(c *config.Config) *radarrCollector { return &radarrCollector{ - config: c, - configFile: cf, + config: c, movieMetric: prometheus.NewDesc( "radarr_movie_total", "Total number of movies", nil, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": c.URLLabel()}, ), movieDownloadedMetric: prometheus.NewDesc( "radarr_movie_downloaded_total", "Total number of downloaded movies", nil, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": c.URLLabel()}, ), movieMonitoredMetric: prometheus.NewDesc( "radarr_movie_monitored_total", "Total number of monitored movies", nil, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": c.URLLabel()}, ), movieUnmonitoredMetric: prometheus.NewDesc( "radarr_movie_unmonitored_total", "Total number of unmonitored movies", nil, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": c.URLLabel()}, ), movieWantedMetric: prometheus.NewDesc( "radarr_movie_wanted_total", "Total number of wanted movies", nil, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": c.URLLabel()}, ), movieMissingMetric: prometheus.NewDesc( "radarr_movie_missing_total", "Total number of missing movies", nil, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": c.URLLabel()}, ), movieFileSizeMetric: prometheus.NewDesc( "radarr_movie_filesize_total", "Total filesize of all movies", nil, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": c.URLLabel()}, ), movieQualitiesMetric: prometheus.NewDesc( "radarr_movie_quality_total", "Total number of downloaded movies by quality", []string{"quality"}, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": c.URLLabel()}, ), errorMetric: prometheus.NewDesc( "radarr_collector_error", "Error while collecting metrics", nil, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": c.URLLabel()}, ), } } @@ -95,7 +93,7 @@ func (collector *radarrCollector) Describe(ch chan<- *prometheus.Desc) { } func (collector *radarrCollector) Collect(ch chan<- prometheus.Metric) { - c, err := client.NewClient(collector.config, collector.configFile) + c, err := client.NewClient(collector.config) if err != nil { log.Errorf("Error creating client: %s", err) ch <- prometheus.NewInvalidMetric(collector.errorMetric, err) diff --git a/internal/collector/readarr/author.go b/internal/collector/readarr/author.go index d8ce9ec..ccef8fb 100644 --- a/internal/collector/readarr/author.go +++ b/internal/collector/readarr/author.go @@ -4,15 +4,14 @@ import ( "time" "github.com/onedr0p/exportarr/internal/client" + "github.com/onedr0p/exportarr/internal/config" "github.com/onedr0p/exportarr/internal/model" "github.com/prometheus/client_golang/prometheus" log "github.com/sirupsen/logrus" - "github.com/urfave/cli/v2" ) type readarrCollector struct { - config *cli.Context // App configuration - configFile *model.Config // *arr configuration from config.xml + config *config.Config // App configuration authorMetric *prometheus.Desc // Total number of authors authorDownloadedMetric *prometheus.Desc // Total number of downloaded authors authorMonitoredMetric *prometheus.Desc // Total number of monitored authors @@ -27,81 +26,80 @@ type readarrCollector struct { errorMetric *prometheus.Desc // Error Description for use with InvalidMetric } -func NewReadarrCollector(c *cli.Context, cf *model.Config) *readarrCollector { +func NewReadarrCollector(c *config.Config) *readarrCollector { return &readarrCollector{ - config: c, - configFile: cf, + config: c, authorMetric: prometheus.NewDesc( "readarr_author_total", "Total number of authors", nil, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": c.URLLabel()}, ), authorDownloadedMetric: prometheus.NewDesc( "readarr_author_downloaded_total", "Total number of downloaded authors", nil, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": c.URLLabel()}, ), authorMonitoredMetric: prometheus.NewDesc( "readarr_author_monitored_total", "Total number of monitored authors", nil, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": c.URLLabel()}, ), authorUnmonitoredMetric: prometheus.NewDesc( "readarr_author_unmonitored_total", "Total number of unmonitored authors", nil, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": c.URLLabel()}, ), authorFileSizeMetric: prometheus.NewDesc( "readarr_author_filesize_bytes", "Total filesize of all authors in bytes", nil, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": c.URLLabel()}, ), bookMetric: prometheus.NewDesc( "readarr_book_total", "Total number of books", nil, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": c.URLLabel()}, ), bookGrabbedMetric: prometheus.NewDesc( "readarr_book_grabbed_total", "Total number of grabbed books", nil, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": c.URLLabel()}, ), bookDownloadedMetric: prometheus.NewDesc( "readarr_book_downloaded_total", "Total number of downloaded books", nil, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": c.URLLabel()}, ), bookMonitoredMetric: prometheus.NewDesc( "readarr_book_monitored_total", "Total number of monitored books", nil, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": c.URLLabel()}, ), bookUnmonitoredMetric: prometheus.NewDesc( "readarr_book_unmonitored_total", "Total number of unmonitored books", nil, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": c.URLLabel()}, ), bookMissingMetric: prometheus.NewDesc( "readarr_book_missing_total", "Total number of missing books", nil, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": c.URLLabel()}, ), errorMetric: prometheus.NewDesc( "readarr_collector_error", "Error while collecting metrics", nil, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": c.URLLabel()}, ), } } @@ -121,7 +119,7 @@ func (c *readarrCollector) Describe(ch chan<- *prometheus.Desc) { func (collector *readarrCollector) Collect(ch chan<- prometheus.Metric) { total := time.Now() - c, err := client.NewClient(collector.config, collector.configFile) + c, err := client.NewClient(collector.config) if err != nil { log.Errorf("Error creating client: %s", err) ch <- prometheus.NewInvalidMetric(collector.errorMetric, err) diff --git a/internal/collector/shared/health.go b/internal/collector/shared/health.go index 998d686..1707eb9 100644 --- a/internal/collector/shared/health.go +++ b/internal/collector/shared/health.go @@ -4,34 +4,32 @@ import ( "fmt" "github.com/onedr0p/exportarr/internal/client" + "github.com/onedr0p/exportarr/internal/config" "github.com/onedr0p/exportarr/internal/model" "github.com/prometheus/client_golang/prometheus" log "github.com/sirupsen/logrus" - "github.com/urfave/cli/v2" ) type systemHealthCollector struct { - config *cli.Context // App configuration - configFile *model.Config // *arr configuration from config.xml + config *config.Config // App configuration systemHealthMetric *prometheus.Desc // Total number of health issues errorMetric *prometheus.Desc // Error Description for use with InvalidMetric } -func NewSystemHealthCollector(c *cli.Context, cf *model.Config) *systemHealthCollector { +func NewSystemHealthCollector(c *config.Config) *systemHealthCollector { return &systemHealthCollector{ - config: c, - configFile: cf, + config: c, systemHealthMetric: prometheus.NewDesc( - fmt.Sprintf("%s_system_health_issues", c.Command.Name), + fmt.Sprintf("%s_system_health_issues", c.Arr), "Total number of health issues by source, type, message and wikiurl", []string{"source", "type", "message", "wikiurl"}, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": c.URLLabel()}, ), errorMetric: prometheus.NewDesc( - fmt.Sprintf("%s_health_collector_error", c.Command.Name), + fmt.Sprintf("%s_health_collector_error", c.Arr), "Error while collecting metrics", nil, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": c.URLLabel()}, ), } } @@ -41,7 +39,7 @@ func (collector *systemHealthCollector) Describe(ch chan<- *prometheus.Desc) { } func (collector *systemHealthCollector) Collect(ch chan<- prometheus.Metric) { - c, err := client.NewClient(collector.config, collector.configFile) + c, err := client.NewClient(collector.config) if err != nil { log.Errorf("Error creating client: %s", err) ch <- prometheus.NewInvalidMetric(collector.errorMetric, err) diff --git a/internal/collector/shared/history.go b/internal/collector/shared/history.go index 8ae6a31..bd81398 100644 --- a/internal/collector/shared/history.go +++ b/internal/collector/shared/history.go @@ -4,34 +4,32 @@ import ( "fmt" "github.com/onedr0p/exportarr/internal/client" + "github.com/onedr0p/exportarr/internal/config" "github.com/onedr0p/exportarr/internal/model" "github.com/prometheus/client_golang/prometheus" log "github.com/sirupsen/logrus" - "github.com/urfave/cli/v2" ) type historyCollector struct { - config *cli.Context // App configuration - configFile *model.Config // *arr configuration from config.xml + config *config.Config // App configuration historyMetric *prometheus.Desc // Total number of history items errorMetric *prometheus.Desc // Error Description for use with InvalidMetric } -func NewHistoryCollector(c *cli.Context, cf *model.Config) *historyCollector { +func NewHistoryCollector(c *config.Config) *historyCollector { return &historyCollector{ - config: c, - configFile: cf, + config: c, historyMetric: prometheus.NewDesc( - fmt.Sprintf("%s_history_total", c.Command.Name), + fmt.Sprintf("%s_history_total", c.Arr), "Total number of item in the history", nil, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": c.URLLabel()}, ), errorMetric: prometheus.NewDesc( - fmt.Sprintf("%s_history_collector_error", c.Command.Name), + fmt.Sprintf("%s_history_collector_error", c.Arr), "Error while collecting metrics", nil, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": c.URLLabel()}, ), } } @@ -41,7 +39,7 @@ func (collector *historyCollector) Describe(ch chan<- *prometheus.Desc) { } func (collector *historyCollector) Collect(ch chan<- prometheus.Metric) { - c, err := client.NewClient(collector.config, collector.configFile) + c, err := client.NewClient(collector.config) if err != nil { log.Errorf("Error creating client: %s", err) ch <- prometheus.NewInvalidMetric(collector.errorMetric, err) diff --git a/internal/collector/shared/queue.go b/internal/collector/shared/queue.go index 459622a..0b1783c 100644 --- a/internal/collector/shared/queue.go +++ b/internal/collector/shared/queue.go @@ -4,34 +4,32 @@ import ( "fmt" "github.com/onedr0p/exportarr/internal/client" + "github.com/onedr0p/exportarr/internal/config" "github.com/onedr0p/exportarr/internal/model" "github.com/prometheus/client_golang/prometheus" log "github.com/sirupsen/logrus" - "github.com/urfave/cli/v2" ) type queueCollector struct { - config *cli.Context // App configuration - configFile *model.Config // *arr configuration from config.xml + config *config.Config // App configuration queueMetric *prometheus.Desc // Total number of queue items errorMetric *prometheus.Desc // Error Description for use with InvalidMetric } -func NewQueueCollector(c *cli.Context, cf *model.Config) *queueCollector { +func NewQueueCollector(c *config.Config) *queueCollector { return &queueCollector{ - config: c, - configFile: cf, + config: c, queueMetric: prometheus.NewDesc( - fmt.Sprintf("%s_queue_total", c.Command.Name), + fmt.Sprintf("%s_queue_total", c.Arr), "Total number of items in the queue by status, download_status, and download_state", []string{"status", "download_status", "download_state"}, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": c.URLLabel()}, ), errorMetric: prometheus.NewDesc( - fmt.Sprintf("%s_queue_collector_error", c.Command.Name), + fmt.Sprintf("%s_queue_collector_error", c.Arr), "Error while collecting metrics", nil, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": c.URLLabel()}, ), } } @@ -41,7 +39,7 @@ func (collector *queueCollector) Describe(ch chan<- *prometheus.Desc) { } func (collector *queueCollector) Collect(ch chan<- prometheus.Metric) { - c, err := client.NewClient(collector.config, collector.configFile) + c, err := client.NewClient(collector.config) if err != nil { log.Errorf("Error creating client: %s", err) ch <- prometheus.NewInvalidMetric(collector.errorMetric, err) @@ -49,10 +47,10 @@ func (collector *queueCollector) Collect(ch chan<- prometheus.Metric) { } params := map[string]string{"page": "1"} - if collector.config.Bool("enable-unknown-queue-items") { - if collector.config.Command.Name == "sonarr" { + if collector.config.EnableUnknownQueueItems { + if collector.config.Arr == "sonarr" { params["includeUnknownSeriesItems"] = "true" - } else if collector.config.Command.Name == "radarr" { + } else if collector.config.Arr == "radarr" { params["includeUnknownMovieItems"] = "true" } } diff --git a/internal/collector/shared/rootfolder.go b/internal/collector/shared/rootfolder.go index 36a3108..c93a5bf 100644 --- a/internal/collector/shared/rootfolder.go +++ b/internal/collector/shared/rootfolder.go @@ -4,34 +4,32 @@ import ( "fmt" "github.com/onedr0p/exportarr/internal/client" + "github.com/onedr0p/exportarr/internal/config" "github.com/onedr0p/exportarr/internal/model" "github.com/prometheus/client_golang/prometheus" log "github.com/sirupsen/logrus" - "github.com/urfave/cli/v2" ) type rootFolderCollector struct { - config *cli.Context // App configuration - configFile *model.Config // *arr configuration from config.xml + config *config.Config // App configuration rootFolderMetric *prometheus.Desc // Total number of root folders errorMetric *prometheus.Desc // Error Description for use with InvalidMetric } -func NewRootFolderCollector(c *cli.Context, cf *model.Config) *rootFolderCollector { +func NewRootFolderCollector(c *config.Config) *rootFolderCollector { return &rootFolderCollector{ - config: c, - configFile: cf, + config: c, rootFolderMetric: prometheus.NewDesc( - fmt.Sprintf("%s_rootfolder_freespace_bytes", c.Command.Name), + fmt.Sprintf("%s_rootfolder_freespace_bytes", c.Arr), "Root folder space in bytes by path", []string{"path"}, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": c.URLLabel()}, ), errorMetric: prometheus.NewDesc( - fmt.Sprintf("%s_rootfolder_collector_error", c.Command.Name), + fmt.Sprintf("%s_rootfolder_collector_error", c.Arr), "Error while collecting metrics", nil, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": c.URLLabel()}, ), } } @@ -41,7 +39,7 @@ func (collector *rootFolderCollector) Describe(ch chan<- *prometheus.Desc) { } func (collector *rootFolderCollector) Collect(ch chan<- prometheus.Metric) { - c, err := client.NewClient(collector.config, collector.configFile) + c, err := client.NewClient(collector.config) if err != nil { log.Errorf("Error creating client: %s", err) ch <- prometheus.NewInvalidMetric(collector.errorMetric, err) diff --git a/internal/collector/shared/status.go b/internal/collector/shared/status.go index c7de233..e080880 100644 --- a/internal/collector/shared/status.go +++ b/internal/collector/shared/status.go @@ -4,34 +4,33 @@ import ( "fmt" "github.com/onedr0p/exportarr/internal/client" + "github.com/onedr0p/exportarr/internal/config" "github.com/onedr0p/exportarr/internal/model" "github.com/prometheus/client_golang/prometheus" log "github.com/sirupsen/logrus" - "github.com/urfave/cli/v2" ) type systemStatusCollector struct { - config *cli.Context // App configuration + config *config.Config // App configuration configFile *model.Config // *arr configuration from config.xml systemStatus *prometheus.Desc // Total number of system statuses errorMetric *prometheus.Desc // Error Description for use with InvalidMetric } -func NewSystemStatusCollector(c *cli.Context, cf *model.Config) *systemStatusCollector { +func NewSystemStatusCollector(c *config.Config) *systemStatusCollector { return &systemStatusCollector{ - config: c, - configFile: cf, + config: c, systemStatus: prometheus.NewDesc( - fmt.Sprintf("%s_system_status", c.Command.Name), + fmt.Sprintf("%s_system_status", c.Arr), "System Status", nil, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": c.URLLabel()}, ), errorMetric: prometheus.NewDesc( - fmt.Sprintf("%s_status_collector_error", c.Command.Name), + fmt.Sprintf("%s_status_collector_error", c.Arr), "Error while collecting metrics", nil, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": c.URLLabel()}, ), } } @@ -41,7 +40,7 @@ func (collector *systemStatusCollector) Describe(ch chan<- *prometheus.Desc) { } func (collector *systemStatusCollector) Collect(ch chan<- prometheus.Metric) { - c, err := client.NewClient(collector.config, collector.configFile) + c, err := client.NewClient(collector.config) if err != nil { log.Errorf("Error creating client: %s", err) ch <- prometheus.NewInvalidMetric(collector.errorMetric, err) diff --git a/internal/collector/sonarr/series.go b/internal/collector/sonarr/series.go index 97d2f54..42dba89 100644 --- a/internal/collector/sonarr/series.go +++ b/internal/collector/sonarr/series.go @@ -5,15 +5,14 @@ import ( "time" "github.com/onedr0p/exportarr/internal/client" + "github.com/onedr0p/exportarr/internal/config" "github.com/onedr0p/exportarr/internal/model" "github.com/prometheus/client_golang/prometheus" log "github.com/sirupsen/logrus" - "github.com/urfave/cli/v2" ) type sonarrCollector struct { - config *cli.Context // App configuration - configFile *model.Config // *arr configuration from config.xml + config *config.Config // App configuration seriesMetric *prometheus.Desc // Total number of series seriesDownloadedMetric *prometheus.Desc // Total number of downloaded series seriesMonitoredMetric *prometheus.Desc // Total number of monitored series @@ -32,105 +31,104 @@ type sonarrCollector struct { errorMetric *prometheus.Desc // Error Description for use with InvalidMetric } -func NewSonarrCollector(c *cli.Context, cf *model.Config) *sonarrCollector { +func NewSonarrCollector(conf *config.Config) *sonarrCollector { return &sonarrCollector{ - config: c, - configFile: cf, + config: conf, seriesMetric: prometheus.NewDesc( "sonarr_series_total", "Total number of series", nil, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": conf.URLLabel()}, ), seriesDownloadedMetric: prometheus.NewDesc( "sonarr_series_downloaded_total", "Total number of downloaded series", nil, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": conf.URLLabel()}, ), seriesMonitoredMetric: prometheus.NewDesc( "sonarr_series_monitored_total", "Total number of monitored series", nil, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": conf.URLLabel()}, ), seriesUnmonitoredMetric: prometheus.NewDesc( "sonarr_series_unmonitored_total", "Total number of unmonitored series", nil, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": conf.URLLabel()}, ), seriesFileSizeMetric: prometheus.NewDesc( "sonarr_series_filesize_bytes", "Total fizesize of all series in bytes", nil, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": conf.URLLabel()}, ), seasonMetric: prometheus.NewDesc( "sonarr_season_total", "Total number of seasons", nil, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": conf.URLLabel()}, ), seasonDownloadedMetric: prometheus.NewDesc( "sonarr_season_downloaded_total", "Total number of downloaded seasons", nil, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": conf.URLLabel()}, ), seasonMonitoredMetric: prometheus.NewDesc( "sonarr_season_monitored_total", "Total number of monitored seasons", nil, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": conf.URLLabel()}, ), seasonUnmonitoredMetric: prometheus.NewDesc( "sonarr_season_unmonitored_total", "Total number of unmonitored seasons", nil, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": conf.URLLabel()}, ), episodeMetric: prometheus.NewDesc( "sonarr_episode_total", "Total number of episodes", nil, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": conf.URLLabel()}, ), episodeMonitoredMetric: prometheus.NewDesc( "sonarr_episode_monitored_total", "Total number of monitored episodes", nil, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": conf.URLLabel()}, ), episodeUnmonitoredMetric: prometheus.NewDesc( "sonarr_episode_unmonitored_total", "Total number of unmonitored episodes", nil, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": conf.URLLabel()}, ), episodeDownloadedMetric: prometheus.NewDesc( "sonarr_episode_downloaded_total", "Total number of downloaded episodes", nil, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": conf.URLLabel()}, ), episodeMissingMetric: prometheus.NewDesc( "sonarr_episode_missing_total", "Total number of missing episodes", nil, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": conf.URLLabel()}, ), episodeQualitiesMetric: prometheus.NewDesc( "sonarr_episode_quality_total", "Total number of downloaded episodes by quality", []string{"quality"}, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": conf.URLLabel()}, ), errorMetric: prometheus.NewDesc( "sonarr_collector_error", "Error while collecting metrics", nil, - prometheus.Labels{"url": c.String("url")}, + prometheus.Labels{"url": conf.URLLabel()}, ), } } @@ -155,7 +153,7 @@ func (collector *sonarrCollector) Describe(ch chan<- *prometheus.Desc) { func (collector *sonarrCollector) Collect(ch chan<- prometheus.Metric) { total := time.Now() - c, err := client.NewClient(collector.config, collector.configFile) + c, err := client.NewClient(collector.config) if err != nil { log.Errorf("Error creating client: %s", err) ch <- prometheus.NewInvalidMetric(collector.errorMetric, err) @@ -215,7 +213,7 @@ func (collector *sonarrCollector) Collect(ch chan<- prometheus.Metric) { } } - if collector.config.Bool("enable-additional-metrics") { + if collector.config.EnableAdditionalMetrics { textra := time.Now() episodeFile := model.EpisodeFile{} params := map[string]string{"seriesId": fmt.Sprintf("%d", s.Id)} @@ -271,7 +269,7 @@ func (collector *sonarrCollector) Collect(ch chan<- prometheus.Metric) { ch <- prometheus.MustNewConstMetric(collector.episodeDownloadedMetric, prometheus.GaugeValue, float64(episodesDownloaded)) ch <- prometheus.MustNewConstMetric(collector.episodeMissingMetric, prometheus.GaugeValue, float64(episodesMissing.TotalRecords)) - if collector.config.Bool("enable-additional-metrics") { + if collector.config.EnableAdditionalMetrics { ch <- prometheus.MustNewConstMetric(collector.episodeMonitoredMetric, prometheus.GaugeValue, float64(episodesMonitored)) ch <- prometheus.MustNewConstMetric(collector.episodeUnmonitoredMetric, prometheus.GaugeValue, float64(episodesUnmonitored)) diff --git a/internal/commands/lidarr.go b/internal/commands/lidarr.go new file mode 100644 index 0000000..415e9e8 --- /dev/null +++ b/internal/commands/lidarr.go @@ -0,0 +1,34 @@ +package commands + +import ( + "github.com/prometheus/client_golang/prometheus" + "github.com/spf13/cobra" + + lidarrCollector "github.com/onedr0p/exportarr/internal/collector/lidarr" + sharedCollector "github.com/onedr0p/exportarr/internal/collector/shared" +) + +func init() { + rootCmd.AddCommand(lidarrCmd) +} + +var lidarrCmd = &cobra.Command{ + Use: "lidarr", + Short: "Prometheus Exporter for Lidarr", + Long: "Prometheus Exporter for Lidarr.", + RunE: func(cmd *cobra.Command, args []string) error { + conf.Arr = "lidarr" + conf.ApiVersion = "v1" + serveHttp(func(r *prometheus.Registry) { + r.MustRegister( + lidarrCollector.NewLidarrCollector(conf), + sharedCollector.NewQueueCollector(conf), + sharedCollector.NewHistoryCollector(conf), + sharedCollector.NewRootFolderCollector(conf), + sharedCollector.NewSystemStatusCollector(conf), + sharedCollector.NewSystemHealthCollector(conf), + ) + }) + return nil + }, +} diff --git a/internal/commands/prowlarr.go b/internal/commands/prowlarr.go new file mode 100644 index 0000000..3c65a68 --- /dev/null +++ b/internal/commands/prowlarr.go @@ -0,0 +1,32 @@ +package commands + +import ( + "github.com/prometheus/client_golang/prometheus" + "github.com/spf13/cobra" + + prowlarrCollector "github.com/onedr0p/exportarr/internal/collector/prowlarr" + sharedCollector "github.com/onedr0p/exportarr/internal/collector/shared" +) + +func init() { + rootCmd.AddCommand(prowlarrCmd) +} + +var prowlarrCmd = &cobra.Command{ + Use: "prowlarr", + Short: "Prometheus Exporter for Prowlarr", + Long: "Prometheus Exporter for Prowlarr.", + RunE: func(cmd *cobra.Command, args []string) error { + conf.Arr = "prowlarr" + conf.ApiVersion = "v1" + serveHttp(func(r *prometheus.Registry) { + r.MustRegister( + prowlarrCollector.NewProwlarrCollector(conf), + sharedCollector.NewHistoryCollector(conf), + sharedCollector.NewSystemStatusCollector(conf), + sharedCollector.NewSystemHealthCollector(conf), + ) + }) + return nil + }, +} diff --git a/internal/commands/radarr.go b/internal/commands/radarr.go new file mode 100644 index 0000000..6c174f1 --- /dev/null +++ b/internal/commands/radarr.go @@ -0,0 +1,33 @@ +package commands + +import ( + "github.com/prometheus/client_golang/prometheus" + "github.com/spf13/cobra" + + radarrCollector "github.com/onedr0p/exportarr/internal/collector/radarr" + sharedCollector "github.com/onedr0p/exportarr/internal/collector/shared" +) + +func init() { + rootCmd.AddCommand(radarrCmd) +} + +var radarrCmd = &cobra.Command{ + Use: "radarr", + Short: "Prometheus Exporter for Radarr", + Long: "Prometheus Exporter for Radarr.", + RunE: func(cmd *cobra.Command, args []string) error { + conf.Arr = "radarr" + serveHttp(func(r *prometheus.Registry) { + r.MustRegister( + radarrCollector.NewRadarrCollector(conf), + sharedCollector.NewQueueCollector(conf), + sharedCollector.NewHistoryCollector(conf), + sharedCollector.NewRootFolderCollector(conf), + sharedCollector.NewSystemStatusCollector(conf), + sharedCollector.NewSystemHealthCollector(conf), + ) + }) + return nil + }, +} diff --git a/internal/commands/readarr.go b/internal/commands/readarr.go new file mode 100644 index 0000000..e9218f5 --- /dev/null +++ b/internal/commands/readarr.go @@ -0,0 +1,34 @@ +package commands + +import ( + "github.com/prometheus/client_golang/prometheus" + "github.com/spf13/cobra" + + readarrCollector "github.com/onedr0p/exportarr/internal/collector/readarr" + sharedCollector "github.com/onedr0p/exportarr/internal/collector/shared" +) + +func init() { + rootCmd.AddCommand(readarrCmd) +} + +var readarrCmd = &cobra.Command{ + Use: "readarr", + Short: "Prometheus Exporter for Readarr", + Long: "Prometheus Exporter for Readarr.", + RunE: func(cmd *cobra.Command, args []string) error { + conf.Arr = "readarr" + conf.ApiVersion = "v1" + serveHttp(func(r *prometheus.Registry) { + r.MustRegister( + readarrCollector.NewReadarrCollector(conf), + sharedCollector.NewQueueCollector(conf), + sharedCollector.NewHistoryCollector(conf), + sharedCollector.NewRootFolderCollector(conf), + sharedCollector.NewSystemStatusCollector(conf), + sharedCollector.NewSystemHealthCollector(conf), + ) + }) + return nil + }, +} diff --git a/internal/commands/root.go b/internal/commands/root.go new file mode 100644 index 0000000..4cb9cb6 --- /dev/null +++ b/internal/commands/root.go @@ -0,0 +1,151 @@ +package commands + +import ( + "context" + "fmt" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + + "github.com/onedr0p/exportarr/internal/config" + "github.com/onedr0p/exportarr/internal/handlers" +) + +var GRACEFUL_TIMEOUT = 5 * time.Second + +var ( + conf = &config.Config{} + + rootCmd = &cobra.Command{ + Use: "exportarr", + Short: "exportarr is a AIO Prometheus exporter for *arr applications", + Long: `exportarr is a Prometheus exporter for *arr applications. +It can export metrics from Radarr, Sonarr, Lidarr, Readarr, and Prowlarr. +More information available at the Github Repo (https://github.com/onedr0p/exportarr)`, + } +) + +func Execute() error { + return rootCmd.Execute() +} + +func init() { + cobra.OnInitialize(validateFlags, initConfig) + + rootCmd.PersistentFlags().StringP("log-level", "l", "info", "Log level (debug, info, warn, error, fatal, panic)") + 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") + rootCmd.PersistentFlags().StringP("api-key-file", "f", "", "File containing API Key for *arr instance") + rootCmd.PersistentFlags().Int("port", 0, "Port to listen on") + rootCmd.PersistentFlags().StringP("interface", "i", "", "IP address to listen on") + rootCmd.PersistentFlags().Bool("disable-ssl-verify", false, "Disable SSL verification") + rootCmd.PersistentFlags().String("auth-username", "", "Username for basic auth") + rootCmd.PersistentFlags().String("auth-password", "", "Password for basic auth") + rootCmd.PersistentFlags().Bool("form-auth", false, "Use form based authentication") + rootCmd.PersistentFlags().Bool("enable-unknown-queue-items", false, "Enable unknown queue items") + rootCmd.PersistentFlags().Bool("enable-additional-metric", false, "Enable additional metric") +} + +// validateFlags performs logical validation of flags, content is validated in the config package +func validateFlags() { + flags := rootCmd.PersistentFlags() + apiKey, _ := flags.GetString("api-key") + apiKeyFile, _ := flags.GetString("api-key-file") + apiKeySet := apiKey != "" || apiKeyFile != "" + url, _ := flags.GetString("url") + xmlConfig, _ := flags.GetString("config") + + err := validation.Errors{ + "api-key": validation.Validate(apiKey, + validation.When(apiKeyFile != "", validation.Empty.Error("api-key and api-key-file are mutually exclusive")), + ), + "api-key-file": validation.Validate(apiKeyFile, + validation.When(apiKey != "", validation.Empty.Error("api-key and api-key-file are mutually exclusive")), + ), + "config": validation.Validate(xmlConfig, + validation.When(url == "" || !apiKeySet, validation.Required.Error("url & api-key must be set, or config must be set")), + validation.When(url != "" && apiKeySet, validation.Empty.Error("only two of url, api-key/api-key-file. and config can be set")), + validation.When(apiKey == "" && apiKeyFile == "", validation.Required.Error("one of api-key, api-key-file, or config is required")), + ), + } + if err.Filter() != nil { + for _, e := range err { + log.Fatal(e) + } + } +} + +func initConfig() { + var err error + conf, err = config.LoadConfig(rootCmd.PersistentFlags()) + if err != nil { + log.Fatal(err) + } + + if err := conf.Validate(); err != nil { + log.Fatal(err) + } + level, err := log.ParseLevel(conf.LogLevel) + if err != nil { + log.Errorf("Invalid log level %s, using default level: info", conf.LogLevel) + level = log.InfoLevel + } + log.SetLevel(level) +} + +type registerFunc func(registry *prometheus.Registry) + +func serveHttp(fn registerFunc) { + var srv http.Server + + idleConnsClosed := make(chan struct{}) + go func() { + sigchan := make(chan os.Signal, 1) + signal.Notify(sigchan, os.Interrupt) + signal.Notify(sigchan, syscall.SIGTERM) + sig := <-sigchan + log.Infof("Received signal %s, shutting down", 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) + } + close(idleConnsClosed) + }() + + registry := prometheus.NewRegistry() + fn(registry) + + handler := promhttp.HandlerFor(registry, promhttp.HandlerOpts{}) + http.HandleFunc("/", handlers.IndexHandler) + http.HandleFunc("/healthz", handlers.HealthzHandler) + http.Handle("/metrics", handler) + + log.Infof("Listening on %s:%d", conf.Interface, 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) + } + <-idleConnsClosed +} + +// 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) + handler.ServeHTTP(w, r) + }) +} diff --git a/internal/commands/sonarr.go b/internal/commands/sonarr.go new file mode 100644 index 0000000..6e41d18 --- /dev/null +++ b/internal/commands/sonarr.go @@ -0,0 +1,33 @@ +package commands + +import ( + "github.com/prometheus/client_golang/prometheus" + "github.com/spf13/cobra" + + sharedCollector "github.com/onedr0p/exportarr/internal/collector/shared" + sonarrCollector "github.com/onedr0p/exportarr/internal/collector/sonarr" +) + +func init() { + rootCmd.AddCommand(sonarrCmd) +} + +var sonarrCmd = &cobra.Command{ + Use: "sonarr", + Short: "Prometheus Exporter for Sonarr", + Long: "Prometheus Exporter for Sonarr.", + RunE: func(cmd *cobra.Command, args []string) error { + conf.Arr = "sonarr" + serveHttp(func(r *prometheus.Registry) { + r.MustRegister( + sonarrCollector.NewSonarrCollector(conf), + sharedCollector.NewQueueCollector(conf), + sharedCollector.NewHistoryCollector(conf), + sharedCollector.NewRootFolderCollector(conf), + sharedCollector.NewSystemStatusCollector(conf), + sharedCollector.NewSystemHealthCollector(conf), + ) + }) + return nil + }, +} diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..5bf41e2 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,131 @@ +package config + +import ( + "fmt" + "net/url" + "os" + "regexp" + "strings" + + "github.com/go-ozzo/ozzo-validation/is" + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/knadh/koanf/providers/confmap" + "github.com/knadh/koanf/providers/env" + "github.com/knadh/koanf/providers/file" + "github.com/knadh/koanf/providers/posflag" + "github.com/knadh/koanf/v2" + flag "github.com/spf13/pflag" +) + +type Config struct { + Arr string `koanf:"arr"` + LogLevel string `koanf:"log-level"` + URL string `koanf:"url"` + ApiKey string `koanf:"api-key"` + ApiKeyFile string `koanf:"api-key-file"` + ApiVersion string `koanf:"api-version"` + XMLConfig string `koanf:"config"` + Port int `koanf:"port"` + Interface string `koanf:"interface"` + DisableSSLVerify bool `koanf:"disable-ssl-verify"` + AuthUsername string `koanf:"auth-username"` + AuthPassword string `koanf:"auth-password"` + FormAuth bool `koanf:"form-auth"` + EnableUnknownQueueItems bool `koanf:"enable-unknown-queue-items"` + EnableAdditionalMetrics bool `koanf:"enable-additional-metric"` +} + +func (c *Config) UseBasicAuth() bool { + return !c.FormAuth && c.AuthUsername != "" && c.AuthPassword != "" +} + +func (c *Config) UseFormAuth() bool { + return c.FormAuth +} + +// URLLabel() exists for backwards compatibility -- prior versions built the URL in the client, +// meaning that the "url" metric label was missing the Port & base path that the XMLConfig provided. +func (c *Config) URLLabel() string { + if c.XMLConfig != "" { + u, err := url.Parse(c.URL) + if err != nil { + // Should be unreachable as long as we validate that the URL is valid in LoadConfig/Validate + return "Could Not Parse URL" + } + return u.Scheme + "://" + u.Host + } + return c.URL +} +func LoadConfig(flags *flag.FlagSet) (*Config, error) { + k := koanf.New(".") + + // Defaults + err := k.Load(confmap.Provider(map[string]interface{}{ + "log-level": "info", + "api-version": "v3", + "port": "8081", + "interface": "0.0.0.0", + }, "."), nil) + if err != nil { + return nil, err + } + + // Environment + err = k.Load(env.Provider("", ".", func(s string) string { + return strings.Replace(strings.ToLower(s), "_", "-", -1) + }), nil) + if err != nil { + return nil, err + } + + // Flags + if err = k.Load(posflag.Provider(flags, ".", k), nil); err != nil { + return nil, err + } + + // XMLConfig + xmlConfig := k.String("config") + if xmlConfig != "" { + err = k.Load(file.Provider(xmlConfig), XMLParser(), koanf.WithMergeFunc(XMLParser().Merge)) + if err != nil { + return nil, err + } + } + + // API Key File + apiKeyFile := k.String("api-key-file") + if apiKeyFile != "" { + data, err := os.ReadFile(apiKeyFile) + if err != nil { + return nil, fmt.Errorf("Couldn't Read API Key file %w", err) + } + + k.Set("api-key", string(data)) + } + + var out Config + if err := k.Unmarshal("", &out); err != nil { + return nil, err + } + + return &out, nil +} + +func (c *Config) Validate() error { + return validation.ValidateStruct(c, + validation.Field(&c.URL, validation.Required, is.URL), + validation.Field(&c.ApiKey, + validation.Required, + validation.Match(regexp.MustCompile(`([a-z0-9]{32})`)). + Error("Invalid API Key, must be 32 characters long and only contain lowercase letters and numbers")), + validation.Field(&c.Port, validation.Required), + validation.Field(&c.Interface, validation.Required, is.IP), + validation.Field(&c.AuthUsername, + validation.When(c.AuthPassword != "", validation.Required.Error("auth-username is required when auth-password is set")), + validation.When(c.FormAuth, validation.Required.Error("auth-username is required when form-auth is set"))), + validation.Field(&c.AuthPassword, + validation.When(c.AuthUsername != "", validation.Required.Error("auth-password is required when auth-username is set")), + validation.When(c.FormAuth, validation.Required.Error("auth-password is required when form-auth is set"))), + validation.Field(&c.ApiVersion, validation.Required, validation.In("v1", "v3")), + ) +} diff --git a/internal/config/config_test.go b/internal/config/config_test.go new file mode 100644 index 0000000..0371195 --- /dev/null +++ b/internal/config/config_test.go @@ -0,0 +1,307 @@ +package config + +import ( + "testing" + + "github.com/spf13/pflag" + "github.com/stretchr/testify/require" +) + +func testFlagSet() *pflag.FlagSet { + out := pflag.NewFlagSet("test", pflag.ContinueOnError) + out.StringP("log-level", "l", "info", "Log level (debug, info, warn, error, fatal, panic)") + out.StringP("config", "c", "", "*arr config.xml file for parsing authentication information") + out.StringP("url", "u", "", "URL to *arr instance") + out.StringP("api-key", "k", "", "API Key for *arr instance") + out.StringP("api-key-file", "f", "", "File containing API Key for *arr instance") + out.Int("port", 0, "Port to listen on") + out.StringP("interface", "i", "", "IP address to listen on") + out.Bool("disable-ssl-verify", false, "Disable SSL verification") + out.String("auth-username", "", "Username for basic auth") + out.String("auth-password", "", "Password for basic auth") + out.Bool("form-auth", false, "Use form based authentication") + out.Bool("enable-unknown-queue-items", false, "Enable unknown queue items") + out.Bool("enable-additional-metric", false, "Enable additional metric") + return out +} +func TestLoadConfig_Defaults(t *testing.T) { + require := require.New(t) + + config, err := LoadConfig(&pflag.FlagSet{}) + require.NoError(err) + require.Equal("info", config.LogLevel) + require.Equal("v3", config.ApiVersion) + require.Equal(8081, config.Port) + require.Equal("0.0.0.0", config.Interface) +} + +func TestLoadConfig_Flags(t *testing.T) { + flags := testFlagSet() + flags.Set("log-level", "debug") + flags.Set("url", "http://localhost:8989") + flags.Set("api-key", "abcdef0123456789abcdef0123456789") + flags.Set("port", "1234") + flags.Set("interface", "1.2.3.4") + flags.Set("disable-ssl-verify", "true") + flags.Set("auth-username", "user") + flags.Set("auth-password", "pass") + flags.Set("form-auth", "true") + flags.Set("enable-unknown-queue-items", "true") + flags.Set("enable-additional-metric", "true") + + require := require.New(t) + config, err := LoadConfig(flags) + require.NoError(err) + + require.Equal("debug", config.LogLevel) + require.Equal("http://localhost:8989", config.URL) + require.Equal("abcdef0123456789abcdef0123456789", config.ApiKey) + require.Equal(1234, config.Port) + require.Equal("1.2.3.4", config.Interface) + require.True(config.DisableSSLVerify) + require.Equal("user", config.AuthUsername) + require.Equal("pass", config.AuthPassword) + require.True(config.FormAuth) + require.True(config.EnableUnknownQueueItems) + require.True(config.EnableAdditionalMetrics) + // Defaults fall through + require.Equal("v3", config.ApiVersion) + require.True(config.UseFormAuth()) + require.False(config.UseBasicAuth()) + + flags.Set("form-auth", "false") + config, err = LoadConfig(flags) + require.NoError(err) + require.False(config.UseFormAuth()) + require.True(config.UseBasicAuth()) +} + +func TestLoadConfig_Environment(t *testing.T) { + require := require.New(t) + + // Set environment variables + t.Setenv("URL", "http://localhost:8989") + t.Setenv("API_KEY", "abcdef0123456789abcdef0123456789") + t.Setenv("PORT", "1234") + t.Setenv("INTERFACE", "1.2.3.4") + t.Setenv("DISABLE_SSL_VERIFY", "true") + t.Setenv("AUTH_USERNAME", "user") + t.Setenv("AUTH_PASSWORD", "pass") + t.Setenv("FORM_AUTH", "true") + t.Setenv("ENABLE_UNKNOWN_QUEUE_ITEMS", "true") + t.Setenv("ENABLE_ADDITIONAL_METRIC", "true") + + config, err := LoadConfig(&pflag.FlagSet{}) + require.NoError(err) + + require.Equal("http://localhost:8989", config.URL) + require.Equal("abcdef0123456789abcdef0123456789", config.ApiKey) + require.Equal(1234, config.Port) + require.Equal("1.2.3.4", config.Interface) + require.True(config.DisableSSLVerify) + require.Equal("user", config.AuthUsername) + require.Equal("pass", config.AuthPassword) + require.True(config.FormAuth) + require.True(config.EnableUnknownQueueItems) + require.True(config.EnableAdditionalMetrics) + // Defaults fall through + require.Equal("v3", config.ApiVersion) +} + +func TestLoadConfig_XMLConfig(t *testing.T) { + flags := testFlagSet() + flags.Set("config", "test_fixtures/config.test_xml") + flags.Set("url", "http://localhost") + + require := require.New(t) + config, err := LoadConfig(flags) + require.NoError(err) + + require.Equal("http://localhost:7878/asdf", config.URL) + require.Equal("abcdef0123456789abcdef0123456789", config.ApiKey) +} + +func TestLoadConfig_ApiKeyFile(t *testing.T) { + flags := testFlagSet() + flags.Set("api-key-file", "test_fixtures/api_key") + + require := require.New(t) + config, err := LoadConfig(flags) + require.NoError(err) + + require.Equal("abcdef0123456789abcdef0123456783", config.ApiKey) +} + +func TestLoadConfig_OverrideOrder(t *testing.T) { + + require := require.New(t) + flags := testFlagSet() + + t.Setenv("API_KEY", "abcdef0123456789abcdef0123456781") + config, err := LoadConfig(flags) + require.NoError(err) + require.Equal("abcdef0123456789abcdef0123456781", config.ApiKey) + + flags.Set("api-key", "abcdef0123456789abcdef0123456780") + + config, err = LoadConfig(flags) + require.NoError(err) + require.Equal("abcdef0123456789abcdef0123456780", config.ApiKey) + + flags.Set("config", "test_fixtures/config.test_xml") + config, err = LoadConfig(flags) + require.NoError(err) + require.Equal("abcdef0123456789abcdef0123456789", config.ApiKey) + + flags.Set("api-key-file", "test_fixtures/api_key") + config, err = LoadConfig(flags) + require.NoError(err) + require.Equal("abcdef0123456789abcdef0123456783", config.ApiKey) +} + +func TestValidate(t *testing.T) { + parameters := []struct { + name string + config *Config + shouldError bool + }{ + { + name: "good", + config: &Config{ + LogLevel: "debug", + URL: "http://localhost", + ApiKey: "abcdef0123456789abcdef0123456789", + ApiVersion: "v3", + Port: 1234, + Interface: "0.0.0.0", + }, + }, + { + name: "good-basic-auth", + config: &Config{ + LogLevel: "debug", + URL: "http://localhost", + ApiKey: "abcdef0123456789abcdef0123456789", + ApiVersion: "v3", + Port: 1234, + Interface: "0.0.0.0", + AuthUsername: "user", + AuthPassword: "pass", + }, + }, + { + name: "good-form-auth", + config: &Config{ + LogLevel: "debug", + URL: "http://localhost", + ApiKey: "abcdef0123456789abcdef0123456789", + ApiVersion: "v3", + Port: 1234, + Interface: "0.0.0.0", + AuthUsername: "user", + AuthPassword: "pass", + FormAuth: true, + }, + }, + { + name: "bad-api-key", + config: &Config{ + LogLevel: "debug", + URL: "http://localhost", + ApiKey: "abcdef0123456789abcdef01234567", + ApiVersion: "v3", + Port: 1234, + Interface: "0.0.0.0", + }, + shouldError: true, + }, + { + name: "bad-api-version", + config: &Config{ + LogLevel: "debug", + URL: "http://localhost", + ApiKey: "abcdef0123456789abcdef0123456789", + ApiVersion: "v2", + Port: 1234, + Interface: "0.0.0.0", + }, + shouldError: true, + }, + { + name: "missing-port", + config: &Config{ + LogLevel: "debug", + URL: "http://localhost", + ApiKey: "abcdef0123456789abcdef0123456789", + ApiVersion: "v3", + Port: 0, + Interface: "0.0.0.0", + }, + shouldError: true, + }, + { + name: "bad-interface", + config: &Config{ + LogLevel: "debug", + URL: "http://localhost", + ApiKey: "abcdef0123456789abcdef0123456789", + ApiVersion: "v3", + Port: 1234, + Interface: "0.0.0", + }, + shouldError: true, + }, + { + name: "password-needs-username", + config: &Config{ + LogLevel: "debug", + URL: "http://localhost", + ApiKey: "abcdef0123456789abcdef0123456789", + ApiVersion: "v3", + Port: 1234, + Interface: "0.0.0.0", + AuthPassword: "password", + }, + shouldError: true, + }, + { + name: "username-needs-password", + config: &Config{ + LogLevel: "debug", + URL: "http://localhost", + ApiKey: "abcdef0123456789abcdef0123456789", + ApiVersion: "v3", + Port: 1234, + Interface: "0.0.0.0", + AuthUsername: "username", + }, + shouldError: true, + }, + { + name: "form-auth-needs-user-and-password", + config: &Config{ + LogLevel: "debug", + URL: "http://localhost", + ApiKey: "abcdef0123456789abcdef0123456789", + ApiVersion: "v3", + Port: 1234, + Interface: "0.0.0.0", + FormAuth: true, + }, + shouldError: true, + }, + } + + for _, p := range parameters { + t.Run(p.name, func(t *testing.T) { + require := require.New(t) + + err := p.config.Validate() + if p.shouldError { + require.Error(err) + } else { + require.NoError(err) + } + }) + } +} diff --git a/internal/config/test_fixtures/api_key b/internal/config/test_fixtures/api_key new file mode 100644 index 0000000..abdfe76 --- /dev/null +++ b/internal/config/test_fixtures/api_key @@ -0,0 +1 @@ +abcdef0123456789abcdef0123456783 \ No newline at end of file diff --git a/internal/config/test_fixtures/config.test_xml b/internal/config/test_fixtures/config.test_xml new file mode 100644 index 0000000..bf1e8f6 --- /dev/null +++ b/internal/config/test_fixtures/config.test_xml @@ -0,0 +1,5 @@ + + /asdf + abcdef0123456789abcdef0123456789 + 7878 + \ No newline at end of file diff --git a/internal/config/xml_parser.go b/internal/config/xml_parser.go new file mode 100644 index 0000000..8d99dd7 --- /dev/null +++ b/internal/config/xml_parser.go @@ -0,0 +1,53 @@ +package config + +import ( + "encoding/xml" + "errors" + "net/url" +) + +type xmlConfig struct { + XMLName xml.Name `xml:"Config"` + ApiKey string `xml:"ApiKey"` + Port string `xml:"Port"` + UrlBase string `xml:"UrlBase"` +} + +type XML struct{} + +func XMLParser() *XML { + return &XML{} +} + +func (p *XML) Unmarshal(b []byte) (map[string]interface{}, error) { + var config xmlConfig + if err := xml.Unmarshal(b, &config); err != nil { + return nil, err + } + + return map[string]interface{}{ + "api-key": config.ApiKey, + "url-base": config.UrlBase, + "target-port": config.Port, + }, nil +} + +func (p *XML) Marshal(o map[string]interface{}) ([]byte, error) { + return nil, errors.New("not implemented") +} + +func (p *XML) Merge(src, dest map[string]interface{}) error { + dest["api-key"] = src["api-key"] + dest["api-version"] = src["api-version"] + + u, err := url.Parse(dest["url"].(string)) + if err != nil { + return err + } + + // Add or replace target port + u.Host = u.Hostname() + ":" + src["target-port"].(string) + u = u.JoinPath(src["url-base"].(string)) + dest["url"] = u.String() + return nil +} diff --git a/internal/utils/utils.go b/internal/utils/utils.go deleted file mode 100644 index 5e921c9..0000000 --- a/internal/utils/utils.go +++ /dev/null @@ -1,55 +0,0 @@ -package utils - -import ( - "encoding/xml" - "fmt" - "io/ioutil" - "net/url" - "os" - "regexp" - - "github.com/onedr0p/exportarr/internal/model" - log "github.com/sirupsen/logrus" -) - -// IsValidApikey - Check if the API Key is 32 characters and only a-z0-9 -func IsValidApikey(str string) bool { - found, err := regexp.MatchString("([a-z0-9]{32})", str) - if err != nil { - return false - } - return found -} - -// IsValidUrl - Checks if the URL is valid -func IsValidUrl(str string) bool { - u, err := url.Parse(str) - return err == nil && u.Scheme != "" && u.Host != "" -} - -// IsFileThere - Checks if the file is there -func IsFileThere(filename string) bool { - info, err := os.Stat(filename) - if os.IsNotExist(err) { - return false - } - return !info.IsDir() -} - -// GetArrConfigFromFile - Get the config from config.xml -func GetArrConfigFromFile(file string) (*model.Config, error) { - xmlFile, err := os.Open(file) - if err != nil { - fmt.Println(err) - return nil, err - } - defer xmlFile.Close() - byteValue, _ := ioutil.ReadAll(xmlFile) - - var config model.Config - xml.Unmarshal(byteValue, &config) - - log.Infof("Getting Config from %s", file) - - return &config, nil -}