Fix prometheus functionality

This commit is contained in:
2026-05-02 23:12:40 +02:00
parent 897ae5dfb7
commit a74934c439
+117 -79
View File
@@ -1,40 +1,86 @@
import argparse
import time import time
from typing import Any
import prometheus_client import prometheus_client
from prometheus_client import CollectorRegistry, Gauge, Info
from bmspy import client
from bmspy.classes import UPS, BMSScalarField, BMSMultiField, BMSInfoField
from bmspy.utilities import debugger from bmspy.utilities import debugger
from bmspy.server import collect_data
DAEMON_UPDATE_PERIOD = 30
def prometheus_export(daemonize=True, filename=None): def prometheus_create_metric(
global debug registry: CollectorRegistry, ups_data: dict[str, UPS]
if not can_export_prometheus: ) -> dict[str, Any]:
raise ModuleNotFoundError( """Create one Gauge per scalar/multi field and one Info per info field; skip duplicates."""
"Unable to export to Prometheus. Is prometheus-client installed?" metric: dict[str, Any] = {}
) for ups_name, ups_obj in ups_data.items():
for name, field in ups_obj.items():
if name in metric:
continue
helpmsg = field.get("help") or ""
units = field.get("units")
if units:
helpmsg = "{} ({})".format(helpmsg, units)
if isinstance(field, BMSScalarField):
metric[name] = Gauge(name, helpmsg, ["ups"], registry=registry)
elif isinstance(field, BMSMultiField):
label = field.label
metric[name] = Gauge(
name, helpmsg, ["ups", label], registry=registry
)
elif isinstance(field, BMSInfoField):
metric[name] = Info(name, helpmsg, ["ups"], registry=registry)
return metric
data = dict()
# Initialize data structure, to fill in help values
while bool(data) is False:
data = collect_data()
time.sleep(1)
registry = prometheus_client.CollectorRegistry(auto_describe=True) def prometheus_populate_metric(
# Set up the metric data structure for Prometheus metric: dict[str, Any], ups_data: dict[str, UPS]
metric = prometheus_create_metric(registry, data) ) -> None:
# Populate the metric data structure this period """Populate metric values from UPS data using isinstance checks on field types."""
prometheus_populate_metric(metric, data) for ups_name, ups_obj in ups_data.items():
for name, field in ups_obj.items():
if name not in metric:
continue
if isinstance(field, BMSScalarField):
metric[name].labels(ups=ups_name).set(field.raw_value)
elif isinstance(field, BMSMultiField):
label = field.label
for idx, value in field.raw_values.items():
metric[name].labels(**{"ups": ups_name, label: str(idx)}).set(value)
elif isinstance(field, BMSInfoField):
metric[name].labels(ups=ups_name).info({name: field.info})
def prometheus_export(
daemonize: bool = True,
filename: str | None = None,
socket_path: str = "/run/bmspy/bms",
ups: str | None = None,
debug: int = 0,
) -> bool | None:
"""Export BMS data to Prometheus; daemonize or write to textfile."""
ups_data: dict[str, UPS] = {}
while not ups_data:
ups_data = client.read_data(socket_path, "prometheus", ups=ups, debug=debug)
if not ups_data:
time.sleep(1)
registry = CollectorRegistry(auto_describe=True)
metric = prometheus_create_metric(registry, ups_data)
prometheus_populate_metric(metric, ups_data)
if daemonize: if daemonize:
prometheus_client.start_http_server(9999, registry=registry) prometheus_client.start_http_server(9999, registry=registry)
while True: # pragma: no cover
while True:
# Delay, collect new data, and start again
time.sleep(DAEMON_UPDATE_PERIOD) time.sleep(DAEMON_UPDATE_PERIOD)
# Reset data, so it is re-populated correctly ups_data = {}
data = dict() while not ups_data:
while bool(data) is False: ups_data = client.read_data(socket_path, "prometheus", ups=ups, debug=debug)
data = collect_data() prometheus_populate_metric(metric, ups_data)
time.sleep(1)
prometheus_populate_metric(metric, data)
prometheus_client.generate_latest(registry) prometheus_client.generate_latest(registry)
else: else:
if filename is None: if filename is None:
@@ -44,61 +90,53 @@ def prometheus_export(daemonize=True, filename=None):
return True return True
def prometheus_create_metric(registry, data): def main() -> None:
metric = dict() """Entry point for bmspy-prometheus command."""
for name, contains in data.items(): parser = argparse.ArgumentParser(
helpmsg = "" description="Query JBD BMS and report status to Prometheus",
if contains.get("help") is not None: add_help=True,
helpmsg = contains.get("help") )
if contains.get("units"): parser.add_argument(
helpmsg += " (" + contains.get("units") + ")" "--socket",
if contains.get("value") is not None: "-s",
metric[name] = prometheus_client.Gauge(name, helpmsg, registry=registry) dest="socket",
# Has multiple values, each a different label action="store",
elif contains.get("values") is not None: default="/run/bmspy/bms",
if contains.get("label") is None: help="Socket to communicate with daemon",
debugger("ERROR: no label for {0} specified".format(name)) )
label = contains.get("label") parser.add_argument(
metric[name] = prometheus_client.Gauge( "--ups",
name, helpmsg, [label], registry=registry dest="ups",
) action="store",
elif contains.get("info") is not None: default=None,
metric[name] = prometheus_client.Info(name, helpmsg, registry=registry) help="Only export data for this UPS name (default: all)",
else: )
pass parser.add_argument(
return metric "--file",
"-f",
dest="filename",
type=str,
action="store",
default=None,
help="Write Prometheus textfile to this path (non-daemonized)",
)
parser.add_argument(
"--verbose",
"-v",
action="count",
default=0,
help="Print more verbose information (can be specified multiple times)",
)
args = parser.parse_args()
prometheus_export(
daemonize=args.filename is None,
filename=args.filename,
socket_path=args.socket,
ups=args.ups,
debug=args.verbose,
)
def prometheus_populate_metric(metric, data):
for name, contains in data.items():
if contains.get("value") is not None:
value = contains.get("value")
metric[name].set(value)
# doesn't have a value, but has [1-4]:
if contains.get("values") is not None and isinstance(
contains.get("values"), dict
):
for idx, label_value in contains.get("values").items():
metric[name].labels(idx).set(label_value)
if contains.get("info"):
value = contains.get("info")
metric[name].info({name: value})
else:
pass
# TODO fork bms daemon if need be?
def main():
debugger("TODO. At present, run from bmspy directly.")
# influxdb_export(bucket=args.influx_bucket, \
# url=args.influx_url, \
# org=args.influx_org, \
# token=args.influx_token, \
# daemonize=True)
if __name__ == "__main__": if __name__ == "__main__":
main() main()