├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── CHANGELOG.md
├── Cargo.lock
├── Cargo.toml
├── Dockerfile
├── LICENSE
├── README.md
├── example-data
├── LIST-UPS-1.txt
├── LIST-VAR-1.txt
├── LIST-VAR-2.txt
├── LIST-VAR-3.txt
├── LIST-VAR-4.txt
├── LIST-VAR-5.txt
├── VER-1.txt
└── VER-2.txt
├── manage
├── check.sh
├── docker
│ ├── build.sh
│ ├── clean.sh
│ ├── docker-compose.yml
│ ├── prometheus.yml
│ └── run.sh
├── integration_test.sh
└── nut-server-mock.py
├── metrics.md
└── src
├── common.rs
├── config.rs
├── http_server.rs
├── main.rs
├── meta.rs
├── metrics.rs
├── nut_client.rs
└── openmetrics_builder.rs
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches: ["*"]
6 | pull_request:
7 | branches: ["*"]
8 | release:
9 | types: [published]
10 | workflow_dispatch: {}
11 |
12 | env:
13 | CARGO_TERM_COLOR: always
14 | DOCKER_BUILD_PLATFORMS: >-
15 | linux/386,
16 | linux/amd64,
17 | linux/arm/v6,
18 | linux/arm/v7,
19 | linux/arm64/v8,
20 | DOCKER_REPO: hon95/prometheus-nut-exporter
21 |
22 | jobs:
23 | check:
24 | runs-on: ubuntu-22.04
25 | steps:
26 | - name: Checkout
27 | uses: actions/checkout@v3
28 | - name: Add dependencies
29 | run: rustup component add clippy
30 | - name: Build
31 | run: cargo build --verbose
32 | - name: Check code
33 | run: manage/check.sh
34 | - name: Run integration test
35 | run: manage/integration_test.sh
36 |
37 | publish-bleeding:
38 | if: github.event_name == 'push' && github.ref == 'refs/heads/master'
39 | needs: check
40 | runs-on: ubuntu-22.04
41 | steps:
42 | - name: Checkout code
43 | uses: actions/checkout@v3
44 | - name: Update version
45 | run: echo "version=$(echo 0.0.0-SNAPSHOT+$(TZ=Etc/GMT date "+%Y-%m-%dT%H:%M:%SZ"))" | tee -a $GITHUB_ENV
46 | - name: Set up QEMU
47 | uses: docker/setup-qemu-action@v2
48 | with:
49 | platforms: ${{ env.DOCKER_BUILD_PLATFORMS }}
50 | - name: Set up Docker Buildx
51 | uses: docker/setup-buildx-action@v2
52 | - name: Login to Docker Hub
53 | uses: docker/login-action@v2
54 | with:
55 | username: ${{ secrets.DOCKER_USERNAME }}
56 | password: ${{ secrets.DOCKER_PASSWORD }}
57 | - name: Build and push to Docker Hub
58 | uses: docker/build-push-action@v3
59 | with:
60 | build-args: |
61 | APP_VERSION=${{ env.version }}
62 | platforms: ${{ env.DOCKER_BUILD_PLATFORMS }}
63 | tags: |
64 | ${{ env.DOCKER_REPO }}:latest
65 | ${{ env.DOCKER_REPO }}:bleeding
66 | push: true
67 |
68 | publish-stable:
69 | if: github.event_name == 'release' && github.event.action == 'published' && startsWith(github.ref, 'refs/tags/v')
70 | needs: check
71 | runs-on: ubuntu-22.04
72 | steps:
73 | - name: Checkout code
74 | uses: actions/checkout@v3
75 | - name: Extract version
76 | run: |
77 | # Get version from git tag
78 | VERSION="$(echo ${GITHUB_REF#refs/tags/v})"
79 | echo "version=$VERSION" | tee -a $GITHUB_ENV
80 | # Check if semantic version (3 numbers, ignore pre-release and metadata)
81 | echo $VERSION | grep -Po '^\d+\.\d+\.\d+'
82 | # Extract other version representations
83 | echo "version_major=$(echo $VERSION | grep -Po '^\d+')" | tee -a $GITHUB_ENV
84 | echo "version_minor=$(echo $VERSION | grep -Po '^\d+\.\d+')" | tee -a $GITHUB_ENV
85 | echo "version_patch=$(echo $VERSION | grep -Po '^\d+\.\d+\.\d+')" | tee -a $GITHUB_ENV
86 | - name: Set up QEMU
87 | uses: docker/setup-qemu-action@v2
88 | with:
89 | platforms: ${{ env.DOCKER_BUILD_PLATFORMS }}
90 | - name: Set up Docker Buildx
91 | uses: docker/setup-buildx-action@v2
92 | - name: Login to Docker Hub
93 | uses: docker/login-action@v2
94 | with:
95 | username: ${{ secrets.DOCKER_USERNAME }}
96 | password: ${{ secrets.DOCKER_PASSWORD }}
97 | - name: Build and push to Docker Hub
98 | uses: docker/build-push-action@v3
99 | with:
100 | build-args: |
101 | APP_VERSION=${{ env.version }}
102 | platforms: ${{ env.DOCKER_BUILD_PLATFORMS }}
103 | tags: |
104 | ${{ env.DOCKER_REPO }}:latest
105 | ${{ env.DOCKER_REPO }}:${{ env.version_major }}
106 | ${{ env.DOCKER_REPO }}:${{ env.version_minor }}
107 | ${{ env.DOCKER_REPO }}:${{ env.version_patch }}
108 | ${{ env.DOCKER_REPO }}:stable
109 | push: true
110 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /target/
2 | /.local/
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 | All notable changes to this project will be documented in this file.
3 |
4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6 |
7 | ## [Unreleased]
8 |
9 | ### Added
10 |
11 | ### Changed
12 |
13 | ### Deprecated
14 |
15 | ### Removed
16 |
17 | ### Fixed
18 |
19 | ### Security
20 |
21 | ## [1.2.1] - 2022-08-03
22 |
23 | ### Added
24 |
25 | - Added `BOOST` status to the `nut_ups_status` state set.
26 |
27 | ## [1.2.0] - 2022-07-29
28 |
29 | ### Added
30 |
31 | - Added metrics (@uncleBorsch):
32 | - `nut_temperature_celsius` (`ups.temperature`)
33 | - `nut_delay_shutdown_seconds` (`ups.delay.shutdown`)
34 | - `nut_delay_start_seconds` (`ups.delay.start`)
35 | - `nut_battery_voltage_high_volts` (`battery.voltage.high`)
36 | - `nut_battery_voltage_low_volts` (`battery.voltage.low`)
37 | - Added more UPS metadata (@uncleBorsch).
38 | - Added support for binding to a specific IP address through the `HTTP_ADDRESS` environment variable (@nsapa).
39 | - Added metric `nut_ups_status` as a state set with support for many UPS statuses (`OL`, `OB`, `LB`, `CHRG` etc.).
40 | - Added labels `driver_version`, `driver_version_internal`, `driver_version_data` and `manufacturing_date` to the `nut_ups_info` metric.
41 | - Added proper signal handling to shutdown gracefully and not hang.
42 | - Added multi-architecture support (Docker images for different architectures will get published to Docker Hub).
43 |
44 | ### Changed
45 |
46 | - Changed default log level to `info`.
47 | - Made the target port default to 3493 instead of requiring one to be provided.
48 | - Made the Prometheus/OpenMetrics output (more) OpenMetrics 1.0.0-compliant.
49 |
50 | ### Deprecated
51 |
52 | - Deprecated `nut_info` and added `nut_server_info` as an identical but better named replacement.
53 | - Deprecated `nut_status` as it has very limited support for UPS statuses and doesn't always work as intended. See `nut_ups_status` for the replacement.
54 | - Deprecated the `type` and `nut_version` labels from the `nut_ups_info` metric. Use `device_type` and `driver_version` instead.
55 | - Deprecated `nut_battery_volts`, `nut_input_volts` and `nut_output_volts`. Replacements were added in v1.1.0.
56 |
57 | ### Removed
58 |
59 | - Removed the tini init system from the Docker image, since signals are handled properly now.
60 |
61 | ### Fixed
62 |
63 | - Fixed typo in the `device.mfr` variable (@nsapa).
64 |
65 | ## [1.1.1] - 2021-04-11
66 |
67 | ### Added
68 |
69 | - Added proper logging with adjustable log level.
70 | - Added duplicate compatibility metrics to compensate for the renamed metrics in the previous release.
71 |
72 | ### Changed
73 |
74 | - Changed request logging to use log level `debug` instead of the `LOG_REQUESTS_CONSOLE` environment variable.
75 |
76 | ### Fixed
77 |
78 | - Fixed failing to parse non-semantic NUT versions.
79 |
80 | ## [1.1.0] - 2021-04-07
81 |
82 | ### Added
83 |
84 | - Added UPS description to `nut_ups_info`.
85 | - Added lots of more metrics.
86 |
87 | ### Changed
88 |
89 | - Replaced Docker image tags `stable` and `bleeding` with `latest`, `X`, `X.Y` and `X.Y.Z` (parts of the semantic version).
90 | - Renamed a few voltage-related metrics (slightly breaking).
91 |
92 | ### Fixed
93 |
94 | - Added Tini as container entrypoint to handle signals properly (i.e. not stall when exiting).
95 | - Fixed parsing error when multiple UPSes exist.
96 |
97 | ## [1.0.1] - 2020-06-29
98 |
99 | ### Added
100 |
101 | - Added metadata metrics `nut_info` for the NUT server and `nut_exporter_info` for the exporter.
102 |
103 | ### Changed
104 |
105 | - Improved error messages sent to client.
106 |
107 | ### Fixed
108 |
109 | - Fixed malformed labels for `nut_ups_info`.
110 |
111 | ## [1.0.0] - 2020-06-18
112 |
113 | Initial release.
114 |
--------------------------------------------------------------------------------
/Cargo.lock:
--------------------------------------------------------------------------------
1 | # This file is automatically @generated by Cargo.
2 | # It is not intended for manual editing.
3 | version = 3
4 |
5 | [[package]]
6 | name = "aho-corasick"
7 | version = "0.7.18"
8 | source = "registry+https://github.com/rust-lang/crates.io-index"
9 | checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
10 | dependencies = [
11 | "memchr",
12 | ]
13 |
14 | [[package]]
15 | name = "atty"
16 | version = "0.2.14"
17 | source = "registry+https://github.com/rust-lang/crates.io-index"
18 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
19 | dependencies = [
20 | "hermit-abi",
21 | "libc",
22 | "winapi",
23 | ]
24 |
25 | [[package]]
26 | name = "autocfg"
27 | version = "1.1.0"
28 | source = "registry+https://github.com/rust-lang/crates.io-index"
29 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
30 |
31 | [[package]]
32 | name = "bitflags"
33 | version = "1.3.2"
34 | source = "registry+https://github.com/rust-lang/crates.io-index"
35 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
36 |
37 | [[package]]
38 | name = "bytes"
39 | version = "1.2.0"
40 | source = "registry+https://github.com/rust-lang/crates.io-index"
41 | checksum = "f0b3de4a0c5e67e16066a0715723abd91edc2f9001d09c46e1dca929351e130e"
42 |
43 | [[package]]
44 | name = "cfg-if"
45 | version = "1.0.0"
46 | source = "registry+https://github.com/rust-lang/crates.io-index"
47 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
48 |
49 | [[package]]
50 | name = "chrono"
51 | version = "0.4.19"
52 | source = "registry+https://github.com/rust-lang/crates.io-index"
53 | checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
54 | dependencies = [
55 | "libc",
56 | "num-integer",
57 | "num-traits",
58 | "time",
59 | "winapi",
60 | ]
61 |
62 | [[package]]
63 | name = "env_logger"
64 | version = "0.9.0"
65 | source = "registry+https://github.com/rust-lang/crates.io-index"
66 | checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3"
67 | dependencies = [
68 | "atty",
69 | "humantime",
70 | "log",
71 | "regex",
72 | "termcolor",
73 | ]
74 |
75 | [[package]]
76 | name = "fnv"
77 | version = "1.0.7"
78 | source = "registry+https://github.com/rust-lang/crates.io-index"
79 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
80 |
81 | [[package]]
82 | name = "form_urlencoded"
83 | version = "1.0.1"
84 | source = "registry+https://github.com/rust-lang/crates.io-index"
85 | checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191"
86 | dependencies = [
87 | "matches",
88 | "percent-encoding",
89 | ]
90 |
91 | [[package]]
92 | name = "futures-channel"
93 | version = "0.3.21"
94 | source = "registry+https://github.com/rust-lang/crates.io-index"
95 | checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010"
96 | dependencies = [
97 | "futures-core",
98 | ]
99 |
100 | [[package]]
101 | name = "futures-core"
102 | version = "0.3.21"
103 | source = "registry+https://github.com/rust-lang/crates.io-index"
104 | checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3"
105 |
106 | [[package]]
107 | name = "futures-sink"
108 | version = "0.3.21"
109 | source = "registry+https://github.com/rust-lang/crates.io-index"
110 | checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868"
111 |
112 | [[package]]
113 | name = "futures-task"
114 | version = "0.3.21"
115 | source = "registry+https://github.com/rust-lang/crates.io-index"
116 | checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a"
117 |
118 | [[package]]
119 | name = "futures-util"
120 | version = "0.3.21"
121 | source = "registry+https://github.com/rust-lang/crates.io-index"
122 | checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a"
123 | dependencies = [
124 | "futures-core",
125 | "futures-task",
126 | "pin-project-lite",
127 | "pin-utils",
128 | ]
129 |
130 | [[package]]
131 | name = "h2"
132 | version = "0.3.13"
133 | source = "registry+https://github.com/rust-lang/crates.io-index"
134 | checksum = "37a82c6d637fc9515a4694bbf1cb2457b79d81ce52b3108bdeea58b07dd34a57"
135 | dependencies = [
136 | "bytes",
137 | "fnv",
138 | "futures-core",
139 | "futures-sink",
140 | "futures-util",
141 | "http",
142 | "indexmap",
143 | "slab",
144 | "tokio",
145 | "tokio-util",
146 | "tracing",
147 | ]
148 |
149 | [[package]]
150 | name = "hashbrown"
151 | version = "0.12.3"
152 | source = "registry+https://github.com/rust-lang/crates.io-index"
153 | checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
154 |
155 | [[package]]
156 | name = "hermit-abi"
157 | version = "0.1.19"
158 | source = "registry+https://github.com/rust-lang/crates.io-index"
159 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
160 | dependencies = [
161 | "libc",
162 | ]
163 |
164 | [[package]]
165 | name = "http"
166 | version = "0.2.8"
167 | source = "registry+https://github.com/rust-lang/crates.io-index"
168 | checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399"
169 | dependencies = [
170 | "bytes",
171 | "fnv",
172 | "itoa",
173 | ]
174 |
175 | [[package]]
176 | name = "http-body"
177 | version = "0.4.5"
178 | source = "registry+https://github.com/rust-lang/crates.io-index"
179 | checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1"
180 | dependencies = [
181 | "bytes",
182 | "http",
183 | "pin-project-lite",
184 | ]
185 |
186 | [[package]]
187 | name = "httparse"
188 | version = "1.7.1"
189 | source = "registry+https://github.com/rust-lang/crates.io-index"
190 | checksum = "496ce29bb5a52785b44e0f7ca2847ae0bb839c9bd28f69acac9b99d461c0c04c"
191 |
192 | [[package]]
193 | name = "httpdate"
194 | version = "1.0.2"
195 | source = "registry+https://github.com/rust-lang/crates.io-index"
196 | checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
197 |
198 | [[package]]
199 | name = "humantime"
200 | version = "2.1.0"
201 | source = "registry+https://github.com/rust-lang/crates.io-index"
202 | checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
203 |
204 | [[package]]
205 | name = "hyper"
206 | version = "0.14.20"
207 | source = "registry+https://github.com/rust-lang/crates.io-index"
208 | checksum = "02c929dc5c39e335a03c405292728118860721b10190d98c2a0f0efd5baafbac"
209 | dependencies = [
210 | "bytes",
211 | "futures-channel",
212 | "futures-core",
213 | "futures-util",
214 | "h2",
215 | "http",
216 | "http-body",
217 | "httparse",
218 | "httpdate",
219 | "itoa",
220 | "pin-project-lite",
221 | "socket2",
222 | "tokio",
223 | "tower-service",
224 | "tracing",
225 | "want",
226 | ]
227 |
228 | [[package]]
229 | name = "idna"
230 | version = "0.2.3"
231 | source = "registry+https://github.com/rust-lang/crates.io-index"
232 | checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8"
233 | dependencies = [
234 | "matches",
235 | "unicode-bidi",
236 | "unicode-normalization",
237 | ]
238 |
239 | [[package]]
240 | name = "indexmap"
241 | version = "1.9.1"
242 | source = "registry+https://github.com/rust-lang/crates.io-index"
243 | checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e"
244 | dependencies = [
245 | "autocfg",
246 | "hashbrown",
247 | ]
248 |
249 | [[package]]
250 | name = "itoa"
251 | version = "1.0.2"
252 | source = "registry+https://github.com/rust-lang/crates.io-index"
253 | checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d"
254 |
255 | [[package]]
256 | name = "lazy_static"
257 | version = "1.4.0"
258 | source = "registry+https://github.com/rust-lang/crates.io-index"
259 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
260 |
261 | [[package]]
262 | name = "libc"
263 | version = "0.2.126"
264 | source = "registry+https://github.com/rust-lang/crates.io-index"
265 | checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836"
266 |
267 | [[package]]
268 | name = "lock_api"
269 | version = "0.4.7"
270 | source = "registry+https://github.com/rust-lang/crates.io-index"
271 | checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53"
272 | dependencies = [
273 | "autocfg",
274 | "scopeguard",
275 | ]
276 |
277 | [[package]]
278 | name = "log"
279 | version = "0.4.17"
280 | source = "registry+https://github.com/rust-lang/crates.io-index"
281 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
282 | dependencies = [
283 | "cfg-if",
284 | ]
285 |
286 | [[package]]
287 | name = "matches"
288 | version = "0.1.9"
289 | source = "registry+https://github.com/rust-lang/crates.io-index"
290 | checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
291 |
292 | [[package]]
293 | name = "memchr"
294 | version = "2.5.0"
295 | source = "registry+https://github.com/rust-lang/crates.io-index"
296 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
297 |
298 | [[package]]
299 | name = "mio"
300 | version = "0.8.4"
301 | source = "registry+https://github.com/rust-lang/crates.io-index"
302 | checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf"
303 | dependencies = [
304 | "libc",
305 | "log",
306 | "wasi 0.11.0+wasi-snapshot-preview1",
307 | "windows-sys",
308 | ]
309 |
310 | [[package]]
311 | name = "num-integer"
312 | version = "0.1.45"
313 | source = "registry+https://github.com/rust-lang/crates.io-index"
314 | checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
315 | dependencies = [
316 | "autocfg",
317 | "num-traits",
318 | ]
319 |
320 | [[package]]
321 | name = "num-traits"
322 | version = "0.2.15"
323 | source = "registry+https://github.com/rust-lang/crates.io-index"
324 | checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
325 | dependencies = [
326 | "autocfg",
327 | ]
328 |
329 | [[package]]
330 | name = "num_cpus"
331 | version = "1.13.1"
332 | source = "registry+https://github.com/rust-lang/crates.io-index"
333 | checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1"
334 | dependencies = [
335 | "hermit-abi",
336 | "libc",
337 | ]
338 |
339 | [[package]]
340 | name = "once_cell"
341 | version = "1.13.0"
342 | source = "registry+https://github.com/rust-lang/crates.io-index"
343 | checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1"
344 |
345 | [[package]]
346 | name = "parking_lot"
347 | version = "0.12.1"
348 | source = "registry+https://github.com/rust-lang/crates.io-index"
349 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
350 | dependencies = [
351 | "lock_api",
352 | "parking_lot_core",
353 | ]
354 |
355 | [[package]]
356 | name = "parking_lot_core"
357 | version = "0.9.3"
358 | source = "registry+https://github.com/rust-lang/crates.io-index"
359 | checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929"
360 | dependencies = [
361 | "cfg-if",
362 | "libc",
363 | "redox_syscall",
364 | "smallvec",
365 | "windows-sys",
366 | ]
367 |
368 | [[package]]
369 | name = "percent-encoding"
370 | version = "2.1.0"
371 | source = "registry+https://github.com/rust-lang/crates.io-index"
372 | checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
373 |
374 | [[package]]
375 | name = "pin-project-lite"
376 | version = "0.2.9"
377 | source = "registry+https://github.com/rust-lang/crates.io-index"
378 | checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"
379 |
380 | [[package]]
381 | name = "pin-utils"
382 | version = "0.1.0"
383 | source = "registry+https://github.com/rust-lang/crates.io-index"
384 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
385 |
386 | [[package]]
387 | name = "proc-macro2"
388 | version = "1.0.42"
389 | source = "registry+https://github.com/rust-lang/crates.io-index"
390 | checksum = "c278e965f1d8cf32d6e0e96de3d3e79712178ae67986d9cf9151f51e95aac89b"
391 | dependencies = [
392 | "unicode-ident",
393 | ]
394 |
395 | [[package]]
396 | name = "prometheus-nut-exporter"
397 | version = "0.0.0"
398 | dependencies = [
399 | "chrono",
400 | "env_logger",
401 | "hyper",
402 | "lazy_static",
403 | "log",
404 | "regex",
405 | "tokio",
406 | "url",
407 | ]
408 |
409 | [[package]]
410 | name = "quote"
411 | version = "1.0.20"
412 | source = "registry+https://github.com/rust-lang/crates.io-index"
413 | checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804"
414 | dependencies = [
415 | "proc-macro2",
416 | ]
417 |
418 | [[package]]
419 | name = "redox_syscall"
420 | version = "0.2.16"
421 | source = "registry+https://github.com/rust-lang/crates.io-index"
422 | checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
423 | dependencies = [
424 | "bitflags",
425 | ]
426 |
427 | [[package]]
428 | name = "regex"
429 | version = "1.6.0"
430 | source = "registry+https://github.com/rust-lang/crates.io-index"
431 | checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b"
432 | dependencies = [
433 | "aho-corasick",
434 | "memchr",
435 | "regex-syntax",
436 | ]
437 |
438 | [[package]]
439 | name = "regex-syntax"
440 | version = "0.6.27"
441 | source = "registry+https://github.com/rust-lang/crates.io-index"
442 | checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244"
443 |
444 | [[package]]
445 | name = "scopeguard"
446 | version = "1.1.0"
447 | source = "registry+https://github.com/rust-lang/crates.io-index"
448 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
449 |
450 | [[package]]
451 | name = "signal-hook-registry"
452 | version = "1.4.0"
453 | source = "registry+https://github.com/rust-lang/crates.io-index"
454 | checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0"
455 | dependencies = [
456 | "libc",
457 | ]
458 |
459 | [[package]]
460 | name = "slab"
461 | version = "0.4.7"
462 | source = "registry+https://github.com/rust-lang/crates.io-index"
463 | checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef"
464 | dependencies = [
465 | "autocfg",
466 | ]
467 |
468 | [[package]]
469 | name = "smallvec"
470 | version = "1.9.0"
471 | source = "registry+https://github.com/rust-lang/crates.io-index"
472 | checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1"
473 |
474 | [[package]]
475 | name = "socket2"
476 | version = "0.4.4"
477 | source = "registry+https://github.com/rust-lang/crates.io-index"
478 | checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0"
479 | dependencies = [
480 | "libc",
481 | "winapi",
482 | ]
483 |
484 | [[package]]
485 | name = "syn"
486 | version = "1.0.98"
487 | source = "registry+https://github.com/rust-lang/crates.io-index"
488 | checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd"
489 | dependencies = [
490 | "proc-macro2",
491 | "quote",
492 | "unicode-ident",
493 | ]
494 |
495 | [[package]]
496 | name = "termcolor"
497 | version = "1.1.3"
498 | source = "registry+https://github.com/rust-lang/crates.io-index"
499 | checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
500 | dependencies = [
501 | "winapi-util",
502 | ]
503 |
504 | [[package]]
505 | name = "time"
506 | version = "0.1.44"
507 | source = "registry+https://github.com/rust-lang/crates.io-index"
508 | checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255"
509 | dependencies = [
510 | "libc",
511 | "wasi 0.10.0+wasi-snapshot-preview1",
512 | "winapi",
513 | ]
514 |
515 | [[package]]
516 | name = "tinyvec"
517 | version = "1.6.0"
518 | source = "registry+https://github.com/rust-lang/crates.io-index"
519 | checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
520 | dependencies = [
521 | "tinyvec_macros",
522 | ]
523 |
524 | [[package]]
525 | name = "tinyvec_macros"
526 | version = "0.1.0"
527 | source = "registry+https://github.com/rust-lang/crates.io-index"
528 | checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
529 |
530 | [[package]]
531 | name = "tokio"
532 | version = "1.20.1"
533 | source = "registry+https://github.com/rust-lang/crates.io-index"
534 | checksum = "7a8325f63a7d4774dd041e363b2409ed1c5cbbd0f867795e661df066b2b0a581"
535 | dependencies = [
536 | "autocfg",
537 | "bytes",
538 | "libc",
539 | "memchr",
540 | "mio",
541 | "num_cpus",
542 | "once_cell",
543 | "parking_lot",
544 | "pin-project-lite",
545 | "signal-hook-registry",
546 | "socket2",
547 | "tokio-macros",
548 | "winapi",
549 | ]
550 |
551 | [[package]]
552 | name = "tokio-macros"
553 | version = "1.8.0"
554 | source = "registry+https://github.com/rust-lang/crates.io-index"
555 | checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484"
556 | dependencies = [
557 | "proc-macro2",
558 | "quote",
559 | "syn",
560 | ]
561 |
562 | [[package]]
563 | name = "tokio-util"
564 | version = "0.7.3"
565 | source = "registry+https://github.com/rust-lang/crates.io-index"
566 | checksum = "cc463cd8deddc3770d20f9852143d50bf6094e640b485cb2e189a2099085ff45"
567 | dependencies = [
568 | "bytes",
569 | "futures-core",
570 | "futures-sink",
571 | "pin-project-lite",
572 | "tokio",
573 | "tracing",
574 | ]
575 |
576 | [[package]]
577 | name = "tower-service"
578 | version = "0.3.2"
579 | source = "registry+https://github.com/rust-lang/crates.io-index"
580 | checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
581 |
582 | [[package]]
583 | name = "tracing"
584 | version = "0.1.35"
585 | source = "registry+https://github.com/rust-lang/crates.io-index"
586 | checksum = "a400e31aa60b9d44a52a8ee0343b5b18566b03a8321e0d321f695cf56e940160"
587 | dependencies = [
588 | "cfg-if",
589 | "pin-project-lite",
590 | "tracing-core",
591 | ]
592 |
593 | [[package]]
594 | name = "tracing-core"
595 | version = "0.1.28"
596 | source = "registry+https://github.com/rust-lang/crates.io-index"
597 | checksum = "7b7358be39f2f274f322d2aaed611acc57f382e8eb1e5b48cb9ae30933495ce7"
598 | dependencies = [
599 | "once_cell",
600 | ]
601 |
602 | [[package]]
603 | name = "try-lock"
604 | version = "0.2.3"
605 | source = "registry+https://github.com/rust-lang/crates.io-index"
606 | checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
607 |
608 | [[package]]
609 | name = "unicode-bidi"
610 | version = "0.3.8"
611 | source = "registry+https://github.com/rust-lang/crates.io-index"
612 | checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
613 |
614 | [[package]]
615 | name = "unicode-ident"
616 | version = "1.0.2"
617 | source = "registry+https://github.com/rust-lang/crates.io-index"
618 | checksum = "15c61ba63f9235225a22310255a29b806b907c9b8c964bcbd0a2c70f3f2deea7"
619 |
620 | [[package]]
621 | name = "unicode-normalization"
622 | version = "0.1.21"
623 | source = "registry+https://github.com/rust-lang/crates.io-index"
624 | checksum = "854cbdc4f7bc6ae19c820d44abdc3277ac3e1b2b93db20a636825d9322fb60e6"
625 | dependencies = [
626 | "tinyvec",
627 | ]
628 |
629 | [[package]]
630 | name = "url"
631 | version = "2.2.2"
632 | source = "registry+https://github.com/rust-lang/crates.io-index"
633 | checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c"
634 | dependencies = [
635 | "form_urlencoded",
636 | "idna",
637 | "matches",
638 | "percent-encoding",
639 | ]
640 |
641 | [[package]]
642 | name = "want"
643 | version = "0.3.0"
644 | source = "registry+https://github.com/rust-lang/crates.io-index"
645 | checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0"
646 | dependencies = [
647 | "log",
648 | "try-lock",
649 | ]
650 |
651 | [[package]]
652 | name = "wasi"
653 | version = "0.10.0+wasi-snapshot-preview1"
654 | source = "registry+https://github.com/rust-lang/crates.io-index"
655 | checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
656 |
657 | [[package]]
658 | name = "wasi"
659 | version = "0.11.0+wasi-snapshot-preview1"
660 | source = "registry+https://github.com/rust-lang/crates.io-index"
661 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
662 |
663 | [[package]]
664 | name = "winapi"
665 | version = "0.3.9"
666 | source = "registry+https://github.com/rust-lang/crates.io-index"
667 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
668 | dependencies = [
669 | "winapi-i686-pc-windows-gnu",
670 | "winapi-x86_64-pc-windows-gnu",
671 | ]
672 |
673 | [[package]]
674 | name = "winapi-i686-pc-windows-gnu"
675 | version = "0.4.0"
676 | source = "registry+https://github.com/rust-lang/crates.io-index"
677 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
678 |
679 | [[package]]
680 | name = "winapi-util"
681 | version = "0.1.5"
682 | source = "registry+https://github.com/rust-lang/crates.io-index"
683 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
684 | dependencies = [
685 | "winapi",
686 | ]
687 |
688 | [[package]]
689 | name = "winapi-x86_64-pc-windows-gnu"
690 | version = "0.4.0"
691 | source = "registry+https://github.com/rust-lang/crates.io-index"
692 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
693 |
694 | [[package]]
695 | name = "windows-sys"
696 | version = "0.36.1"
697 | source = "registry+https://github.com/rust-lang/crates.io-index"
698 | checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2"
699 | dependencies = [
700 | "windows_aarch64_msvc",
701 | "windows_i686_gnu",
702 | "windows_i686_msvc",
703 | "windows_x86_64_gnu",
704 | "windows_x86_64_msvc",
705 | ]
706 |
707 | [[package]]
708 | name = "windows_aarch64_msvc"
709 | version = "0.36.1"
710 | source = "registry+https://github.com/rust-lang/crates.io-index"
711 | checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
712 |
713 | [[package]]
714 | name = "windows_i686_gnu"
715 | version = "0.36.1"
716 | source = "registry+https://github.com/rust-lang/crates.io-index"
717 | checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
718 |
719 | [[package]]
720 | name = "windows_i686_msvc"
721 | version = "0.36.1"
722 | source = "registry+https://github.com/rust-lang/crates.io-index"
723 | checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
724 |
725 | [[package]]
726 | name = "windows_x86_64_gnu"
727 | version = "0.36.1"
728 | source = "registry+https://github.com/rust-lang/crates.io-index"
729 | checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1"
730 |
731 | [[package]]
732 | name = "windows_x86_64_msvc"
733 | version = "0.36.1"
734 | source = "registry+https://github.com/rust-lang/crates.io-index"
735 | checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
736 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "prometheus-nut-exporter"
3 | version = "0.0.0"
4 | authors = ["HON95"]
5 | license = "GPLv3"
6 | description = "A Prometheus exporter for Network UPS Tools (NUT)."
7 | keywords = ["prometheus", "openmetrics", "nut", "exporter"]
8 | homepage = "https://github.com/HON95/prometheus-nut-exporter"
9 | repository = "https://github.com/HON95/prometheus-nut-exporter"
10 | edition = "2021"
11 |
12 | [dependencies]
13 | tokio = { version = "1.20.*", features = ["full"] }
14 | hyper = { version = "0.14.*", features = ["full"] }
15 | chrono = "0.4.*"
16 | url = "2.2.*"
17 | regex = "1.6.*"
18 | lazy_static = "1.4.*"
19 | log = "0.4.*"
20 | env_logger = "0.9.*"
21 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | ARG APP_VERSION=0.0.0-SNAPSHOT
2 | ARG APP_GID=5000
3 | ARG APP_UID=5000
4 | ARG ALPINE_VERSION=3.16
5 | ARG RUST_VERSION=1.62.0
6 |
7 |
8 | ## Builder stage
9 | FROM --platform=$BUILDPLATFORM alpine:$ALPINE_VERSION AS builder
10 | WORKDIR /app
11 |
12 | # Install Rust
13 | RUN apk add --no-cache build-base rustup
14 | ARG RUST_VERSION
15 | RUN rustup-init --default-toolchain=$RUST_VERSION -y
16 | ENV PATH="$PATH:/root/.cargo/bin"
17 |
18 | # Fetch deps using dummy app
19 | COPY Cargo.toml .
20 | COPY Cargo.lock .
21 | RUN mkdir src \
22 | && echo "fn main() {}" > src/main.rs \
23 | && cargo fetch \
24 | && rm -rf src/
25 |
26 | # Build app
27 | COPY src/ src/
28 | ARG APP_VERSION
29 | RUN sed -i "s/^.*\bAPP_VERSION\b.*$/pub const APP_VERSION: \&str = \"$APP_VERSION\";/g" src/meta.rs
30 | ARG TARGETPLATFORM
31 | RUN \
32 | case $TARGETPLATFORM in \
33 | "linux/386") RUST_TARGET="i686-unknown-linux-musl" ;; \
34 | "linux/amd64") RUST_TARGET="x86_64-unknown-linux-musl" ;; \
35 | "linux/arm/v6") RUST_TARGET="arm-unknown-linux-musleabi" ;; \
36 | "linux/arm/v7") RUST_TARGET="armv7-unknown-linux-musleabi" ;; \
37 | "linux/arm64") RUST_TARGET="aarch64-unknown-linux-musl" ;; \
38 | *) false ;; \
39 | esac \
40 | && rustup target add $RUST_TARGET \
41 | && cargo rustc --target=$RUST_TARGET --release -- -C linker=rust-lld -D warnings \
42 | && mv target/$RUST_TARGET/release/prometheus-nut-exporter .
43 |
44 | ## Runtime stage
45 | FROM alpine:$ALPINE_VERSION AS runtime
46 | WORKDIR /app
47 |
48 | # Add non-root user
49 | ARG APP_GID
50 | ARG APP_UID
51 | RUN addgroup -S -g $APP_GID app && adduser -S -G app -u $APP_UID app
52 |
53 | # Add executable
54 | COPY --from=builder /app/prometheus-nut-exporter ./
55 | RUN chown app:app prometheus-nut-exporter
56 |
57 | USER app
58 | ENTRYPOINT ["./prometheus-nut-exporter"]
59 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU General Public License is a free, copyleft license for
11 | software and other kinds of works.
12 |
13 | The licenses for most software and other practical works are designed
14 | to take away your freedom to share and change the works. By contrast,
15 | the GNU General Public License is intended to guarantee your freedom to
16 | share and change all versions of a program--to make sure it remains free
17 | software for all its users. We, the Free Software Foundation, use the
18 | GNU General Public License for most of our software; it applies also to
19 | any other work released this way by its authors. You can apply it to
20 | your programs, too.
21 |
22 | When we speak of free software, we are referring to freedom, not
23 | price. Our General Public Licenses are designed to make sure that you
24 | have the freedom to distribute copies of free software (and charge for
25 | them if you wish), that you receive source code or can get it if you
26 | want it, that you can change the software or use pieces of it in new
27 | free programs, and that you know you can do these things.
28 |
29 | To protect your rights, we need to prevent others from denying you
30 | these rights or asking you to surrender the rights. Therefore, you have
31 | certain responsibilities if you distribute copies of the software, or if
32 | you modify it: responsibilities to respect the freedom of others.
33 |
34 | For example, if you distribute copies of such a program, whether
35 | gratis or for a fee, you must pass on to the recipients the same
36 | freedoms that you received. You must make sure that they, too, receive
37 | or can get the source code. And you must show them these terms so they
38 | know their rights.
39 |
40 | Developers that use the GNU GPL protect your rights with two steps:
41 | (1) assert copyright on the software, and (2) offer you this License
42 | giving you legal permission to copy, distribute and/or modify it.
43 |
44 | For the developers' and authors' protection, the GPL clearly explains
45 | that there is no warranty for this free software. For both users' and
46 | authors' sake, the GPL requires that modified versions be marked as
47 | changed, so that their problems will not be attributed erroneously to
48 | authors of previous versions.
49 |
50 | Some devices are designed to deny users access to install or run
51 | modified versions of the software inside them, although the manufacturer
52 | can do so. This is fundamentally incompatible with the aim of
53 | protecting users' freedom to change the software. The systematic
54 | pattern of such abuse occurs in the area of products for individuals to
55 | use, which is precisely where it is most unacceptable. Therefore, we
56 | have designed this version of the GPL to prohibit the practice for those
57 | products. If such problems arise substantially in other domains, we
58 | stand ready to extend this provision to those domains in future versions
59 | of the GPL, as needed to protect the freedom of users.
60 |
61 | Finally, every program is threatened constantly by software patents.
62 | States should not allow patents to restrict development and use of
63 | software on general-purpose computers, but in those that do, we wish to
64 | avoid the special danger that patents applied to a free program could
65 | make it effectively proprietary. To prevent this, the GPL assures that
66 | patents cannot be used to render the program non-free.
67 |
68 | The precise terms and conditions for copying, distribution and
69 | modification follow.
70 |
71 | TERMS AND CONDITIONS
72 |
73 | 0. Definitions.
74 |
75 | "This License" refers to version 3 of the GNU General Public License.
76 |
77 | "Copyright" also means copyright-like laws that apply to other kinds of
78 | works, such as semiconductor masks.
79 |
80 | "The Program" refers to any copyrightable work licensed under this
81 | License. Each licensee is addressed as "you". "Licensees" and
82 | "recipients" may be individuals or organizations.
83 |
84 | To "modify" a work means to copy from or adapt all or part of the work
85 | in a fashion requiring copyright permission, other than the making of an
86 | exact copy. The resulting work is called a "modified version" of the
87 | earlier work or a work "based on" the earlier work.
88 |
89 | A "covered work" means either the unmodified Program or a work based
90 | on the Program.
91 |
92 | To "propagate" a work means to do anything with it that, without
93 | permission, would make you directly or secondarily liable for
94 | infringement under applicable copyright law, except executing it on a
95 | computer or modifying a private copy. Propagation includes copying,
96 | distribution (with or without modification), making available to the
97 | public, and in some countries other activities as well.
98 |
99 | To "convey" a work means any kind of propagation that enables other
100 | parties to make or receive copies. Mere interaction with a user through
101 | a computer network, with no transfer of a copy, is not conveying.
102 |
103 | An interactive user interface displays "Appropriate Legal Notices"
104 | to the extent that it includes a convenient and prominently visible
105 | feature that (1) displays an appropriate copyright notice, and (2)
106 | tells the user that there is no warranty for the work (except to the
107 | extent that warranties are provided), that licensees may convey the
108 | work under this License, and how to view a copy of this License. If
109 | the interface presents a list of user commands or options, such as a
110 | menu, a prominent item in the list meets this criterion.
111 |
112 | 1. Source Code.
113 |
114 | The "source code" for a work means the preferred form of the work
115 | for making modifications to it. "Object code" means any non-source
116 | form of a work.
117 |
118 | A "Standard Interface" means an interface that either is an official
119 | standard defined by a recognized standards body, or, in the case of
120 | interfaces specified for a particular programming language, one that
121 | is widely used among developers working in that language.
122 |
123 | The "System Libraries" of an executable work include anything, other
124 | than the work as a whole, that (a) is included in the normal form of
125 | packaging a Major Component, but which is not part of that Major
126 | Component, and (b) serves only to enable use of the work with that
127 | Major Component, or to implement a Standard Interface for which an
128 | implementation is available to the public in source code form. A
129 | "Major Component", in this context, means a major essential component
130 | (kernel, window system, and so on) of the specific operating system
131 | (if any) on which the executable work runs, or a compiler used to
132 | produce the work, or an object code interpreter used to run it.
133 |
134 | The "Corresponding Source" for a work in object code form means all
135 | the source code needed to generate, install, and (for an executable
136 | work) run the object code and to modify the work, including scripts to
137 | control those activities. However, it does not include the work's
138 | System Libraries, or general-purpose tools or generally available free
139 | programs which are used unmodified in performing those activities but
140 | which are not part of the work. For example, Corresponding Source
141 | includes interface definition files associated with source files for
142 | the work, and the source code for shared libraries and dynamically
143 | linked subprograms that the work is specifically designed to require,
144 | such as by intimate data communication or control flow between those
145 | subprograms and other parts of the work.
146 |
147 | The Corresponding Source need not include anything that users
148 | can regenerate automatically from other parts of the Corresponding
149 | Source.
150 |
151 | The Corresponding Source for a work in source code form is that
152 | same work.
153 |
154 | 2. Basic Permissions.
155 |
156 | All rights granted under this License are granted for the term of
157 | copyright on the Program, and are irrevocable provided the stated
158 | conditions are met. This License explicitly affirms your unlimited
159 | permission to run the unmodified Program. The output from running a
160 | covered work is covered by this License only if the output, given its
161 | content, constitutes a covered work. This License acknowledges your
162 | rights of fair use or other equivalent, as provided by copyright law.
163 |
164 | You may make, run and propagate covered works that you do not
165 | convey, without conditions so long as your license otherwise remains
166 | in force. You may convey covered works to others for the sole purpose
167 | of having them make modifications exclusively for you, or provide you
168 | with facilities for running those works, provided that you comply with
169 | the terms of this License in conveying all material for which you do
170 | not control copyright. Those thus making or running the covered works
171 | for you must do so exclusively on your behalf, under your direction
172 | and control, on terms that prohibit them from making any copies of
173 | your copyrighted material outside their relationship with you.
174 |
175 | Conveying under any other circumstances is permitted solely under
176 | the conditions stated below. Sublicensing is not allowed; section 10
177 | makes it unnecessary.
178 |
179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180 |
181 | No covered work shall be deemed part of an effective technological
182 | measure under any applicable law fulfilling obligations under article
183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
184 | similar laws prohibiting or restricting circumvention of such
185 | measures.
186 |
187 | When you convey a covered work, you waive any legal power to forbid
188 | circumvention of technological measures to the extent such circumvention
189 | is effected by exercising rights under this License with respect to
190 | the covered work, and you disclaim any intention to limit operation or
191 | modification of the work as a means of enforcing, against the work's
192 | users, your or third parties' legal rights to forbid circumvention of
193 | technological measures.
194 |
195 | 4. Conveying Verbatim Copies.
196 |
197 | You may convey verbatim copies of the Program's source code as you
198 | receive it, in any medium, provided that you conspicuously and
199 | appropriately publish on each copy an appropriate copyright notice;
200 | keep intact all notices stating that this License and any
201 | non-permissive terms added in accord with section 7 apply to the code;
202 | keep intact all notices of the absence of any warranty; and give all
203 | recipients a copy of this License along with the Program.
204 |
205 | You may charge any price or no price for each copy that you convey,
206 | and you may offer support or warranty protection for a fee.
207 |
208 | 5. Conveying Modified Source Versions.
209 |
210 | You may convey a work based on the Program, or the modifications to
211 | produce it from the Program, in the form of source code under the
212 | terms of section 4, provided that you also meet all of these conditions:
213 |
214 | a) The work must carry prominent notices stating that you modified
215 | it, and giving a relevant date.
216 |
217 | b) The work must carry prominent notices stating that it is
218 | released under this License and any conditions added under section
219 | 7. This requirement modifies the requirement in section 4 to
220 | "keep intact all notices".
221 |
222 | c) You must license the entire work, as a whole, under this
223 | License to anyone who comes into possession of a copy. This
224 | License will therefore apply, along with any applicable section 7
225 | additional terms, to the whole of the work, and all its parts,
226 | regardless of how they are packaged. This License gives no
227 | permission to license the work in any other way, but it does not
228 | invalidate such permission if you have separately received it.
229 |
230 | d) If the work has interactive user interfaces, each must display
231 | Appropriate Legal Notices; however, if the Program has interactive
232 | interfaces that do not display Appropriate Legal Notices, your
233 | work need not make them do so.
234 |
235 | A compilation of a covered work with other separate and independent
236 | works, which are not by their nature extensions of the covered work,
237 | and which are not combined with it such as to form a larger program,
238 | in or on a volume of a storage or distribution medium, is called an
239 | "aggregate" if the compilation and its resulting copyright are not
240 | used to limit the access or legal rights of the compilation's users
241 | beyond what the individual works permit. Inclusion of a covered work
242 | in an aggregate does not cause this License to apply to the other
243 | parts of the aggregate.
244 |
245 | 6. Conveying Non-Source Forms.
246 |
247 | You may convey a covered work in object code form under the terms
248 | of sections 4 and 5, provided that you also convey the
249 | machine-readable Corresponding Source under the terms of this License,
250 | in one of these ways:
251 |
252 | a) Convey the object code in, or embodied in, a physical product
253 | (including a physical distribution medium), accompanied by the
254 | Corresponding Source fixed on a durable physical medium
255 | customarily used for software interchange.
256 |
257 | b) Convey the object code in, or embodied in, a physical product
258 | (including a physical distribution medium), accompanied by a
259 | written offer, valid for at least three years and valid for as
260 | long as you offer spare parts or customer support for that product
261 | model, to give anyone who possesses the object code either (1) a
262 | copy of the Corresponding Source for all the software in the
263 | product that is covered by this License, on a durable physical
264 | medium customarily used for software interchange, for a price no
265 | more than your reasonable cost of physically performing this
266 | conveying of source, or (2) access to copy the
267 | Corresponding Source from a network server at no charge.
268 |
269 | c) Convey individual copies of the object code with a copy of the
270 | written offer to provide the Corresponding Source. This
271 | alternative is allowed only occasionally and noncommercially, and
272 | only if you received the object code with such an offer, in accord
273 | with subsection 6b.
274 |
275 | d) Convey the object code by offering access from a designated
276 | place (gratis or for a charge), and offer equivalent access to the
277 | Corresponding Source in the same way through the same place at no
278 | further charge. You need not require recipients to copy the
279 | Corresponding Source along with the object code. If the place to
280 | copy the object code is a network server, the Corresponding Source
281 | may be on a different server (operated by you or a third party)
282 | that supports equivalent copying facilities, provided you maintain
283 | clear directions next to the object code saying where to find the
284 | Corresponding Source. Regardless of what server hosts the
285 | Corresponding Source, you remain obligated to ensure that it is
286 | available for as long as needed to satisfy these requirements.
287 |
288 | e) Convey the object code using peer-to-peer transmission, provided
289 | you inform other peers where the object code and Corresponding
290 | Source of the work are being offered to the general public at no
291 | charge under subsection 6d.
292 |
293 | A separable portion of the object code, whose source code is excluded
294 | from the Corresponding Source as a System Library, need not be
295 | included in conveying the object code work.
296 |
297 | A "User Product" is either (1) a "consumer product", which means any
298 | tangible personal property which is normally used for personal, family,
299 | or household purposes, or (2) anything designed or sold for incorporation
300 | into a dwelling. In determining whether a product is a consumer product,
301 | doubtful cases shall be resolved in favor of coverage. For a particular
302 | product received by a particular user, "normally used" refers to a
303 | typical or common use of that class of product, regardless of the status
304 | of the particular user or of the way in which the particular user
305 | actually uses, or expects or is expected to use, the product. A product
306 | is a consumer product regardless of whether the product has substantial
307 | commercial, industrial or non-consumer uses, unless such uses represent
308 | the only significant mode of use of the product.
309 |
310 | "Installation Information" for a User Product means any methods,
311 | procedures, authorization keys, or other information required to install
312 | and execute modified versions of a covered work in that User Product from
313 | a modified version of its Corresponding Source. The information must
314 | suffice to ensure that the continued functioning of the modified object
315 | code is in no case prevented or interfered with solely because
316 | modification has been made.
317 |
318 | If you convey an object code work under this section in, or with, or
319 | specifically for use in, a User Product, and the conveying occurs as
320 | part of a transaction in which the right of possession and use of the
321 | User Product is transferred to the recipient in perpetuity or for a
322 | fixed term (regardless of how the transaction is characterized), the
323 | Corresponding Source conveyed under this section must be accompanied
324 | by the Installation Information. But this requirement does not apply
325 | if neither you nor any third party retains the ability to install
326 | modified object code on the User Product (for example, the work has
327 | been installed in ROM).
328 |
329 | The requirement to provide Installation Information does not include a
330 | requirement to continue to provide support service, warranty, or updates
331 | for a work that has been modified or installed by the recipient, or for
332 | the User Product in which it has been modified or installed. Access to a
333 | network may be denied when the modification itself materially and
334 | adversely affects the operation of the network or violates the rules and
335 | protocols for communication across the network.
336 |
337 | Corresponding Source conveyed, and Installation Information provided,
338 | in accord with this section must be in a format that is publicly
339 | documented (and with an implementation available to the public in
340 | source code form), and must require no special password or key for
341 | unpacking, reading or copying.
342 |
343 | 7. Additional Terms.
344 |
345 | "Additional permissions" are terms that supplement the terms of this
346 | License by making exceptions from one or more of its conditions.
347 | Additional permissions that are applicable to the entire Program shall
348 | be treated as though they were included in this License, to the extent
349 | that they are valid under applicable law. If additional permissions
350 | apply only to part of the Program, that part may be used separately
351 | under those permissions, but the entire Program remains governed by
352 | this License without regard to the additional permissions.
353 |
354 | When you convey a copy of a covered work, you may at your option
355 | remove any additional permissions from that copy, or from any part of
356 | it. (Additional permissions may be written to require their own
357 | removal in certain cases when you modify the work.) You may place
358 | additional permissions on material, added by you to a covered work,
359 | for which you have or can give appropriate copyright permission.
360 |
361 | Notwithstanding any other provision of this License, for material you
362 | add to a covered work, you may (if authorized by the copyright holders of
363 | that material) supplement the terms of this License with terms:
364 |
365 | a) Disclaiming warranty or limiting liability differently from the
366 | terms of sections 15 and 16 of this License; or
367 |
368 | b) Requiring preservation of specified reasonable legal notices or
369 | author attributions in that material or in the Appropriate Legal
370 | Notices displayed by works containing it; or
371 |
372 | c) Prohibiting misrepresentation of the origin of that material, or
373 | requiring that modified versions of such material be marked in
374 | reasonable ways as different from the original version; or
375 |
376 | d) Limiting the use for publicity purposes of names of licensors or
377 | authors of the material; or
378 |
379 | e) Declining to grant rights under trademark law for use of some
380 | trade names, trademarks, or service marks; or
381 |
382 | f) Requiring indemnification of licensors and authors of that
383 | material by anyone who conveys the material (or modified versions of
384 | it) with contractual assumptions of liability to the recipient, for
385 | any liability that these contractual assumptions directly impose on
386 | those licensors and authors.
387 |
388 | All other non-permissive additional terms are considered "further
389 | restrictions" within the meaning of section 10. If the Program as you
390 | received it, or any part of it, contains a notice stating that it is
391 | governed by this License along with a term that is a further
392 | restriction, you may remove that term. If a license document contains
393 | a further restriction but permits relicensing or conveying under this
394 | License, you may add to a covered work material governed by the terms
395 | of that license document, provided that the further restriction does
396 | not survive such relicensing or conveying.
397 |
398 | If you add terms to a covered work in accord with this section, you
399 | must place, in the relevant source files, a statement of the
400 | additional terms that apply to those files, or a notice indicating
401 | where to find the applicable terms.
402 |
403 | Additional terms, permissive or non-permissive, may be stated in the
404 | form of a separately written license, or stated as exceptions;
405 | the above requirements apply either way.
406 |
407 | 8. Termination.
408 |
409 | You may not propagate or modify a covered work except as expressly
410 | provided under this License. Any attempt otherwise to propagate or
411 | modify it is void, and will automatically terminate your rights under
412 | this License (including any patent licenses granted under the third
413 | paragraph of section 11).
414 |
415 | However, if you cease all violation of this License, then your
416 | license from a particular copyright holder is reinstated (a)
417 | provisionally, unless and until the copyright holder explicitly and
418 | finally terminates your license, and (b) permanently, if the copyright
419 | holder fails to notify you of the violation by some reasonable means
420 | prior to 60 days after the cessation.
421 |
422 | Moreover, your license from a particular copyright holder is
423 | reinstated permanently if the copyright holder notifies you of the
424 | violation by some reasonable means, this is the first time you have
425 | received notice of violation of this License (for any work) from that
426 | copyright holder, and you cure the violation prior to 30 days after
427 | your receipt of the notice.
428 |
429 | Termination of your rights under this section does not terminate the
430 | licenses of parties who have received copies or rights from you under
431 | this License. If your rights have been terminated and not permanently
432 | reinstated, you do not qualify to receive new licenses for the same
433 | material under section 10.
434 |
435 | 9. Acceptance Not Required for Having Copies.
436 |
437 | You are not required to accept this License in order to receive or
438 | run a copy of the Program. Ancillary propagation of a covered work
439 | occurring solely as a consequence of using peer-to-peer transmission
440 | to receive a copy likewise does not require acceptance. However,
441 | nothing other than this License grants you permission to propagate or
442 | modify any covered work. These actions infringe copyright if you do
443 | not accept this License. Therefore, by modifying or propagating a
444 | covered work, you indicate your acceptance of this License to do so.
445 |
446 | 10. Automatic Licensing of Downstream Recipients.
447 |
448 | Each time you convey a covered work, the recipient automatically
449 | receives a license from the original licensors, to run, modify and
450 | propagate that work, subject to this License. You are not responsible
451 | for enforcing compliance by third parties with this License.
452 |
453 | An "entity transaction" is a transaction transferring control of an
454 | organization, or substantially all assets of one, or subdividing an
455 | organization, or merging organizations. If propagation of a covered
456 | work results from an entity transaction, each party to that
457 | transaction who receives a copy of the work also receives whatever
458 | licenses to the work the party's predecessor in interest had or could
459 | give under the previous paragraph, plus a right to possession of the
460 | Corresponding Source of the work from the predecessor in interest, if
461 | the predecessor has it or can get it with reasonable efforts.
462 |
463 | You may not impose any further restrictions on the exercise of the
464 | rights granted or affirmed under this License. For example, you may
465 | not impose a license fee, royalty, or other charge for exercise of
466 | rights granted under this License, and you may not initiate litigation
467 | (including a cross-claim or counterclaim in a lawsuit) alleging that
468 | any patent claim is infringed by making, using, selling, offering for
469 | sale, or importing the Program or any portion of it.
470 |
471 | 11. Patents.
472 |
473 | A "contributor" is a copyright holder who authorizes use under this
474 | License of the Program or a work on which the Program is based. The
475 | work thus licensed is called the contributor's "contributor version".
476 |
477 | A contributor's "essential patent claims" are all patent claims
478 | owned or controlled by the contributor, whether already acquired or
479 | hereafter acquired, that would be infringed by some manner, permitted
480 | by this License, of making, using, or selling its contributor version,
481 | but do not include claims that would be infringed only as a
482 | consequence of further modification of the contributor version. For
483 | purposes of this definition, "control" includes the right to grant
484 | patent sublicenses in a manner consistent with the requirements of
485 | this License.
486 |
487 | Each contributor grants you a non-exclusive, worldwide, royalty-free
488 | patent license under the contributor's essential patent claims, to
489 | make, use, sell, offer for sale, import and otherwise run, modify and
490 | propagate the contents of its contributor version.
491 |
492 | In the following three paragraphs, a "patent license" is any express
493 | agreement or commitment, however denominated, not to enforce a patent
494 | (such as an express permission to practice a patent or covenant not to
495 | sue for patent infringement). To "grant" such a patent license to a
496 | party means to make such an agreement or commitment not to enforce a
497 | patent against the party.
498 |
499 | If you convey a covered work, knowingly relying on a patent license,
500 | and the Corresponding Source of the work is not available for anyone
501 | to copy, free of charge and under the terms of this License, through a
502 | publicly available network server or other readily accessible means,
503 | then you must either (1) cause the Corresponding Source to be so
504 | available, or (2) arrange to deprive yourself of the benefit of the
505 | patent license for this particular work, or (3) arrange, in a manner
506 | consistent with the requirements of this License, to extend the patent
507 | license to downstream recipients. "Knowingly relying" means you have
508 | actual knowledge that, but for the patent license, your conveying the
509 | covered work in a country, or your recipient's use of the covered work
510 | in a country, would infringe one or more identifiable patents in that
511 | country that you have reason to believe are valid.
512 |
513 | If, pursuant to or in connection with a single transaction or
514 | arrangement, you convey, or propagate by procuring conveyance of, a
515 | covered work, and grant a patent license to some of the parties
516 | receiving the covered work authorizing them to use, propagate, modify
517 | or convey a specific copy of the covered work, then the patent license
518 | you grant is automatically extended to all recipients of the covered
519 | work and works based on it.
520 |
521 | A patent license is "discriminatory" if it does not include within
522 | the scope of its coverage, prohibits the exercise of, or is
523 | conditioned on the non-exercise of one or more of the rights that are
524 | specifically granted under this License. You may not convey a covered
525 | work if you are a party to an arrangement with a third party that is
526 | in the business of distributing software, under which you make payment
527 | to the third party based on the extent of your activity of conveying
528 | the work, and under which the third party grants, to any of the
529 | parties who would receive the covered work from you, a discriminatory
530 | patent license (a) in connection with copies of the covered work
531 | conveyed by you (or copies made from those copies), or (b) primarily
532 | for and in connection with specific products or compilations that
533 | contain the covered work, unless you entered into that arrangement,
534 | or that patent license was granted, prior to 28 March 2007.
535 |
536 | Nothing in this License shall be construed as excluding or limiting
537 | any implied license or other defenses to infringement that may
538 | otherwise be available to you under applicable patent law.
539 |
540 | 12. No Surrender of Others' Freedom.
541 |
542 | If conditions are imposed on you (whether by court order, agreement or
543 | otherwise) that contradict the conditions of this License, they do not
544 | excuse you from the conditions of this License. If you cannot convey a
545 | covered work so as to satisfy simultaneously your obligations under this
546 | License and any other pertinent obligations, then as a consequence you may
547 | not convey it at all. For example, if you agree to terms that obligate you
548 | to collect a royalty for further conveying from those to whom you convey
549 | the Program, the only way you could satisfy both those terms and this
550 | License would be to refrain entirely from conveying the Program.
551 |
552 | 13. Use with the GNU Affero General Public License.
553 |
554 | Notwithstanding any other provision of this License, you have
555 | permission to link or combine any covered work with a work licensed
556 | under version 3 of the GNU Affero General Public License into a single
557 | combined work, and to convey the resulting work. The terms of this
558 | License will continue to apply to the part which is the covered work,
559 | but the special requirements of the GNU Affero General Public License,
560 | section 13, concerning interaction through a network will apply to the
561 | combination as such.
562 |
563 | 14. Revised Versions of this License.
564 |
565 | The Free Software Foundation may publish revised and/or new versions of
566 | the GNU General Public License from time to time. Such new versions will
567 | be similar in spirit to the present version, but may differ in detail to
568 | address new problems or concerns.
569 |
570 | Each version is given a distinguishing version number. If the
571 | Program specifies that a certain numbered version of the GNU General
572 | Public License "or any later version" applies to it, you have the
573 | option of following the terms and conditions either of that numbered
574 | version or of any later version published by the Free Software
575 | Foundation. If the Program does not specify a version number of the
576 | GNU General Public License, you may choose any version ever published
577 | by the Free Software Foundation.
578 |
579 | If the Program specifies that a proxy can decide which future
580 | versions of the GNU General Public License can be used, that proxy's
581 | public statement of acceptance of a version permanently authorizes you
582 | to choose that version for the Program.
583 |
584 | Later license versions may give you additional or different
585 | permissions. However, no additional obligations are imposed on any
586 | author or copyright holder as a result of your choosing to follow a
587 | later version.
588 |
589 | 15. Disclaimer of Warranty.
590 |
591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599 |
600 | 16. Limitation of Liability.
601 |
602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610 | SUCH DAMAGES.
611 |
612 | 17. Interpretation of Sections 15 and 16.
613 |
614 | If the disclaimer of warranty and limitation of liability provided
615 | above cannot be given local legal effect according to their terms,
616 | reviewing courts shall apply local law that most closely approximates
617 | an absolute waiver of all civil liability in connection with the
618 | Program, unless a warranty or assumption of liability accompanies a
619 | copy of the Program in return for a fee.
620 |
621 | END OF TERMS AND CONDITIONS
622 |
623 | How to Apply These Terms to Your New Programs
624 |
625 | If you develop a new program, and you want it to be of the greatest
626 | possible use to the public, the best way to achieve this is to make it
627 | free software which everyone can redistribute and change under these terms.
628 |
629 | To do so, attach the following notices to the program. It is safest
630 | to attach them to the start of each source file to most effectively
631 | state the exclusion of warranty; and each file should have at least
632 | the "copyright" line and a pointer to where the full notice is found.
633 |
634 |
635 | Copyright (C)
636 |
637 | This program is free software: you can redistribute it and/or modify
638 | it under the terms of the GNU General Public License as published by
639 | the Free Software Foundation, either version 3 of the License, or
640 | (at your option) any later version.
641 |
642 | This program is distributed in the hope that it will be useful,
643 | but WITHOUT ANY WARRANTY; without even the implied warranty of
644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645 | GNU General Public License for more details.
646 |
647 | You should have received a copy of the GNU General Public License
648 | along with this program. If not, see .
649 |
650 | Also add information on how to contact you by electronic and paper mail.
651 |
652 | If the program does terminal interaction, make it output a short
653 | notice like this when it starts in an interactive mode:
654 |
655 | Copyright (C)
656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657 | This is free software, and you are welcome to redistribute it
658 | under certain conditions; type `show c' for details.
659 |
660 | The hypothetical commands `show w' and `show c' should show the appropriate
661 | parts of the General Public License. Of course, your program's commands
662 | might be different; for a GUI interface, you would use an "about box".
663 |
664 | You should also get your employer (if you work as a programmer) or school,
665 | if any, to sign a "copyright disclaimer" for the program, if necessary.
666 | For more information on this, and how to apply and follow the GNU GPL, see
667 | .
668 |
669 | The GNU General Public License does not permit incorporating your program
670 | into proprietary programs. If your program is a subroutine library, you
671 | may consider it more useful to permit linking proprietary applications with
672 | the library. If this is what you want to do, use the GNU Lesser General
673 | Public License instead of this License. But first, please read
674 | .
675 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Prometheus NUT Exporter
2 |
3 | [](https://github.com/HON95/prometheus-nut-exporter/releases)
4 | [](https://github.com/HON95/prometheus-nut-exporter/actions?query=workflow%3ACI)
5 | [](https://app.fossa.com/projects/git%2Bgithub.com%2FHON95%2Fprometheus-nut-exporter?ref=badge_shield)
6 | [](https://hub.docker.com/r/hon95/prometheus-nut-exporter)
7 |
8 | 
9 |
10 | A Prometheus/OpenMetrics exporter for uninterruptable power supplies (UPSes) using Network UPS Tools (NUT).
11 |
12 | ## Usage
13 |
14 | ### NUT
15 |
16 | Set up NUT in server mode and make sure the TCP port (3493 by default) is accessible (without authentication).
17 |
18 | If you want to test that it's working, run `telnet 3493` and then `VER`, `LIST UPS` and `LIST VAR `.
19 |
20 | ### Docker
21 |
22 | Example `docker-compose.yml`:
23 |
24 | ```yaml
25 | version: "3.7"
26 |
27 | services:
28 | nut-exporter:
29 | # Stable v1
30 | image: hon95/prometheus-nut-exporter:1
31 | environment:
32 | - TZ=Europe/Oslo
33 | - HTTP_PATH=/metrics
34 | # Defaults
35 | #- RUST_LOG=info
36 | #- HTTP_PORT=9995
37 | #- HTTP_PATH=/nut
38 | #- LOG_REQUESTS_CONSOLE=false
39 | #- PRINT_METRICS_AND_EXIT=false
40 | ports:
41 | - "9995:9995/tcp"
42 | ```
43 |
44 | ### Prometheus
45 |
46 | Example `prometheus.yml`:
47 |
48 | ```yaml
49 | global:
50 | scrape_interval: 15s
51 | scrape_timeout: 10s
52 |
53 | scrape_configs:
54 | - job_name: "nut"
55 | static_configs:
56 | # Insert NUT server address here
57 | - targets: ["nut-server:3493"]
58 | relabel_configs:
59 | - source_labels: [__address__]
60 | target_label: __param_target
61 | - source_labels: [__param_target]
62 | target_label: instance
63 | - target_label: __address__
64 | # Insert NUT exporter address here
65 | replacement: nut-exporter:9995
66 | ```
67 |
68 | In the above example, `nut-exporter:9995` is the address and port of the NUT _exporter_ while `nut-server:3493` is the address and port of the NUT _server_ to query through the exporter.
69 |
70 | ### Kubernetes Resource Usage
71 |
72 | Example container resources requests and limits.
73 | This was done by scraping one NUT server with two UPSes.
74 | Resource usage was observed across 7 days period.
75 | This can be lowered even more but should be sufficient as a starting point.
76 |
77 | ```yaml
78 | resources:
79 | limits:
80 | cpu: "10m"
81 | memory: "16Mi"
82 | requests:
83 | cpu: "1m"
84 | memory: "8Mi"
85 | ```
86 |
87 | ### Grafana
88 |
89 | [Example dashboard](https://grafana.com/grafana/dashboards/14371)
90 |
91 | ## Configuration
92 |
93 | ### Docker Image Versions
94 |
95 | Use e.g. `1` for stable v1.y.z releases and `latest` for bleeding/dev/unstable releases.
96 |
97 | ### Environment Variables
98 |
99 | - `RUST_LOG` (defaults to `info`): The log level used by the console/STDOUT. Set to `debug` so show HTTP requests and `trace` to show extensive debugging info.
100 | - `HTTP_ADDRESS` (defaults to `::`): The HTTP server will listen on this IP. Set to `127.0.0.1` or `::1` to only allow local access.
101 | - `HTTP_PORT` (defaults to `9995`): The HTTP server port.
102 | - `HTTP_PATH` (defaults to `nut`): The HTTP server metrics path. You may want to set it to `/metrics` on new setups to avoid extra Prometheus configuration (not changed here due to compatibility).
103 | - `PRINT_METRICS_AND_EXIT` (defaults to `false`): Print a Markdown-formatted table consisting of all metrics and then immediately exit. Used mainly for generating documentation.
104 |
105 | ## Metrics
106 |
107 | See [metrics](metrics.md).
108 |
109 | ## License
110 |
111 | GNU General Public License version 3 (GPLv3).
112 |
--------------------------------------------------------------------------------
/example-data/LIST-UPS-1.txt:
--------------------------------------------------------------------------------
1 | BEGIN LIST UPS
2 | UPS ups-1 "desc 1"
3 | END LIST UPS
4 |
--------------------------------------------------------------------------------
/example-data/LIST-VAR-1.txt:
--------------------------------------------------------------------------------
1 | BEGIN LIST VAR ups-1
2 | VAR ups-1 battery.charge "97"
3 | VAR ups-1 battery.charge.low "20"
4 | VAR ups-1 battery.charge.warning "20"
5 | VAR ups-1 battery.runtime "1693"
6 | VAR ups-1 battery.temperature "29.9"
7 | VAR ups-1 battery.type "PbAc"
8 | VAR ups-1 battery.voltage "40.1"
9 | VAR ups-1 battery.voltage.nominal "40.0"
10 | VAR ups-1 device.mfr "Hewlett-Packard"
11 | VAR ups-1 device.model "HP T1500 G3 UPS"
12 | VAR ups-1 device.part "DF321A"
13 | VAR ups-1 device.serial "HIDDEN"
14 | VAR ups-1 device.type "ups"
15 | VAR ups-1 input.frequency "50.0"
16 | VAR ups-1 input.transfer.high "266.0"
17 | VAR ups-1 input.transfer.low "194.0"
18 | VAR ups-1 input.voltage "230.0"
19 | VAR ups-1 input.voltage.nominal "230"
20 | VAR ups-1 output.current "1.3"
21 | VAR ups-1 output.frequency "50.0"
22 | VAR ups-1 output.frequency.nominal "50"
23 | VAR ups-1 output.voltage "230.0"
24 | VAR ups-1 output.voltage.nominal "230"
25 | VAR ups-1 ups.beeper.status "enabled"
26 | VAR ups-1 ups.delay.shutdown "20"
27 | VAR ups-1 ups.delay.start "30"
28 | VAR ups-1 ups.firmware "100"
29 | VAR ups-1 ups.load "31"
30 | VAR ups-1 ups.load.nominal "80"
31 | VAR ups-1 ups.mfr "Hewlett-Packard"
32 | VAR ups-1 ups.model "HP T1500 G3 UPS"
33 | VAR ups-1 ups.power "302.0"
34 | VAR ups-1 ups.power.nominal "1400"
35 | VAR ups-1 ups.productid "4de5"
36 | VAR ups-1 ups.serial "HIDDEN"
37 | VAR ups-1 ups.status "OL CHRG"
38 | VAR ups-1 ups.test.result "Done and passed"
39 | VAR ups-1 ups.timer.reboot "-1"
40 | VAR ups-1 ups.timer.shutdown "-1"
41 | VAR ups-1 ups.timer.start "-1"
42 | VAR ups-1 ups.vendorid "03f0"
43 | END LIST VAR ups-1
44 |
--------------------------------------------------------------------------------
/example-data/LIST-VAR-2.txt:
--------------------------------------------------------------------------------
1 | BEGIN LIST VAR ups-2
2 | VAR ups-2 ambient.1.humidity.alarm.high "60.00"
3 | VAR ups-2 ambient.1.humidity.alarm.low "30.00"
4 | VAR ups-2 ambient.1.temperature.alarm.high "40.00"
5 | VAR ups-2 ambient.1.temperature.alarm.low "10.00"
6 | VAR ups-2 battery.charge "100.00"
7 | VAR ups-2 battery.charge.restart "15"
8 | VAR ups-2 battery.current "0.00"
9 | VAR ups-2 battery.date "10/06/15"
10 | VAR ups-2 battery.packs "0.00"
11 | VAR ups-2 battery.packs.bad "0.00"
12 | VAR ups-2 battery.runtime "9180.00"
13 | VAR ups-2 battery.runtime.low "120"
14 | VAR ups-2 battery.voltage "27.50"
15 | VAR ups-2 battery.voltage.nominal "0.00"
16 | VAR ups-2 device.mfr "APC"
17 | VAR ups-2 device.model "Smart-UPS 1000"
18 | VAR ups-2 device.serial "MASKED"
19 | VAR ups-2 device.type "ups"
20 | VAR ups-2 driver.name "snmp-ups"
21 | VAR ups-2 driver.parameter.pollfreq "10"
22 | VAR ups-2 driver.parameter.pollinterval "2"
23 | VAR ups-2 driver.parameter.port "192.168.1.9"
24 | VAR ups-2 driver.parameter.snmp_version "v2c"
25 | VAR ups-2 driver.parameter.synchronous "no"
26 | VAR ups-2 driver.version "2.7.4"
27 | VAR ups-2 driver.version.data "apcc MIB 1.2"
28 | VAR ups-2 driver.version.internal "0.97"
29 | VAR ups-2 input.frequency "50.00"
30 | VAR ups-2 input.sensitivity "medium"
31 | VAR ups-2 input.transfer.high "253"
32 | VAR ups-2 input.transfer.low "208"
33 | VAR ups-2 input.transfer.reason "selfTest"
34 | VAR ups-2 input.voltage "234.70"
35 | VAR ups-2 input.voltage.maximum "236.10"
36 | VAR ups-2 input.voltage.minimum "233.20"
37 | VAR ups-2 output.current "0.00"
38 | VAR ups-2 output.frequency "50.00"
39 | VAR ups-2 output.voltage "234.70"
40 | VAR ups-2 output.voltage.nominal "220"
41 | VAR ups-2 ups.delay.shutdown "90"
42 | VAR ups-2 ups.delay.start "0"
43 | VAR ups-2 ups.firmware "600.3.I"
44 | VAR ups-2 ups.id "UPS_1K"
45 | VAR ups-2 ups.load "5.80"
46 | VAR ups-2 ups.mfr "APC"
47 | VAR ups-2 ups.mfr.date "04/23/05"
48 | VAR ups-2 ups.model "Smart-UPS 1000"
49 | VAR ups-2 ups.serial "MASKED"
50 | VAR ups-2 ups.status "OL"
51 | VAR ups-2 ups.temperature "28.80"
52 | VAR ups-2 ups.test.date "03/11/2021"
53 | VAR ups-2 ups.test.result "Ok"
54 | END LIST VAR ups-2
55 |
--------------------------------------------------------------------------------
/example-data/LIST-VAR-3.txt:
--------------------------------------------------------------------------------
1 | BEGIN LIST VAR ups-3
2 | VAR ups-3 battery.alarm.threshold "0"
3 | VAR ups-3 battery.charge "100.0"
4 | VAR ups-3 battery.charge.restart "00"
5 | VAR ups-3 battery.date "01/09/15"
6 | VAR ups-3 battery.runtime "3300"
7 | VAR ups-3 battery.runtime.low "120"
8 | VAR ups-3 battery.voltage "13.85"
9 | VAR ups-3 battery.voltage.nominal "012"
10 | VAR ups-3 device.mfr "APC"
11 | VAR ups-3 device.model "Smart-UPS 620 "
12 | VAR ups-3 device.serial "MASKED"
13 | VAR ups-3 device.type "ups"
14 | VAR ups-3 driver.name "apcsmart"
15 | VAR ups-3 driver.parameter.pollinterval "2"
16 | VAR ups-3 driver.parameter.port "/dev/ttyS0"
17 | VAR ups-3 driver.parameter.synchronous "no"
18 | VAR ups-3 driver.version "2.7.4"
19 | VAR ups-3 driver.version.internal "3.1"
20 | VAR ups-3 input.frequency "50.00"
21 | VAR ups-3 input.quality "FF"
22 | VAR ups-3 input.sensitivity "H"
23 | VAR ups-3 input.transfer.high "253"
24 | VAR ups-3 input.transfer.low "208"
25 | VAR ups-3 input.transfer.reason "unacceptable utility voltage rate of change"
26 | VAR ups-3 input.voltage "230.4"
27 | VAR ups-3 input.voltage.maximum "230.4"
28 | VAR ups-3 input.voltage.minimum "228.9"
29 | VAR ups-3 output.voltage "230.4"
30 | VAR ups-3 output.voltage.nominal "230"
31 | VAR ups-3 ups.delay.shutdown "020"
32 | VAR ups-3 ups.delay.start "000"
33 | VAR ups-3 ups.firmware "22.6.I"
34 | VAR ups-3 ups.id "ups-3 "
35 | VAR ups-3 ups.load "014.9"
36 | VAR ups-3 ups.mfr "APC"
37 | VAR ups-3 ups.mfr.date "09/20/01"
38 | VAR ups-3 ups.model "Smart-UPS 620 "
39 | VAR ups-3 ups.serial "MASKED"
40 | VAR ups-3 ups.status "OL"
41 | VAR ups-3 ups.test.interval "1209600"
42 | VAR ups-3 ups.test.result "NO"
43 | END LIST VAR ups-3
44 |
--------------------------------------------------------------------------------
/example-data/LIST-VAR-4.txt:
--------------------------------------------------------------------------------
1 | BEGIN LIST VAR ups-4
2 | VAR ups-4 battery.charge "100"
3 | VAR ups-4 battery.charge.low "10"
4 | VAR ups-4 battery.charge.warning "20"
5 | VAR ups-4 battery.mfr.date "1"
6 | VAR ups-4 battery.runtime "1320"
7 | VAR ups-4 battery.runtime.low "300"
8 | VAR ups-4 battery.type "PbAcid"
9 | VAR ups-4 battery.voltage "260.0"
10 | VAR ups-4 battery.voltage.nominal "120"
11 | VAR ups-4 device.mfr "1"
12 | VAR ups-4 device.model "2200R"
13 | VAR ups-4 device.serial "HIDDEN"
14 | VAR ups-4 device.type "ups"
15 | VAR ups-4 driver.name "usbhid-ups"
16 | VAR ups-4 driver.parameter.offdelay "60"
17 | VAR ups-4 driver.parameter.ondelay "120"
18 | VAR ups-4 driver.parameter.pollfreq "30"
19 | VAR ups-4 driver.parameter.pollinterval "2"
20 | VAR ups-4 driver.parameter.port "auto"
21 | VAR ups-4 driver.parameter.synchronous "no"
22 | VAR ups-4 driver.version "2.7.4"
23 | VAR ups-4 driver.version.data "CyberPower HID 0.4"
24 | VAR ups-4 driver.version.internal "0.41"
25 | VAR ups-4 input.transfer.high "290"
26 | VAR ups-4 input.transfer.low "165"
27 | VAR ups-4 input.voltage "238.7"
28 | VAR ups-4 input.voltage.nominal "230"
29 | VAR ups-4 output.voltage "237.2"
30 | VAR ups-4 ups.beeper.status "enabled"
31 | VAR ups-4 ups.delay.shutdown "60"
32 | VAR ups-4 ups.delay.start "120"
33 | VAR ups-4 ups.load "21"
34 | VAR ups-4 ups.mfr "1"
35 | VAR ups-4 ups.model "2200R"
36 | VAR ups-4 ups.productid "0601"
37 | VAR ups-4 ups.realpower.nominal "2200"
38 | VAR ups-4 ups.serial "HIDDEN"
39 | VAR ups-4 ups.status "OL"
40 | VAR ups-4 ups.timer.shutdown "-60"
41 | VAR ups-4 ups.timer.start "-60"
42 | VAR ups-4 ups.vendorid "0764"
43 | END LIST VAR ups-4
44 |
--------------------------------------------------------------------------------
/example-data/LIST-VAR-5.txt:
--------------------------------------------------------------------------------
1 | battery.charge "100"
2 | battery.runtime "2667"
3 | battery.voltage "13.60"
4 | battery.voltage.high "13.80"
5 | battery.voltage.low "10.40"
6 | battery.voltage.nominal "12.0"
7 | device.type "ups"
8 | driver.name "blazer_usb"
9 | driver.parameter.pollinterval "5"
10 | driver.parameter.port "auto"
11 | driver.parameter.runtimecal "400,100,800,50"
12 | driver.parameter.synchronous "no"
13 | driver.version "2.7.4"
14 | driver.version.internal "0.12"
15 | input.current.nominal "8.0"
16 | input.frequency "50.0"
17 | input.frequency.nominal "50"
18 | input.voltage "209.3"
19 | input.voltage.fault "209.3"
20 | input.voltage.nominal "230"
21 | output.voltage "246.9"
22 | ups.beeper.status "enabled"
23 | ups.delay.shutdown "30"
24 | ups.delay.start "180"
25 | ups.load "15"
26 | ups.productid "0000"
27 | ups.status "OL BOOST"
28 | ups.type "offline / line interactive"
29 | ups.vendorid "0001"
30 |
--------------------------------------------------------------------------------
/example-data/VER-1.txt:
--------------------------------------------------------------------------------
1 | Network UPS Tools upsd 2.7.4 - http://www.networkupstools.org/
2 |
--------------------------------------------------------------------------------
/example-data/VER-2.txt:
--------------------------------------------------------------------------------
1 | Network UPS Tools upsd DSM6-2-25510-201118 - http://www.networkupstools.org/
2 |
--------------------------------------------------------------------------------
/manage/check.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -u
4 |
5 | src_dir="src/"
6 |
7 | function fail {
8 | echo
9 | echo -e "\e[31mFailed!\e[0m" >&2
10 | exit 1
11 | }
12 |
13 | function check_fail {
14 | if (( $? != 0 )); then
15 | fail
16 | fi
17 | }
18 |
19 | echo "Running Clippy ..."
20 | clippy_args="-D warnings \
21 | -A clippy::branches-sharing-code \
22 | -A clippy::vec-init-then-push"
23 | cargo clippy -- $clippy_args
24 | check_fail
25 |
26 | echo
27 | echo "Checking for trailing whitespace ..."
28 | ! egrep -RHn "\s+$" "$src_dir"
29 | check_fail
30 |
31 | echo
32 | echo "Checking for multiple empty lines ..."
33 | for file in $(find "$src_dir" -type f); do
34 | line_number=0
35 | empty_lines=0
36 | while IFS= read -r line; do
37 | ((line_number++))
38 | if [[ $line == "" ]]; then
39 | ((empty_lines++))
40 | else
41 | empty_lines=0
42 | fi
43 | if (( empty_lines > 1 )); then
44 | echo "Bad file: $file [line $line_number]" >&2
45 | fail
46 | fi
47 | done <"$file"
48 | done
49 |
50 | echo
51 | echo "Checking for empty line at end of files ..."
52 | for file in $(find "$src_dir" -type f); do
53 | if [[ $(tail -c1 "$file") != "" ]]; then
54 | echo "Bad file: $file" >&2
55 | fail
56 | fi
57 | done
58 |
59 | echo
60 | echo -e "\e[32mSuccess!\e[0m"
61 |
--------------------------------------------------------------------------------
/manage/docker/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -eu
4 |
5 | DC="docker-compose -f manage/docker/docker-compose.yml"
6 |
7 | export DOCKER_BUILDKIT=1
8 |
9 | $DC build
10 |
--------------------------------------------------------------------------------
/manage/docker/clean.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -eu
4 |
5 | DC="docker-compose -f manage/docker/docker-compose.yml"
6 |
7 | echo "Downing DC resources ..."
8 | $DC down
9 |
10 | echo
11 | echo "Emptying Docker build cache (global) ..."
12 | #docker image prune -af
13 | docker builder prune -af
14 |
15 | echo
16 | echo "Deleting local data ..."
17 | sudo rm -rf .local/
18 |
--------------------------------------------------------------------------------
/manage/docker/docker-compose.yml:
--------------------------------------------------------------------------------
1 | # Docker Compose file for dev setup
2 |
3 | services:
4 | exporter:
5 | build:
6 | context: ../..
7 | environment:
8 | - TZ=Europe/Oslo
9 | - RUST_BACKTRACE=1
10 | - RUST_LOG=debug
11 | - HTTP_PORT=9995
12 | - HTTP_PATH=/nut
13 | #- PRINT_METRICS_AND_EXIT=true
14 | # ports:
15 | # - "127.0.0.1:9995:9995/tcp"
16 | network_mode: host
17 |
18 | prometheus:
19 | image: prom/prometheus:latest
20 | environment:
21 | - TZ=Europe/Oslo
22 | volumes:
23 | - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
24 | - ../../.local/prometheus_data/:/prometheus/:rw
25 | # ports:
26 | # - "127.0.0.1:9090:9090/tcp"
27 | network_mode: host
28 |
--------------------------------------------------------------------------------
/manage/docker/prometheus.yml:
--------------------------------------------------------------------------------
1 | global:
2 | scrape_interval: 5s
3 | scrape_timeout: 5s
4 |
5 | scrape_configs:
6 | - job_name: "nut"
7 | static_configs:
8 | # NUT server
9 | - targets: ["localhost:3493"]
10 | metrics_path: /nut
11 | relabel_configs:
12 | - source_labels: [__address__]
13 | target_label: __param_target
14 | - source_labels: [__param_target]
15 | target_label: instance
16 | - target_label: __address__
17 | # NUT exporter
18 | replacement: localhost:9995
19 |
--------------------------------------------------------------------------------
/manage/docker/run.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -eu
4 |
5 | PROM_DATA_DIR=".local/prometheus_data"
6 | DC="docker-compose -f manage/docker/docker-compose.yml"
7 |
8 | # Add Prometheus data dir with correct permissions
9 | mkdir -p "$PROM_DATA_DIR"
10 | chmod 777 "$PROM_DATA_DIR"
11 |
12 | $DC up
13 |
--------------------------------------------------------------------------------
/manage/integration_test.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -u -o pipefail
4 |
5 | # Start mock
6 | manage/nut-server-mock.py &>/dev/null &
7 | mock_pid=$!
8 |
9 | # Start exporter
10 | cargo run &>/dev/null &
11 | exporter_pid=$!
12 |
13 | # Teardown on exit
14 | function teardown {
15 | kill -9 $mock_pid &>/dev/null
16 | kill -9 $exporter_pid &>/dev/null
17 | }
18 | trap teardown EXIT
19 |
20 | # Wait for startup
21 | sleep 1
22 |
23 | # Scrape
24 | scrape_content=$(curl -sSf "http://localhost:9995/nut?target=localhost:3493")
25 | if [[ $? != 0 ]]; then
26 | echo "Failure!"
27 | echo "Scrape failed."
28 | exit 1
29 | fi
30 |
31 | # Validate result
32 | if ! echo $scrape_content | grep 'nut_status{ups="alpha"} 1' &>/dev/null; then
33 | echo "Failure!"
34 | echo "Scraped result contains errors."
35 | exit 1
36 | fi
37 |
38 | echo "Success!"
39 |
--------------------------------------------------------------------------------
/manage/nut-server-mock.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | # A mock backend for the NUT server.
4 | # Linted with flake8.
5 |
6 | import socket
7 | import sys
8 |
9 | # Server endpoint
10 | SERVER_HOST = ""
11 | SERVER_PORT = 3493
12 | # Max request length in bytes (avoid DoS)
13 | RECV_BUFFER_MAX_BYTES = 4096
14 | UPS_EXPECTED = "alpha"
15 |
16 | DATA_VER = """\
17 | Network UPS Tools upsd 2.7.4 - http://www.networkupstools.org/
18 | """
19 | COMMAND_UPS_LIST = "list ups"
20 | DATA_UPS_LIST = """\
21 | BEGIN LIST UPS
22 | UPS alpha "desc 1"
23 | END LIST UPS
24 | """
25 | COMMAND_VAR_LIST = "list var" # Plus UPS name
26 | DATA_VAR_LIST = """\
27 | BEGIN LIST VAR alpha
28 | VAR alpha battery.charge "100"
29 | VAR alpha battery.charge.low "10"
30 | VAR alpha battery.charge.warning "20"
31 | VAR alpha battery.mfr.date "1"
32 | VAR alpha battery.runtime "1320"
33 | VAR alpha battery.runtime.low "300"
34 | VAR alpha battery.type "PbAcid"
35 | VAR alpha battery.voltage "260.0"
36 | VAR alpha battery.voltage.nominal "120"
37 | VAR alpha device.mfr "1"
38 | VAR alpha device.model "2200R"
39 | VAR alpha device.serial "HIDDEN"
40 | VAR alpha device.type "ups"
41 | VAR alpha driver.name "usbhid-ups"
42 | VAR alpha driver.parameter.offdelay "60"
43 | VAR alpha driver.parameter.ondelay "120"
44 | VAR alpha driver.parameter.pollfreq "30"
45 | VAR alpha driver.parameter.pollinterval "2"
46 | VAR alpha driver.parameter.port "auto"
47 | VAR alpha driver.parameter.synchronous "no"
48 | VAR alpha driver.version "2.7.4"
49 | VAR alpha driver.version.data "CyberPower HID 0.4"
50 | VAR alpha driver.version.internal "0.41"
51 | VAR alpha input.transfer.high "290"
52 | VAR alpha input.transfer.low "165"
53 | VAR alpha input.voltage "238.7"
54 | VAR alpha input.voltage.nominal "230"
55 | VAR alpha output.voltage "237.2"
56 | VAR alpha ups.beeper.status "enabled"
57 | VAR alpha ups.delay.shutdown "60"
58 | VAR alpha ups.delay.start "120"
59 | VAR alpha ups.load "21"
60 | VAR alpha ups.mfr "1"
61 | VAR alpha ups.model "2200R"
62 | VAR alpha ups.productid "0601"
63 | VAR alpha ups.realpower.nominal "2200"
64 | VAR alpha ups.serial "HIDDEN"
65 | VAR alpha ups.status "OL CHRG"
66 | VAR alpha ups.timer.shutdown "-60"
67 | VAR alpha ups.timer.start "-60"
68 | VAR alpha ups.vendorid "0764"
69 | END LIST VAR alpha
70 | """
71 |
72 |
73 | class EmptyObject:
74 | pass
75 |
76 |
77 | def main():
78 | print(f"Starting mock NUT server on {SERVER_HOST}:{SERVER_PORT}")
79 | server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
80 | server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
81 | server.bind((SERVER_HOST, SERVER_PORT))
82 | server.listen()
83 |
84 | # Accept clients
85 | while True:
86 | client = EmptyObject()
87 | try:
88 | client.connection, (client.address, client.port) = server.accept()
89 | except OSError:
90 | # Socket closed (probably)
91 | break
92 | with client.connection:
93 | try:
94 | log("New client", client)
95 | handleClient(client)
96 | log("Closing client", client)
97 | except Exception as err:
98 | log(f"Error {type(err).__name__} during request: {err}", client, error=True)
99 |
100 |
101 | def handleClient(client):
102 | # Use wrapper so we can easily reassign it for modifications
103 | lineBufferPtr = EmptyObject()
104 | lineBufferPtr.value = bytearray()
105 | while True:
106 | line = readRequestLine(lineBufferPtr, client)
107 | if not line:
108 | break
109 | handleRequest(line, client)
110 |
111 | def readRequestLine(lineBufferPtr, client):
112 | while True:
113 | ok, line = readRequestLineInner(lineBufferPtr, client)
114 | if not ok:
115 | return None
116 | if line:
117 | log(f"New request: {line}", client)
118 | return line
119 |
120 | # Fail if max length reached (without any complete lines)
121 | bufferSize = len(lineBufferPtr.value)
122 | if bufferSize >= RECV_BUFFER_MAX_BYTES:
123 | log("Error: Too long request", client, error=True)
124 | return None
125 |
126 |
127 | def readRequestLineInner(lineBufferPtr, client):
128 | bufferSize = len(lineBufferPtr.value)
129 | data = client.connection.recv(RECV_BUFFER_MAX_BYTES - bufferSize)
130 | # Check EOF
131 | if not data:
132 | return False, None
133 | lineBufferPtr.value.extend(data)
134 | bufferSize += len(data)
135 |
136 | # Check for line ending (using simple LF)
137 | i = -1
138 | while True:
139 | i += 1
140 | if i >= bufferSize:
141 | break
142 |
143 | # Strip leading spaces
144 | if i == 0 and lineBufferPtr.value[i] == ord(' '):
145 | lineBufferPtr.value = lineBufferPtr.value[:i] + lineBufferPtr.value[i+1:]
146 | bufferSize -= 1
147 | i -= 1
148 | continue
149 |
150 | # Strip CR
151 | if lineBufferPtr.value[i] == ord('\r'):
152 | lineBufferPtr.value = lineBufferPtr.value[:i] + lineBufferPtr.value[i+1:]
153 | bufferSize -= 1
154 | i -= 1
155 | continue
156 |
157 | # Check if complete line, extract from buffer and return early if found
158 | if lineBufferPtr.value[i] == ord('\n'):
159 | line = lineBufferPtr.value[:i].decode()
160 | lineBufferPtr.value = lineBufferPtr.value[i+1:]
161 | return True, line
162 |
163 | # No lines were found (yet)
164 | return True, None
165 |
166 |
167 | def handleRequest(line, client):
168 | def sendText(message):
169 | client.connection.sendall(message.encode())
170 | lowerLine = line.lower()
171 | lineParts = line.split()
172 | numLineParts = len(lineParts)
173 | if numLineParts == 1 and lowerLine.startswith("ver"):
174 | sendText(DATA_VER)
175 | elif numLineParts == 2 and lowerLine.startswith("list ups"):
176 | sendText(DATA_UPS_LIST)
177 | elif numLineParts == 3 and lowerLine.startswith("list var"):
178 | if lineParts[2] == UPS_EXPECTED:
179 | sendText(DATA_VAR_LIST)
180 | else:
181 | sendText("ERR UPS not found\n")
182 | elif numLineParts == 1 and lowerLine.startswith("logout"):
183 | sendText(DATA_VER)
184 | else:
185 | sendText("ERR Unknown command\n")
186 |
187 |
188 | def log(message, client=None, error=False):
189 | output = sys.stderr if error else sys.stdout
190 | if client:
191 | print(f"[{client.address}:{client.port}] " + message, file=output)
192 | else:
193 | print(message, file=output)
194 |
195 |
196 | if __name__ == "__main__":
197 | main()
198 |
--------------------------------------------------------------------------------
/metrics.md:
--------------------------------------------------------------------------------
1 | # Metrics
2 |
3 | | Metric | NUT Var | Unit | Description |
4 | | - | - | - | - |
5 | | `nut_exporter_info` | | | Metadata about the exporter. |
6 | | `nut_server_info` | | | Metadata about the NUT server. |
7 | | `nut_ups_info` | | | Metadata about the UPS. |
8 | | `nut_info` | | | Metadata about the NUT server. (Deprecated, use nut_server_info instead.) |
9 | | `nut_ups_status` | `ups.status` | | UPS status. Check for a specific status with the "status" label. |
10 | | `nut_beeper_status` | `ups.beeper.status` | | If the beeper is enabled. Unknown (0), enabled (1), disabled (2) or muted (3). |
11 | | `nut_uptime_seconds` | `device.uptime` | `seconds` | Device uptime. |
12 | | `nut_load` | `ups.load` | | Load. (0-1) |
13 | | `nut_temperature_celsius` | `ups.temperature` | `celsius` | UPS temperature |
14 | | `nut_battery_charge` | `battery.charge` | | Battery level. (0-1) |
15 | | `nut_battery_charge_low` | `battery.charge.low` | | Battery level threshold for low state. (0-1) |
16 | | `nut_battery_charge_warning` | `battery.charge.warning` | | Battery level threshold for warning state. (0-1) |
17 | | `nut_battery_charge_restart` | `battery.charge.restart` | | Battery level threshold for restarting after power-off. (0-1) |
18 | | `nut_battery_runtime_seconds` | `battery.runtime` | `seconds` | Battery runtime. |
19 | | `nut_battery_runtime_low_seconds` | `battery.runtime.low` | `seconds` | Battery runtime threshold for state low. |
20 | | `nut_battery_runtime_restart_seconds` | `battery.runtime.restart` | `seconds` | Battery runtime threshold for restart after power-off. |
21 | | `nut_delay_shutdown_seconds` | `ups.delay.shutdown` | `seconds` | Interval to wait after shutdown with delay command. |
22 | | `nut_delay_start_seconds` | `ups.delay.start` | `seconds` | Interval to wait before (re)starting the load. |
23 | | `nut_battery_voltage_volts` | `battery.voltage` | `volts` | Battery voltage. |
24 | | `nut_battery_voltage_nominal_volts` | `battery.voltage.nominal` | `volts` | Battery voltage (nominal). |
25 | | `nut_battery_voltage_high_volts` | `battery.voltage.high` | `volts` | Battery voltage for full (charge level calculation). |
26 | | `nut_battery_voltage_low_volts` | `battery.voltage.low` | `volts` | Battery voltage for empty (charge level calculation). |
27 | | `nut_battery_temperature_celsius` | `battery.temperature` | `celsius` | Battery temperature. |
28 | | `nut_input_voltage_volts` | `input.voltage` | `volts` | Input voltage. |
29 | | `nut_input_voltage_nominal_volts` | `input.voltage.nominal` | `volts` | Input voltage (nominal). |
30 | | `nut_input_voltage_minimum_volts` | `input.voltage.minimum` | `volts` | Input voltage (minimum seen). |
31 | | `nut_input_voltage_maximum_volts` | `input.voltage.maximum` | `volts` | Input voltage (maximum seen). |
32 | | `nut_input_transfer_low_volts` | `input.transfer.low` | `volts` | Input lower transfer threshold. |
33 | | `nut_input_transfer_high_volts` | `input.transfer.high` | `volts` | Input upper transfer threshold. |
34 | | `nut_input_current_amperes` | `input.current` | `amperes` | Input current. |
35 | | `nut_input_current_nominal_amperes` | `input.current.nominal` | `amperes` | Input current (nominal). |
36 | | `nut_input_frequency_hertz` | `input.frequency` | `hertz` | Input frequency. |
37 | | `nut_input_frequency_nominal_hertz` | `input.frequency.nominal` | `hertz` | Input frequency (nominal). |
38 | | `nut_input_frequency_low_hertz` | `input.frequency.low` | `hertz` | Input frequency (low). |
39 | | `nut_input_frequency_high_hertz` | `input.frequency.high` | `hertz` | Input frequency (high). |
40 | | `nut_output_voltage_volts` | `output.voltage` | `volts` | Output voltage. |
41 | | `nut_output_voltage_nominal_volts` | `output.voltage.nominal` | `volts` | Output voltage (nominal). |
42 | | `nut_output_current_amperes` | `output.current` | `amperes` | Output current. |
43 | | `nut_output_current_nominal_amperes` | `output.current.nominal` | `amperes` | Output current (nominal). |
44 | | `nut_output_frequency_hertz` | `output.frequency` | `hertz` | Output frequency. |
45 | | `nut_output_frequency_nominal_hertz` | `output.frequency.nominal` | `hertz` | Output frequency (nominal). |
46 | | `nut_power_watts` | `ups.power` | `watts` | Apparent power. |
47 | | `nut_power_nominal_watts` | `ups.power.nominal` | `watts` | Apparent power (nominal). |
48 | | `nut_real_power_watts` | `ups.realpower` | `watts` | Real power. |
49 | | `nut_real_power_nominal_watts` | `ups.realpower.nominal` | `watts` | Real power (nominal). |
50 | | `nut_status` | `ups.status` | | UPS status. Unknown (0), on line (1, "OL"), on battery (2, "OB"), or low battery (3, "LB"). (Deprecated, use nut_ups_status instead.) |
51 | | `nut_battery_volts` | `battery.voltage` | `volts` | Battery voltage. (Deprecated, use nut_battery_voltage_volts instead.) |
52 | | `nut_input_volts` | `input.voltage` | `volts` | Input voltage. (Deprecated, use nut_input_voltage_volts instead.) |
53 | | `nut_output_volts` | `output.voltage` | `volts` | Output voltage. (Deprecated, use nut_output_voltage_volts instead.) |
54 |
55 | (Generated by running with `PRINT_METRICS_AND_EXIT=true`.)
56 |
57 | Feel free to suggest adding more metrics (including a printout of what the variable and value looks like for your UPS)!
58 |
59 | ## UPS Status
60 |
61 | The `nut_ups_status` metric family describes a set of statuses, specified in the `status` label. The meanings of the different statuses is shown below.
62 |
63 | | Status | Description |
64 | | - | - |
65 | | `OL` | Online |
66 | | `OB` | On battery |
67 | | `LB` | Low battery (critical) |
68 | | `CHRG` | Charging |
69 | | `RB` | Replace battery |
70 | | `FSD` | Forced shutdown |
71 | | `BYPASS` | Battery bypass |
72 | | `SD` | Shutdown |
73 | | `CP` | Cable power |
74 | | `BOOST` | Boosted voltage |
75 | | `OFF` | Off |
76 |
77 | Which statuses different UPSes support varies, but `OL` (online) and `OB` (on battery) is (almost?) always supported.
78 |
79 | ## Miscellanea
80 |
81 | To check if a specific UPS is unavailable, use something like: `absent(nut_status{job="...", ups="..."})`
82 |
--------------------------------------------------------------------------------
/src/common.rs:
--------------------------------------------------------------------------------
1 | use std::error::Error;
2 |
3 | pub type ErrorResult = Result>;
4 |
--------------------------------------------------------------------------------
/src/config.rs:
--------------------------------------------------------------------------------
1 | use std::net::{IpAddr, Ipv6Addr};
2 |
3 | #[derive(Debug, Clone)]
4 | pub struct Config {
5 | pub http_address: IpAddr,
6 | pub http_port: u16,
7 | pub http_path: String,
8 | pub print_metrics_and_exit: bool,
9 | }
10 |
11 | impl Config {
12 | pub const DEFAULT_LOG_LEVEL: &'static str = "info";
13 | pub const DEFAULT_NUT_PORT: u16 = 3493;
14 |
15 | const DEFAULT_HTTP_ADDRESS: IpAddr = IpAddr::V6(Ipv6Addr::UNSPECIFIED);
16 | const DEFAULT_HTTP_PORT: u16 = 9995;
17 | const DEFAULT_HTTP_PATH: &'static str = "/nut";
18 | const DEFAULT_PRINT_METRICS_AND_EXIT: bool = false;
19 | }
20 |
21 | pub fn read_config() -> Config {
22 | let mut config = Config {
23 | http_address: Config::DEFAULT_HTTP_ADDRESS,
24 | http_port: Config::DEFAULT_HTTP_PORT,
25 | http_path: Config::DEFAULT_HTTP_PATH.to_owned(),
26 | print_metrics_and_exit: Config::DEFAULT_PRINT_METRICS_AND_EXIT,
27 | };
28 |
29 | if let Ok(http_address_str) = std::env::var("HTTP_ADDRESS") {
30 | if let Ok(http_address) = http_address_str.parse::() {
31 | config.http_address = http_address;
32 | }
33 | }
34 |
35 | if let Ok(http_port_str) = std::env::var("HTTP_PORT") {
36 | if let Ok(http_port) = http_port_str.parse::() {
37 | config.http_port = http_port;
38 | }
39 | }
40 | if let Ok(http_path) = std::env::var("HTTP_PATH") {
41 | if http_path.starts_with('/') {
42 | config.http_path = http_path;
43 | }
44 | }
45 | if let Ok(print_metrics_and_exit_str) = std::env::var("PRINT_METRICS_AND_EXIT") {
46 | if let Ok(print_metrics_and_exit) = print_metrics_and_exit_str.parse::() {
47 | config.print_metrics_and_exit = print_metrics_and_exit;
48 | }
49 | }
50 |
51 | config
52 | }
53 |
--------------------------------------------------------------------------------
/src/http_server.rs:
--------------------------------------------------------------------------------
1 | use std::collections::HashMap;
2 | use std::convert::Infallible;
3 | use std::fmt::Write as _;
4 | use std::net::{SocketAddr};
5 |
6 | use hyper::{Body, Method, Request, Response, Server, StatusCode};
7 | use hyper::service::{make_service_fn, service_fn};
8 | use hyper::server::conn::AddrStream;
9 | use lazy_static::lazy_static;
10 | use regex::Regex;
11 | use tokio::sync::broadcast::Receiver;
12 | use url::form_urlencoded;
13 |
14 | use crate::meta::{APP_NAME, APP_AUTHOR, APP_VERSION};
15 | use crate::common::ErrorResult;
16 | use crate::config::Config;
17 | use crate::nut_client::scrape_nut;
18 | use crate::openmetrics_builder::build_openmetrics_content;
19 |
20 | const CONTENT_TYPE_TEXT: &str = "text/plain; charset=UTF-8";
21 | const CONTENT_TYPE_OPENMETRICS: &str = "application/openmetrics-text; version=1.0.0; charset=UTF-8";
22 | const CONTENT_TYPE_OPENMETRICS_BASE: &str = "application/openmetrics-text";
23 |
24 | pub async fn run_server(config: Config, mut shutdown_channel: Receiver) {
25 | // Bind to endpoint
26 | let endpoint = SocketAddr::new(config.http_address, config.http_port);
27 | log::info!("Binding to endpoint: http://{}", endpoint);
28 | let server_builder = match Server::try_bind(&endpoint) {
29 | Ok(builder) => builder,
30 | Err(err) => {
31 | log::error!("Server failed to bind to endpoint: {}", err);
32 | return;
33 | },
34 | };
35 |
36 | // Setup server
37 | let shutdown_future = async {
38 | shutdown_channel.recv().await.unwrap();
39 | };
40 | let config1 = config.clone();
41 | let service_maker = make_service_fn(move |conn: &AddrStream| {
42 | let config2 = config1.clone();
43 | let remote_addr = conn.remote_addr();
44 | async move {
45 | Ok::<_, Infallible>(service_fn(move |request: Request| {
46 | entrypoint(config2.clone(), request, remote_addr)
47 | }))
48 | }
49 | });
50 | let server_task = server_builder.serve(service_maker).with_graceful_shutdown(shutdown_future);
51 |
52 | // Run server
53 | if let Err(err) = server_task.await {
54 | log::error!("Server error: {}", err);
55 | }
56 | }
57 |
58 | async fn entrypoint(config: Config, request: Request, remote_addr: SocketAddr) -> Result, Infallible> {
59 | log::trace!("HTTP request from: {}", remote_addr);
60 | log::trace!("HTTP request URL: {}", request.uri().path());
61 |
62 | let metrics_path = &config.http_path;
63 | let is_method_get = request.method() == Method::GET;
64 | let path = request.uri().path();
65 | let response: Response;
66 | if path == "/" {
67 | if is_method_get {
68 | response = endpoint_home(&config);
69 | } else {
70 | response = endpoint_method_not_allowed();
71 | }
72 | } else if path == metrics_path {
73 | if is_method_get {
74 | response = endpoint_metrics(&config, &request).await;
75 | } else {
76 | response = endpoint_method_not_allowed();
77 | }
78 | } else {
79 | response = endpoint_not_found();
80 | }
81 |
82 | // Log request to console
83 | log::debug!("Request: {} {} {} {}", remote_addr, request.method(), request.uri().path(), response.status().to_string());
84 |
85 | Ok(response)
86 | }
87 |
88 | fn endpoint_home(config: &Config) -> Response {
89 | let mut content = String::new();
90 | let _ = writeln!(content, "{} version {} by {}.", APP_NAME, APP_VERSION, APP_AUTHOR);
91 | let _ = writeln!(content);
92 | let _ = writeln!(content, "Usage: {}?target=", config.http_path);
93 |
94 | Response::builder().status(StatusCode::OK).body(Body::from(content)).unwrap()
95 | }
96 |
97 | fn endpoint_not_found() -> Response {
98 | Response::builder().status(StatusCode::NOT_FOUND).body(Body::from("Not found\n")).unwrap()
99 | }
100 |
101 | fn endpoint_method_not_allowed() -> Response {
102 | Response::builder().status(StatusCode::METHOD_NOT_ALLOWED).body(Body::from("Method not allowed\n")).unwrap()
103 | }
104 |
105 | async fn endpoint_metrics(config: &Config, request: &Request) -> Response {
106 | // Check for and parse target
107 | let usage_message = format!("Usage: {}?target=", config.http_path);
108 | let target = match parse_target(request) {
109 | Ok(target) => target,
110 | Err(err) => return Response::builder().status(StatusCode::BAD_REQUEST).body(Body::from(format!("{}\n\n{}", err, usage_message))).unwrap(),
111 | };
112 |
113 | // Try to scrape NUT server
114 | let (upses, nut_version) = match scrape_nut(&target).await {
115 | Ok(x) => x,
116 | Err(err) => return Response::builder().status(StatusCode::SERVICE_UNAVAILABLE).body(Body::from(err.to_string())).unwrap(),
117 | };
118 |
119 | // Generate OpenMetrics output
120 | let content = build_openmetrics_content(&upses, &nut_version);
121 |
122 | // Set content type
123 | let mut content_type = CONTENT_TYPE_TEXT;
124 | if let Some(accept_header) = request.headers().get("accept") {
125 | if let Ok(accept_str) = accept_header.to_str() {
126 | if accept_str.contains(CONTENT_TYPE_OPENMETRICS_BASE) {
127 | content_type = CONTENT_TYPE_OPENMETRICS;
128 | }
129 | }
130 | }
131 |
132 | Response::builder().status(StatusCode::OK).header("Content-Type", content_type).body(Body::from(content)).unwrap()
133 | }
134 |
135 | fn parse_target(request: &Request) -> ErrorResult {
136 | lazy_static! {
137 | // Match domain, IPv4 address or IPv6 addres, with optional port number
138 | static ref TARGET_PATTERN: Regex = Regex::new(r#"^(?P\[[^\]]+\]|[^:]+)(?::(?P[0-9]+))?$"#).unwrap();
139 | }
140 |
141 | let query_args: HashMap = form_urlencoded::parse(request.uri().query().unwrap_or("").as_bytes()).into_owned().collect();
142 | let target_raw = match query_args.get("target") {
143 | Some(target_raw) => target_raw,
144 | None => return Err("Missing target.".into()),
145 | };
146 |
147 | let default_port = Config::DEFAULT_NUT_PORT.to_string();
148 | let target = match TARGET_PATTERN.captures(target_raw) {
149 | Some(captures) => {
150 | let host = captures.name("host").unwrap().as_str();
151 | let port = match captures.name("port") {
152 | Some(port) => port.as_str(),
153 | None => default_port.as_str(),
154 | };
155 | format!("{}:{}", host, port)
156 | },
157 | None => return Err("Malformed list element for VAR list query.".into()),
158 | };
159 |
160 | Ok(target)
161 | }
162 |
--------------------------------------------------------------------------------
/src/main.rs:
--------------------------------------------------------------------------------
1 | mod common;
2 | mod config;
3 | mod http_server;
4 | mod meta;
5 | mod metrics;
6 | mod nut_client;
7 | mod openmetrics_builder;
8 |
9 | use tokio::signal::unix::{signal, SignalKind};
10 | use tokio::sync::broadcast;
11 |
12 | #[tokio::main]
13 | async fn main() {
14 | // Setup logger
15 | env_logger::Builder::from_env(env_logger::Env::default().default_filter_or(config::Config::DEFAULT_LOG_LEVEL)).init();
16 |
17 | // Setup config
18 | let config = config::read_config();
19 | if config.print_metrics_and_exit {
20 | metrics::print_metrics();
21 | return;
22 | }
23 |
24 | // Start server
25 | let (shutdown_tx, mut shutdown_rx) = broadcast::channel(1);
26 | let server_task = tokio::spawn(http_server::run_server(config, shutdown_tx.subscribe()));
27 |
28 | // Listen for shutdown signals
29 | let mut sigint_stream = signal(SignalKind::interrupt()).unwrap();
30 | let mut sigterm_stream = signal(SignalKind::terminate()).unwrap();
31 | tokio::select! {
32 | _ = shutdown_rx.recv() => {
33 | log::debug!("Received internal shutdown signal.");
34 | },
35 | _ = sigint_stream.recv() => {
36 | log::debug!("Received interrupt signal.");
37 | shutdown_tx.send(true).unwrap();
38 | },
39 | _ = sigterm_stream.recv() => {
40 | log::debug!("Received termination signal.");
41 | shutdown_tx.send(true).unwrap();
42 | },
43 | }
44 |
45 | // Wait for server
46 | server_task.await.unwrap();
47 | }
48 |
--------------------------------------------------------------------------------
/src/meta.rs:
--------------------------------------------------------------------------------
1 | pub const APP_NAME: &str = "Prometheus NUT Exporter";
2 | pub const APP_AUTHOR: &str = "HON95";
3 | // Automatically set during build
4 | pub const APP_VERSION: &str = "0.0.0-SNAPSHOT";
5 |
--------------------------------------------------------------------------------
/src/metrics.rs:
--------------------------------------------------------------------------------
1 | use std::collections::HashMap;
2 |
3 | use lazy_static::lazy_static;
4 |
5 | pub type VarMap = HashMap;
6 | pub type UpsVarMap = HashMap;
7 | pub type NutVersion = String;
8 |
9 | pub const UPS_DESCRIPTION_PSEUDOVAR: &str = "_description";
10 |
11 | #[derive(Debug, Copy, Clone, PartialEq, Eq)]
12 | pub enum VarTransform {
13 | None,
14 | Percentage,
15 | BeeperStatus,
16 | OldUpsStatus,
17 | }
18 |
19 | #[derive(Debug, Clone)]
20 | pub struct Metric {
21 | pub metric: &'static str,
22 | pub help: &'static str,
23 | pub type_: &'static str,
24 | pub unit: &'static str,
25 | pub nut_var: &'static str,
26 | pub var_transform: VarTransform,
27 | pub is_integer: bool,
28 | }
29 |
30 | pub const UPS_STATUS_ELEMENTS: [&str; 11] = [
31 | "OL", // online
32 | "OB", // on battery
33 | "LB", // low battery (critical)
34 | "CHRG", // charging
35 | "RB", // replace battery
36 | "FSD", // forced shutdown
37 | "BYPASS", // battery bypass
38 | "SD", // shutdown
39 | "CP", // cable power
40 | "BOOST", // boosted voltage
41 | "OFF", // off
42 | ];
43 |
44 | // Special metrics
45 | pub const EXPORTER_INFO_METRIC: Metric = Metric {
46 | metric: "nut_exporter_info",
47 | help: "Metadata about the exporter.",
48 | type_: "info",
49 | unit: "",
50 | nut_var: "",
51 | var_transform: VarTransform::None,
52 | is_integer: true,
53 | };
54 | pub const SERVER_INFO_METRIC: Metric = Metric {
55 | metric: "nut_server_info",
56 | help: "Metadata about the NUT server.",
57 | type_: "info",
58 | unit: "",
59 | nut_var: "",
60 | var_transform: VarTransform::None,
61 | is_integer: true,
62 | };
63 | pub const UPS_INFO_METRIC: Metric = Metric {
64 | metric: "nut_ups_info",
65 | help: "Metadata about the UPS.",
66 | type_: "info",
67 | unit: "",
68 | nut_var: "",
69 | var_transform: VarTransform::None,
70 | is_integer: true,
71 | };
72 | pub const UPS_STATUS_METRIC: Metric = Metric {
73 | metric: "nut_ups_status",
74 | help: "UPS status. Check for a specific status with the \"status\" label.",
75 | type_: "stateset",
76 | unit: "",
77 | nut_var: "ups.status",
78 | var_transform: VarTransform::None,
79 | is_integer: true,
80 | };
81 | // Deprecated special metrics
82 | pub const OLD_SERVER_INFO_METRIC: Metric = Metric {
83 | metric: "nut_info",
84 | help: "Metadata about the NUT server. (Deprecated, use nut_server_info instead.)",
85 | type_: "info",
86 | unit: "",
87 | nut_var: "",
88 | var_transform: VarTransform::None,
89 | is_integer: true,
90 | };
91 |
92 | // Basic metrics
93 | pub static BASIC_METRICS: [Metric; 44] = [
94 | // Status, uptime, load
95 | Metric {
96 | metric: "nut_beeper_status",
97 | help: "If the beeper is enabled. Unknown (0), enabled (1), disabled (2) or muted (3).",
98 | type_: "gauge",
99 | unit: "",
100 | nut_var: "ups.beeper.status",
101 | var_transform: VarTransform::BeeperStatus,
102 | is_integer: true,
103 | },
104 | Metric {
105 | metric: "nut_uptime_seconds",
106 | help: "Device uptime.",
107 | type_: "gauge",
108 | unit: "seconds",
109 | nut_var: "device.uptime",
110 | var_transform: VarTransform::None,
111 | is_integer: true,
112 | },
113 | Metric {
114 | metric: "nut_load",
115 | help: "Load. (0-1)",
116 | type_: "gauge",
117 | unit: "",
118 | nut_var: "ups.load",
119 | var_transform: VarTransform::Percentage,
120 | is_integer: false,
121 | },
122 | Metric {
123 | metric: "nut_temperature_celsius",
124 | help: "UPS temperature",
125 | type_: "gauge",
126 | unit: "celsius",
127 | nut_var: "ups.temperature",
128 | var_transform: VarTransform::None,
129 | is_integer: false,
130 | },
131 | // Battery
132 | Metric {
133 | metric: "nut_battery_charge",
134 | help: "Battery level. (0-1)",
135 | type_: "gauge",
136 | unit: "",
137 | nut_var: "battery.charge",
138 | var_transform: VarTransform::Percentage,
139 | is_integer: false,
140 | },
141 | Metric {
142 | metric: "nut_battery_charge_low",
143 | help: "Battery level threshold for low state. (0-1)",
144 | type_: "gauge",
145 | unit: "",
146 | nut_var: "battery.charge.low",
147 | var_transform: VarTransform::Percentage,
148 | is_integer: false,
149 | },
150 | Metric {
151 | metric: "nut_battery_charge_warning",
152 | help: "Battery level threshold for warning state. (0-1)",
153 | type_: "gauge",
154 | unit: "",
155 | nut_var: "battery.charge.warning",
156 | var_transform: VarTransform::Percentage,
157 | is_integer: false,
158 | },
159 | Metric {
160 | metric: "nut_battery_charge_restart",
161 | help: "Battery level threshold for restarting after power-off. (0-1)",
162 | type_: "gauge",
163 | unit: "",
164 | nut_var: "battery.charge.restart",
165 | var_transform: VarTransform::Percentage,
166 | is_integer: false,
167 | },
168 | Metric {
169 | metric: "nut_battery_runtime_seconds",
170 | help: "Battery runtime.",
171 | type_: "gauge",
172 | unit: "seconds",
173 | nut_var: "battery.runtime",
174 | var_transform: VarTransform::None,
175 | is_integer: true,
176 | },
177 | Metric {
178 | metric: "nut_battery_runtime_low_seconds",
179 | help: "Battery runtime threshold for state low.",
180 | type_: "gauge",
181 | unit: "seconds",
182 | nut_var: "battery.runtime.low",
183 | var_transform: VarTransform::None,
184 | is_integer: true,
185 | },
186 | Metric {
187 | metric: "nut_battery_runtime_restart_seconds",
188 | help: "Battery runtime threshold for restart after power-off.",
189 | type_: "gauge",
190 | unit: "seconds",
191 | nut_var: "battery.runtime.restart",
192 | var_transform: VarTransform::None,
193 | is_integer: true,
194 | },
195 | Metric {
196 | metric: "nut_delay_shutdown_seconds",
197 | help: "Interval to wait after shutdown with delay command.",
198 | type_: "gauge",
199 | unit: "seconds",
200 | nut_var: "ups.delay.shutdown",
201 | var_transform: VarTransform::None,
202 | is_integer: true,
203 | },
204 | Metric {
205 | metric: "nut_delay_start_seconds",
206 | help: "Interval to wait before (re)starting the load.",
207 | type_: "gauge",
208 | unit: "seconds",
209 | nut_var: "ups.delay.start",
210 | var_transform: VarTransform::None,
211 | is_integer: true,
212 | },
213 | Metric {
214 | metric: "nut_battery_voltage_volts",
215 | help: "Battery voltage.",
216 | type_: "gauge",
217 | unit: "volts",
218 | nut_var: "battery.voltage",
219 | var_transform: VarTransform::None,
220 | is_integer: false,
221 | },
222 | Metric {
223 | metric: "nut_battery_voltage_nominal_volts",
224 | help: "Battery voltage (nominal).",
225 | type_: "gauge",
226 | unit: "volts",
227 | nut_var: "battery.voltage.nominal",
228 | var_transform: VarTransform::None,
229 | is_integer: false,
230 | },
231 | Metric {
232 | metric: "nut_battery_voltage_high_volts",
233 | help: "Battery voltage for full (charge level calculation).",
234 | type_: "gauge",
235 | unit: "volts",
236 | nut_var: "battery.voltage.high",
237 | var_transform: VarTransform::None,
238 | is_integer: false,
239 | },
240 | Metric {
241 | metric: "nut_battery_voltage_low_volts",
242 | help: "Battery voltage for empty (charge level calculation).",
243 | type_: "gauge",
244 | unit: "volts",
245 | nut_var: "battery.voltage.low",
246 | var_transform: VarTransform::None,
247 | is_integer: false,
248 | },
249 | Metric {
250 | metric: "nut_battery_temperature_celsius",
251 | help: "Battery temperature.",
252 | type_: "gauge",
253 | unit: "celsius",
254 | nut_var: "battery.temperature",
255 | var_transform: VarTransform::None,
256 | is_integer: false,
257 | },
258 | // Input
259 | Metric {
260 | metric: "nut_input_voltage_volts",
261 | help: "Input voltage.",
262 | type_: "gauge",
263 | unit: "volts",
264 | nut_var: "input.voltage",
265 | var_transform: VarTransform::None,
266 | is_integer: false,
267 | },
268 | Metric {
269 | metric: "nut_input_voltage_nominal_volts",
270 | help: "Input voltage (nominal).",
271 | type_: "gauge",
272 | unit: "volts",
273 | nut_var: "input.voltage.nominal",
274 | var_transform: VarTransform::None,
275 | is_integer: false,
276 | },
277 | Metric {
278 | metric: "nut_input_voltage_minimum_volts",
279 | help: "Input voltage (minimum seen).",
280 | type_: "gauge",
281 | unit: "volts",
282 | nut_var: "input.voltage.minimum",
283 | var_transform: VarTransform::None,
284 | is_integer: false,
285 | },
286 | Metric {
287 | metric: "nut_input_voltage_maximum_volts",
288 | help: "Input voltage (maximum seen).",
289 | type_: "gauge",
290 | unit: "volts",
291 | nut_var: "input.voltage.maximum",
292 | var_transform: VarTransform::None,
293 | is_integer: false,
294 | },
295 | Metric {
296 | metric: "nut_input_transfer_low_volts",
297 | help: "Input lower transfer threshold.",
298 | type_: "gauge",
299 | unit: "volts",
300 | nut_var: "input.transfer.low",
301 | var_transform: VarTransform::None,
302 | is_integer: false,
303 | },
304 | Metric {
305 | metric: "nut_input_transfer_high_volts",
306 | help: "Input upper transfer threshold.",
307 | type_: "gauge",
308 | unit: "volts",
309 | nut_var: "input.transfer.high",
310 | var_transform: VarTransform::None,
311 | is_integer: false,
312 | },
313 | Metric {
314 | metric: "nut_input_current_amperes",
315 | help: "Input current.",
316 | type_: "gauge",
317 | unit: "amperes",
318 | nut_var: "input.current",
319 | var_transform: VarTransform::None,
320 | is_integer: false,
321 | },
322 | Metric {
323 | metric: "nut_input_current_nominal_amperes",
324 | help: "Input current (nominal).",
325 | type_: "gauge",
326 | unit: "amperes",
327 | nut_var: "input.current.nominal",
328 | var_transform: VarTransform::None,
329 | is_integer: false,
330 | },
331 | Metric {
332 | metric: "nut_input_frequency_hertz",
333 | help: "Input frequency.",
334 | type_: "gauge",
335 | unit: "hertz",
336 | nut_var: "input.frequency",
337 | var_transform: VarTransform::None,
338 | is_integer: false,
339 | },
340 | Metric {
341 | metric: "nut_input_frequency_nominal_hertz",
342 | help: "Input frequency (nominal).",
343 | type_: "gauge",
344 | unit: "hertz",
345 | nut_var: "input.frequency.nominal",
346 | var_transform: VarTransform::None,
347 | is_integer: false,
348 | },
349 | Metric {
350 | metric: "nut_input_frequency_low_hertz",
351 | help: "Input frequency (low).",
352 | type_: "gauge",
353 | unit: "hertz",
354 | nut_var: "input.frequency.low",
355 | var_transform: VarTransform::None,
356 | is_integer: false,
357 | },
358 | Metric {
359 | metric: "nut_input_frequency_high_hertz",
360 | help: "Input frequency (high).",
361 | type_: "gauge",
362 | unit: "hertz",
363 | nut_var: "input.frequency.high",
364 | var_transform: VarTransform::None,
365 | is_integer: false,
366 | },
367 | // Output
368 | Metric {
369 | metric: "nut_output_voltage_volts",
370 | help: "Output voltage.",
371 | type_: "gauge",
372 | unit: "volts",
373 | nut_var: "output.voltage",
374 | var_transform: VarTransform::None,
375 | is_integer: false,
376 | },
377 | Metric {
378 | metric: "nut_output_voltage_nominal_volts",
379 | help: "Output voltage (nominal).",
380 | type_: "gauge",
381 | unit: "volts",
382 | nut_var: "output.voltage.nominal",
383 | var_transform: VarTransform::None,
384 | is_integer: false,
385 | },
386 | Metric {
387 | metric: "nut_output_current_amperes",
388 | help: "Output current.",
389 | type_: "gauge",
390 | unit: "amperes",
391 | nut_var: "output.current",
392 | var_transform: VarTransform::None,
393 | is_integer: false,
394 | },
395 | Metric {
396 | metric: "nut_output_current_nominal_amperes",
397 | help: "Output current (nominal).",
398 | type_: "gauge",
399 | unit: "amperes",
400 | nut_var: "output.current.nominal",
401 | var_transform: VarTransform::None,
402 | is_integer: false,
403 | },
404 | Metric {
405 | metric: "nut_output_frequency_hertz",
406 | help: "Output frequency.",
407 | type_: "gauge",
408 | unit: "hertz",
409 | nut_var: "output.frequency",
410 | var_transform: VarTransform::None,
411 | is_integer: false,
412 | },
413 | Metric {
414 | metric: "nut_output_frequency_nominal_hertz",
415 | help: "Output frequency (nominal).",
416 | type_: "gauge",
417 | unit: "hertz",
418 | nut_var: "output.frequency.nominal",
419 | var_transform: VarTransform::None,
420 | is_integer: false,
421 | },
422 | // Power
423 | Metric {
424 | metric: "nut_power_watts",
425 | help: "Apparent power.",
426 | type_: "gauge",
427 | unit: "watts",
428 | nut_var: "ups.power",
429 | var_transform: VarTransform::None,
430 | is_integer: false,
431 | },
432 | Metric {
433 | metric: "nut_power_nominal_watts",
434 | help: "Apparent power (nominal).",
435 | type_: "gauge",
436 | unit: "watts",
437 | nut_var: "ups.power.nominal",
438 | var_transform: VarTransform::None,
439 | is_integer: false,
440 | },
441 | Metric {
442 | metric: "nut_real_power_watts",
443 | help: "Real power.",
444 | type_: "gauge",
445 | unit: "watts",
446 | nut_var: "ups.realpower",
447 | var_transform: VarTransform::None,
448 | is_integer: false,
449 | },
450 | Metric {
451 | metric: "nut_real_power_nominal_watts",
452 | help: "Real power (nominal).",
453 | type_: "gauge",
454 | unit: "watts",
455 | nut_var: "ups.realpower.nominal",
456 | var_transform: VarTransform::None,
457 | is_integer: false,
458 | },
459 | // Deprecated
460 | Metric {
461 | metric: "nut_status",
462 | help: "UPS status. Unknown (0), on line (1, \"OL\"), on battery (2, \"OB\"), or low battery (3, \"LB\"). (Deprecated, use nut_ups_status instead.)",
463 | type_: "gauge",
464 | unit: "",
465 | nut_var: "ups.status",
466 | var_transform: VarTransform::OldUpsStatus,
467 | is_integer: true,
468 | },
469 | Metric {
470 | metric: "nut_battery_volts",
471 | help: "Battery voltage. (Deprecated, use nut_battery_voltage_volts instead.)",
472 | type_: "gauge",
473 | unit: "volts",
474 | nut_var: "battery.voltage",
475 | var_transform: VarTransform::None,
476 | is_integer: false,
477 | },
478 | Metric {
479 | metric: "nut_input_volts",
480 | help: "Input voltage. (Deprecated, use nut_input_voltage_volts instead.)",
481 | type_: "gauge",
482 | unit: "volts",
483 | nut_var: "input.voltage",
484 | var_transform: VarTransform::None,
485 | is_integer: false,
486 | },
487 | Metric {
488 | metric: "nut_output_volts",
489 | help: "Output voltage. (Deprecated, use nut_output_voltage_volts instead.)",
490 | type_: "gauge",
491 | unit: "volts",
492 | nut_var: "output.voltage",
493 | var_transform: VarTransform::None,
494 | is_integer: false,
495 | },
496 | ];
497 |
498 | lazy_static! {
499 | // Contains all metrics names, in insertion order
500 | pub static ref METRIC_NAMES: Vec<&'static str> = {
501 | let mut vec: Vec<&'static str> = Vec::new();
502 | vec.push(EXPORTER_INFO_METRIC.metric);
503 | vec.push(SERVER_INFO_METRIC.metric);
504 | vec.push(UPS_INFO_METRIC.metric);
505 | vec.push(OLD_SERVER_INFO_METRIC.metric);
506 | vec.push(UPS_STATUS_METRIC.metric);
507 | for metric in BASIC_METRICS.iter() {
508 | vec.push(metric.metric);
509 | }
510 | vec
511 | };
512 |
513 | // Contains all metrics, indexed by metric name
514 | pub static ref METRICS: HashMap<&'static str, &'static Metric> = {
515 | let mut map: HashMap<&'static str, &'static Metric> = HashMap::new();
516 | map.insert(EXPORTER_INFO_METRIC.metric, &EXPORTER_INFO_METRIC);
517 | map.insert(SERVER_INFO_METRIC.metric, &SERVER_INFO_METRIC);
518 | map.insert(UPS_INFO_METRIC.metric, &UPS_INFO_METRIC);
519 | map.insert(OLD_SERVER_INFO_METRIC.metric, &OLD_SERVER_INFO_METRIC);
520 | map.insert(UPS_STATUS_METRIC.metric, &UPS_STATUS_METRIC);
521 | for metric in BASIC_METRICS.iter() {
522 | map.insert(metric.metric, metric);
523 | }
524 | map
525 | };
526 |
527 | // Contains all metrics based on NUT vars, indexed by var
528 | pub static ref VAR_METRICS: HashMap<&'static str, Vec<&'static Metric>> = {
529 | let mut map: HashMap<&'static str, Vec<&'static Metric>> = HashMap::new();
530 | for metric in BASIC_METRICS.iter() {
531 | map.entry(metric.nut_var).or_insert_with(Vec::new).push(metric);
532 | }
533 | map
534 | };
535 | }
536 |
537 | // Print metrics as Markdown table.
538 | pub fn print_metrics() {
539 | println!("| Metric | NUT Var | Unit | Description |");
540 | println!("| - | - | - | - |");
541 | let print_metric = |metric: &Metric| {
542 | let row = format!("| `{}` | `{}` | `{}` | {} |", metric.metric, metric.nut_var, metric.unit, metric.help).replace("``", "");
543 | println!("{}", row)
544 | };
545 |
546 | print_metric(&EXPORTER_INFO_METRIC);
547 | print_metric(&SERVER_INFO_METRIC);
548 | print_metric(&UPS_INFO_METRIC);
549 | print_metric(&OLD_SERVER_INFO_METRIC);
550 | print_metric(&UPS_STATUS_METRIC);
551 | for metric in BASIC_METRICS.iter() {
552 | print_metric(metric);
553 | }
554 | }
555 |
--------------------------------------------------------------------------------
/src/nut_client.rs:
--------------------------------------------------------------------------------
1 | use std::collections::HashMap;
2 |
3 | use lazy_static::lazy_static;
4 | use regex::Regex;
5 | use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
6 | use tokio::net::TcpStream;
7 |
8 | use crate::common::ErrorResult;
9 | use crate::metrics::{NutVersion, UPS_DESCRIPTION_PSEUDOVAR, UpsVarMap, VarMap};
10 |
11 | #[derive(Debug, Copy, Clone, PartialEq, Eq)]
12 | enum NutQueryListState {
13 | Initial,
14 | Begun,
15 | Ended,
16 | Malformed,
17 | Error,
18 | }
19 |
20 | pub async fn scrape_nut(target: &str) -> ErrorResult<(UpsVarMap, NutVersion)> {
21 | log::trace!("Connecting to NUT server: {}", target);
22 | let raw_stream = match TcpStream::connect(target).await {
23 | Ok(val) => val,
24 | Err(err) => return Err(format!("Failed to connect to target: {}", err).into()),
25 | };
26 | let mut stream = BufReader::new(raw_stream);
27 |
28 | match scrape_nut_upses(&mut stream).await {
29 | Ok(val) => Ok(val),
30 | Err(err) => Err(format!("Failed to communicate with target: {}", err).into()),
31 | }
32 | }
33 |
34 | async fn scrape_nut_upses(stream: &mut BufReader) -> ErrorResult<(UpsVarMap, NutVersion)> {
35 | let mut upses: UpsVarMap = HashMap::new();
36 | let mut nut_version: NutVersion = "".to_owned();
37 |
38 | query_nut_version(stream, &mut nut_version).await?;
39 | query_nut_upses(stream, &mut upses).await?;
40 | query_nut_vars(stream, &mut upses).await?;
41 |
42 | Ok((upses, nut_version))
43 | }
44 |
45 | async fn query_nut_version(stream: &mut BufReader, nut_version: &mut NutVersion) -> ErrorResult<()> {
46 | lazy_static! {
47 | static ref VERSION_PATTERN: Regex = Regex::new(r#"upsd (?P.+) -"#).unwrap();
48 | }
49 |
50 | stream.write_all(b"VER\n").await?;
51 | log::trace!("NUT query sent: {}", "VER");
52 | if let Some(line) = stream.lines().next_line().await? {
53 | log::trace!("NUT query received: {}", line);
54 | let captures_opt = VERSION_PATTERN.captures(&line);
55 | match captures_opt {
56 | Some(captures) => {
57 | *nut_version = captures["version"].to_owned();
58 | },
59 | None => {
60 | return Err("Failed get NUT version from NUT query. Not a NUT server?".into());
61 | },
62 | }
63 | }
64 |
65 | Ok(())
66 | }
67 |
68 | async fn query_nut_upses(stream: &mut BufReader, upses: &mut UpsVarMap) -> ErrorResult<()> {
69 | lazy_static! {
70 | static ref UPS_PATTERN: Regex = Regex::new(r#"^UPS\s+(?P[\S]+)\s+"(?P[^"]*)"$"#).unwrap();
71 | }
72 |
73 | let line_consumer = |line: &str| {
74 | let captures_opt = UPS_PATTERN.captures(line);
75 | match captures_opt {
76 | Some(captures) => {
77 | let ups = captures["ups"].to_owned();
78 | let desc = captures["desc"].to_owned();
79 | let mut vars: VarMap = HashMap::new();
80 | vars.insert(UPS_DESCRIPTION_PSEUDOVAR.to_owned(), desc);
81 | upses.insert(ups, vars);
82 | },
83 | None => {
84 | return Err("Malformed list element for UPS list query.".into());
85 | },
86 | }
87 |
88 | Ok(())
89 | };
90 |
91 | query_nut_list(stream, "LIST UPS", line_consumer).await?;
92 |
93 | Ok(())
94 | }
95 |
96 | async fn query_nut_vars(stream: &mut BufReader, upses: &mut UpsVarMap) -> ErrorResult<()> {
97 | lazy_static! {
98 | static ref VAR_PATTERN: Regex = Regex::new(r#"^VAR\s+(?P[\S]+)\s+(?P[\S]+)\s+"(?P[^"]*)"$"#).unwrap();
99 | }
100 |
101 | for (ups, vars) in upses.iter_mut() {
102 | let line_consumer = |line: &str| {
103 | let captures_opt = VAR_PATTERN.captures(line);
104 | match captures_opt {
105 | Some(captures) => {
106 | let variable = captures["var"].to_owned();
107 | let value = captures["val"].to_owned();
108 | vars.insert(variable, value);
109 | },
110 | None => {
111 | return Err("Malformed list element for VAR list query.".into());
112 | },
113 | }
114 |
115 | Ok(())
116 | };
117 |
118 | query_nut_list(stream, format!("LIST VAR {}", ups).as_str(), line_consumer).await?;
119 | }
120 |
121 | Ok(())
122 | }
123 |
124 | async fn query_nut_list(stream: &mut BufReader, query: &str, mut line_consumer: F) -> ErrorResult<()>
125 | where F: FnMut(&str) -> ErrorResult<()> + Send {
126 | let query_line = format!("{}\n", query);
127 | stream.write_all(query_line.as_bytes()).await?;
128 | log::trace!("NUT query sent: {}", query);
129 | let mut query_state = NutQueryListState::Initial;
130 | let mut nut_error_message = "".to_owned();
131 | while let Some(line) = stream.lines().next_line().await? {
132 | log::trace!("NUT query received: {}", line);
133 |
134 | // Empty line
135 | if line.is_empty() {
136 | // Skip line
137 | continue;
138 | }
139 | // Start of list
140 | if line.starts_with("BEGIN ") {
141 | if query_state == NutQueryListState::Initial {
142 | query_state = NutQueryListState::Begun;
143 | // Continue with list
144 | continue;
145 | } else {
146 | // Wrong order
147 | query_state = NutQueryListState::Malformed;
148 | break;
149 | }
150 | }
151 | // End of list
152 | if line.starts_with("END ") {
153 | if query_state == NutQueryListState::Begun {
154 | // End list
155 | query_state = NutQueryListState::Ended;
156 | break;
157 | } else {
158 | // Wrong order
159 | query_state = NutQueryListState::Malformed;
160 | break;
161 | }
162 | }
163 | // Error
164 | if line.starts_with("ERR ") {
165 | query_state = NutQueryListState::Error;
166 | nut_error_message = line.strip_prefix("ERR ").unwrap().to_owned();
167 | break;
168 | }
169 |
170 | // Structural error if content outside BEGIN-END section
171 | if query_state != NutQueryListState::Begun {
172 | query_state = NutQueryListState::Malformed;
173 | break;
174 | }
175 |
176 | // Within BEGIN-END so feed line to consumer
177 | line_consumer(&line)?;
178 | }
179 |
180 | // Check if the list didn't finish traversal when no error was encountered
181 | if query_state != NutQueryListState::Ended && query_state != NutQueryListState::Error {
182 | query_state = NutQueryListState::Malformed;
183 | }
184 |
185 | // Check if error or malformed
186 | if query_state == NutQueryListState::Error {
187 | return Err(format!("Received error for query \"{}\": {}", query, nut_error_message).into());
188 | }
189 | if query_state == NutQueryListState::Malformed {
190 | return Err(format!("Malformed list for query \"{}\".", query).into());
191 | }
192 |
193 | Ok(())
194 | }
195 |
--------------------------------------------------------------------------------
/src/openmetrics_builder.rs:
--------------------------------------------------------------------------------
1 | use std::fmt::Write as _;
2 | use std::collections::{HashMap, HashSet};
3 |
4 | use crate::meta::APP_VERSION;
5 | use crate::metrics::{EXPORTER_INFO_METRIC, Metric, METRIC_NAMES, METRICS, OLD_SERVER_INFO_METRIC, SERVER_INFO_METRIC, UPS_DESCRIPTION_PSEUDOVAR, UPS_INFO_METRIC, UPS_STATUS_ELEMENTS, UPS_STATUS_METRIC, UpsVarMap, VAR_METRICS, VarMap, VarTransform};
6 |
7 | pub fn build_openmetrics_content(upses: &UpsVarMap, nut_version: &str) -> String {
8 | // Use vec for stable ordering of metrics within a metric family
9 | let mut metric_lines: HashMap> = METRICS.keys().map(|m| ((*m).to_owned(), Vec::new())).collect();
10 |
11 | // Exporter and server special
12 | metric_lines.get_mut(EXPORTER_INFO_METRIC.metric).unwrap().push(print_exporter_info_metric());
13 | metric_lines.get_mut(SERVER_INFO_METRIC.metric).unwrap().push(print_server_info_metric(nut_version));
14 | metric_lines.get_mut(OLD_SERVER_INFO_METRIC.metric).unwrap().push(print_old_server_info_metric(nut_version));
15 |
16 | // Generate metric lines for all vars for all UPSes
17 | for (ups, vars) in upses.iter() {
18 | // UPS special
19 | metric_lines.get_mut(UPS_INFO_METRIC.metric).unwrap().push(print_ups_info_metric(ups, vars));
20 | metric_lines.get_mut(UPS_STATUS_METRIC.metric).unwrap().append(&mut print_ups_status_metrics(ups, vars));
21 | // UPS vars
22 | for (var, val) in vars.iter() {
23 | if let Some(metrics) = VAR_METRICS.get(var.as_str()) {
24 | for metric in metrics {
25 | if let Some(var_line) = print_basic_var_metric(ups, val, metric) {
26 | metric_lines.get_mut(metric.metric).unwrap().push(var_line);
27 | }
28 | }
29 | }
30 | }
31 | }
32 |
33 | // Print metric info and then all dimensions together
34 | // Use METRIC_NAMES vec for stable ordering of metric families
35 | let mut builder: String = String::new();
36 | for metric_name in METRIC_NAMES.iter() {
37 | let metric = METRICS[metric_name];
38 | if let Some(lines) = metric_lines.get(metric.metric) {
39 | if !lines.is_empty() {
40 | builder.push_str(&print_metric_metadata(metric));
41 | builder.push_str(&lines.concat());
42 | }
43 | }
44 | }
45 | builder.push_str("# EOF\n");
46 |
47 | builder
48 | }
49 |
50 | fn print_metric_metadata(metric: &Metric) -> String {
51 | let mut builder: String = String::new();
52 | let _ = writeln!(builder, "# TYPE {} {}", metric.metric, metric.type_);
53 | let _ = writeln!(builder, "# UNIT {} {}", metric.metric, metric.unit);
54 | if !metric.nut_var.is_empty() {
55 | let _ = writeln!(builder, "# HELP {} {} (\"{}\")", metric.metric, metric.help, metric.nut_var);
56 | } else {
57 | let _ = writeln!(builder, "# HELP {} {}", metric.metric, metric.help);
58 | }
59 |
60 | builder
61 | }
62 |
63 | fn print_exporter_info_metric() -> String {
64 | let metric = EXPORTER_INFO_METRIC;
65 | format!("{metric}{{version=\"{version}\"}} 1\n", metric=metric.metric, version=escape_om(APP_VERSION))
66 | }
67 |
68 | fn print_server_info_metric(nut_version: &str) -> String {
69 | let metric = SERVER_INFO_METRIC;
70 | format!("{metric}{{version=\"{version}\"}} 1\n", metric=metric.metric, version=escape_om(nut_version))
71 | }
72 |
73 | fn print_old_server_info_metric(nut_version: &str) -> String {
74 | let metric = OLD_SERVER_INFO_METRIC;
75 | format!("{metric}{{version=\"{version}\"}} 1\n", metric=metric.metric, version=escape_om(nut_version))
76 | }
77 |
78 | fn print_ups_info_metric(ups: &str, vars: &VarMap) -> String {
79 | let metric = UPS_INFO_METRIC;
80 |
81 | let mut labels_str = String::new();
82 | let _ = write!(labels_str, "ups=\"{}\"", escape_om(ups));
83 | let mut add_var_label = |name: &str, var: &str| {
84 | if let Some(value) = vars.get(var) {
85 | let _ = write!(labels_str, ",{}=\"{}\"", escape_om(name), escape_om(value));
86 | }
87 | };
88 |
89 | add_var_label("description", UPS_DESCRIPTION_PSEUDOVAR);
90 | add_var_label("description2", "device.description");
91 | add_var_label("device_type", "device.type");
92 | add_var_label("location", "device.location");
93 | add_var_label("manufacturer", "device.mfr");
94 | add_var_label("manufacturing_date", "device.mfr.date");
95 | add_var_label("model", "device.model");
96 | add_var_label("battery_type", "battery.type");
97 | add_var_label("driver", "driver.name");
98 | add_var_label("driver_version", "driver.version");
99 | add_var_label("driver_version_internal", "driver.version.internal");
100 | add_var_label("driver_version_data", "driver.version.data");
101 | add_var_label("usb_vendor_id", "ups.vendorid");
102 | add_var_label("usb_product_id", "ups.productid");
103 | add_var_label("ups_firmware", "ups.firmware");
104 | add_var_label("ups_type", "ups.type");
105 | // Deprecated
106 | add_var_label("type", "device.type");
107 | add_var_label("nut_version", "driver.version");
108 |
109 | format!("{}{{{}}} 1\n",metric.metric, labels_str)
110 | }
111 |
112 | fn print_ups_status_metrics(ups: &str, vars: &VarMap) -> Vec {
113 | let metric = UPS_STATUS_METRIC;
114 | let mut lines: Vec = Vec::new();
115 |
116 | let status_raw = match vars.get(metric.nut_var) {
117 | Some(x) => x,
118 | None => return lines,
119 | };
120 | let statuses: HashSet<&str> = HashSet::from_iter(status_raw.split(' '));
121 |
122 | for state in UPS_STATUS_ELEMENTS.iter() {
123 | let value_num = match statuses.contains(state) { false => 0i64, true => 1i64 };
124 | lines.push(format!("{metric}{{ups=\"{ups}\",status=\"{state}\"}} {value}\n", ups=ups, metric=metric.metric, state=state, value=value_num));
125 | }
126 |
127 | lines
128 | }
129 |
130 | fn print_basic_var_metric(ups: &str, value: &str, metric: &Metric) -> Option {
131 | let result_value: f64 = match metric.var_transform {
132 | VarTransform::None => {
133 | match value.parse::() {
134 | Ok(val) => val,
135 | Err(_) => return None,
136 | }
137 | },
138 | VarTransform::Percentage => {
139 | let num_value = match value.parse::() {
140 | Ok(val) => val,
141 | Err(_) => return None,
142 | };
143 | num_value / 100f64
144 | },
145 | VarTransform::BeeperStatus => {
146 | match value {
147 | "enabled" => 1f64,
148 | "disabled" => 2f64,
149 | "muted" => 3f64,
150 | _ => 0f64,
151 | }
152 | },
153 | VarTransform::OldUpsStatus => {
154 | // Remove the second component if present ("LB" etc.)
155 | let value_start = value.split_once(' ').map_or(value, |x| x.0);
156 | match value_start {
157 | "OL" => 1f64,
158 | "OB" => 2f64,
159 | "LB" => 3f64,
160 | _ => 0f64,
161 | }
162 | },
163 | };
164 |
165 | // Make sure floats always contains a decimal point and that ints never do
166 | let result_str = match metric.is_integer {
167 | true => format!("{:.0}", result_value),
168 | false => format!("{:.17}", result_value),
169 | };
170 |
171 | Some(format!("{metric}{{ups=\"{ups}\"}} {value}\n", metric=metric.metric, ups=escape_om(ups), value=result_str))
172 | }
173 |
174 | fn escape_om(raw_text: &str) -> String {
175 | raw_text.chars().map(|c| match c {
176 | '\n' => r#"\n"#.to_string(),
177 | '"' => r#"\""#.to_string(),
178 | '\\' => r#"\\"#.to_string(),
179 | _ => c.to_string(),
180 | }).collect()
181 | }
182 |
--------------------------------------------------------------------------------