mirror of
https://github.com/RWejlgaard/octopus_exporter.git
synced 2026-07-04 22:06:17 +00:00
feat: making things a lot more proper
This commit is contained in:
parent
dd1e39769e
commit
966a7abbf3
10 changed files with 148 additions and 13 deletions
25
.github/dependabot.yml
vendored
Normal file
25
.github/dependabot.yml
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: gomod
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
open-pull-requests-limit: 5
|
||||
groups:
|
||||
go-deps:
|
||||
patterns:
|
||||
- "*"
|
||||
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
groups:
|
||||
actions:
|
||||
patterns:
|
||||
- "*"
|
||||
|
||||
- package-ecosystem: docker
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
5
.github/workflows/pr-check.yml
vendored
5
.github/workflows/pr-check.yml
vendored
|
|
@ -16,3 +16,8 @@ jobs:
|
|||
|
||||
- name: Test
|
||||
run: go test -race ./...
|
||||
|
||||
- name: Lint
|
||||
uses: golangci/golangci-lint-action@v6
|
||||
with:
|
||||
version: latest
|
||||
|
|
|
|||
3
.github/workflows/release.yml
vendored
3
.github/workflows/release.yml
vendored
|
|
@ -66,6 +66,9 @@ jobs:
|
|||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
build-args: |
|
||||
VERSION=${{ steps.version.outputs.version }}
|
||||
COMMIT=${{ github.sha }}
|
||||
tags: |
|
||||
rwejlgaard/octopus_exporter:${{ steps.version.outputs.version }}
|
||||
rwejlgaard/octopus_exporter:latest
|
||||
|
|
|
|||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -1,2 +1,5 @@
|
|||
.env
|
||||
coverage.out
|
||||
/octopus_exporter
|
||||
*.test
|
||||
*.out
|
||||
|
|
|
|||
23
.golangci.yml
Normal file
23
.golangci.yml
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
version: "2"
|
||||
|
||||
run:
|
||||
timeout: 5m
|
||||
|
||||
linters:
|
||||
default: standard
|
||||
enable:
|
||||
- errcheck
|
||||
- govet
|
||||
- ineffassign
|
||||
- staticcheck
|
||||
- unused
|
||||
- misspell
|
||||
- bodyclose
|
||||
- gocritic
|
||||
- unconvert
|
||||
- unparam
|
||||
exclusions:
|
||||
rules:
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- errcheck
|
||||
|
|
@ -1,12 +1,17 @@
|
|||
FROM golang:1.24-alpine AS builder
|
||||
ARG VERSION=dev
|
||||
ARG COMMIT=none
|
||||
WORKDIR /src
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 go build -o /octopus_exporter ./cmd/octopus_exporter
|
||||
RUN CGO_ENABLED=0 go build \
|
||||
-ldflags "-s -w -X main.version=${VERSION} -X main.commit=${COMMIT}" \
|
||||
-o /octopus_exporter ./cmd/octopus_exporter
|
||||
|
||||
FROM scratch
|
||||
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||
COPY --from=builder /octopus_exporter /octopus_exporter
|
||||
USER 65534:65534
|
||||
EXPOSE 9359
|
||||
ENTRYPOINT ["/octopus_exporter"]
|
||||
|
|
|
|||
16
README.md
16
README.md
|
|
@ -34,7 +34,18 @@ Gas metrics are only exposed if a smart gas meter is found on the account.
|
|||
|---|---|---|
|
||||
| `octopus_account_balance_pence` | GraphQL | Account balance in pence (positive = credit, negative = debit) |
|
||||
|
||||
Metrics are updated every 60 seconds.
|
||||
### Exporter health
|
||||
|
||||
| Metric | Description |
|
||||
|---|---|
|
||||
| `octopus_up` | 1 if the last poll cycle completed without errors, 0 otherwise |
|
||||
| `octopus_last_poll_timestamp` | Unix timestamp of the last completed poll cycle |
|
||||
| `octopus_poll_errors_total` | Total number of collector errors across all poll cycles |
|
||||
| `octopus_token_refreshes_total` | Total number of successful JWT token refreshes |
|
||||
| `octopus_rate_limit_retries_total` | Total number of 429 rate-limit retries |
|
||||
| `octopus_build_info{version,commit}` | Build metadata (always 1) |
|
||||
|
||||
Metrics are refreshed every `POLL_INTERVAL` (default 60 seconds).
|
||||
|
||||
## Configuration
|
||||
|
||||
|
|
@ -48,6 +59,7 @@ Metrics are updated every 60 seconds.
|
|||
| `OCTOPUS_GAS_SERIAL` | No | Filter gas meter by serial number |
|
||||
| `OCTOPUS_GAS_DEVICE_ID` | No | Use a specific gas smart device ID directly |
|
||||
| `PORT` | No | Port to expose metrics on (default: `9359`) |
|
||||
| `POLL_INTERVAL` | No | How often to poll Octopus APIs (Go duration, default: `60s`) |
|
||||
|
||||
If no filter variables are set, the exporter auto-discovers the first smart meter of each type found on the account. Use `OCTOPUS_MPAN` / `OCTOPUS_MPRN` to pin to a specific meter on accounts with multiple meters.
|
||||
|
||||
|
|
@ -79,7 +91,7 @@ services:
|
|||
|
||||
## Running from source
|
||||
|
||||
Requires Go 1.22+.
|
||||
Requires Go 1.24+.
|
||||
|
||||
```sh
|
||||
git clone https://github.com/rwejlgaard/octopus_exporter
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ func executeWithRetry(makeReq func() (*http.Request, error)) ([]byte, error) {
|
|||
return nil, err
|
||||
}
|
||||
if resp.StatusCode == http.StatusTooManyRequests {
|
||||
resp.Body.Close()
|
||||
_ = resp.Body.Close()
|
||||
if attempt == maxRetries {
|
||||
return nil, errors.New("rate limited: max retries exceeded")
|
||||
}
|
||||
|
|
@ -82,7 +82,7 @@ func executeWithRetry(makeReq func() (*http.Request, error)) ([]byte, error) {
|
|||
continue
|
||||
}
|
||||
raw, err := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
_ = resp.Body.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
|
|
@ -14,8 +17,10 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
apiKey string
|
||||
port string
|
||||
apiKey string
|
||||
port string
|
||||
version = "dev"
|
||||
commit = "none"
|
||||
)
|
||||
|
||||
func mustEnv(key string) string {
|
||||
|
|
@ -41,9 +46,24 @@ func counter(name, help string) prometheus.Counter {
|
|||
return prometheus.NewCounter(prometheus.CounterOpts{Name: name, Help: help})
|
||||
}
|
||||
|
||||
func parseInterval(key string, def time.Duration) time.Duration {
|
||||
v := os.Getenv(key)
|
||||
if v == "" {
|
||||
return def
|
||||
}
|
||||
d, err := time.ParseDuration(v)
|
||||
if err != nil {
|
||||
log.Printf("invalid %s=%q, falling back to %s: %v", key, v, def, err)
|
||||
return def
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
func main() {
|
||||
apiKey = mustEnv("OCTOPUS_API_KEY")
|
||||
port = envOrDefault("PORT", "9359")
|
||||
pollInterval := parseInterval("POLL_INTERVAL", 60*time.Second)
|
||||
log.Printf("octopus_exporter %s (%s), poll interval %s", version, commit, pollInterval)
|
||||
|
||||
token, err := getKrakenToken(apiKey)
|
||||
if err != nil {
|
||||
|
|
@ -91,16 +111,24 @@ func main() {
|
|||
|
||||
// Exporter health
|
||||
exporterUp := gauge("octopus_up", "1 if the last poll cycle completed without errors, 0 otherwise")
|
||||
lastPollTimestamp := gauge("octopus_last_poll_timestamp", "Unix timestamp of the last completed poll cycle")
|
||||
pollErrors := counter("octopus_poll_errors_total", "Total number of collector errors per poll cycle")
|
||||
tokenRefreshCount := counter("octopus_token_refreshes_total", "Total number of successful JWT token refreshes")
|
||||
rateLimitRetries = counter("octopus_rate_limit_retries_total", "Total number of 429 rate-limit retries across all requests")
|
||||
|
||||
buildInfo := prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Name: "octopus_build_info",
|
||||
Help: "Build metadata exposed as a constant gauge (always 1).",
|
||||
}, []string{"version", "commit"})
|
||||
buildInfo.WithLabelValues(version, commit).Set(1)
|
||||
|
||||
toRegister := []prometheus.Collector{
|
||||
elecDemand, elecLastRead,
|
||||
elecConsumption, elecConsumptionInterval,
|
||||
elecUnitRate, elecStandingCharge,
|
||||
accountBalance,
|
||||
exporterUp, pollErrors, tokenRefreshCount, rateLimitRetries,
|
||||
exporterUp, lastPollTimestamp, pollErrors, tokenRefreshCount, rateLimitRetries,
|
||||
buildInfo,
|
||||
}
|
||||
|
||||
var (
|
||||
|
|
@ -123,10 +151,23 @@ func main() {
|
|||
|
||||
prometheus.MustRegister(toRegister...)
|
||||
|
||||
http.Handle("/metrics", promhttp.Handler())
|
||||
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/metrics", promhttp.Handler())
|
||||
mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte("ok"))
|
||||
})
|
||||
srv := &http.Server{
|
||||
Addr: ":" + port,
|
||||
Handler: mux,
|
||||
ReadHeaderTimeout: 5 * time.Second,
|
||||
}
|
||||
go func() {
|
||||
log.Printf("serving metrics on :%s/metrics", port)
|
||||
if err := http.ListenAndServe(":"+port, nil); err != nil {
|
||||
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
|
@ -163,7 +204,10 @@ func main() {
|
|||
return fn(newT)
|
||||
}
|
||||
|
||||
for {
|
||||
ticker := time.NewTicker(pollInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
poll := func() {
|
||||
var (
|
||||
wg sync.WaitGroup
|
||||
failedAny atomic.Bool
|
||||
|
|
@ -296,7 +340,22 @@ func main() {
|
|||
} else {
|
||||
exporterUp.Set(1)
|
||||
}
|
||||
lastPollTimestamp.Set(float64(time.Now().Unix()))
|
||||
}
|
||||
|
||||
time.Sleep(60 * time.Second)
|
||||
poll()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Println("shutdown signal received, draining HTTP server")
|
||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
if err := srv.Shutdown(shutdownCtx); err != nil {
|
||||
log.Printf("HTTP shutdown error: %v", err)
|
||||
}
|
||||
return
|
||||
case <-ticker.C:
|
||||
poll()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
2
go.mod
2
go.mod
|
|
@ -1,6 +1,6 @@
|
|||
module github.com/pez/octopus_exporter
|
||||
|
||||
go 1.22
|
||||
go 1.24
|
||||
|
||||
require github.com/prometheus/client_golang v1.19.1
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue