├── charts
├── release-config.yaml
└── nut-exporter
│ ├── Chart.yaml
│ ├── templates
│ ├── secret-env.yaml
│ ├── service.yaml
│ ├── dashboard-config.yaml
│ ├── prometheus-rules.yaml
│ ├── service-monitor.yaml
│ ├── ingress.yaml
│ ├── _helper.tpl
│ └── deployment.yaml
│ └── values.yaml
├── .gitignore
├── dashboard
├── capture.png
└── dashboard.json
├── .release_info.md
├── Dockerfile
├── scripts
├── test.sh
└── do_release.sh
├── LICENSE
├── .goreleaser.yml
├── .github
└── workflows
│ ├── release-helm.yml
│ └── release.yml
├── go.mod
├── nut_exporter_test.go
├── go.sum
├── nut_exporter.go
├── collectors
└── nut_collector.go
└── README.md
/charts/release-config.yaml:
--------------------------------------------------------------------------------
1 | release-name-template: "helm-{{ .Name }}-{{ .Version }}"
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *_exporter
2 | scripts/github_api_token
3 | scripts/nut_exporter*
4 | tmp/
5 | dist/
6 |
--------------------------------------------------------------------------------
/dashboard/capture.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DRuggeri/nut_exporter/HEAD/dashboard/capture.png
--------------------------------------------------------------------------------
/charts/nut-exporter/Chart.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | description: Installs NUT exporter in kubernetes
3 | name: nut-exporter
4 | version: 1.0.0
5 | appVersion: "3.1"
6 | sources:
7 | - https://github.com/DRuggeri/nut_exporter
--------------------------------------------------------------------------------
/.release_info.md:
--------------------------------------------------------------------------------
1 | ## Fix
2 | - Do not accidentally drop/ignore strings when coaxing variables to integers. Thanks for the fix in #62, @cbryant42!
3 |
4 | ## Misc
5 | - Align module versioning semantics with expected Go semantics so `go install` will work as one would expect. Thanks for the fix in #60, @Electrenator!
6 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | ### STAGE 1: Build ###
2 |
3 | FROM golang:1-alpine as builder
4 |
5 | WORKDIR /app
6 | COPY . /app
7 | RUN go install
8 |
9 | ### STAGE 2: Setup ###
10 |
11 | FROM alpine
12 | RUN apk add --no-cache \
13 | libc6-compat
14 | COPY --from=builder /go/bin/nut_exporter /nut_exporter
15 | RUN chmod +x /nut_exporter
16 | ENTRYPOINT ["/nut_exporter"]
17 |
--------------------------------------------------------------------------------
/charts/nut-exporter/templates/secret-env.yaml:
--------------------------------------------------------------------------------
1 | {{- if .Values.envSecret }}
2 | apiVersion: v1
3 | kind: Secret
4 | metadata:
5 | name: {{ include "nut-exporter.fullname" . }}-env
6 | labels:
7 | {{- include "nut-exporter.labels" . | nindent 4 }}
8 | stringData:
9 | {{- range $key, $val := .Values.envSecret }}
10 | {{ $key }}: {{ $val | quote }}
11 | {{- end }}
12 |
13 | {{- end }}
14 |
--------------------------------------------------------------------------------
/scripts/test.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash -e
2 |
3 | echo "Building testing binary and running tests..."
4 | #Get into the right directory
5 | cd $(dirname $0)
6 |
7 | export GOOS=""
8 | export GOARCH=""
9 |
10 | #Add this directory to PATH
11 | export PATH="$PATH:`pwd`"
12 |
13 | go build -ldflags "-X main.Version=testing:$(git rev-list -1 HEAD)" -o "nut_exporter" ../
14 |
15 | echo "Running tests..."
16 | cd ../
17 |
18 | go test
19 |
--------------------------------------------------------------------------------
/charts/nut-exporter/templates/service.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Service
3 | metadata:
4 | labels:
5 | {{- include "nut-exporter.labels" . | nindent 4 }}
6 | name: {{ include "nut-exporter.fullname" . }}
7 | spec:
8 | type: {{ .Values.service.type }}
9 | ports:
10 | - port: {{ .Values.service.port }}
11 | targetPort: http
12 | protocol: TCP
13 | name: http
14 | selector:
15 | {{- include "nut-exporter.selectorLabels" . | nindent 4 }}
16 |
--------------------------------------------------------------------------------
/charts/nut-exporter/templates/dashboard-config.yaml:
--------------------------------------------------------------------------------
1 | {{- if .Values.dashboard.enabled }}
2 | apiVersion: v1
3 | kind: ConfigMap
4 | metadata:
5 | name: {{ include "nut-exporter.fullname" . }}-dashboards
6 | labels:
7 | {{- include "nut-exporter.labels" . | nindent 4 }}
8 | {{- toYaml .Values.dashboard.labels | nindent 4 }}
9 | {{- with $.Values.dashboard.namespace }}
10 | namespace: {{ . }}
11 | {{- end }}
12 | data:
13 | nutdashboard.json: |-
14 | {{ $.Files.Get "dashboards/default.json" | nindent 4 }}
15 | ---
16 | {{- end }}
17 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2020 Daniel Ruggeri
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
14 |
--------------------------------------------------------------------------------
/charts/nut-exporter/templates/prometheus-rules.yaml:
--------------------------------------------------------------------------------
1 | {{- if .Values.rules.enabled }}
2 | apiVersion: monitoring.coreos.com/v1
3 | kind: PrometheusRule
4 | metadata:
5 | name: {{ include "nut-exporter.fullname" . }}-rules
6 | labels:
7 | {{- include "nut-exporter.labels" . | nindent 4 }}
8 | {{- with .Values.rules.labels }}
9 | {{- toYaml . | nindent 4 }}
10 | {{- end }}
11 | {{- with $.Values.rules.namespace }}
12 | namespace: {{ . }}
13 | {{- end }}
14 | spec:
15 | groups:
16 | - name: NutExporter
17 | rules:
18 | {{- toYaml .Values.rules.rules | nindent 6 }}
19 | {{- end }}
20 |
--------------------------------------------------------------------------------
/.goreleaser.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 |
3 | before:
4 | hooks:
5 | - go mod tidy
6 | builds:
7 | - env:
8 | - CGO_ENABLED=0
9 | mod_timestamp: '{{ .CommitTimestamp }}'
10 | flags:
11 | - -trimpath
12 | ldflags:
13 | - '-X main.Version={{ .Version }} -X main.Commit={{ .Commit }}'
14 | goos:
15 | - freebsd
16 | - windows
17 | - linux
18 | - darwin
19 | goarch:
20 | - amd64
21 | - '386'
22 | - arm
23 | - arm64
24 | #- riskv64
25 | ignore:
26 | - goos: darwin
27 | goarch: '386'
28 |
29 | archives:
30 | - format: binary
31 | name_template: "{{ .ProjectName }}-v{{ .Version }}-{{ .Os }}-{{ .Arch }}"
32 | release:
33 | draft: false
34 |
--------------------------------------------------------------------------------
/.github/workflows/release-helm.yml:
--------------------------------------------------------------------------------
1 | name: Release Helm Chart
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 |
8 | jobs:
9 | release:
10 | permissions:
11 | contents: write
12 | runs-on: ubuntu-latest
13 | steps:
14 | - name: Checkout
15 | uses: actions/checkout@v3
16 | with:
17 | fetch-depth: 0
18 |
19 | - name: Configure Git
20 | run: |
21 | git config user.name "$GITHUB_ACTOR"
22 | git config user.email "$GITHUB_ACTOR@users.noreply.github.com"
23 | - name: Install Helm
24 | uses: azure/setup-helm@v3
25 |
26 | - name: Run chart-releaser
27 | uses: helm/chart-releaser-action@v1.6.0
28 | env:
29 | CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
30 | with:
31 | skip_existing: true
32 | config: charts/release-config.yaml
33 |
--------------------------------------------------------------------------------
/charts/nut-exporter/templates/service-monitor.yaml:
--------------------------------------------------------------------------------
1 | {{- if .Values.serviceMonitor.enabled }}
2 | apiVersion: monitoring.coreos.com/v1
3 | kind: ServiceMonitor
4 | metadata:
5 | labels:
6 | {{- include "nut-exporter.labels" . | nindent 4 }}
7 | {{- with .Values.serviceMonitor.labels }}
8 | {{- toYaml . | nindent 4 }}
9 | {{- end }}
10 | name: {{ include "nut-exporter.fullname" . }}
11 | {{- with $.Values.serviceMonitor.namespace }}
12 | namespace: {{ . }}
13 | {{- end }}
14 | spec:
15 | endpoints:
16 | - interval: 15s
17 | {{- with $.Values.serviceMonitor.metricRelabelings }}
18 | metricRelabelings:
19 | {{ toYaml . | nindent 6}}
20 | {{- end }}
21 | {{- with $.Values.serviceMonitor.relabelings }}
22 | relabelings:
23 | {{ toYaml . | nindent 6}}
24 | {{- end }}
25 | path: /ups_metrics
26 | port: http
27 | scheme: http
28 | jobLabel: nut-exporter
29 | namespaceSelector:
30 | matchNames:
31 | - {{ .Release.Namespace }}
32 | selector:
33 | matchLabels:
34 | {{- include "nut-exporter.selectorLabels" . | nindent 6 }}
35 | {{- end }}
36 |
--------------------------------------------------------------------------------
/charts/nut-exporter/templates/ingress.yaml:
--------------------------------------------------------------------------------
1 | {{- if .Values.ingress.enabled -}}
2 | apiVersion: networking.k8s.io/v1
3 | kind: Ingress
4 | metadata:
5 | name: {{ include "nut-exporter.fullname" . }}
6 | labels:
7 | {{- include "nut-exporter.labels" . | nindent 4 }}
8 | {{- with .Values.ingress.annotations }}
9 | annotations:
10 | {{- toYaml . | nindent 4 }}
11 | {{- end }}
12 | spec:
13 | {{- with .Values.ingress.className }}
14 | ingressClassName: {{ . }}
15 | {{- end }}
16 | {{- if .Values.ingress.tls }}
17 | tls:
18 | {{- range .Values.ingress.tls }}
19 | - hosts:
20 | {{- range .hosts }}
21 | - {{ . | quote }}
22 | {{- end }}
23 | secretName: {{ .secretName }}
24 | {{- end }}
25 | {{- end }}
26 | rules:
27 | {{- range .Values.ingress.hosts }}
28 | - host: {{ .host | quote }}
29 | http:
30 | paths:
31 | {{- range .paths }}
32 | - path: {{ .path }}
33 | {{- with .pathType }}
34 | pathType: {{ . }}
35 | {{- end }}
36 | backend:
37 | service:
38 | name: {{ include "nut-exporter.fullname" $ }}
39 | port:
40 | number: {{ $.Values.service.port }}
41 | {{- end }}
42 | {{- end }}
43 | {{- end }}
44 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/DRuggeri/nut_exporter/v3
2 |
3 | go 1.22
4 |
5 | toolchain go1.23.4
6 |
7 | require (
8 | github.com/alecthomas/kingpin/v2 v2.4.0
9 | github.com/prometheus/client_golang v1.20.4
10 | github.com/prometheus/exporter-toolkit v0.14.0
11 | github.com/robbiet480/go.nut v0.0.0-20220219091450-bd8f121e1fa1
12 | )
13 |
14 | require (
15 | github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect
16 | github.com/beorn7/perks v1.0.1 // indirect
17 | github.com/cespare/xxhash/v2 v2.3.0 // indirect
18 | github.com/coreos/go-systemd/v22 v22.5.0 // indirect
19 | github.com/jpillora/backoff v1.0.0 // indirect
20 | github.com/klauspost/compress v1.17.9 // indirect
21 | github.com/mdlayher/socket v0.4.1 // indirect
22 | github.com/mdlayher/vsock v1.2.1 // indirect
23 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
24 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect
25 | github.com/prometheus/client_model v0.6.1 // indirect
26 | github.com/prometheus/common v0.63.0 // indirect
27 | github.com/prometheus/procfs v0.15.1 // indirect
28 | github.com/xhit/go-str2duration/v2 v2.1.0 // indirect
29 | golang.org/x/crypto v0.33.0 // indirect
30 | golang.org/x/net v0.35.0 // indirect
31 | golang.org/x/oauth2 v0.25.0 // indirect
32 | golang.org/x/sync v0.11.0 // indirect
33 | golang.org/x/sys v0.30.0 // indirect
34 | golang.org/x/text v0.22.0 // indirect
35 | google.golang.org/protobuf v1.36.5 // indirect
36 | gopkg.in/yaml.v2 v2.4.0 // indirect
37 | )
38 |
--------------------------------------------------------------------------------
/scripts/do_release.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash -e
2 |
3 |
4 | usage (){
5 | echo "$0 - Tag and prepare a release
6 |
7 | USAGE: $0 (major|minor|patch|vX.Y.Z)
8 |
9 | The argument may be one of:
10 | major - Increments the current major version and performs the release
11 | minor - Increments the current minor version and preforms the release
12 | patch - Increments the current patch version and preforms the release
13 | vX.Y.Z - Sets the tag to the value of vX.Y.Z where X=major, Y=minor, and Z=patch
14 | "
15 | exit 1
16 | }
17 |
18 | if [ -z "$1" -o -n "$2" ];then
19 | usage
20 | fi
21 |
22 | TAG=`git describe --tags --abbrev=0`
23 | VERSION="${TAG#[vV]}"
24 | MAJOR="${VERSION%%\.*}"
25 | MINOR="${VERSION#*.}"
26 | MINOR="${MINOR%.*}"
27 | PATCH="${VERSION##*.}"
28 | echo "Current tag: v$MAJOR.$MINOR.$PATCH"
29 |
30 | #Determine what the user wanted
31 | case $1 in
32 | major)
33 | MAJOR=$((MAJOR+1))
34 | MINOR=0
35 | PATCH=0
36 | TAG="v$MAJOR.$MINOR.$PATCH"
37 | ;;
38 | minor)
39 | MINOR=$((MINOR+1))
40 | PATCH=0
41 | TAG="v$MAJOR.$MINOR.$PATCH"
42 | ;;
43 | patch)
44 | PATCH=$((PATCH+1))
45 | TAG="v$MAJOR.$MINOR.$PATCH"
46 | ;;
47 | v*.*.*)
48 | TAG="$1"
49 | ;;
50 | *.*.*)
51 | TAG="v$1"
52 | ;;
53 | *)
54 | usage
55 | ;;
56 | esac
57 |
58 | echo "New tag: $TAG"
59 |
60 | #Get into the right directory
61 | cd $(dirname $0)/..
62 |
63 | vi .release_info.md
64 |
65 | git commit -m "Changes for $TAG" .release_info.md
66 |
67 | git tag $TAG
68 | git push origin
69 | git push origin $TAG
70 |
--------------------------------------------------------------------------------
/charts/nut-exporter/templates/_helper.tpl:
--------------------------------------------------------------------------------
1 | {{/*
2 | Expand the name of the chart.
3 | */}}
4 | {{- define "nut-exporter.name" -}}
5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
6 | {{- end }}
7 |
8 | {{/*
9 | Create a default fully qualified app name.
10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
11 | If release name contains chart name it will be used as a full name.
12 | */}}
13 | {{- define "nut-exporter.fullname" -}}
14 | {{- if .Values.fullnameOverride }}
15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
16 | {{- else }}
17 | {{- $name := default .Chart.Name .Values.nameOverride }}
18 | {{- if contains $name .Release.Name }}
19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }}
20 | {{- else }}
21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
22 | {{- end }}
23 | {{- end }}
24 | {{- end }}
25 |
26 |
27 | {{/*
28 | Create chart name and version as used by the chart label.
29 | */}}
30 | {{- define "nut-exporter.chart" -}}
31 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
32 | {{- end }}
33 |
34 | {{/*
35 | Common labels
36 | */}}
37 | {{- define "nut-exporter.labels" -}}
38 | helm.sh/chart: {{ include "nut-exporter.chart" . }}
39 | {{ include "nut-exporter.selectorLabels" . }}
40 | {{- if .Chart.AppVersion }}
41 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
42 | {{- end }}
43 | app.kubernetes.io/managed-by: {{ .Release.Service }}
44 | {{- end }}
45 |
46 | {{/*
47 | Selector labels
48 | */}}
49 | {{- define "nut-exporter.selectorLabels" -}}
50 | app.kubernetes.io/name: {{ include "nut-exporter.name" . }}
51 | app.kubernetes.io/instance: {{ .Release.Name }}
52 | {{- end }}
53 |
--------------------------------------------------------------------------------
/nut_exporter_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "io"
6 | "net/http"
7 | "os"
8 | "os/exec"
9 | "testing"
10 | "time"
11 | )
12 |
13 | var (
14 | binary = "nut_exporter"
15 | )
16 |
17 | const (
18 | address = "localhost:19100"
19 | )
20 |
21 | func TestSuccessfulLaunch(t *testing.T) {
22 | if _, err := os.Stat(binary); err != nil {
23 | return
24 | }
25 |
26 | exporter := exec.Command(binary, "--web.listen-address", address)
27 | test := func(pid int) error {
28 | if err := queryExporter(address); err != nil {
29 | return err
30 | }
31 | return nil
32 | }
33 |
34 | if err := runCommandAndTests(exporter, address, test); err != nil {
35 | t.Error(err)
36 | }
37 | }
38 |
39 | func queryExporter(address string) error {
40 | resp, err := http.Get(fmt.Sprintf("http://%s/metrics", address))
41 | if err != nil {
42 | return err
43 | }
44 | b, err := io.ReadAll(resp.Body)
45 | if err != nil {
46 | return err
47 | }
48 | if err := resp.Body.Close(); err != nil {
49 | return err
50 | }
51 | if want, have := http.StatusOK, resp.StatusCode; want != have {
52 | return fmt.Errorf("want /metrics status code %d, have %d. Body:\n%s", want, have, b)
53 | }
54 | return nil
55 | }
56 |
57 | func runCommandAndTests(cmd *exec.Cmd, address string, fn func(pid int) error) error {
58 | if err := cmd.Start(); err != nil {
59 | return fmt.Errorf("failed to start command: %s", err)
60 | }
61 | time.Sleep(50 * time.Millisecond)
62 | for i := 0; i < 10; i++ {
63 | if err := queryExporter(address); err == nil {
64 | break
65 | }
66 | time.Sleep(500 * time.Millisecond)
67 | if cmd.Process == nil || i == 9 {
68 | return fmt.Errorf("can't start command")
69 | }
70 | }
71 |
72 | errc := make(chan error)
73 | go func(pid int) {
74 | errc <- fn(pid)
75 | }(cmd.Process.Pid)
76 |
77 | err := <-errc
78 | if cmd.Process != nil {
79 | cmd.Process.Kill()
80 | }
81 | return err
82 | }
83 |
--------------------------------------------------------------------------------
/charts/nut-exporter/templates/deployment.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: {{ include "nut-exporter.fullname" . }}
5 | labels:
6 | {{- include "nut-exporter.labels" . | nindent 4 }}
7 | spec:
8 | replicas: {{ .Values.replicaCount }}
9 | selector:
10 | matchLabels:
11 | {{- include "nut-exporter.selectorLabels" . | nindent 6 }}
12 | template:
13 | metadata:
14 | {{- with .Values.podAnnotations }}
15 | annotations:
16 | {{- toYaml . | nindent 8 }}
17 | {{- end }}
18 | labels:
19 | {{- include "nut-exporter.labels" . | nindent 8 }}
20 | spec:
21 | hostNetwork: {{ .Values.podHostNetwork }}
22 | securityContext:
23 | {{- toYaml .Values.podSecurityContext | nindent 8 }}
24 | containers:
25 | - name: nut-exporter
26 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
27 | imagePullPolicy: {{ .Values.image.pullPolicy }}
28 | securityContext:
29 | {{- toYaml .Values.securityContext | nindent 12 }}
30 | {{- if .Values.envSecret }}
31 | envFrom:
32 | - secretRef:
33 | name: {{ include "nut-exporter.fullname" . }}-env
34 | {{- end }}
35 | {{- with .Values.env }}
36 | env:
37 | {{- toYaml . | nindent 12 }}
38 | {{- end }}
39 | {{- with .Values.extraArgs }}
40 | args:
41 | {{- toYaml . | nindent 12 }}
42 | {{- end }}
43 | ports:
44 | - containerPort: 9199
45 | name: http
46 | protocol: TCP
47 | livenessProbe:
48 | {{- toYaml .Values.livenessProbe | nindent 12 }}
49 | readinessProbe:
50 | {{- toYaml .Values.readinessProbe | nindent 12 }}
51 | resources:
52 | {{- toYaml .Values.resources | nindent 12 }}
53 | {{- with .Values.affinity }}
54 | affinity:
55 | {{- toYaml . | nindent 8 }}
56 | {{- end }}
57 | {{- with .Values.nodeSelector }}
58 | nodeSelector:
59 | {{- toYaml . | nindent 8 }}
60 | {{- end }}
61 | {{- with .Values.tolerations }}
62 | tolerations:
63 | {{- toYaml . | nindent 8 }}
64 | {{- end }}
65 | {{- with .Values.imagePullSecrets }}
66 | imagePullSecrets:
67 | {{- toYaml . | nindent 8 }}
68 | {{- end }}
69 |
--------------------------------------------------------------------------------
/charts/nut-exporter/values.yaml:
--------------------------------------------------------------------------------
1 | image:
2 | repository: ghcr.io/druggeri/nut_exporter
3 | pullPolicy: IfNotPresent
4 | # Overrides the image tag whose default is the chart appVersion.
5 | tag: ""
6 |
7 | replicaCount: 1
8 |
9 | imagePullSecrets: []
10 | nameOverride: ""
11 | fullnameOverride: ""
12 |
13 | dashboard:
14 | enabled: false
15 | namespace: ""
16 | labels:
17 | # Label that config maps with dashboards should have to be added for the Grafana helm chart
18 | # https://github.com/grafana/helm-charts/blob/main/charts/grafana/README.md
19 | grafana_dashboard: "1"
20 |
21 | serviceMonitor:
22 | enabled: false
23 | namespace: ""
24 | labels: {}
25 | # key: value
26 | relabelings: []
27 | # - replacement: "My UPS"
28 | # targetLabel: ups
29 |
30 | extraArgs: []
31 | # - --log.level=debug
32 |
33 | envSecret:
34 | NUT_EXPORTER_PASSWORD: "mypasswd"
35 |
36 | env:
37 | - name: NUT_EXPORTER_SERVER
38 | value: "127.0.0.1"
39 | - name: NUT_EXPORTER_USERNAME
40 | value: "admin"
41 | # - name: NUT_EXPORTER_USERNAME
42 | # valueFrom:
43 | # secretKeyRef:
44 | # name: nut-credentials
45 | # key: username
46 | # - name: NUT_EXPORTER_PASSWORD
47 | # valueFrom:
48 | # secretKeyRef:
49 | # name: nut-credentials
50 | # key: password
51 |
52 |
53 | nodeSelector: {}
54 | # has-ups-server: yes
55 |
56 | tolerations: []
57 | # - key: node-role.kubernetes.io/master
58 | # operator: "Exists"
59 | # effect: NoSchedule
60 |
61 | podAnnotations:
62 | prometheus.io/scrape: "false"
63 | prometheus.io/path: "/ups_metrics"
64 | prometheus.io/port: "9199"
65 |
66 | podSecurityContext: {}
67 | # fsGroup: 2000
68 |
69 | securityContext: {}
70 | # privileged: true
71 | # capabilities:
72 | # drop:
73 | # - ALL
74 | # readOnlyRootFilesystem: true
75 | # runAsNonRoot: true
76 | # runAsUser: 1000
77 |
78 | podHostNetwork: false
79 |
80 | service:
81 | type: ClusterIP
82 | port: 9199
83 |
84 | # This block is for setting up the ingress for more information can be found here: https://kubernetes.io/docs/concepts/services-networking/ingress/
85 | ingress:
86 | enabled: false
87 | className: ""
88 | annotations: {}
89 | # kubernetes.io/ingress.class: nginx
90 | # kubernetes.io/tls-acme: "true"
91 | hosts:
92 | - host: chart-nut-exporter.local
93 | paths:
94 | - path: /
95 | pathType: ImplementationSpecific
96 | tls: []
97 | # - secretName: chart-nut-exporter-tls
98 | # hosts:
99 | # - chart-nut-exporter.local
100 |
101 | livenessProbe:
102 | httpGet:
103 | path: /ups_metrics
104 | port: http
105 | initialDelaySeconds: 10
106 | failureThreshold: 5
107 | timeoutSeconds: 2
108 |
109 | readinessProbe:
110 | httpGet:
111 | path: /ups_metrics
112 | port: http
113 | initialDelaySeconds: 10
114 | failureThreshold: 5
115 | timeoutSeconds: 2
116 |
117 | resources: {}
118 | # We usually recommend not to specify default resources and to leave this as a conscious
119 | # choice for the user. This also increases chances charts run on environments with little
120 | # resources, such as Minikube. If you do want to specify resources, uncomment the following
121 | # lines, adjust them as necessary, and remove the curly braces after 'resources:'.
122 | # limits:
123 | # cpu: 100m
124 | # memory: 128Mi
125 | # requests:
126 | # cpu: 100m
127 | # memory: 128Mi
128 |
129 | rules:
130 | enabled: false
131 | namespace: ""
132 | labels: {}
133 | # key: value
134 | rules:
135 | - alert: UPSBatteryNeedsReplacement
136 | annotations:
137 | message: '{{ $labels.ups }} is indicating a need for a battery replacement.'
138 | expr: network_ups_tools_ups_status{flag="RB"} != 0
139 | for: 60s
140 | labels:
141 | severity: high
142 | - alert: UPSLowBattery
143 | annotations:
144 | message: '{{ $labels.ups }} has low battery and is running on backup. Expect shutdown soon'
145 | expr: network_ups_tools_ups_status{flag="LB"} == 0 and network_ups_tools_ups_status{flag="OL"} == 0
146 | for: 60s
147 | labels:
148 | severity: critical
149 | - alert: UPSRuntimeShort
150 | annotations:
151 | message: '{{ $labels.ups }} has only {{ $value | humanizeDuration}} of battery autonomy'
152 | expr: network_ups_tools_battery_runtime < 300
153 | for: 30s
154 | labels:
155 | severity: high
156 | - alert: UPSMainPowerOutage
157 | annotations:
158 | message: '{{ $labels.ups }} has no main power and is running on backup.'
159 | expr: network_ups_tools_ups_status{flag="OL"} == 0
160 | for: 60s
161 | labels:
162 | severity: critical
163 | - alert: UPSIndicatesWarningStatus
164 | annotations:
165 | message: '{{ $labels.ups }} is indicating a need for a battery replacement.'
166 | expr: network_ups_tools_ups_status{flag="HB"} != 0
167 | for: 60s
168 | labels:
169 | severity: warning
170 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 | on:
3 | - push
4 | - pull_request
5 |
6 | env:
7 | DOCKERHUB_USERNAME: druggeri
8 |
9 | jobs:
10 | test:
11 | name: Test
12 | strategy:
13 | matrix:
14 | go-version:
15 | - '1.23.x'
16 | os:
17 | - ubuntu-latest
18 | - macos-latest
19 | - windows-latest
20 | runs-on: ${{ matrix.os }}
21 |
22 | steps:
23 | - name: Install Go
24 | uses: actions/setup-go@v5
25 | with:
26 | go-version: '1.23'
27 |
28 | - name: Checkout code
29 | uses: actions/checkout@v4
30 |
31 | - name: Test
32 | run: go test ./...
33 |
34 | release:
35 | needs: test
36 | name: Build and release
37 | runs-on: ubuntu-latest
38 | steps:
39 | - name: Checkout
40 | uses: actions/checkout@v4
41 | with:
42 | fetch-depth: 0
43 |
44 | - name: Set up Go
45 | uses: actions/setup-go@v5
46 | with:
47 | go-version: '1.23'
48 |
49 | - name: Run GoReleaser
50 | uses: goreleaser/goreleaser-action@v4
51 | with:
52 | distribution: goreleaser
53 | version: latest
54 | args: build --clean --parallelism=2 --timeout=1h --skip=validate
55 |
56 | - name: Publish release
57 | uses: goreleaser/goreleaser-action@v4
58 | if: startsWith(github.ref, 'refs/tags/v')
59 | with:
60 | distribution: goreleaser
61 | version: latest
62 | args: release --clean --parallelism=2 --timeout=1h --release-notes=.release_info.md
63 | env:
64 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
65 |
66 | containers:
67 | if: startsWith(github.ref, 'refs/tags/v')
68 | needs: release
69 | name: Push containers
70 | runs-on: ubuntu-latest
71 | steps:
72 | - name: Prep variables
73 | run: |
74 | set -u
75 | export PROJECT_NAME=${GITHUB_REPOSITORY##*/}
76 | export TAG=${GITHUB_REF##*/}
77 |
78 | echo "PROJECT_NAME=$PROJECT_NAME" >> $GITHUB_ENV
79 | echo "TAG=$TAG" >> $GITHUB_ENV
80 | echo "URL_LINUX_AMD64=https://github.com/${GITHUB_REPOSITORY}/releases/download/$TAG/${PROJECT_NAME}-${TAG}-linux-amd64" >> $GITHUB_ENV
81 | echo "URL_LINUX_ARM64=https://github.com/${GITHUB_REPOSITORY}/releases/download/$TAG/${PROJECT_NAME}-${TAG}-linux-arm64" >> $GITHUB_ENV
82 | echo "URL_LINUX_ARM=https://github.com/${GITHUB_REPOSITORY}/releases/download/$TAG/${PROJECT_NAME}-${TAG}-linux-arm" >> $GITHUB_ENV
83 |
84 | - name: Setup Docker Buildx
85 | uses: docker/setup-buildx-action@v2
86 |
87 | - name: Set up QEMU
88 | uses: docker/setup-qemu-action@v2
89 |
90 | - name: Log in to the Container registry
91 | uses: docker/login-action@v2
92 | with:
93 | registry: ghcr.io
94 | username: ${{ github.actor }}
95 | password: ${{ secrets.GITHUB_TOKEN }}
96 |
97 | - name: Login to DockerHub
98 | uses: docker/login-action@v2
99 | with:
100 | username: ${{ env.DOCKERHUB_USERNAME }}
101 | password: ${{ secrets.DOCKERHUB_TOKEN }}
102 |
103 | - name: Checkout code
104 | uses: actions/checkout@v2
105 |
106 | - name: Extract metadata (tags, labels) for Docker
107 | id: meta
108 | uses: docker/metadata-action@v4
109 | with:
110 | images: |
111 | ${{ env.DOCKERHUB_USERNAME }}/${{ env.PROJECT_NAME }}
112 | ghcr.io/${{ github.repository }}
113 | tags: |
114 | type=ref,event=branch
115 | type=semver,pattern={{version}}
116 | type=semver,pattern={{major}}.{{minor}}
117 | type=sha
118 |
119 | - name: Create scratch docker image
120 | run: |
121 | echo "
122 | FROM alpine AS builder
123 | RUN apk --no-cache add wget ca-certificates \
124 | && if uname -m | grep 'x86_64' >/dev/null 2>&1; then wget -O /downloaded_file $URL_LINUX_AMD64;fi \
125 | && if uname -m | grep 'aarch64' >/dev/null 2>&1; then wget -O /downloaded_file $URL_LINUX_ARM64;fi \
126 | && if uname -m | grep 'arm' >/dev/null 2>&1; then wget -O /downloaded_file $URL_LINUX_ARM;fi \
127 | && if [ ! -f /downloaded_file ];then echo "===Failed to download for:";uname -m;echo "===";exit 1;fi \
128 | && chmod 755 /downloaded_file
129 |
130 | FROM scratch
131 | COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
132 | COPY --from=builder /downloaded_file /$PROJECT_NAME
133 | ENTRYPOINT [\"/$PROJECT_NAME\"]
134 | " > Dockerfile
135 |
136 | echo "Rendered Dockerfile in $PWD:"
137 | cat Dockerfile
138 |
139 | - name: Build and push Docker images
140 | uses: docker/build-push-action@v4
141 | with:
142 | context: .
143 | platforms: linux/amd64,linux/arm64,linux/arm
144 | push: true
145 | tags: ${{ steps.meta.outputs.tags }}
146 | labels: ${{ steps.meta.outputs.labels }}
147 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY=
2 | github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE=
3 | github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc=
4 | github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
5 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
6 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
7 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
8 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
9 | github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
10 | github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
11 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
12 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
13 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
14 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
15 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
16 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
17 | github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
18 | github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
19 | github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
20 | github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
21 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
22 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
23 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
24 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
25 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
26 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
27 | github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
28 | github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
29 | github.com/mdlayher/vsock v1.2.1 h1:pC1mTJTvjo1r9n9fbm7S1j04rCgCzhCOS5DY0zqHlnQ=
30 | github.com/mdlayher/vsock v1.2.1/go.mod h1:NRfCibel++DgeMD8z/hP+PPTjlNJsdPOmxcnENvE+SE=
31 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
32 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
33 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU=
34 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
35 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
36 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
37 | github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI=
38 | github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
39 | github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
40 | github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
41 | github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k=
42 | github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18=
43 | github.com/prometheus/exporter-toolkit v0.14.0 h1:NMlswfibpcZZ+H0sZBiTjrA3/aBFHkNZqE+iCj5EmRg=
44 | github.com/prometheus/exporter-toolkit v0.14.0/go.mod h1:Gu5LnVvt7Nr/oqTBUC23WILZepW0nffNo10XdhQcwWA=
45 | github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
46 | github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
47 | github.com/robbiet480/go.nut v0.0.0-20220219091450-bd8f121e1fa1 h1:YmFqprZILGlF/X3tvMA4Rwn3ySxyE3hGUajBHkkaZbM=
48 | github.com/robbiet480/go.nut v0.0.0-20220219091450-bd8f121e1fa1/go.mod h1:pL1huxuIlWub46MsMVJg4p7OXkzbPp/APxh9IH0eJjQ=
49 | github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
50 | github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
51 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
52 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
53 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
54 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
55 | github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc=
56 | github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
57 | golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
58 | golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
59 | golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
60 | golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
61 | golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
62 | golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
63 | golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
64 | golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
65 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
66 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
67 | golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
68 | golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
69 | google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
70 | google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
71 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
72 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
73 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
74 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
75 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
76 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
77 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
78 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
79 |
--------------------------------------------------------------------------------
/nut_exporter.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "log/slog"
6 | "net/http"
7 | "os"
8 | "strconv"
9 | "strings"
10 |
11 | "github.com/alecthomas/kingpin/v2"
12 | "github.com/prometheus/client_golang/prometheus"
13 | "github.com/prometheus/client_golang/prometheus/promhttp"
14 |
15 | promcollectors "github.com/prometheus/client_golang/prometheus/collectors"
16 | "github.com/prometheus/exporter-toolkit/web"
17 | "github.com/prometheus/exporter-toolkit/web/kingpinflag"
18 |
19 | "github.com/DRuggeri/nut_exporter/v3/collectors"
20 | )
21 |
22 | var Version = "testing"
23 |
24 | var (
25 | server = kingpin.Flag(
26 | "nut.server", "Hostname or IP address of the server to connect to. ($NUT_EXPORTER_SERVER)",
27 | ).Envar("NUT_EXPORTER_SERVER").Default("127.0.0.1").String()
28 |
29 | serverport = kingpin.Flag(
30 | "nut.serverport", "Port on the NUT server to connect to. ($NUT_EXPORTER_SERVERPORT)",
31 | ).Envar("NUT_EXPORTER_SERVERPORT").Default("3493").Int()
32 |
33 | nutUsername = kingpin.Flag(
34 | "nut.username", "If set, will authenticate with this username to the server. Password must be set in NUT_EXPORTER_PASSWORD environment variable. ($NUT_EXPORTER_USERNAME)",
35 | ).Envar("NUT_EXPORTER_USERNAME").String()
36 | nutPassword = ""
37 |
38 | disableDeviceInfo = kingpin.Flag(
39 | "nut.disable_device_info", "A flag to disable the generation of the device_info meta metric. ($NUT_EXPORTER_DISABLE_DEVICE_INFO)",
40 | ).Envar("NUT_EXPORTER_DISABLE_DEVICE_INFO").Default("false").Bool()
41 |
42 | enableFilter = kingpin.Flag(
43 | "nut.vars_enable", "A comma-separated list of variable names to monitor. See the variable notes in README. ($NUT_EXPORTER_VARIABLES)",
44 | ).Envar("NUT_EXPORTER_VARIABLES").Default("battery.charge,battery.voltage,battery.voltage.nominal,input.voltage,input.voltage.nominal,ups.load,ups.status").String()
45 |
46 | onRegex = kingpin.Flag(
47 | "nut.on_regex", "This regular expression will be used to determine if the var's value should be coaxed to 1 if it is a string. Match is case-insensitive. ($NUT_EXPORTER_ON_REGEX)",
48 | ).Envar("NUT_EXPORTER_ON_REGEX").Default("^(enable|enabled|on|true|active|activated)$").String()
49 |
50 | offRegex = kingpin.Flag(
51 | "nut.off_regex", "This regular expression will be used to determine if the var's value should be coaxed to 0 if it is a string. Match is case-insensitive. ($NUT_EXPORTER_OFF_REGEX)",
52 | ).Envar("NUT_EXPORTER_OFF_REGEX").Default("^(disable|disabled|off|false|inactive|deactivated)$").String()
53 |
54 | statusList = kingpin.Flag(
55 | "nut.statuses", "A comma-separated list of statuses labels that will always be set by the exporter. If NUT does not set these flags, the exporter will force the network_ups_tools_ups_status{flag=\"NAME\"} to 0. See the ups.status notes in README.' ($NUT_EXPORTER_STATUSES)",
56 | ).Envar("NUT_EXPORTER_STATUSES").Default("OL,OB,LB,HB,RB,CHRG,DISCHRG,BYPASS,CAL,OFF,OVER,TRIM,BOOST,FSD,SD").String()
57 |
58 | metricsNamespace = kingpin.Flag(
59 | "metrics.namespace", "Metrics Namespace ($NUT_EXPORTER_METRICS_NAMESPACE)",
60 | ).Envar("NUT_EXPORTER_METRICS_NAMESPACE").Default("network_ups_tools").String()
61 |
62 | tookitFlags = kingpinflag.AddFlags(kingpin.CommandLine, ":9199")
63 |
64 | metricsPath = kingpin.Flag(
65 | "web.telemetry-path", "Path under which to expose the UPS Prometheus metrics ($NUT_EXPORTER_WEB_TELEMETRY_PATH)",
66 | ).Envar("NUT_EXPORTER_WEB_TELEMETRY_PATH").Default("/ups_metrics").String()
67 |
68 | exporterMetricsPath = kingpin.Flag(
69 | "web.exporter-telemetry-path", "Path under which to expose process metrics about this exporter ($NUT_EXPORTER_WEB_EXPORTER_TELEMETRY_PATH)",
70 | ).Envar("NUT_EXPORTER_WEB_EXPORTER_TELEMETRY_PATH").Default("/metrics").String()
71 |
72 | printMetrics = kingpin.Flag(
73 | "printMetrics", "Print the metrics this exporter exposes and exits. Default: false ($NUT_EXPORTER_PRINT_METRICS)",
74 | ).Envar("NUT_EXPORTER_PRINT_METRICS").Default("false").Bool()
75 |
76 | logLevel = kingpin.Flag(
77 | "log.level", "Minimum log level for messages. One of error, warn, info, or debug. Default: info ($NETGEAR_EXPORTER_LOG_LEVEL)",
78 | ).Envar("NUT_EXPORTER__LOG_LEVEL").Default("info").String()
79 |
80 | logJson = kingpin.Flag(
81 | "log.json", "Format log lines as JSON. Default: false ($NETGEAR_EXPORTER_LOG_JSON)",
82 | ).Envar("NUT_EXPORTER__LOG_JSON").Bool()
83 | )
84 | var collectorOpts collectors.NutCollectorOpts
85 |
86 | var logger = slog.New(slog.NewTextHandler(os.Stdout, nil))
87 |
88 | func init() {
89 | prometheus.MustRegister(promcollectors.NewBuildInfoCollector())
90 | }
91 |
92 | type metricsHandler struct {
93 | handlers map[string]*http.Handler
94 | }
95 |
96 | func (h *metricsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
97 | thisCollectorOpts := collectorOpts
98 | thisCollectorOpts.Ups = r.URL.Query().Get("ups")
99 |
100 | if r.URL.Query().Get("server") != "" {
101 | thisCollectorOpts.Server = r.URL.Query().Get("server")
102 | }
103 |
104 | if r.URL.Query().Get("serverport") != "" {
105 | if port, err := strconv.Atoi(r.URL.Query().Get("serverport")); err != nil {
106 | thisCollectorOpts.ServerPort = port
107 | }
108 | }
109 |
110 | if r.URL.Query().Get("username") != "" {
111 | thisCollectorOpts.Username = r.URL.Query().Get("username")
112 | }
113 |
114 | if r.URL.Query().Get("password") != "" {
115 | thisCollectorOpts.Password = r.URL.Query().Get("password")
116 | }
117 |
118 | if r.URL.Query().Get("variables") != "" {
119 | thisCollectorOpts.Variables = strings.Split(r.URL.Query().Get("variables"), ",")
120 | }
121 |
122 | if r.URL.Query().Get("statuses") != "" {
123 | thisCollectorOpts.Statuses = strings.Split(r.URL.Query().Get("statuses"), ",")
124 | }
125 |
126 | var promHandler http.Handler
127 | cacheName := fmt.Sprintf("%s:%d/%s", thisCollectorOpts.Server, thisCollectorOpts.ServerPort, thisCollectorOpts.Ups)
128 | if tmp, ok := h.handlers[cacheName]; ok {
129 | logger.Debug(fmt.Sprintf("Using existing handler for UPS `%s`", cacheName))
130 | promHandler = *tmp
131 | } else {
132 | //Build a custom registry to include only the UPS metrics on the UPS metrics path
133 | logger.Info(fmt.Sprintf("Creating new registry, handler, and collector for UPS `%s`", cacheName))
134 | registry := prometheus.NewRegistry()
135 | promHandler = promhttp.HandlerFor(registry, promhttp.HandlerOpts{Registry: registry})
136 | promHandler = promhttp.InstrumentMetricHandler(registry, promHandler)
137 |
138 | nutCollector, err := collectors.NewNutCollector(thisCollectorOpts, logger)
139 | if err != nil {
140 | w.WriteHeader(http.StatusInternalServerError)
141 | w.Write([]byte("500 - InternalServer Error"))
142 | logger.Error("Internal server error", "err", err)
143 | return
144 | }
145 | registry.MustRegister(nutCollector)
146 | h.handlers[cacheName] = &promHandler
147 | }
148 |
149 | promHandler.ServeHTTP(w, r)
150 | }
151 |
152 | func main() {
153 | //flag.AddFlags(kingpin.CommandLine, promlogConfig)
154 | kingpin.Version(Version)
155 | kingpin.HelpFlag.Short('h')
156 | kingpin.Parse()
157 |
158 | /* Reconfigure logger after parsing arguments */
159 | opts := &slog.HandlerOptions{}
160 | switch *logLevel {
161 | case "error":
162 | opts.Level = slog.LevelError
163 | case "warn":
164 | opts.Level = slog.LevelWarn
165 | case "info":
166 | opts.Level = slog.LevelInfo
167 | case "debug":
168 | opts.Level = slog.LevelDebug
169 | }
170 | if *logJson {
171 | logger = slog.New(slog.NewJSONHandler(os.Stderr, opts))
172 | slog.SetDefault(logger)
173 | } else {
174 | logger = slog.New(slog.NewTextHandler(os.Stderr, opts))
175 | slog.SetDefault(logger)
176 | }
177 |
178 | if *nutUsername != "" {
179 | logger.Debug("Authenticating to NUT server")
180 | nutPassword = os.Getenv("NUT_EXPORTER_PASSWORD")
181 | if nutPassword == "" {
182 | logger.Error("Username set, but NUT_EXPORTER_PASSWORD environment variable missing. Cannot authenticate!")
183 | os.Exit(2)
184 | }
185 | }
186 |
187 | variables := []string{}
188 | hasUpsStatusVariable := false
189 | for _, varName := range strings.Split(*enableFilter, ",") {
190 | // Be nice and clear spaces for those that like them
191 | variable := strings.Trim(varName, " ")
192 | if variable == "" {
193 | continue
194 | }
195 | variables = append(variables, variable)
196 |
197 | // Special handling because this is an important and commonly needed variable
198 | if variable == "ups.status" {
199 | hasUpsStatusVariable = true
200 | }
201 | }
202 |
203 | if !hasUpsStatusVariable {
204 | logger.Warn("Exporter has been started without `ups.status` variable to be exported with --nut.vars_enable. Online/offline/etc statuses will not be reported!")
205 | }
206 |
207 | statuses := []string{}
208 | for _, status := range strings.Split(*statusList, ",") {
209 | // Be nice and clear spaces for those that like them
210 | stat := strings.Trim(status, " ")
211 | if stat == "" {
212 | continue
213 | }
214 | statuses = append(statuses, strings.Trim(stat, " "))
215 | }
216 |
217 | collectorOpts = collectors.NutCollectorOpts{
218 | Namespace: *metricsNamespace,
219 | Server: *server,
220 | ServerPort: *serverport,
221 | Username: *nutUsername,
222 | Password: nutPassword,
223 | DisableDeviceInfo: *disableDeviceInfo,
224 | Variables: variables,
225 | Statuses: statuses,
226 | OnRegex: *onRegex,
227 | OffRegex: *offRegex,
228 | }
229 |
230 | if *printMetrics {
231 | /* Make a channel and function to send output along */
232 | var out chan *prometheus.Desc
233 | eatOutput := func(in <-chan *prometheus.Desc) {
234 | for desc := range in {
235 | /* Weaksauce... no direct access to the variables */
236 | //Desc{fqName: "the_name", help: "help text", constLabels: {}, variableLabels: []}
237 | tmp := desc.String()
238 | vals := strings.Split(tmp, `"`)
239 | fmt.Printf(" %s - %s\n", vals[1], vals[3])
240 | }
241 | }
242 |
243 | /* Interesting juggle here...
244 | - Make a channel the describe function can send output to
245 | - Start the printing function that consumes the output in the background
246 | - Call the describe function to feed the channel (which blocks until the consume function eats a message)
247 | - When the describe function exits after returning the last item, close the channel to end the background consume function
248 | */
249 | fmt.Println("NUT")
250 | nutCollector, _ := collectors.NewNutCollector(collectorOpts, logger)
251 | out = make(chan *prometheus.Desc)
252 | go eatOutput(out)
253 | nutCollector.Describe(out)
254 | close(out)
255 |
256 | os.Exit(0)
257 | }
258 |
259 | logger.Info("Starting nut_exporter", "version", Version)
260 |
261 | handler := &metricsHandler{
262 | handlers: make(map[string]*http.Handler),
263 | }
264 |
265 | http.Handle(*metricsPath, handler)
266 | http.Handle(*exporterMetricsPath, promhttp.Handler())
267 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
268 | w.Write([]byte(`
269 |
NUT Exporter
270 |
271 | NUT Exporter
272 | UPS metrics
273 | Exporter metrics
274 |
275 | `))
276 | })
277 |
278 | srv := &http.Server{}
279 | if err := web.ListenAndServe(srv, tookitFlags, logger); err != nil {
280 | logger.Error(err.Error())
281 | os.Exit(1)
282 | }
283 | }
284 |
--------------------------------------------------------------------------------
/collectors/nut_collector.go:
--------------------------------------------------------------------------------
1 | package collectors
2 |
3 | import (
4 | "fmt"
5 | "log/slog"
6 | "regexp"
7 | "strings"
8 |
9 | "github.com/prometheus/client_golang/prometheus"
10 | nut "github.com/robbiet480/go.nut"
11 | )
12 |
13 | var deviceLabels = []string{"model", "mfr", "serial", "type", "description", "contact", "location", "part", "macaddr"}
14 |
15 | type NutCollector struct {
16 | deviceDesc *prometheus.Desc
17 | logger *slog.Logger
18 | opts *NutCollectorOpts
19 | onRegex *regexp.Regexp
20 | offRegex *regexp.Regexp
21 | }
22 |
23 | type NutCollectorOpts struct {
24 | Namespace string
25 | Server string
26 | ServerPort int
27 | Ups string
28 | Username string
29 | Password string
30 | Variables []string
31 | Statuses []string
32 | OnRegex string
33 | OffRegex string
34 | DisableDeviceInfo bool
35 | }
36 |
37 | func NewNutCollector(opts NutCollectorOpts, logger *slog.Logger) (*NutCollector, error) {
38 | deviceDesc := prometheus.NewDesc(prometheus.BuildFQName(opts.Namespace, "", "device_info"),
39 | "UPS Device information",
40 | deviceLabels, nil,
41 | )
42 | if opts.DisableDeviceInfo {
43 | deviceDesc = nil
44 | }
45 |
46 | var onRegex, offRegex *regexp.Regexp
47 | var err error
48 |
49 | if opts.OnRegex != "" {
50 | onRegex, err = regexp.Compile(fmt.Sprintf("(?i)%s", opts.OnRegex))
51 | if err != nil {
52 | return nil, err
53 | }
54 | }
55 |
56 | if opts.OffRegex != "" {
57 | offRegex, err = regexp.Compile(fmt.Sprintf("(?i)%s", opts.OffRegex))
58 | if err != nil {
59 | return nil, err
60 | }
61 | }
62 |
63 | collector := &NutCollector{
64 | deviceDesc: deviceDesc,
65 | logger: logger,
66 | opts: &opts,
67 | onRegex: onRegex,
68 | offRegex: offRegex,
69 | }
70 |
71 | if opts.Ups != "" {
72 | valid, err := collector.IsValidUPSName(opts.Ups)
73 | if err != nil {
74 | logger.Warn("Error detected while verifying UPS name - proceeding without validation", "error", err)
75 | } else if !valid {
76 | return nil, fmt.Errorf("%s UPS is not a valid name in the NUT server %s", opts.Ups, opts.Server)
77 | }
78 | }
79 |
80 | logger.Info("collector configured", "variables", strings.Join(collector.opts.Variables, ","))
81 | return collector, nil
82 | }
83 |
84 | func (c *NutCollector) Collect(ch chan<- prometheus.Metric) {
85 | c.logger.Debug("Connecting to server", "server", c.opts.Server, "port", c.opts.ServerPort)
86 | client, err := nut.Connect(c.opts.Server, c.opts.ServerPort)
87 | if err != nil {
88 | c.logger.Error("failed connecting to server", "err", err)
89 | ch <- prometheus.NewInvalidMetric(
90 | prometheus.NewDesc(prometheus.BuildFQName(c.opts.Namespace, "", "error"),
91 | "Failure gathering UPS variables", nil, nil),
92 | err)
93 | return
94 | }
95 |
96 | defer client.Disconnect()
97 | c.logger.Debug("Connected to server", "server", c.opts.Server)
98 |
99 | if c.opts.Username != "" && c.opts.Password != "" {
100 | _, err = client.Authenticate(c.opts.Username, c.opts.Password)
101 | if err == nil {
102 | c.logger.Debug("Authenticated", "server", c.opts.Server, "user", c.opts.Username)
103 | } else {
104 | c.logger.Warn("Failed to authenticate to NUT server", "server", c.opts.Server, "user", c.opts.Username)
105 | //Don't bail after logging the warning. Most NUT configurations do not require authn to read variables
106 | }
107 | }
108 |
109 | upsList := []nut.UPS{}
110 | if c.opts.Ups != "" {
111 | ups, err := nut.NewUPS(c.opts.Ups, &client)
112 | if err == nil {
113 | c.logger.Debug("Instantiated UPS", "name", c.opts.Ups)
114 | upsList = append(upsList, ups)
115 | } else {
116 | c.logger.Error("Failure instantiating the UPS", "name", c.opts.Ups, "err", err)
117 | ch <- prometheus.NewInvalidMetric(
118 | prometheus.NewDesc(prometheus.BuildFQName(c.opts.Namespace, "", "error"),
119 | "Failure instantiating the UPS", nil, nil),
120 | err)
121 | return
122 | }
123 | } else {
124 | tmp, err := client.GetUPSList()
125 | if err == nil {
126 | c.logger.Debug("Obtained list of UPS devices")
127 | upsList = tmp
128 | for _, ups := range tmp {
129 | c.logger.Debug("UPS name detection", "name", ups.Name)
130 | }
131 | } else {
132 | c.logger.Error("Failure getting the list of UPS devices", "err", err)
133 | ch <- prometheus.NewInvalidMetric(
134 | prometheus.NewDesc(prometheus.BuildFQName(c.opts.Namespace, "", "error"),
135 | "Failure getting the list of UPS devices", nil, nil),
136 | err)
137 | return
138 | }
139 | }
140 |
141 | if len(upsList) > 1 {
142 | c.logger.Error("Multiple UPS devices were found by NUT for this scrape. For this configuration, you MUST scrape this exporter with a query string parameter indicating which UPS to scrape. Valid values of ups are:")
143 | for _, ups := range upsList {
144 | c.logger.Error(ups.Name)
145 | }
146 | ch <- prometheus.NewInvalidMetric(
147 | prometheus.NewDesc(prometheus.BuildFQName(c.opts.Namespace, "", "error"),
148 | "Multiple UPS devices were found from NUT. Please add a ups= query string", nil, nil),
149 | err)
150 | return
151 | } else {
152 | //Set the name so subsequent scrapes don't have to look it up
153 | c.opts.Ups = upsList[0].Name
154 | }
155 |
156 | for _, ups := range upsList {
157 | device := make(map[string]string)
158 | for _, label := range deviceLabels {
159 | device[label] = ""
160 | }
161 |
162 | c.logger.Debug(
163 | "UPS info",
164 | "name", ups.Name,
165 | "description", ups.Description,
166 | "master", ups.Master,
167 | "nmumber_of_logins", ups.NumberOfLogins,
168 | )
169 | for i, clientName := range ups.Clients {
170 | c.logger.Debug(fmt.Sprintf("client %d", i), "name", clientName)
171 | }
172 | for _, command := range ups.Commands {
173 | c.logger.Debug("ups command", "command", command.Name, "description", command.Description)
174 | }
175 | for _, variable := range ups.Variables {
176 | c.logger.Debug(
177 | "Variable dump",
178 | "variable_name", variable.Name,
179 | "value", variable.Value,
180 | "type", variable.Type,
181 | "description", variable.Description,
182 | "writeable", variable.Writeable,
183 | "maximum_length", variable.MaximumLength,
184 | "original_type", variable.OriginalType,
185 | )
186 | path := strings.Split(variable.Name, ".")
187 | if path[0] == "device" {
188 | device[path[1]] = fmt.Sprintf("%v", variable.Value)
189 | }
190 |
191 | /* Done special processing - now get as general as possible and gather all requested or number-like metrics */
192 | if len(c.opts.Variables) == 0 || sliceContains(c.opts.Variables, variable.Name) {
193 | c.logger.Debug("Export the variable? true")
194 | value := float64(0)
195 |
196 | /* Deal with ups.status specially because it is a collection of 'flags' */
197 | if variable.Name == "ups.status" {
198 | setStatuses := make(map[string]bool)
199 | varDesc := prometheus.NewDesc(prometheus.BuildFQName(c.opts.Namespace, "", strings.Replace(variable.Name, ".", "_", -1)),
200 | fmt.Sprintf("%s (%s)", variable.Description, variable.Name),
201 | []string{"flag"}, nil,
202 | )
203 |
204 | for _, statusFlag := range strings.Split(variable.Value.(string), " ") {
205 | setStatuses[statusFlag] = true
206 | ch <- prometheus.MustNewConstMetric(varDesc, prometheus.GaugeValue, float64(1), statusFlag)
207 | }
208 |
209 | /* If the user specifies the statues that must always be set, handle that here */
210 | if len(c.opts.Statuses) > 0 {
211 | for _, status := range c.opts.Statuses {
212 | /* This status flag was set because we saw it in the output... skip it */
213 | if _, ok := setStatuses[status]; ok {
214 | continue
215 | }
216 | ch <- prometheus.MustNewConstMetric(varDesc, prometheus.GaugeValue, float64(0), status)
217 | }
218 | }
219 | continue
220 | }
221 |
222 | /* This is overkill - the library only deals with bool, string, int64 and float64 */
223 | switch v := variable.Value.(type) {
224 | case bool:
225 | if v {
226 | value = float64(1)
227 | }
228 | case int:
229 | value = float64(v)
230 | case int8:
231 | value = float64(v)
232 | case int16:
233 | value = float64(v)
234 | case int64:
235 | value = float64(v)
236 | case float32:
237 | value = float64(v)
238 | case float64:
239 | value = float64(v)
240 | case string:
241 | /* All numbers should be coaxed to native types by the library, so see if we can figure out
242 | if this string could possible represent a binary value
243 | */
244 | if c.onRegex != nil && c.onRegex.MatchString(variable.Value.(string)) {
245 | c.logger.Debug("Converted string to 1 due to regex match", "value", variable.Value.(string))
246 | value = float64(1)
247 | } else if c.offRegex != nil && c.offRegex.MatchString(variable.Value.(string)) {
248 | c.logger.Debug("Converted string to 0 due to regex match", "value", variable.Value.(string))
249 | value = float64(0)
250 | } else {
251 | c.logger.Debug("Cannot convert string to binary 0/1", "value", variable.Value.(string))
252 | continue
253 | }
254 | default:
255 | c.logger.Warn("Unknown variable type from nut client library", "name", variable.Name, "type", fmt.Sprintf("%T", v), "claimed_type", variable.Type, "value", v)
256 | continue
257 | }
258 |
259 | name := strings.ReplaceAll(variable.Name, ".", "_")
260 | name = strings.ReplaceAll(name, "-", "_")
261 |
262 | fqName := prometheus.BuildFQName(c.opts.Namespace, "", name)
263 | varDesc := prometheus.NewDesc(fqName,
264 | fmt.Sprintf("%s (%s)", variable.Description, variable.Name),
265 | nil, nil,
266 | )
267 |
268 | c.logger.Debug("Collecting as prometheus metric", "name", fqName, "value", value)
269 | ch <- prometheus.MustNewConstMetric(varDesc, prometheus.GaugeValue, value)
270 | } else {
271 | c.logger.Debug("Export the variable? false", "count", len(c.opts.Variables), "variables", strings.Join(c.opts.Variables, ","))
272 | }
273 | }
274 |
275 | // Only provide device info if not disabled
276 | if !c.opts.DisableDeviceInfo {
277 | deviceValues := []string{}
278 | for _, label := range deviceLabels {
279 | deviceValues = append(deviceValues, device[label])
280 | }
281 | ch <- prometheus.MustNewConstMetric(c.deviceDesc, prometheus.GaugeValue, float64(1), deviceValues...)
282 | }
283 | }
284 | }
285 |
286 | func (c *NutCollector) Describe(ch chan<- *prometheus.Desc) {
287 | if !c.opts.DisableDeviceInfo {
288 | ch <- c.deviceDesc
289 | }
290 | }
291 |
292 | func sliceContains(c []string, value string) bool {
293 | for _, sliceValue := range c {
294 | if sliceValue == value {
295 | return true
296 | }
297 | }
298 | return false
299 | }
300 |
301 | func (c *NutCollector) IsValidUPSName(upsName string) (bool, error) {
302 | result := false
303 |
304 | c.logger.Debug(fmt.Sprintf("Connecting to server and verifying `%s` is a valid UPS name", upsName), "server", c.opts.Server)
305 | client, err := nut.Connect(c.opts.Server)
306 | if err != nil {
307 | c.logger.Error("error while connecting to server", "err", err)
308 | return result, err
309 | }
310 |
311 | defer client.Disconnect()
312 |
313 | if c.opts.Username != "" && c.opts.Password != "" {
314 | _, err = client.Authenticate(c.opts.Username, c.opts.Password)
315 | if err != nil {
316 | c.logger.Warn("Failed to authenticate to NUT server", "server", c.opts.Server, "user", c.opts.Username)
317 | //Don't bail after logging the warning. Most NUT configurations do not require authn to get the UPS list
318 | }
319 | }
320 |
321 | tmp, err := client.GetUPSList()
322 | if err != nil {
323 | c.logger.Error("Failure getting the list of UPS devices", "err", err)
324 | return result, err
325 | }
326 |
327 | for _, ups := range tmp {
328 | c.logger.Debug("UPS name detection", "name", ups.Name)
329 | if ups.Name == upsName {
330 | result = true
331 | }
332 | }
333 |
334 | c.logger.Debug(fmt.Sprintf("Validity result for UPS named `%s`", upsName), "valid", result)
335 | return result, nil
336 | }
337 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Network UPS Tools (NUT) Prometheus Exporter
2 |
3 | A [Prometheus](https://prometheus.io) exporter for the Network UPS Tools server. This exporter utilizes the [go.nut](https://github.com/robbiet480/go.nut) project as a network client of the NUT platform. The exporter is written in a way to permit an administrator to scrape one or many UPS devices visible to a NUT client as well as one or all NUT variables. A single instance of this exporter can scrape one or many NUT servers as well.
4 |
5 | A sample [dashboard](dashboard/dashboard.json) for Grafana is also available
6 | 
7 |
8 | ## Variables and information
9 | The variables exposed to a NUT client by the NUT system are the lifeblood of a deployment. These variables are consumed by this exporter and coaxed to Prometheus types.
10 |
11 | * See the [NUT documentation](https://networkupstools.org/docs/user-manual.chunked/_variables.html) for a list of all possible variables
12 | * Variables are set as prometheus metrics with the `ups` name added as a lable. Example: `ups.load` is set as `network_ups_tools_ups_load 100`
13 | * The exporter SHOULD be called with the ups to scrape set in the query string. Example: `https://127.0.0.1:9199/ups_metrics?ups=foo`
14 | * If the exporter scrapes NUT and detects more than one UPS, it is an error condition that will fail the scrape. In this case, use a variant of the scrape config example below for your environment
15 | * Default configs usually permit reading variables without authentication. If you have disabled this, see the Usage below to set credentials
16 | * This exporter will always export the device.* metrics as labels, except for uptime, with a constant value of 1
17 | * Setting the `nut.vars_enable` parameter to an empty string will cause all numeric variables to be exported
18 | * NUT may return strings as values for some variables. Prometheus supports only float values, so the `on_regex` and `off_regex` parameters can be used to convert these to 0 or 1 in some cases
19 | * Not all driver and UPS implementations provide all variables. Run this exporter with log.level at debug or use the `LIST VAR` upsc command to see available variables for your UPS
20 | * All number-like values are coaxed to the appropriate go type by the library and are set as the value of the exported metric
21 | * Boolean values are coaxed to 0 (false) or 1 (true)
22 |
23 | ### ups.status handling
24 | The special `ups.status` variable is returned by NUT as a string containing a list of status flags.
25 | There may be one or more flags set depending on the driver in use and the current state of the UPS.
26 | For example, `OL TRIM CHRG` indicates the UPS is online, stepping down incoming voltage and charging the battery.
27 |
28 | The metric `network_ups_tools_ups_status` will be set with a label for each flag returned with a constant value of `1`
29 |
30 | **Example**
31 | The above example will coax `OL TRIM CHRG` to...
32 | ```
33 | network_ups_tools_ups_status{flag="OL"} 1
34 | network_ups_tools_ups_status{flag="TRIM"} 1
35 | network_ups_tools_ups_status{flag="CHRG"} 1
36 | ```
37 |
38 | The exporter supports the `--nut.statuses` flag to allow you to force certain statuses to be exported at all times, regardless of whether NUT reports the status.
39 | This defaults to the below known list of statuses.
40 |
41 | **Example**
42 | Without changing the defaults, the status of `OL TRIM CHRG` will cause the following labels and values to be exported:
43 | ```
44 | network_ups_tools_ups_status{flag="OL"} 1
45 | network_ups_tools_ups_status{flag="TRIM"} 1
46 | network_ups_tools_ups_status{flag="CHRG"} 1
47 | network_ups_tools_ups_status{flag="OB"} 0
48 | network_ups_tools_ups_status{flag="LB"} 0
49 | network_ups_tools_ups_status{flag="HB"} 0
50 | network_ups_tools_ups_status{flag="RB"} 0
51 | network_ups_tools_ups_status{flag="DISCHRG"} 0
52 | network_ups_tools_ups_status{flag="BYPASS"} 0
53 | network_ups_tools_ups_status{flag="CAL"} 0
54 | network_ups_tools_ups_status{flag="OFF"} 0
55 | network_ups_tools_ups_status{flag="OVER"} 0
56 | network_ups_tools_ups_status{flag="BOOST"} 0
57 | network_ups_tools_ups_status{flag="FSD"} 0
58 | network_ups_tools_ups_status{flag="SD"} 0
59 | ```
60 | Because each UPS differs, it is advisable to observe your UPS under various conditions to know which of these statuses will never apply.
61 |
62 |
63 | #### Alerting on ups.status
64 | **IMPORTANT NOTE:** Not all UPSs utilize all values! What is reported by NUT depends greatly on the driver and the intelligence of the UPS.
65 | It is strongly suggested to observe your UPS under both "normal" and "abnormal" conditions to know what to expect NUT will report.
66 |
67 | As noted above, the UPS status is a special case and is handled with flags set as labels on the `network_ups_tools_ups_status` metric. Therefore, alerting can be configured for specific statuses. Examples:
68 | * **Alert if the UPS has exited 'online' mode**: `network_ups_tools_ups_status{flag="OL"} == 0`
69 | * **Alert if the UPS has gone on battery**: `network_ups_tools_ups_status{flag="OB"} == 1`
70 | * **Alert if any status changed in the past 5 minutes** `changes(network_ups_tools_ups_status[5m]) > 0`
71 |
72 | Unfortunately, the NUT documentation does not call out the full list of statuses each driver implements nor what a user can expect for a status.
73 | The following values were detected in the [NUT driver documentation](https://github.com/networkupstools/nut/blob/master/docs/new-drivers.txt):
74 | * OL - On line (mains is present)
75 | * OB - On battery (mains is not present)
76 | * LB - Low battery
77 | * HB - High battery
78 | * RB - The battery needs to be replaced
79 | * CHRG - The battery is charging
80 | * DISCHRG - The battery is discharging (inverter is providing load power)
81 | * BYPASS - UPS bypass circuit is active -- no battery protection is available
82 | * CAL - UPS is currently performing runtime calibration (on battery)
83 | * OFF - UPS is offline and is not supplying power to the load
84 | * OVER - UPS is overloaded
85 | * TRIM - UPS is trimming incoming voltage (called "buck" in some hardware)
86 | * BOOST - UPS is boosting incoming voltage
87 | * FSD and SD - Forced Shutdown
88 | Therefore, these are all enabled in the default value for `--nut.statuses`.
89 |
90 | ### Query String Parameters
91 | The exporter allows for per-scrape overrides of command line parameters by passing query string parameters. This enables a single nut_exporter to scrape multiple NUT servers
92 |
93 | The following query string parameters can be passed to the `/ups_metrics` path:
94 | * `ups` - Required if more than one UPS is present in NUT
95 | * `server` - Overrides the command line parameter `--nut.server`
96 | * `username` - Overrides the command line parameter `--nut.username`
97 | * `password` - Overrides the environment variable NUT_EXPORTER_PASSWORD. It is **strongly** recommended to avoid passing credentials over http unless the exporter is configured with TLS
98 | * `variables` - Overrides the command line parameter `--nut.vars_enable`
99 | * `statuses` - Overrides the command line parameter `--nut.statuses`
100 | See the example scrape configurations below for how to utilize this capability
101 |
102 | ### Example Prometheus Scrape Configurations
103 | Note that this exporter will scrape only one UPS per scrape invocation. If there are multiple UPS devices visible to NUT, you MUST ensure that you set up different scrape configs for each UPS device. Here is an example configuration for such a use case:
104 |
105 | ```
106 | - job_name: nut-primary
107 | metrics_path: /ups_metrics
108 | static_configs:
109 | - targets: ['myserver:9199']
110 | labels:
111 | ups: "primary"
112 | params:
113 | ups: [ "primary" ]
114 | - job_name: nut-secondary
115 | metrics_path: /ups_metrics
116 | static_configs:
117 | - targets: ['myserver:9199']
118 | labels:
119 | ups: "secondary"
120 | params:
121 | ups: [ "secondary" ]
122 | ```
123 |
124 | You can also configure a single exporter to scrape several NUT servers like so:
125 | ```
126 | - job_name: nut-primary
127 | metrics_path: /ups_metrics
128 | static_configs:
129 | - targets: ['exporterserver:9199']
130 | labels:
131 | ups: "primary"
132 | params:
133 | ups: [ "primary" ]
134 | server: [ "nutserver1" ]
135 | - job_name: nut-secondary
136 | metrics_path: /ups_metrics
137 | static_configs:
138 | - targets: ['exporterserver:9199']
139 | labels:
140 | ups: "secondary"
141 | params:
142 | ups: [ "secondary" ]
143 | server: [ "nutserver2" ]
144 | ```
145 |
146 | Or use a more robust relabel config similar to the [snmp_exporter](https://github.com/prometheus/snmp_exporter) (thanks to @sshaikh for the example):
147 | ```
148 | - job_name: ups
149 | static_configs:
150 | - targets: ['server1','server2'] # nut exporter
151 | metrics_path: /ups_metrics
152 | relabel_configs:
153 | - source_labels: [__address__]
154 | target_label: __param_server
155 | - source_labels: [__param_server]
156 | target_label: instance
157 | - target_label: __address__
158 | replacement: nut-exporter.local:9199
159 | ```
160 |
161 |
162 |
163 | ## Installation
164 |
165 | ### Binaries
166 |
167 | Download the already existing [binaries](https://github.com/DRuggeri/nut_exporter/releases) for your platform:
168 |
169 | ```bash
170 | $ ./nut_exporter
171 | ```
172 |
173 | ### From source
174 |
175 | Using the standard `go install` (you must have [Go](https://golang.org/) already installed in your local machine):
176 |
177 | ```bash
178 | $ go install github.com/DRuggeri/nut_exporter/v3@latest
179 | $ nut_exporter
180 | ```
181 |
182 | ### With Docker
183 | An official scratch-based Docker image is built with every tag and pushed to DockerHub and ghcr. Additionally, PRs will be tested by GitHubs actions.
184 |
185 | The following images are available for use:
186 | - [druggeri/nut_exporter](https://hub.docker.com/r/druggeri/nut_exporter)
187 | - [ghcr.io/DRuggeri/nut_exporter](https://ghcr.io/DRuggeri/nut_exporter)
188 |
189 |
190 |
191 | ## Usage
192 |
193 | ### Flags
194 |
195 | ```
196 | usage: nut_exporter []
197 |
198 |
199 | Flags:
200 | -h, --[no-]help Show context-sensitive help (also try --help-long and --help-man).
201 | --nut.server="127.0.0.1" Hostname or IP address of the server to connect to. ($NUT_EXPORTER_SERVER) ($NUT_EXPORTER_SERVER)
202 | --nut.serverport=3493 Port on the NUT server to connect to. ($NUT_EXPORTER_SERVERPORT) ($NUT_EXPORTER_SERVERPORT)
203 | --nut.username=NUT.USERNAME
204 | If set, will authenticate with this username to the server. Password must be set in NUT_EXPORTER_PASSWORD environment variable. ($NUT_EXPORTER_USERNAME)
205 | ($NUT_EXPORTER_USERNAME)
206 | --[no-]nut.disable_device_info
207 | A flag to disable the generation of the device_info meta metric. ($NUT_EXPORTER_DISABLE_DEVICE_INFO) ($NUT_EXPORTER_DISABLE_DEVICE_INFO)
208 | --nut.vars_enable="battery.charge,battery.voltage,battery.voltage.nominal,input.voltage,input.voltage.nominal,ups.load,ups.status"
209 | A comma-separated list of variable names to monitor. See the variable notes in README. ($NUT_EXPORTER_VARIABLES) ($NUT_EXPORTER_VARIABLES)
210 | --nut.on_regex="^(enable|enabled|on|true|active|activated)$"
211 | This regular expression will be used to determine if the var's value should be coaxed to 1 if it is a string. Match is case-insensitive. ($NUT_EXPORTER_ON_REGEX)
212 | ($NUT_EXPORTER_ON_REGEX)
213 | --nut.off_regex="^(disable|disabled|off|false|inactive|deactivated)$"
214 | This regular expression will be used to determine if the var's value should be coaxed to 0 if it is a string. Match is case-insensitive. ($NUT_EXPORTER_OFF_REGEX)
215 | ($NUT_EXPORTER_OFF_REGEX)
216 | --nut.statuses="OL,OB,LB,HB,RB,CHRG,DISCHRG,BYPASS,CAL,OFF,OVER,TRIM,BOOST,FSD,SD"
217 | A comma-separated list of statuses labels that will always be set by the exporter. If NUT does not set these flags, the exporter will force the
218 | network_ups_tools_ups_status{flag="NAME"} to 0. See the ups.status notes in README.' ($NUT_EXPORTER_STATUSES) ($NUT_EXPORTER_STATUSES)
219 | --metrics.namespace="network_ups_tools"
220 | Metrics Namespace ($NUT_EXPORTER_METRICS_NAMESPACE) ($NUT_EXPORTER_METRICS_NAMESPACE)
221 | --[no-]web.systemd-socket Use systemd socket activation listeners instead of port listeners (Linux only).
222 | --web.listen-address=:9199 ...
223 | Addresses on which to expose metrics and web interface. Repeatable for multiple addresses. Examples: `:9100` or `[::1]:9100` for http, `vsock://:9100` for vsock
224 | --web.config.file="" Path to configuration file that can enable TLS or authentication. See: https://github.com/prometheus/exporter-toolkit/blob/master/docs/web-configuration.md
225 | --web.telemetry-path="/ups_metrics"
226 | Path under which to expose the UPS Prometheus metrics ($NUT_EXPORTER_WEB_TELEMETRY_PATH) ($NUT_EXPORTER_WEB_TELEMETRY_PATH)
227 | --web.exporter-telemetry-path="/metrics"
228 | Path under which to expose process metrics about this exporter ($NUT_EXPORTER_WEB_EXPORTER_TELEMETRY_PATH) ($NUT_EXPORTER_WEB_EXPORTER_TELEMETRY_PATH)
229 | --[no-]printMetrics Print the metrics this exporter exposes and exits. Default: false ($NUT_EXPORTER_PRINT_METRICS) ($NUT_EXPORTER_PRINT_METRICS)
230 | --log.level="info" Minimum log level for messages. One of error, warn, info, or debug. Default: info ($NETGEAR_EXPORTER_LOG_LEVEL) ($NUT_EXPORTER__LOG_LEVEL)
231 | --[no-]log.json Format log lines as JSON. Default: false ($NETGEAR_EXPORTER_LOG_JSON) ($NUT_EXPORTER__LOG_JSON)
232 | --[no-]version Show application version.
233 | ```
234 |
235 |
236 |
237 | ## TLS and basic authentication
238 |
239 | The NUT Exporter supports TLS and basic authentication.
240 |
241 | To use TLS and/or basic authentication, you need to pass a configuration file
242 | using the `--web.config.file` parameter. The format of the file is described
243 | [in the exporter-toolkit repository](https://github.com/prometheus/exporter-toolkit/blob/master/docs/web-configuration.md).
244 |
245 | ## Metrics
246 |
247 | ### NUT
248 | This collector is the workhorse of the exporter. Default metrics are exported for the device and scrape stats. The `network_ups_tools_ups_variable` metric is exported with labels of `ups` and `variable` with the value set as noted in the README
249 |
250 | ```
251 | network_ups_tools_device_info - UPS device information
252 | network_ups_tools_VARIABLE_NAME - Variable from Network UPS Tools as noted in the variable notes above
253 | ```
254 |
255 | ## Helm Chart
256 | To install the [Helm](https://helm.sh/docs/) chart into a Kubernetes cluster run:
257 | ```sh
258 | helm repo add nut-exporter https://github.com/DRuggeri/nut_exporter
259 | helm install nut-exporter/nut-exporter nut-exporter
260 | ```
261 |
--------------------------------------------------------------------------------
/dashboard/dashboard.json:
--------------------------------------------------------------------------------
1 | {
2 | "__inputs": [
3 | {
4 | "name": "DS_PROMETHEUS",
5 | "label": "Prometheus",
6 | "description": "",
7 | "type": "datasource",
8 | "pluginId": "prometheus",
9 | "pluginName": "Prometheus"
10 | }
11 | ],
12 | "__elements": {},
13 | "__requires": [
14 | {
15 | "type": "grafana",
16 | "id": "grafana",
17 | "name": "Grafana",
18 | "version": "11.2.0"
19 | },
20 | {
21 | "type": "datasource",
22 | "id": "prometheus",
23 | "name": "Prometheus",
24 | "version": "1.0.0"
25 | },
26 | {
27 | "type": "panel",
28 | "id": "stat",
29 | "name": "Stat",
30 | "version": ""
31 | },
32 | {
33 | "type": "panel",
34 | "id": "gauge",
35 | "name": "Gauge",
36 | "version": ""
37 | },
38 | {
39 | "type": "panel",
40 | "id": "timeseries",
41 | "name": "Time series",
42 | "version": ""
43 | }
44 | ],
45 | "annotations": {
46 | "list": [
47 | {
48 | "$$hashKey": "object:7",
49 | "builtIn": 1,
50 | "datasource": {
51 | "type": "datasource",
52 | "uid": "grafana"
53 | },
54 | "enable": true,
55 | "hide": true,
56 | "iconColor": "rgba(0, 211, 255, 1)",
57 | "name": "Annotations & Alerts",
58 | "type": "dashboard"
59 | }
60 | ]
61 | },
62 | "editable": true,
63 | "fiscalYearStartMonth": 0,
64 | "graphTooltip": 0,
65 | "id": 26,
66 | "links": [],
67 | "panels": [
68 | {
69 | "collapsed": false,
70 | "gridPos": {
71 | "h": 1,
72 | "w": 24,
73 | "x": 0,
74 | "y": 0
75 | },
76 | "id": 17,
77 | "panels": [],
78 | "repeat": "ups",
79 | "title": "$ups",
80 | "type": "row"
81 | },
82 | {
83 | "datasource": {
84 | "type": "prometheus",
85 | "uid": "${DS_PROMETHEUS}"
86 | },
87 | "description": " * OL - On line (mains is present)\n * OB - On battery (mains is not present)\n * LB - Low battery\n * HB - High battery\n * RB - The battery needs to be replaced\n * CHRG - The battery is charging\n * DISCHRG - The battery is discharging (inverter is providing load power)\n * BYPASS - UPS bypass circuit is active -- no battery protection is available\n * CAL - UPS is currently performing runtime calibration (on battery)\n * OFF - UPS is offline and is not supplying power to the load\n * OVER - UPS is overloaded\n * TRIM - UPS is trimming incoming voltage (called \"buck\" in some hardware)\n * BOOST - UPS is boosting incoming voltage\n * FSD and SD - Forced Shutdown",
88 | "fieldConfig": {
89 | "defaults": {
90 | "color": {
91 | "mode": "fixed"
92 | },
93 | "mappings": [],
94 | "thresholds": {
95 | "mode": "absolute",
96 | "steps": [
97 | {
98 | "color": "green",
99 | "value": null
100 | },
101 | {
102 | "color": "red",
103 | "value": 80
104 | }
105 | ]
106 | }
107 | },
108 | "overrides": []
109 | },
110 | "gridPos": {
111 | "h": 3,
112 | "w": 3,
113 | "x": 0,
114 | "y": 1
115 | },
116 | "id": 26,
117 | "options": {
118 | "colorMode": "value",
119 | "graphMode": "none",
120 | "justifyMode": "auto",
121 | "orientation": "auto",
122 | "percentChangeColorMode": "standard",
123 | "reduceOptions": {
124 | "calcs": [
125 | "lastNotNull"
126 | ],
127 | "fields": "",
128 | "values": false
129 | },
130 | "showPercentChange": false,
131 | "textMode": "name",
132 | "wideLayout": true
133 | },
134 | "pluginVersion": "11.3.0",
135 | "targets": [
136 | {
137 | "datasource": {
138 | "type": "prometheus",
139 | "uid": "${DS_PROMETHEUS}"
140 | },
141 | "editorMode": "code",
142 | "expr": "network_ups_tools_ups_status{instance=~\"$instance\",job=~\"$job\",ups=\"$ups\"} == 1",
143 | "interval": "",
144 | "legendFormat": "{{flag}}",
145 | "range": true,
146 | "refId": "A"
147 | }
148 | ],
149 | "title": "UPS Status",
150 | "transparent": true,
151 | "type": "stat"
152 | },
153 | {
154 | "datasource": {
155 | "type": "prometheus",
156 | "uid": "${DS_PROMETHEUS}"
157 | },
158 | "description": "",
159 | "fieldConfig": {
160 | "defaults": {
161 | "color": {
162 | "mode": "fixed"
163 | },
164 | "mappings": [],
165 | "thresholds": {
166 | "mode": "absolute",
167 | "steps": [
168 | {
169 | "color": "green",
170 | "value": null
171 | },
172 | {
173 | "color": "red",
174 | "value": 80
175 | }
176 | ]
177 | }
178 | },
179 | "overrides": []
180 | },
181 | "gridPos": {
182 | "h": 3,
183 | "w": 10,
184 | "x": 3,
185 | "y": 1
186 | },
187 | "id": 19,
188 | "options": {
189 | "colorMode": "value",
190 | "graphMode": "none",
191 | "justifyMode": "auto",
192 | "orientation": "auto",
193 | "percentChangeColorMode": "standard",
194 | "reduceOptions": {
195 | "calcs": [
196 | "mean"
197 | ],
198 | "fields": "",
199 | "values": false
200 | },
201 | "showPercentChange": false,
202 | "textMode": "name",
203 | "wideLayout": true
204 | },
205 | "pluginVersion": "11.3.0",
206 | "targets": [
207 | {
208 | "datasource": {
209 | "type": "prometheus",
210 | "uid": "${DS_PROMETHEUS}"
211 | },
212 | "expr": "network_ups_tools_device_info{instance=~\"$instance\",job=~\"$job\",ups=\"$ups\"}",
213 | "interval": "",
214 | "legendFormat": "{{mfr}}",
215 | "refId": "A"
216 | }
217 | ],
218 | "title": "Manufacturer",
219 | "transparent": true,
220 | "type": "stat"
221 | },
222 | {
223 | "datasource": {
224 | "type": "prometheus",
225 | "uid": "${DS_PROMETHEUS}"
226 | },
227 | "description": "",
228 | "fieldConfig": {
229 | "defaults": {
230 | "color": {
231 | "mode": "fixed"
232 | },
233 | "mappings": [],
234 | "thresholds": {
235 | "mode": "absolute",
236 | "steps": [
237 | {
238 | "color": "green",
239 | "value": null
240 | },
241 | {
242 | "color": "red",
243 | "value": 80
244 | }
245 | ]
246 | }
247 | },
248 | "overrides": []
249 | },
250 | "gridPos": {
251 | "h": 3,
252 | "w": 11,
253 | "x": 13,
254 | "y": 1
255 | },
256 | "id": 20,
257 | "options": {
258 | "colorMode": "value",
259 | "graphMode": "none",
260 | "justifyMode": "auto",
261 | "orientation": "auto",
262 | "percentChangeColorMode": "standard",
263 | "reduceOptions": {
264 | "calcs": [
265 | "mean"
266 | ],
267 | "fields": "",
268 | "values": false
269 | },
270 | "showPercentChange": false,
271 | "textMode": "name",
272 | "wideLayout": true
273 | },
274 | "pluginVersion": "11.3.0",
275 | "targets": [
276 | {
277 | "datasource": {
278 | "type": "prometheus",
279 | "uid": "${DS_PROMETHEUS}"
280 | },
281 | "expr": "network_ups_tools_device_info{instance=~\"$instance\",job=~\"$job\",ups=\"$ups\"}",
282 | "interval": "",
283 | "legendFormat": "{{model}}",
284 | "refId": "A"
285 | }
286 | ],
287 | "title": "Model",
288 | "transparent": true,
289 | "type": "stat"
290 | },
291 | {
292 | "datasource": {
293 | "type": "prometheus",
294 | "uid": "${DS_PROMETHEUS}"
295 | },
296 | "fieldConfig": {
297 | "defaults": {
298 | "color": {
299 | "mode": "thresholds"
300 | },
301 | "mappings": [],
302 | "max": 100,
303 | "min": 0,
304 | "thresholds": {
305 | "mode": "absolute",
306 | "steps": [
307 | {
308 | "color": "dark-red",
309 | "value": null
310 | },
311 | {
312 | "color": "dark-orange",
313 | "value": 30
314 | },
315 | {
316 | "color": "dark-yellow",
317 | "value": 60
318 | },
319 | {
320 | "color": "dark-green",
321 | "value": 80
322 | }
323 | ]
324 | },
325 | "unit": "percent"
326 | },
327 | "overrides": []
328 | },
329 | "gridPos": {
330 | "h": 4,
331 | "w": 3,
332 | "x": 0,
333 | "y": 4
334 | },
335 | "id": 2,
336 | "options": {
337 | "minVizHeight": 75,
338 | "minVizWidth": 75,
339 | "orientation": "auto",
340 | "reduceOptions": {
341 | "calcs": [
342 | "last"
343 | ],
344 | "fields": "",
345 | "values": false
346 | },
347 | "showThresholdLabels": false,
348 | "showThresholdMarkers": true,
349 | "sizing": "auto"
350 | },
351 | "pluginVersion": "11.3.0",
352 | "targets": [
353 | {
354 | "datasource": {
355 | "type": "prometheus",
356 | "uid": "${DS_PROMETHEUS}"
357 | },
358 | "expr": "network_ups_tools_battery_charge{instance=~\"$instance\",job=~\"$job\",ups=\"$ups\"}",
359 | "instant": false,
360 | "interval": "",
361 | "legendFormat": "",
362 | "refId": "A"
363 | }
364 | ],
365 | "title": "Battery Charge",
366 | "transparent": true,
367 | "type": "gauge"
368 | },
369 | {
370 | "datasource": {
371 | "type": "prometheus",
372 | "uid": "${DS_PROMETHEUS}"
373 | },
374 | "description": "",
375 | "fieldConfig": {
376 | "defaults": {
377 | "color": {
378 | "mode": "palette-classic"
379 | },
380 | "custom": {
381 | "axisBorderShow": false,
382 | "axisCenteredZero": false,
383 | "axisColorMode": "text",
384 | "axisLabel": "",
385 | "axisPlacement": "auto",
386 | "barAlignment": 0,
387 | "barWidthFactor": 0.6,
388 | "drawStyle": "line",
389 | "fillOpacity": 50,
390 | "gradientMode": "opacity",
391 | "hideFrom": {
392 | "legend": false,
393 | "tooltip": false,
394 | "viz": false
395 | },
396 | "insertNulls": false,
397 | "lineInterpolation": "linear",
398 | "lineWidth": 1,
399 | "pointSize": 5,
400 | "scaleDistribution": {
401 | "type": "linear"
402 | },
403 | "showPoints": "never",
404 | "spanNulls": false,
405 | "stacking": {
406 | "group": "A",
407 | "mode": "none"
408 | },
409 | "thresholdsStyle": {
410 | "mode": "area"
411 | }
412 | },
413 | "mappings": [],
414 | "min": 0,
415 | "thresholds": {
416 | "mode": "absolute",
417 | "steps": [
418 | {
419 | "color": "red",
420 | "value": null
421 | },
422 | {
423 | "color": "orange",
424 | "value": 300
425 | },
426 | {
427 | "color": "transparent",
428 | "value": 900
429 | }
430 | ]
431 | },
432 | "unit": "s"
433 | },
434 | "overrides": []
435 | },
436 | "gridPos": {
437 | "h": 6,
438 | "w": 10,
439 | "x": 3,
440 | "y": 4
441 | },
442 | "id": 11,
443 | "options": {
444 | "alertThreshold": true,
445 | "legend": {
446 | "calcs": [],
447 | "displayMode": "list",
448 | "placement": "bottom",
449 | "showLegend": false
450 | },
451 | "tooltip": {
452 | "mode": "multi",
453 | "sort": "none"
454 | }
455 | },
456 | "pluginVersion": "11.3.0",
457 | "targets": [
458 | {
459 | "datasource": {
460 | "type": "prometheus",
461 | "uid": "${DS_PROMETHEUS}"
462 | },
463 | "expr": "network_ups_tools_battery_runtime{instance=~\"$instance\",job=~\"$job\",ups=\"$ups\"}",
464 | "interval": "",
465 | "legendFormat": "{{ups}}",
466 | "refId": "A"
467 | }
468 | ],
469 | "title": "Battery Run Time Left",
470 | "transparent": true,
471 | "type": "timeseries"
472 | },
473 | {
474 | "datasource": {
475 | "type": "prometheus",
476 | "uid": "${DS_PROMETHEUS}"
477 | },
478 | "fieldConfig": {
479 | "defaults": {
480 | "color": {
481 | "mode": "palette-classic"
482 | },
483 | "custom": {
484 | "axisBorderShow": false,
485 | "axisCenteredZero": false,
486 | "axisColorMode": "text",
487 | "axisLabel": "",
488 | "axisPlacement": "auto",
489 | "barAlignment": 0,
490 | "barWidthFactor": 0.6,
491 | "drawStyle": "line",
492 | "fillOpacity": 50,
493 | "gradientMode": "opacity",
494 | "hideFrom": {
495 | "legend": false,
496 | "tooltip": false,
497 | "viz": false
498 | },
499 | "insertNulls": false,
500 | "lineInterpolation": "linear",
501 | "lineWidth": 1,
502 | "pointSize": 5,
503 | "scaleDistribution": {
504 | "type": "linear"
505 | },
506 | "showPoints": "never",
507 | "spanNulls": false,
508 | "stacking": {
509 | "group": "A",
510 | "mode": "none"
511 | },
512 | "thresholdsStyle": {
513 | "mode": "off"
514 | }
515 | },
516 | "mappings": [],
517 | "max": 100,
518 | "min": 0,
519 | "thresholds": {
520 | "mode": "absolute",
521 | "steps": [
522 | {
523 | "color": "green",
524 | "value": null
525 | },
526 | {
527 | "color": "red",
528 | "value": 80
529 | }
530 | ]
531 | },
532 | "unit": "percent"
533 | },
534 | "overrides": []
535 | },
536 | "gridPos": {
537 | "h": 6,
538 | "w": 11,
539 | "x": 13,
540 | "y": 4
541 | },
542 | "id": 15,
543 | "options": {
544 | "alertThreshold": true,
545 | "legend": {
546 | "calcs": [],
547 | "displayMode": "list",
548 | "placement": "bottom",
549 | "showLegend": false
550 | },
551 | "tooltip": {
552 | "mode": "multi",
553 | "sort": "none"
554 | }
555 | },
556 | "pluginVersion": "11.3.0",
557 | "targets": [
558 | {
559 | "datasource": {
560 | "type": "prometheus",
561 | "uid": "${DS_PROMETHEUS}"
562 | },
563 | "expr": "network_ups_tools_ups_load{instance=~\"$instance\",job=~\"$job\",ups=\"$ups\"}",
564 | "interval": "",
565 | "legendFormat": "{{ups}}",
566 | "refId": "A"
567 | }
568 | ],
569 | "title": "Load",
570 | "transparent": true,
571 | "type": "timeseries"
572 | },
573 | {
574 | "datasource": {
575 | "type": "prometheus",
576 | "uid": "${DS_PROMETHEUS}"
577 | },
578 | "fieldConfig": {
579 | "defaults": {
580 | "mappings": [],
581 | "thresholds": {
582 | "mode": "absolute",
583 | "steps": [
584 | {
585 | "color": "dark-red",
586 | "value": null
587 | },
588 | {
589 | "color": "dark-green",
590 | "value": 120
591 | }
592 | ]
593 | },
594 | "unit": "s"
595 | },
596 | "overrides": []
597 | },
598 | "gridPos": {
599 | "h": 2,
600 | "w": 3,
601 | "x": 0,
602 | "y": 8
603 | },
604 | "id": 24,
605 | "options": {
606 | "colorMode": "value",
607 | "graphMode": "none",
608 | "justifyMode": "auto",
609 | "orientation": "auto",
610 | "percentChangeColorMode": "standard",
611 | "reduceOptions": {
612 | "calcs": [
613 | "mean"
614 | ],
615 | "fields": "",
616 | "values": false
617 | },
618 | "showPercentChange": false,
619 | "textMode": "value",
620 | "wideLayout": true
621 | },
622 | "pluginVersion": "11.3.0",
623 | "targets": [
624 | {
625 | "datasource": {
626 | "type": "prometheus",
627 | "uid": "${DS_PROMETHEUS}"
628 | },
629 | "expr": "network_ups_tools_battery_runtime{instance=~\"$instance\",job=~\"$job\",ups=\"$ups\"}",
630 | "interval": "",
631 | "legendFormat": "",
632 | "refId": "A"
633 | }
634 | ],
635 | "title": "Battery Runtime",
636 | "transparent": true,
637 | "type": "stat"
638 | },
639 | {
640 | "datasource": {
641 | "type": "prometheus",
642 | "uid": "${DS_PROMETHEUS}"
643 | },
644 | "fieldConfig": {
645 | "defaults": {
646 | "mappings": [],
647 | "max": 140,
648 | "min": 90,
649 | "thresholds": {
650 | "mode": "absolute",
651 | "steps": [
652 | {
653 | "color": "dark-red",
654 | "value": null
655 | },
656 | {
657 | "color": "dark-green",
658 | "value": 95
659 | },
660 | {
661 | "color": "dark-yellow",
662 | "value": 125
663 | },
664 | {
665 | "color": "dark-red",
666 | "value": 135
667 | }
668 | ]
669 | }
670 | },
671 | "unit": "volt",
672 | "overrides": []
673 | },
674 | "gridPos": {
675 | "h": 6,
676 | "w": 3,
677 | "x": 0,
678 | "y": 10
679 | },
680 | "id": 5,
681 | "options": {
682 | "minVizHeight": 75,
683 | "minVizWidth": 75,
684 | "orientation": "auto",
685 | "reduceOptions": {
686 | "calcs": [
687 | "last"
688 | ],
689 | "fields": "",
690 | "values": false
691 | },
692 | "showThresholdLabels": false,
693 | "showThresholdMarkers": true,
694 | "sizing": "auto"
695 | },
696 | "pluginVersion": "11.3.0",
697 | "targets": [
698 | {
699 | "datasource": {
700 | "type": "prometheus",
701 | "uid": "${DS_PROMETHEUS}"
702 | },
703 | "expr": "network_ups_tools_input_voltage{instance=~\"$instance\",job=~\"$job\",ups=\"$ups\"}",
704 | "interval": "",
705 | "legendFormat": "",
706 | "refId": "A"
707 | }
708 | ],
709 | "title": "Line Volts",
710 | "transparent": true,
711 | "type": "gauge"
712 | },
713 | {
714 | "datasource": {
715 | "type": "prometheus",
716 | "uid": "${DS_PROMETHEUS}"
717 | },
718 | "fieldConfig": {
719 | "defaults": {
720 | "color": {
721 | "mode": "palette-classic"
722 | },
723 | "custom": {
724 | "axisBorderShow": false,
725 | "axisCenteredZero": false,
726 | "axisColorMode": "text",
727 | "axisLabel": "",
728 | "axisPlacement": "auto",
729 | "barAlignment": 0,
730 | "barWidthFactor": 0.6,
731 | "drawStyle": "line",
732 | "fillOpacity": 50,
733 | "gradientMode": "opacity",
734 | "hideFrom": {
735 | "legend": false,
736 | "tooltip": false,
737 | "viz": false
738 | },
739 | "insertNulls": false,
740 | "lineInterpolation": "linear",
741 | "lineWidth": 1,
742 | "pointSize": 5,
743 | "scaleDistribution": {
744 | "type": "linear"
745 | },
746 | "showPoints": "never",
747 | "spanNulls": false,
748 | "stacking": {
749 | "group": "A",
750 | "mode": "none"
751 | },
752 | "thresholdsStyle": {
753 | "mode": "off"
754 | }
755 | },
756 | "mappings": [],
757 | "max": 140,
758 | "min": 90,
759 | "thresholds": {
760 | "mode": "absolute",
761 | "steps": [
762 | {
763 | "color": "green",
764 | "value": null
765 | },
766 | {
767 | "color": "red",
768 | "value": 80
769 | }
770 | ]
771 | },
772 | "unit": "volt"
773 | },
774 | "overrides": []
775 | },
776 | "gridPos": {
777 | "h": 6,
778 | "w": 21,
779 | "x": 3,
780 | "y": 10
781 | },
782 | "id": 12,
783 | "options": {
784 | "alertThreshold": true,
785 | "legend": {
786 | "calcs": [],
787 | "displayMode": "list",
788 | "placement": "bottom",
789 | "showLegend": false
790 | },
791 | "tooltip": {
792 | "mode": "multi",
793 | "sort": "none"
794 | }
795 | },
796 | "pluginVersion": "11.3.0",
797 | "targets": [
798 | {
799 | "datasource": {
800 | "type": "prometheus",
801 | "uid": "${DS_PROMETHEUS}"
802 | },
803 | "expr": "network_ups_tools_input_voltage{instance=~\"$instance\",job=~\"$job\",ups=\"$ups\"}",
804 | "interval": "",
805 | "legendFormat": "{{ups}}",
806 | "range": true,
807 | "refId": "A"
808 | }
809 | ],
810 | "title": "",
811 | "transparent": true,
812 | "type": "timeseries"
813 | },
814 | {
815 | "datasource": {
816 | "type": "prometheus",
817 | "uid": "${DS_PROMETHEUS}"
818 | },
819 | "fieldConfig": {
820 | "defaults": {
821 | "mappings": [
822 | {
823 | "options": {
824 | "match": "null",
825 | "result": {
826 | "text": "N/A"
827 | }
828 | },
829 | "type": "special"
830 | }
831 | ],
832 | "max": 30,
833 | "min": 0,
834 | "thresholds": {
835 | "mode": "absolute",
836 | "steps": [
837 | {
838 | "color": "dark-red",
839 | "value": null
840 | },
841 | {
842 | "color": "dark-green",
843 | "value": 22
844 | },
845 | {
846 | "color": "dark-red",
847 | "value": 28
848 | }
849 | ]
850 | },
851 | "unit": "volt"
852 | },
853 | "overrides": []
854 | },
855 | "gridPos": {
856 | "h": 6,
857 | "w": 3,
858 | "x": 0,
859 | "y": 16
860 | },
861 | "id": 4,
862 | "options": {
863 | "minVizHeight": 75,
864 | "minVizWidth": 75,
865 | "orientation": "horizontal",
866 | "reduceOptions": {
867 | "calcs": [
868 | "last"
869 | ],
870 | "fields": "",
871 | "values": false
872 | },
873 | "showThresholdLabels": false,
874 | "showThresholdMarkers": true,
875 | "sizing": "auto"
876 | },
877 | "pluginVersion": "11.3.0",
878 | "targets": [
879 | {
880 | "datasource": {
881 | "type": "prometheus",
882 | "uid": "${DS_PROMETHEUS}"
883 | },
884 | "expr": "network_ups_tools_battery_voltage{instance=~\"$instance\",job=~\"$job\",ups=\"$ups\"}",
885 | "interval": "",
886 | "legendFormat": "__auto",
887 | "refId": "A"
888 | },
889 | {
890 | "datasource": {
891 | "type": "prometheus",
892 | "uid": "${DS_PROMETHEUS}"
893 | },
894 | "expr": "network_ups_tools_battery_voltage_low{instance=~\"$instance\",job=~\"$job\",ups=\"$ups\"}",
895 | "interval": "",
896 | "legendFormat": "voltage",
897 | "refId": "B"
898 | },
899 | {
900 | "datasource": {
901 | "type": "prometheus",
902 | "uid": "${DS_PROMETHEUS}"
903 | },
904 | "expr": "network_ups_tools_battery_voltage_high{instance=~\"$instance\",job=~\"$job\",ups=\"$ups\"}",
905 | "interval": "",
906 | "legendFormat": "voltage",
907 | "refId": "C"
908 | },
909 | {
910 | "datasource": {
911 | "type": "prometheus",
912 | "uid": "${DS_PROMETHEUS}"
913 | },
914 | "expr": "network_ups_tools_battery_voltage_high{instance=~\"$instance\",job=~\"$job\",ups=\"$ups\"}",
915 | "interval": "",
916 | "legendFormat": "voltage",
917 | "refId": "D"
918 | }
919 | ],
920 | "transformations": [
921 | {
922 | "id": "configFromData",
923 | "options": {
924 | "applyTo": {
925 | "id": "byFrameRefID",
926 | "options": "A"
927 | },
928 | "configRefId": "B",
929 | "mappings": [
930 | {
931 | "fieldName": "voltage",
932 | "handlerKey": "min"
933 | }
934 | ]
935 | }
936 | },
937 | {
938 | "id": "configFromData",
939 | "options": {
940 | "applyTo": {
941 | "id": "byFrameRefID",
942 | "options": "A"
943 | },
944 | "configRefId": "C",
945 | "mappings": [
946 | {
947 | "fieldName": "voltage",
948 | "handlerKey": "max"
949 | }
950 | ]
951 | }
952 | },
953 | {
954 | "id": "configFromData",
955 | "options": {
956 | "applyTo": {
957 | "id": "byFrameRefID",
958 | "options": "A"
959 | },
960 | "configRefId": "D",
961 | "mappings": [
962 | {
963 | "fieldName": "voltage",
964 | "handlerKey": "threshold1"
965 | }
966 | ]
967 | }
968 | }
969 | ],
970 | "title": "Battery Volts",
971 | "transparent": true,
972 | "type": "gauge"
973 | },
974 | {
975 | "datasource": {
976 | "type": "prometheus",
977 | "uid": "${DS_PROMETHEUS}"
978 | },
979 | "fieldConfig": {
980 | "defaults": {
981 | "color": {
982 | "mode": "thresholds"
983 | },
984 | "custom": {
985 | "axisBorderShow": false,
986 | "axisCenteredZero": false,
987 | "axisColorMode": "text",
988 | "axisLabel": "",
989 | "axisPlacement": "auto",
990 | "barAlignment": 0,
991 | "barWidthFactor": 0.6,
992 | "drawStyle": "line",
993 | "fillOpacity": 50,
994 | "gradientMode": "opacity",
995 | "hideFrom": {
996 | "legend": false,
997 | "tooltip": false,
998 | "viz": false
999 | },
1000 | "insertNulls": false,
1001 | "lineInterpolation": "linear",
1002 | "lineWidth": 1,
1003 | "pointSize": 5,
1004 | "scaleDistribution": {
1005 | "type": "linear"
1006 | },
1007 | "showPoints": "never",
1008 | "spanNulls": false,
1009 | "stacking": {
1010 | "group": "A",
1011 | "mode": "none"
1012 | },
1013 | "thresholdsStyle": {
1014 | "mode": "dashed"
1015 | }
1016 | },
1017 | "mappings": [],
1018 | "min": 0,
1019 | "thresholds": {
1020 | "mode": "absolute",
1021 | "steps": [
1022 | {
1023 | "color": "green",
1024 | "value": null
1025 | },
1026 | {
1027 | "color": "red",
1028 | "value": 80
1029 | }
1030 | ]
1031 | },
1032 | "unit": "volt"
1033 | },
1034 | "overrides": []
1035 | },
1036 | "gridPos": {
1037 | "h": 6,
1038 | "w": 21,
1039 | "x": 3,
1040 | "y": 16
1041 | },
1042 | "id": 13,
1043 | "options": {
1044 | "alertThreshold": true,
1045 | "legend": {
1046 | "calcs": [],
1047 | "displayMode": "list",
1048 | "placement": "bottom",
1049 | "showLegend": false
1050 | },
1051 | "tooltip": {
1052 | "mode": "multi",
1053 | "sort": "none"
1054 | }
1055 | },
1056 | "pluginVersion": "11.3.0",
1057 | "targets": [
1058 | {
1059 | "datasource": {
1060 | "type": "prometheus",
1061 | "uid": "${DS_PROMETHEUS}"
1062 | },
1063 | "editorMode": "code",
1064 | "expr": "network_ups_tools_battery_voltage{instance=~\"$instance\",job=~\"$job\",ups=\"$ups\"}",
1065 | "interval": "",
1066 | "legendFormat": "{{ups}}",
1067 | "refId": "A"
1068 | },
1069 | {
1070 | "datasource": {
1071 | "type": "prometheus",
1072 | "uid": "${DS_PROMETHEUS}"
1073 | },
1074 | "editorMode": "code",
1075 | "expr": "network_ups_tools_battery_voltage_low{instance=~\"$instance\",job=~\"$job\",ups=\"$ups\"}",
1076 | "interval": "",
1077 | "legendFormat": "voltage",
1078 | "refId": "B"
1079 | },
1080 | {
1081 | "datasource": {
1082 | "type": "prometheus",
1083 | "uid": "${DS_PROMETHEUS}"
1084 | },
1085 | "editorMode": "code",
1086 | "expr": "network_ups_tools_battery_voltage_high{instance=~\"$instance\",job=~\"$job\",ups=\"$ups\"}*1.1",
1087 | "interval": "",
1088 | "legendFormat": "voltage",
1089 | "refId": "C"
1090 | },
1091 | {
1092 | "datasource": {
1093 | "type": "prometheus",
1094 | "uid": "${DS_PROMETHEUS}"
1095 | },
1096 | "editorMode": "code",
1097 | "expr": "network_ups_tools_battery_voltage_high{instance=~\"$instance\",job=~\"$job\",ups=\"$ups\"}",
1098 | "interval": "",
1099 | "legendFormat": "voltage",
1100 | "refId": "D"
1101 | }
1102 | ],
1103 | "transformations": [
1104 | {
1105 | "id": "configFromData",
1106 | "options": {
1107 | "configRefId": "B",
1108 | "mappings": [
1109 | {
1110 | "fieldName": "voltage",
1111 | "handlerKey": "min"
1112 | }
1113 | ]
1114 | }
1115 | },
1116 | {
1117 | "id": "configFromData",
1118 | "options": {
1119 | "configRefId": "C",
1120 | "mappings": [
1121 | {
1122 | "fieldName": "voltage",
1123 | "handlerKey": "max"
1124 | }
1125 | ]
1126 | }
1127 | },
1128 | {
1129 | "id": "configFromData",
1130 | "options": {
1131 | "configRefId": "D",
1132 | "mappings": [
1133 | {
1134 | "fieldName": "voltage",
1135 | "handlerKey": "threshold1"
1136 | }
1137 | ]
1138 | }
1139 | }
1140 | ],
1141 | "title": "",
1142 | "transparent": true,
1143 | "type": "timeseries"
1144 | }
1145 | ],
1146 | "preload": false,
1147 | "refresh": false,
1148 | "schemaVersion": 40,
1149 | "tags": [],
1150 | "templating": {
1151 | "list": [
1152 | {
1153 | "current": {},
1154 | "datasource": {
1155 | "type": "prometheus",
1156 | "uid": "${DS_PROMETHEUS}"
1157 | },
1158 | "definition": "label_values(network_ups_tools_device_info,instance)",
1159 | "includeAll": false,
1160 | "label": "Instance",
1161 | "name": "instance",
1162 | "options": [],
1163 | "query": {
1164 | "qryType": 1,
1165 | "query": "label_values(network_ups_tools_device_info,instance)",
1166 | "refId": "PrometheusVariableQueryEditor-VariableQuery"
1167 | },
1168 | "refresh": 1,
1169 | "regex": "",
1170 | "sort": 1,
1171 | "type": "query"
1172 | },
1173 | {
1174 | "current": {},
1175 | "datasource": {
1176 | "type": "prometheus",
1177 | "uid": "${DS_PROMETHEUS}"
1178 | },
1179 | "definition": "label_values(network_ups_tools_device_info,job)",
1180 | "includeAll": false,
1181 | "label": "Job",
1182 | "name": "job",
1183 | "options": [],
1184 | "query": {
1185 | "qryType": 1,
1186 | "query": "label_values(network_ups_tools_device_info,job)",
1187 | "refId": "PrometheusVariableQueryEditor-VariableQuery"
1188 | },
1189 | "refresh": 1,
1190 | "regex": "",
1191 | "sort": 1,
1192 | "type": "query"
1193 | },
1194 | {
1195 | "datasource": {
1196 | "type": "prometheus",
1197 | "uid": "${DS_PROMETHEUS}"
1198 | },
1199 | "definition": "label_values(network_ups_tools_device_info,ups)",
1200 | "includeAll": false,
1201 | "label": "UPS",
1202 | "name": "ups",
1203 | "options": [],
1204 | "query": "label_values(network_ups_tools_device_info,ups)",
1205 | "refresh": 1,
1206 | "regex": "",
1207 | "type": "query"
1208 | }
1209 | ]
1210 | },
1211 | "time": {
1212 | "from": "now-24h",
1213 | "to": "now"
1214 | },
1215 | "timepicker": {},
1216 | "timezone": "",
1217 | "title": "UPS statistics",
1218 | "uid": "j4a-DMWRk",
1219 | "version": 1,
1220 | "weekStart": ""
1221 | }
1222 |
--------------------------------------------------------------------------------