diff --git a/Dockerfile b/Dockerfile index e69de29..65d8f6b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM golang:1.25-alpine AS builder +WORKDIR /app/ +ADD go.mod go.sum ./ +RUN go mod download +ADD . . +RUN GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags="-w -s" -o unraid-exporter main.go + +FROM golang:1.25-alpine +WORKDIR /app/ +COPY --from=builder /app/unraid-exporter /app/unraid-exporter +ENTRYPOINT ["/app/unraid-exporter"] diff --git a/Makefile b/Makefile index 432d34e..60c73ba 100644 --- a/Makefile +++ b/Makefile @@ -1,49 +1,33 @@ .DEFAULT_GOAL := default -IMAGE ?= rg.fr-par.scw.cloud/averagemarcus-private/unraid-exporter:latest +IMAGE ?= rg.fr-par.scw.cloud/averagemarcus/unraid-exporter:latest .PHONY: test # Run all tests, linting and format checks test: lint check-format run-tests .PHONY: lint # Perform lint checks against code lint: - @echo "⚠️ 'lint' unimplemented" - # GO Projects - # @go vet && golint -set_exit_status ./... + @go vet && golint -set_exit_status ./... .PHONY: check-format # Checks code formatting and returns a non-zero exit code if formatting errors found check-format: - @echo "⚠️ 'check-format' unimplemented" - # GO Projects - # @gofmt -e -l . + @gofmt -e -l . .PHONY: format # Performs automatic format fixes on all code format: - @echo "⚠️ 'format' unimplemented" - # GO Projects - # @gofmt -s -w . + @gofmt -s -w . .PHONY: run-tests # Runs all tests run-tests: - @echo "⚠️ 'run-tests' unimplemented" - # GO Projects - # @go test - # Node Projects - # @npm test + @go test .PHONY: fetch-deps # Fetch all project dependencies fetch-deps: - @echo "⚠️ 'fetch-deps' unimplemented" - # GO Projects - # @go mod tidy - # Node Projects - # @npm install + @go mod tidy .PHONY: build # Build the project build: lint check-format fetch-deps - @echo "⚠️ 'build' unimplemented" - # GO Projects - # @go build -o PROJECT_NAME main.go + @go build -o unraid-exporter main.go .PHONY: docker-build # Build the docker image docker-build: diff --git a/collector.go b/collector.go new file mode 100644 index 0000000..31efa8f --- /dev/null +++ b/collector.go @@ -0,0 +1,45 @@ +package main + +import ( + "github.com/prometheus/client_golang/prometheus" +) + +type UnraidCollector struct { + parityStatusMetric *prometheus.Desc + parityProgressMetric *prometheus.Desc + parityErrorCountMetric *prometheus.Desc + + metrics []prometheus.Metric +} + +func newUnraidCollector() *UnraidCollector { + return &UnraidCollector{ + parityStatusMetric: prometheus.NewDesc("unraid_parity_check_status", + "Parity check status of the Unraid array", + []string{"status"}, + nil, + ), + parityProgressMetric: prometheus.NewDesc("unraid_parity_check_progress", + "Parity check progress percentage", + nil, nil, + ), + parityErrorCountMetric: prometheus.NewDesc("unraid_parity_check_error_count", + "Parity check error count", + nil, nil, + ), + + metrics: []prometheus.Metric{}, + } +} + +func (collector *UnraidCollector) Describe(ch chan<- *prometheus.Desc) { + ch <- collector.parityStatusMetric + ch <- collector.parityProgressMetric + ch <- collector.parityErrorCountMetric +} + +func (collector *UnraidCollector) Collect(ch chan<- prometheus.Metric) { + for _, m := range collector.metrics { + ch <- m + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..2e639f8 --- /dev/null +++ b/go.mod @@ -0,0 +1,23 @@ +module unraid-exporter + +go 1.25.4 + +require ( + github.com/hasura/go-graphql-client v0.15.0 + github.com/prometheus/client_golang v1.23.2 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/coder/websocket v1.8.13 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/procfs v0.16.1 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect + golang.org/x/sys v0.35.0 // indirect + google.golang.org/protobuf v1.36.8 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7255403 --- /dev/null +++ b/go.sum @@ -0,0 +1,52 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE= +github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hasura/go-graphql-client v0.15.0 h1:C8gO+pilV5jyH7zuvQ0tJwxt/QSXRrhEJz35phPLk9Y= +github.com/hasura/go-graphql-client v0.15.0/go.mod h1:jfSZtBER3or+88Q9vFhWHiFMPppfYILRyl+0zsgPIIw= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= +google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..26a8794 --- /dev/null +++ b/main.go @@ -0,0 +1,129 @@ +package main + +import ( + "context" + "flag" + "fmt" + "log" + "net/http" + "os" + "time" + + graphql "github.com/hasura/go-graphql-client" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +var ( + interval int + timeout int + port int + endpoint string + + graphqlClient *graphql.Client +) + +var statuses = []string{ + "NEVER_RUN", + "RUNNING", + "PAUSED", + "COMPLETED", + "CANCELLED", + "FAILED", +} + +func init() { + flag.IntVar(&interval, "interval", 30, "Duration, in seconds, between checks") + flag.IntVar(&timeout, "timeout", 5000, "Timeout in ms when connecting to Unraid") + flag.IntVar(&port, "port", 9091, "The port to listen on") + flag.StringVar(&endpoint, "endpoint", "", "The Unraid GraphQL endpoint") + flag.Parse() + + envEndpoint := os.Getenv("UNRAID_ENDPOINT") + if envEndpoint != "" { + endpoint = envEndpoint + } + if endpoint == "" { + panic("Endpoint must be provided") + } + + token := os.Getenv("UNRAID_TOKEN") + if token == "" { + panic("UNRAID_TOKEN env var must be provided") + } + + graphqlClient = graphql.NewClient( + endpoint, + &http.Client{ + Timeout: time.Millisecond * time.Duration(timeout), + }, + ). + WithRequestModifier(func(r *http.Request) { + r.Header.Set("content-type", "application/json") + r.Header.Set("x-api-key", token) + }) +} + +func main() { + fmt.Println("Unraid-exporter") + collector := newUnraidCollector() + prometheus.MustRegister(collector) + + go func() { + if err := fetchUnraidStatus(collector); err != nil { + fmt.Printf("failed to get status from Unraid: %v\n", err) + } + for range time.Tick(time.Second * time.Duration(interval)) { + if err := fetchUnraidStatus(collector); err != nil { + fmt.Printf("failed to get status from Unraid: %v\n", err) + } + } + }() + + http.Handle("/metrics", promhttp.Handler()) + fmt.Printf("Starting server on %d\n", port) + log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), nil)) +} + +func fetchUnraidStatus(collector *UnraidCollector) error { + var query struct { + Array struct { + ParityCheckStatus struct { + Progress int64 + Status string + Date string + Duration int64 + Errors int64 + Speed string + } + } + } + + err := graphqlClient.Query(context.Background(), &query, nil) + if err != nil { + return err + } + + collector.metrics = []prometheus.Metric{ + prometheus.MustNewConstMetric( + collector.parityProgressMetric, prometheus.CounterValue, + float64(query.Array.ParityCheckStatus.Progress), + ), + prometheus.MustNewConstMetric( + collector.parityErrorCountMetric, prometheus.CounterValue, + float64(query.Array.ParityCheckStatus.Errors), + ), + } + for _, status := range statuses { + val := 0 + if status == query.Array.ParityCheckStatus.Status { + val = 1 + } + collector.metrics = append(collector.metrics, prometheus.MustNewConstMetric( + collector.parityStatusMetric, prometheus.CounterValue, + float64(val), status, + )) + } + + return nil +}