A high-performance Prometheus and OpenTelemetry (OTLP) exporter for UPS (Uninterruptible Power Supply) devices, written in Rust. It queries UPS metrics by connecting directly to the local upsd daemon over TCP and exposes them via a Prometheus scraper endpoint and/or pushes them periodically to an OpenTelemetry collector.
This exporter is designed to run continuously on low-power, resource-constrained edge hardware such as the Raspberry Pi Zero (ARMv6). Building the exporter in Rust provides significant benefits over traditional implementations:
- Minimal Resource Footprint: Running as a daemon on a Raspberry Pi Zero, it consumes only ~1.0 MB of RAM (Resident Set Size / RSS) and has a systemd-reported memory usage of 84 KB. Compare this to Go (~10-20MB) or Python (~15-30MB).
- Near-Zero CPU Usage: The exporter uses less than 0.2% CPU on the single-core Broadcom BCM2835 processor (ARMv6) of a Raspberry Pi Zero, ensuring system resource consumption remains negligible.
- Zero Runtime Dependencies: When compiled with the MUSL target (
arm-unknown-linux-musleabihf), the binary is statically linked and has no dependencies on external C libraries, meaning you can drop it onto any Linux distribution and it will run immediately. - Tiny Binary Size: The compiled target release binary is only ~600 KiB in size.
- High Concurrency Performance: The multi-threaded Prometheus listener and background OTLP push loops operate with zero overhead.
Rather than spawning the external upsc binary or bringing in heavy async runtimes like tokio to talk to the upsd server, this exporter implements a native, standard-library-only TCP client that communicates directly with the NUT daemon over TCP port 3493.
This architectural decision has significant resource benefits:
- No Subprocess Overhead: Spawning subprocesses (
Command::new("upsc")) uses significant CPU cycles and creates fork-bomb risks under high scrape concurrency. Communicating directly over TCP loopback is instantaneous and uses zero extra processes. - Unmatched Memory Efficiency: By avoiding async runtimes (
tokio) and client libraries, we keep our daemon's memory footprint at only ~1.0 MB of RAM (Resident Set Size / RSS), with a systemd-reported memory usage of 92 KB. A typicaltokio-based daemon requires 10–15 MB just to maintain runtime threads. - Ultra-Compact Binary Size: By using blocking standard library features and keeping dependencies minimal, the compiled static binary size remains under 600 KiB (compared to 5–8 MB for async equivalents).
- Stall Protection: Equipped with 5-second socket timeouts for connection, read, and write operations, making it impossible for the query to hang or block the daemon indefinitely.
- Dual Export: Supports Prometheus scraping (
/metricsendpoint) and periodic OpenTelemetry (OTLP HTTP JSON) metrics push. - Active Power Calculations: Automatically calculates active power usage from current load percentage and nominal real power rating.
- Financial Cost Forecasting: Forecasts hourly, daily, and yearly energy costs in USD based on a configurable kWh rate.
- Extremely Lightweight: Compiled as a static Rust binary with minimal resource footprint (ideal for low-power devices like Raspberry Pi Zeros).
- Systemd Ready: Includes a pre-configured service template and a Makefile for easy installation.
- Grafana Dashboard: Includes a pre-configured dashboard located in
grafana/ups-exporter-dashboard.json.
| Metric Name | Description | Labels |
|---|---|---|
ups_battery_charge_percent |
UPS battery charge percentage | ups |
ups_battery_runtime_seconds |
UPS battery remaining runtime in seconds | ups |
ups_battery_voltage_volts |
UPS battery voltage | ups |
ups_input_voltage_volts |
UPS input line voltage | ups |
ups_output_voltage_volts |
UPS output line voltage | ups |
ups_load_percent |
UPS load percentage | ups |
ups_realpower_nominal_watts |
UPS nominal real power rating | ups |
ups_power_active_watts |
Calculated active power usage of the load | ups |
ups_cost_hourly_USD |
Calculated hourly electricity cost | ups |
ups_cost_daily_USD |
Calculated daily electricity cost | ups |
ups_cost_yearly_USD |
Calculated yearly electricity cost | ups |
- Network UPS Tools (
nut) daemon (upsd) and drivers installed and configured on the server (running on port3493). - Rust Toolchain (if compiling from source).
On Debian/Ubuntu-based systems, you can install and configure NUT as follows:
-
Install NUT packages:
sudo apt update sudo apt install nut
-
Configure the driver for your USB UPS: Add the driver configuration to
/etc/nut/ups.conf. For USB-based CyberPower or other USB HID UPS systems, use theusbhid-upsdriver:[cyberpower] driver = usbhid-ups port = auto desc = "Cyber Power System PR1500LCDRT2U"
-
Configure the NUT mode: Edit
/etc/nut/nut.confand set the mode tostandalone:MODE=standalone -
Start the services:
sudo upsdrvctl start sudo systemctl restart nut-server nut-client
-
Verify the installation: Verify that the
upsddaemon is responding on port3493using Python or Netcat:python3 -c "import socket; s = socket.socket(); s.connect(('127.0.0.1', 3493)); s.sendall(b'LIST VAR cyberpower\n'); print(s.recv(1024).decode())"
make build
sudo make installsudo make setup-serviceThe exporter is configured via /etc/default/ups-exporter.
To modify options such as the Prometheus port, the target UPS name, the kWh rate, or OTLP endpoints:
- Edit
/etc/default/ups-exporter:
# /etc/default/ups-exporter
UPS_EXPORTER_OPTS="-prom-port 9102 -ups-name cyberpower -ups-label gamer -kwh-rate 0.15 -otlp-endpoint http://localhost:4318/v1/metrics"- Restart the service:
sudo systemctl restart ups-exporterThe following flags are available on the ups-exporter binary:
-prom-port <port>: Port for Prometheus metrics (default:9102)-enable-prom <true|false>: Enable/disable Prometheus scraper endpoint (default:true)-otlp-endpoint <url>: OTLP HTTP receiver endpoint (e.g.http://localhost:4318/v1/metrics)-ups-name <name>: Name of the UPS to query via upsc (default:cyberpower)-ups-label <label>: Label name for the UPS device in metrics (default:gamer)-kwh-rate <rate>: USD electricity rate per kWh (default:0.15)-interval <secs>: OTLP metric push interval in seconds (default:15)-debug: Enable verbose debug logging-h, --help: Print the help menu
To scrape metrics directly from the exporter, add the following job to your prometheus.yml:
scrape_configs:
- job_name: 'ups-exporter'
static_configs:
- targets: ['localhost:9102']
scrape_interval: 15sIf you prefer pushing metrics to an OTel Collector, configure the OTLP flag in /etc/default/ups-exporter:
UPS_EXPORTER_OPTS="-otlp-endpoint http://my-otel-collector:4318/v1/metrics -interval 15"Apache License 2.0