From 3d59a7a462ef2c9e4d1549335ae6fa3f7010da8e Mon Sep 17 00:00:00 2001 From: Russell Troxel Date: Mon, 13 Mar 2023 15:59:20 -0700 Subject: [PATCH] [Refactor] (1/2) Refactor `client` to leverage a custom Transport & `url.URL` (#106) * Refactor Client & Add Retries Signed-off-by: Russell Troxel * Add Tests Signed-off-by: Russell Troxel * Catch a few missed DoRequest callsites Signed-off-by: Russell Troxel --------- Signed-off-by: Russell Troxel --- go.mod | 4 + go.sum | 31 ++--- internal/client/client.go | 134 ++++++++------------ internal/client/client_test.go | 123 ++++++++++++++++++ internal/client/transport.go | 59 +++++++++ internal/client/transport_test.go | 158 ++++++++++++++++++++++++ internal/collector/lidarr/music.go | 20 ++- internal/collector/prowlarr/stats.go | 21 +++- internal/collector/radarr/movie.go | 13 +- internal/collector/readarr/author.go | 13 +- internal/collector/shared/health.go | 13 +- internal/collector/shared/history.go | 13 +- internal/collector/shared/queue.go | 24 +++- internal/collector/shared/rootfolder.go | 13 +- internal/collector/shared/status.go | 14 ++- internal/collector/sonarr/series.go | 21 +++- internal/utils/utils.go | 10 -- 17 files changed, 546 insertions(+), 138 deletions(-) create mode 100644 internal/client/client_test.go create mode 100644 internal/client/transport.go create mode 100644 internal/client/transport_test.go diff --git a/go.mod b/go.mod index 6cb5840..146bea2 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.19 require ( github.com/prometheus/client_golang v1.14.0 github.com/sirupsen/logrus v1.9.0 + github.com/stretchr/testify v1.7.0 github.com/urfave/cli/v2 v2.25.0 ) @@ -12,8 +13,10 @@ require ( 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/golang/protobuf v1.5.2 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // 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 @@ -21,4 +24,5 @@ require ( github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // 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 d991d1f..a0b1502 100644 --- a/go.sum +++ b/go.sum @@ -51,8 +51,6 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU= -github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -113,8 +111,8 @@ github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -144,8 +142,10 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o 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= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +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= @@ -165,25 +165,18 @@ github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXP github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= -github.com/prometheus/client_golang v1.12.1 h1:ZiaPsmm9uiBeaSMRznKsCDNtPCS0T3JVDGF+06gjBzk= github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= -github.com/prometheus/client_golang v1.12.2 h1:51L9cDoUHVrXx4zWYlcLQIZ+d+VXHgqnYKkIuq4g/34= -github.com/prometheus/client_golang v1.12.2/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= -github.com/prometheus/client_golang v1.13.0 h1:b71QUfeo5M8gq2+evJdTPfZhYMAU0uKPkyPJ7TPsloU= -github.com/prometheus/client_golang v1.13.0/go.mod h1:vTeo+zgvILHsnnj/39Ou/1fPN5nJFOEMgftOUOmlvYQ= github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4= github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE= github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= @@ -191,7 +184,6 @@ github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= @@ -201,21 +193,15 @@ 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.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= -github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 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/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 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/urfave/cli/v2 v2.4.0 h1:m2pxjjDFgDxSPtO8WSdbndj17Wu2y8vOT86wE/tjr+I= -github.com/urfave/cli/v2 v2.4.0/go.mod h1:NX9W0zmTvedE5oDoOMs2RTC8RvdK98NTYZE5LbaEYPg= -github.com/urfave/cli/v2 v2.10.3 h1:oi571Fxz5aHugfBAJd5nkwSk3fzATXtMlpxdLylSCMo= -github.com/urfave/cli/v2 v2.10.3/go.mod h1:f8iq5LtQ/bLxafbdBSLPPNsgaW0l/2fYYEHhAyPlwvo= github.com/urfave/cli/v2 v2.11.1 h1:UKK6SP7fV3eKOefbS87iT9YHefv7iB/53ih6e+GNAsE= github.com/urfave/cli/v2 v2.11.1/go.mod h1:f8iq5LtQ/bLxafbdBSLPPNsgaW0l/2fYYEHhAyPlwvo= github.com/urfave/cli/v2 v2.25.0 h1:ykdZKuQey2zq0yin/l7JOm9Mh+pg72ngYMeB0ABn6q8= @@ -326,7 +312,6 @@ golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -351,7 +336,6 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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 h1:XfKQ4OlFl8okEOr5UvAqFRVj8pY/4yfcXrddB8qAbU0= 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= @@ -410,7 +394,6 @@ golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= @@ -486,24 +469,24 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/internal/client/client.go b/internal/client/client.go index b3dd124..c51fc32 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -1,121 +1,95 @@ package client import ( - "compress/gzip" "crypto/tls" - "encoding/base64" "encoding/json" - "errors" "fmt" - "io" "net/http" + "net/url" "os" "github.com/onedr0p/exportarr/internal/model" - "github.com/onedr0p/exportarr/internal/utils" log "github.com/sirupsen/logrus" "github.com/urfave/cli/v2" ) // Client struct is a Radarr client to request an instance of a Radarr type Client struct { - config *cli.Context - configFile *model.Config httpClient http.Client + URL url.URL } // NewClient method initializes a new Radarr client. -func NewClient(c *cli.Context, cf *model.Config) *Client { +func NewClient(c *cli.Context, cf *model.Config) (*Client, error) { + var baseURL *url.URL + auth := AuthConfig{ + Username: c.String("basic-auth-username"), + Password: c.String("basic-auth-password"), + } + + 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, "api", apiVersion) + auth.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) + } + baseURL = baseURL.JoinPath("api", apiVersion) + + if c.String("api-key") != "" { + auth.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) + } + auth.ApiKey = string(data) + } + } + baseTransport := http.DefaultTransport + if c.Bool("disable-ssl-verify") { + baseTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + } return &Client{ - config: c, - configFile: cf, httpClient: http.Client{ CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, + Transport: NewArrTransport(auth, baseTransport), }, - } + URL: *baseURL, + }, nil } // DoRequest - Take a HTTP Request and return Unmarshaled data -func (c *Client) DoRequest(endpoint string, target interface{}) error { - apiVersion := c.configFile.ApiVersion - - var url string - var apiKey string - - // Use the values from config.xml if using the config flag - if c.config.String("config") != "" { - url = fmt.Sprintf("%s:%s%s/api/%s/%s", - c.config.String("url"), - c.configFile.Port, - utils.FormatURLBase(c.configFile.UrlBase), - apiVersion, - endpoint, - ) - apiKey = c.configFile.ApiKey - } else { - // Otherwise use the value provided in the api-key flag - url = fmt.Sprintf("%s/api/%s/%s", - c.config.String("url"), - apiVersion, - endpoint, - ) - if c.config.String("api-key") != "" { - apiKey = c.config.String("api-key") - } else if c.config.String("api-key-file") != "" { - data, err := os.ReadFile(c.config.String("api-key-file")) - if err != nil { - log.Fatalf("An error has occurred during reading of API Key file %v", err) - return err - } - apiKey = string(data) +func (c *Client) DoRequest(endpoint string, target interface{}, queryParams ...map[string]string) error { + for _, m := range queryParams { + for k, v := range m { + c.URL.Query().Add(k, v) } } - + url := c.URL.JoinPath(endpoint).String() log.Infof("Sending HTTP request to %s", url) - http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: c.config.Bool("disable-ssl-verify")} req, err := http.NewRequest("GET", url, nil) if err != nil { - log.Fatalf("An error has occurred when creating HTTP request %v", err) - return err + return fmt.Errorf("Failed to create HTTP Request(%s): %w", url, err) } - - if c.config.String("basic-auth-username") != "" && c.config.String("basic-auth-password") != "" { - req.Header.Add("Authorization", fmt.Sprintf("Basic %s", - base64.StdEncoding.EncodeToString([]byte(c.config.String("basic-auth-username")+":"+c.config.String("basic-auth-password"))), - )) - } - req.Header.Add("X-Api-Key", apiKey) - req.Header.Add("Accept-Encoding", "gzip") - resp, err := c.httpClient.Do(req) if err != nil { - log.Fatalf("An error has occurred during retrieving statistics %v", err) - return err - } - if !(resp.StatusCode >= 200 && resp.StatusCode < 300) { - errMsg := fmt.Sprintf("An error has occurred during retrieving statistics from %s HTTP status %s", url, resp.Status) - if location := resp.Header.Get("Location"); location != "" { - errMsg += " (Location: " + location + ")" - } - log.Fatal(errMsg) - return errors.New(errMsg) + return fmt.Errorf("Failed to execute HTTP Request(%s): %w", url, err) } defer resp.Body.Close() - - var bodyReader io.ReadCloser - switch resp.Header.Get("Content-Encoding") { - case "gzip": - bodyReader, err = gzip.NewReader(resp.Body) - if err != nil { - log.Fatalf("An error has occurred reading gzipped statistics %v", err) - return err - } - defer bodyReader.Close() - default: - bodyReader = resp.Body - } - return json.NewDecoder(bodyReader).Decode(target) + return json.NewDecoder(resp.Body).Decode(target) } diff --git a/internal/client/client_test.go b/internal/client/client_test.go new file mode 100644 index 0000000..2e80d82 --- /dev/null +++ b/internal/client/client_test.go @@ -0,0 +1,123 @@ +package client + +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" +) + +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"}, + } + + 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) + 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") +} + +func TestDoRequest(t *testing.T) { + parameters := []struct { + name string + endpoint string + queryParams map[string]string + expectedURL string + }{ + { + name: "noParams", + endpoint: "queue", + expectedURL: "http://localhost:7878/api/v3/queue", + }, + { + name: "params", + endpoint: "test", + queryParams: map[string]string{ + "page": "1", + "testParam": "asdf", + }, + expectedURL: "http://localhost:7878/api/v3/test?page=1&testParam=asdf", + }, + } + for _, param := range parameters { + t.Run(param.name, func(t *testing.T) { + require := require.New(t) + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, "{\"test\": \"asdf2\"}") + })) + defer ts.Close() + + c := testContext(map[string]string{ + "url": ts.URL, + }) + cf := model.NewConfig() + + target := struct { + Test string `json:"test"` + }{} + expected := target + expected.Test = "asdf2" + client, err := NewClient(c, cf) + 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) + require.Nil(err, "DoRequest should not return an error: %s", err) + require.Equal(expected, target, "DoRequest should return the correct data") + }) + } +} diff --git a/internal/client/transport.go b/internal/client/transport.go new file mode 100644 index 0000000..1382a1b --- /dev/null +++ b/internal/client/transport.go @@ -0,0 +1,59 @@ +package client + +import ( + "fmt" + "net/http" +) + +// ArrTransport is a http.RoundTripper that adds authentication to requests +type ArrTransport struct { + inner http.RoundTripper + auth AuthConfig +} + +type AuthConfig struct { + Username string + Password string + ApiKey string +} + +func NewArrTransport(auth AuthConfig, inner http.RoundTripper) *ArrTransport { + return &ArrTransport{ + inner: inner, + auth: auth, + } +} + +func (t *ArrTransport) RoundTrip(req *http.Request) (*http.Response, error) { + if t.auth.Username != "" && t.auth.Password != "" { + req.SetBasicAuth(t.auth.Username, t.auth.Password) + } + req.Header.Add("X-Api-Key", t.auth.ApiKey) + + resp, err := t.inner.RoundTrip(req) + if err != nil || resp.StatusCode >= 500 { + retries := 2 + for i := 0; i < retries; i++ { + resp, err = t.inner.RoundTrip(req) + if err == nil && resp.StatusCode < 500 { + return resp, nil + } + } + if err != nil { + return nil, fmt.Errorf("Error sending HTTP Request: %w", err) + } else { + return nil, fmt.Errorf("Received Server Error Status Code: %d", resp.StatusCode) + } + } + if resp.StatusCode >= 400 && resp.StatusCode <= 499 { + return nil, fmt.Errorf("Received Client Error Status Code: %d", resp.StatusCode) + } + if resp.StatusCode >= 300 && resp.StatusCode <= 399 { + if location, err := resp.Location(); err == nil { + return nil, fmt.Errorf("Received Redirect Status Code: %d, Location: %s", resp.StatusCode, location.String()) + } else { + return nil, fmt.Errorf("Received Redirect Status Code: %d, ", resp.StatusCode) + } + } + return resp, nil +} diff --git a/internal/client/transport_test.go b/internal/client/transport_test.go new file mode 100644 index 0000000..63b2183 --- /dev/null +++ b/internal/client/transport_test.go @@ -0,0 +1,158 @@ +package client + +import ( + "encoding/base64" + "fmt" + "net/http" + "testing" + + "github.com/stretchr/testify/require" +) + +var ( + TEST_USER = "testuser1" + TEST_PASS = "hunter2" + TEST_KEY = "abcdef1234567890abcdef1234567890" +) + +type testRoundTripFunc func(req *http.Request) (*http.Response, error) + +func (t testRoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { + return t(req) +} + +func TestRoundTrip_Auth(t *testing.T) { + parameters := []struct { + name string + auth AuthConfig + testFunc func(req *http.Request) (*http.Response, error) + }{ + { + name: "BasicAuth", + auth: AuthConfig{ + Username: TEST_USER, + Password: TEST_PASS, + ApiKey: TEST_KEY, + }, + testFunc: func(req *http.Request) (*http.Response, error) { + require.NotNil(t, req, "Request should not be nil") + require.NotNil(t, req.Header, "Request header should not be nil") + require.NotEmpty(t, req.Header.Get("Authorization"), "Authorization header should be set") + require.Equal( + t, + "Basic "+base64.StdEncoding.EncodeToString([]byte(TEST_USER+":"+TEST_PASS)), + req.Header.Get("Authorization"), + "Authorization Header set to wrong value", + ) + require.NotEmpty(t, req.Header.Get("X-Api-Key"), "X-Api-Key header should be set") + require.Equal(t, TEST_KEY, req.Header.Get("X-Api-Key"), "X-Api-Key Header set to wrong value") + return &http.Response{ + StatusCode: 200, + Body: nil, + Header: make(http.Header), + }, nil + }, + }, + { + name: "ApiKey", + auth: AuthConfig{ + Username: "", + Password: "", + ApiKey: TEST_KEY, + }, + testFunc: func(req *http.Request) (*http.Response, error) { + require.NotNil(t, req, "Request should not be nil") + require.NotNil(t, req.Header, "Request header should not be nil") + require.Empty(t, req.Header.Get("Authorization"), "Authorization header should be empty") + require.NotEmpty(t, req.Header.Get("X-Api-Key"), "X-Api-Key header should be set") + require.Equal(t, TEST_KEY, req.Header.Get("X-Api-Key"), "X-Api-Key Header set to wrong value") + return &http.Response{ + StatusCode: 200, + Body: nil, + Header: make(http.Header), + }, nil + }, + }, + } + for _, param := range parameters { + t.Run(param.name, func(t *testing.T) { + transport := NewArrTransport(param.auth, testRoundTripFunc(param.testFunc)) + client := &http.Client{Transport: transport} + req, err := http.NewRequest("GET", "http://example.com", nil) + require.Nil(t, err, "Error creating request: %s", err) + _, err = client.Do(req) + require.Nil(t, err, "Error sending request: %s", err) + }) + } +} + +func TestRoundTrip_Retries(t *testing.T) { + parameters := []struct { + name string + testFunc func(req *http.Request) (*http.Response, error) + }{ + { + name: "500", + testFunc: func(req *http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 500, + Body: nil, + Header: make(http.Header), + }, nil + }, + }, + { + name: "Err", + testFunc: func(req *http.Request) (*http.Response, error) { + return nil, &http.ProtocolError{} + }, + }, + } + for _, param := range parameters { + t.Run(param.name, func(t *testing.T) { + require := require.New(t) + auth := AuthConfig{ + ApiKey: TEST_KEY, + } + attempts := 0 + transport := NewArrTransport(auth, testRoundTripFunc(func(req *http.Request) (*http.Response, error) { + attempts++ + return param.testFunc(req) + })) + client := &http.Client{Transport: transport} + req, err := http.NewRequest("GET", "http://example.com", nil) + require.Nil(err, "Error creating request: %s", err) + _, err = client.Do(req) + require.NotNil(err, "Error should be returned from Do()") + require.Equal(3, attempts, "Should retry 3 times") + }) + } +} + +func TestRoundTrip_StatusCodes(t *testing.T) { + parameters := []int{200, 201, 202, 204, 301, 302, 400, 401, 403, 404, 500, 503} + for _, param := range parameters { + t.Run(fmt.Sprintf("%d", param), func(t *testing.T) { + require := require.New(t) + auth := AuthConfig{ + ApiKey: TEST_KEY, + } + transport := NewArrTransport(auth, testRoundTripFunc(func(req *http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: param, + Body: nil, + Header: make(http.Header), + }, nil + })) + client := &http.Client{Transport: transport} + req, err := http.NewRequest("GET", "http://example.com", nil) + require.Nil(err, "Error creating request: %s", err) + _, err = client.Do(req) + if param >= 200 && param < 300 { + require.Nil(err, "Should Not error on 2XX: %s", err) + } else { + require.NotNil(err, "Should error on non-2XX") + } + }) + } +} diff --git a/internal/collector/lidarr/music.go b/internal/collector/lidarr/music.go index 8d171c4..b1e8d1e 100644 --- a/internal/collector/lidarr/music.go +++ b/internal/collector/lidarr/music.go @@ -122,8 +122,18 @@ func (collector *lidarrCollector) Describe(ch chan<- *prometheus.Desc) { } func (collector *lidarrCollector) Collect(ch chan<- prometheus.Metric) { - c := client.NewClient(collector.config, collector.configFile) - + c, err := client.NewClient(collector.config, collector.configFile) + if err != nil { + log.Errorf("Error creating client: %w", err) + ch <- prometheus.NewInvalidMetric( + prometheus.NewDesc( + "lidarr_collector_error", + "Error Collecting from Lidarr", + nil, + prometheus.Labels{"url": collector.config.String("url")}), + err) + return + } var artistsFileSize int64 var ( artistsMonitored = 0 @@ -156,7 +166,8 @@ func (collector *lidarrCollector) Collect(ch chan<- prometheus.Metric) { if collector.config.Bool("enable-additional-metrics") { songFile := model.SongFile{} - if err := c.DoRequest(fmt.Sprintf("%s?artistid=%d", "trackfile", s.Id), &songFile); err != nil { + params := map[string]string{"artistid": fmt.Sprintf("%d", s.Id)} + if err := c.DoRequest("trackfile", &songFile, params); err != nil { log.Fatal(err) } for _, e := range songFile { @@ -166,7 +177,8 @@ func (collector *lidarrCollector) Collect(ch chan<- prometheus.Metric) { } album := model.Album{} - if err := c.DoRequest(fmt.Sprintf("%s?artistid=%d", "album", s.Id), &album); err != nil { + params = map[string]string{"artistid": fmt.Sprintf("%d", s.Id)} + if err := c.DoRequest("album", &album, params); err != nil { log.Fatal(err) } for _, a := range album { diff --git a/internal/collector/prowlarr/stats.go b/internal/collector/prowlarr/stats.go index 23a61d0..e1dfe98 100644 --- a/internal/collector/prowlarr/stats.go +++ b/internal/collector/prowlarr/stats.go @@ -1,7 +1,6 @@ package collector import ( - "fmt" "sync" "time" @@ -240,7 +239,18 @@ func (collector *prowlarrCollector) Describe(ch chan<- *prometheus.Desc) { func (collector *prowlarrCollector) Collect(ch chan<- prometheus.Metric) { total := time.Now() - c := client.NewClient(collector.config, collector.configFile) + c, err := client.NewClient(collector.config, collector.configFile) + if err != nil { + log.Errorf("Error creating client: %w", err) + ch <- prometheus.NewInvalidMetric( + prometheus.NewDesc( + "prowlarr_collector_error", + "Error Collecting from Prowlarr", + nil, + prometheus.Labels{"url": collector.config.String("url")}), + err) + return + } var enabledIndexers = 0 @@ -268,8 +278,11 @@ func (collector *prowlarrCollector) Collect(ch chan<- prometheus.Metric) { stats := model.IndexerStatResponse{} startDate := collector.lastStatUpdate.In(time.UTC) endDate := time.Now().In(time.UTC) - req := fmt.Sprintf("indexerstats?startDate=%s&endDate=%s", startDate.Format(time.RFC3339), endDate.Format(time.RFC3339)) - if err := c.DoRequest(req, &stats); err != nil { + params := map[string]string{ + "startDate": startDate.Format(time.RFC3339), + "endDate": endDate.Format(time.RFC3339), + } + if err := c.DoRequest("indexerstats", &stats, params); err != nil { log.Fatal(err) } collector.lastStatUpdate = endDate diff --git a/internal/collector/radarr/movie.go b/internal/collector/radarr/movie.go index 53909d0..d3451bc 100644 --- a/internal/collector/radarr/movie.go +++ b/internal/collector/radarr/movie.go @@ -88,7 +88,18 @@ func (collector *radarrCollector) Describe(ch chan<- *prometheus.Desc) { } func (collector *radarrCollector) Collect(ch chan<- prometheus.Metric) { - c := client.NewClient(collector.config, collector.configFile) + c, err := client.NewClient(collector.config, collector.configFile) + if err != nil { + log.Errorf("Error creating client: %w", err) + ch <- prometheus.NewInvalidMetric( + prometheus.NewDesc( + "radarr_collector_error", + "Error Collecting from Radarr", + nil, + prometheus.Labels{"url": collector.config.String("url")}), + err) + return + } var fileSize int64 var ( downloaded = 0 diff --git a/internal/collector/readarr/author.go b/internal/collector/readarr/author.go index 8a84987..a7f5627 100644 --- a/internal/collector/readarr/author.go +++ b/internal/collector/readarr/author.go @@ -114,7 +114,18 @@ func (c *readarrCollector) Describe(ch chan<- *prometheus.Desc) { func (collector *readarrCollector) Collect(ch chan<- prometheus.Metric) { total := time.Now() - c := client.NewClient(collector.config, collector.configFile) + c, err := client.NewClient(collector.config, collector.configFile) + if err != nil { + log.Errorf("Error creating client: %w", err) + ch <- prometheus.NewInvalidMetric( + prometheus.NewDesc( + "readarr_collector_error", + "Error Collecting from Readarr", + nil, + prometheus.Labels{"url": collector.config.String("url")}), + err) + return + } tauthors := []time.Duration{} var authorsFileSize int64 var ( diff --git a/internal/collector/shared/health.go b/internal/collector/shared/health.go index 0be3e94..8f93aa1 100644 --- a/internal/collector/shared/health.go +++ b/internal/collector/shared/health.go @@ -34,7 +34,18 @@ func (collector *systemHealthCollector) Describe(ch chan<- *prometheus.Desc) { } func (collector *systemHealthCollector) Collect(ch chan<- prometheus.Metric) { - c := client.NewClient(collector.config, collector.configFile) + c, err := client.NewClient(collector.config, collector.configFile) + if err != nil { + log.Errorf("Error creating client: %w", err) + ch <- prometheus.NewInvalidMetric( + prometheus.NewDesc( + fmt.Sprintf("%s_collector_error", collector.config.Command.Name), + "Error Collecting metrics", + nil, + prometheus.Labels{"url": collector.config.String("url")}), + err) + return + } systemHealth := model.SystemHealth{} if err := c.DoRequest("health", &systemHealth); err != nil { log.Fatal(err) diff --git a/internal/collector/shared/history.go b/internal/collector/shared/history.go index 74141c9..b597b56 100644 --- a/internal/collector/shared/history.go +++ b/internal/collector/shared/history.go @@ -34,7 +34,18 @@ func (collector *historyCollector) Describe(ch chan<- *prometheus.Desc) { } func (collector *historyCollector) Collect(ch chan<- prometheus.Metric) { - c := client.NewClient(collector.config, collector.configFile) + c, err := client.NewClient(collector.config, collector.configFile) + if err != nil { + log.Errorf("Error creating client: %w", err) + ch <- prometheus.NewInvalidMetric( + prometheus.NewDesc( + fmt.Sprintf("%s_collector_error", collector.config.Command.Name), + "Error Collecting metrics", + nil, + prometheus.Labels{"url": collector.config.String("url")}), + err) + return + } history := model.History{} if err := c.DoRequest("history", &history); err != nil { log.Fatal(err) diff --git a/internal/collector/shared/queue.go b/internal/collector/shared/queue.go index e8a6e81..2fb8107 100644 --- a/internal/collector/shared/queue.go +++ b/internal/collector/shared/queue.go @@ -34,19 +34,30 @@ func (collector *queueCollector) Describe(ch chan<- *prometheus.Desc) { } func (collector *queueCollector) Collect(ch chan<- prometheus.Metric) { - c := client.NewClient(collector.config, collector.configFile) + c, err := client.NewClient(collector.config, collector.configFile) + if err != nil { + log.Errorf("Error creating client: %w", err) + ch <- prometheus.NewInvalidMetric( + prometheus.NewDesc( + fmt.Sprintf("%s_collector_error", collector.config.Command.Name), + "Error Collecting metrics", + nil, + prometheus.Labels{"url": collector.config.String("url")}), + err) + return + } - unknownItemsQuery := "" + params := map[string]string{"page": "1"} if collector.config.Bool("enable-unknown-queue-items") { if collector.config.Command.Name == "sonarr" { - unknownItemsQuery = "&includeUnknownSeriesItems=true" + params["includeUnknownSeriesItems"] = "true" } else if collector.config.Command.Name == "radarr" { - unknownItemsQuery = "&includeUnknownMovieItems=true" + params["includeUnknownMovieItems"] = "true" } } queue := model.Queue{} - if err := c.DoRequest(fmt.Sprintf("queue?page=1%s", unknownItemsQuery), &queue); err != nil { + if err := c.DoRequest("queue", &queue, params); err != nil { log.Fatal(err) } // Calculate total pages @@ -56,7 +67,8 @@ func (collector *queueCollector) Collect(ch chan<- prometheus.Metric) { queueStatusAll = append(queueStatusAll, queue.Records...) if totalPages > 1 { for page := 2; page <= totalPages; page++ { - if err := c.DoRequest(fmt.Sprintf("queue?page=%d%s", page, unknownItemsQuery), &queue); err != nil { + params["page"] = fmt.Sprintf("%d", page) + if err := c.DoRequest("queue", &queue, params); err != nil { log.Fatal(err) } queueStatusAll = append(queueStatusAll, queue.Records...) diff --git a/internal/collector/shared/rootfolder.go b/internal/collector/shared/rootfolder.go index fff9ba4..a274bb8 100644 --- a/internal/collector/shared/rootfolder.go +++ b/internal/collector/shared/rootfolder.go @@ -34,7 +34,18 @@ func (collector *rootFolderCollector) Describe(ch chan<- *prometheus.Desc) { } func (collector *rootFolderCollector) Collect(ch chan<- prometheus.Metric) { - c := client.NewClient(collector.config, collector.configFile) + c, err := client.NewClient(collector.config, collector.configFile) + if err != nil { + log.Errorf("Error creating client: %w", err) + ch <- prometheus.NewInvalidMetric( + prometheus.NewDesc( + fmt.Sprintf("%s_collector_error", collector.config.Command.Name), + "Error Collecting metrics", + nil, + prometheus.Labels{"url": collector.config.String("url")}), + err) + return + } rootFolders := model.RootFolder{} if err := c.DoRequest("rootfolder", &rootFolders); err != nil { log.Fatal(err) diff --git a/internal/collector/shared/status.go b/internal/collector/shared/status.go index 7dcef58..0ce5e3e 100644 --- a/internal/collector/shared/status.go +++ b/internal/collector/shared/status.go @@ -6,6 +6,7 @@ import ( "github.com/onedr0p/exportarr/internal/client" "github.com/onedr0p/exportarr/internal/model" "github.com/prometheus/client_golang/prometheus" + log "github.com/sirupsen/logrus" "github.com/urfave/cli/v2" ) @@ -33,7 +34,18 @@ func (collector *systemStatusCollector) Describe(ch chan<- *prometheus.Desc) { } func (collector *systemStatusCollector) Collect(ch chan<- prometheus.Metric) { - c := client.NewClient(collector.config, collector.configFile) + c, err := client.NewClient(collector.config, collector.configFile) + if err != nil { + log.Errorf("Error creating client: %w", err) + ch <- prometheus.NewInvalidMetric( + prometheus.NewDesc( + fmt.Sprintf("%s_collector_error", collector.config.Command.Name), + "Error Collecting metrics", + nil, + prometheus.Labels{"url": collector.config.String("url")}), + err) + return + } systemStatus := model.SystemStatus{} if err := c.DoRequest("system/status", &systemStatus); err != nil { ch <- prometheus.MustNewConstMetric(collector.systemStatus, prometheus.GaugeValue, float64(0.0)) diff --git a/internal/collector/sonarr/series.go b/internal/collector/sonarr/series.go index c8269fe..f747461 100644 --- a/internal/collector/sonarr/series.go +++ b/internal/collector/sonarr/series.go @@ -148,7 +148,18 @@ func (collector *sonarrCollector) Describe(ch chan<- *prometheus.Desc) { func (collector *sonarrCollector) Collect(ch chan<- prometheus.Metric) { total := time.Now() - c := client.NewClient(collector.config, collector.configFile) + c, err := client.NewClient(collector.config, collector.configFile) + if err != nil { + log.Errorf("Error creating client: %w", err) + ch <- prometheus.NewInvalidMetric( + prometheus.NewDesc( + "sonarr_collector_error", + "Error Collecting from Lidarr", + nil, + prometheus.Labels{"url": collector.config.String("url")}), + err) + return + } var seriesFileSize int64 var ( seriesDownloaded = 0 @@ -204,7 +215,8 @@ func (collector *sonarrCollector) Collect(ch chan<- prometheus.Metric) { if collector.config.Bool("enable-additional-metrics") { textra := time.Now() episodeFile := model.EpisodeFile{} - if err := c.DoRequest(fmt.Sprintf("%s?seriesId=%d", "episodefile", s.Id), &episodeFile); err != nil { + params := map[string]string{"seriesId": fmt.Sprintf("%d", s.Id)} + if err := c.DoRequest("episodefile", &episodeFile, params); err != nil { log.Fatal(err) } for _, e := range episodeFile { @@ -214,7 +226,7 @@ func (collector *sonarrCollector) Collect(ch chan<- prometheus.Metric) { } episode := model.Episode{} - if err := c.DoRequest(fmt.Sprintf("%s?seriesId=%d", "episode", s.Id), &episode); err != nil { + if err := c.DoRequest("episode", &episode, params); err != nil { log.Fatal(err) } for _, e := range episode { @@ -232,7 +244,8 @@ func (collector *sonarrCollector) Collect(ch chan<- prometheus.Metric) { } episodesMissing := model.Missing{} - if err := c.DoRequest("wanted/missing?sortKey=airDateUtc", &episodesMissing); err != nil { + params := map[string]string{"sortKey": "airDateUtc"} + if err := c.DoRequest("wanted/missing", &episodesMissing, params); err != nil { log.Fatal(err) } diff --git a/internal/utils/utils.go b/internal/utils/utils.go index d0ffde7..5e921c9 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -7,7 +7,6 @@ import ( "net/url" "os" "regexp" - "strings" "github.com/onedr0p/exportarr/internal/model" log "github.com/sirupsen/logrus" @@ -37,15 +36,6 @@ func IsFileThere(filename string) bool { return !info.IsDir() } -// FormatURLBase - Formats a base URL -func FormatURLBase(urlBase string) string { - u := strings.Trim(urlBase, "/") - if urlBase == "" { - return u - } - return fmt.Sprintf("/%s", strings.Trim(u, "/")) -} - // GetArrConfigFromFile - Get the config from config.xml func GetArrConfigFromFile(file string) (*model.Config, error) { xmlFile, err := os.Open(file)