From 2ee84226cee6553a7fd7e4f7f5d2ec3f25c4aa21 Mon Sep 17 00:00:00 2001 From: "Cristobal E." Date: Mon, 8 Dec 2025 10:07:47 -0600 Subject: [PATCH] Prometheus bugfixes (#1982) * Bug fixes and Distinctive aliases Fixed an exception when a Type was not registered in the dictionary Added distinctive aliases for hardware and sensors to prevent confusion due to duplicated names Reviewed units and factors * Skip null values as per #1979 * On prometheus null value show full tagName if possible * Using Dictionaries to keep track of hardware and sensor's names * Fixing some logic errors. * Bugfix: Typo in message * Report up to 5 values per sensor. * Renamed variables for clarity Modified labels as requested * Removed unused variables Fixed error in dictionary key * Fixed a logic error * Removed remote debug * Replaced alilas logic for a simpler `name (id)` * Bugfix: Prometheus requires a colon here Specially in strict mode and in victoriametrics. * Forcing invariant locale conversion of numbers. * Minor Code formatting * Bug fixes and Distinctive aliases Fixed an exception when a Type was not registered in the dictionary Added distinctive aliases for hardware and sensors to prevent confusion due to duplicated names Reviewed units and factors * Skip null values as per #1979 * On prometheus null value show full tagName if possible * Using Dictionaries to keep track of hardware and sensor's names * Fixing some logic errors. * Bugfix: Typo in message * Report up to 5 values per sensor. * Renamed variables for clarity Modified labels as requested * Removed unused variables Fixed error in dictionary key * Fixed a logic error * Removed remote debug * Replaced alilas logic for a simpler `name (id)` * Bugfix: Prometheus requires a colon here Specially in strict mode and in victoriametrics. * Forcing invariant locale conversion of numbers. * Minor Code formatting * Misc --------- Co-authored-by: PhyxionNL <7643972+PhyxionNL@users.noreply.github.com> --- LibreHardwareMonitor/Utilities/HttpServer.cs | 139 ++++++++++++++++++- 1 file changed, 138 insertions(+), 1 deletion(-) diff --git a/LibreHardwareMonitor/Utilities/HttpServer.cs b/LibreHardwareMonitor/Utilities/HttpServer.cs index c7f8584..281a8f3 100644 --- a/LibreHardwareMonitor/Utilities/HttpServer.cs +++ b/LibreHardwareMonitor/Utilities/HttpServer.cs @@ -376,6 +376,12 @@ public class HttpServer return; } + if (requestedFile == "metrics") + { + await SendPrometheusAsync(context.Response, request); + return; + } + if (requestedFile.Contains("Sensor")) { var sensorResult = new Dictionary(); @@ -563,6 +569,137 @@ public class HttpServer response.Close(); } + private string GeneratePrometheusResponse(Node node) + { + const int MaxValues = 5; + + string responseStr = ""; + string lastTagName = ""; + + /// Dictionary to convert all data to base units for OpenMetrics + /// SensorType, Item1 suffix, Item2 factor + var units = new Dictionary + { + { SensorType.Clock, ("hertz", 1000000)}, //originally megahertz + { SensorType.Conductivity, ("seconds_per_centimeter", 0.000001) }, //originally microseconds per centimeter + { SensorType.Control, ("percent", 1) }, + { SensorType.Current, ("amperes", 1) }, + { SensorType.Data, ("bytes", 1000000000) }, //originally GB + { SensorType.Energy, ("watthour", 0.001) }, + { SensorType.Factor, ("", 1) }, + { SensorType.Fan, ("rpm", 1) }, + { SensorType.Flow, ("liters_per_hour", 1) }, + { SensorType.Frequency, ("hertz", 1) }, + { SensorType.Humidity, ("percent", 1) }, + { SensorType.Level, ("percent", 1) }, + { SensorType.Load, ("percent", 1) }, + { SensorType.Noise, ("decibels", 1) }, + { SensorType.Power, ("watts", 1) }, + { SensorType.SmallData, ("bytes", 1024*1024) }, //originally MiB + { SensorType.Temperature, ("celsius", 1) }, + { SensorType.Throughput, ("bytes_per_second", 1) }, + { SensorType.TimeSpan, ("seconds", 1) }, + { SensorType.Timing, ("seconds", 0.000000001 ) }, //originally nanoseconds + { SensorType.Voltage, ("volts", 1) }, + }; + + for (int i = 0; i < node.Nodes.Count; i++) + { + if (node.Nodes[i].GetType().Name == "HardwareNode") + { + responseStr += GeneratePrometheusResponse(node.Nodes[i]); + } + + if (node.Nodes[i].GetType().Name == "TypeNode") + { + string tagHardware = ""; + string valueHardwareName = ""; + string valueHardwareId = ((HardwareNode)node).Hardware.Identifier.ToString(); + + if (((HardwareNode)node).Hardware.Parent != null) + { + tagHardware = ((HardwareNode)node).Hardware.Parent.HardwareType.ToString(); + valueHardwareName = ((HardwareNode)node).Hardware.Parent.Name; + } + else + { + tagHardware = ((HardwareNode)node).Hardware.HardwareType.ToString(); + valueHardwareName = node.Text; + } + + string valueHardwareAlias = $"{valueHardwareName} ({valueHardwareId})"; + + foreach (SensorNode sensor in node.Nodes[i].Nodes) + { + string valueSensorName = sensor.Text.Replace("#", String.Empty); + + // Variables needed in dictionary lookup and error message + string tagSensorType = sensor.Sensor.SensorType.ToString(); + + double factor = 1; + string tagSensorUnits = ""; + + // Get factor and unit suffix from dictionary ... + if (units.ContainsKey(sensor.Sensor.SensorType)) + { + factor = units[sensor.Sensor.SensorType].Item2; + tagSensorUnits = (units[sensor.Sensor.SensorType].Item1.Length == 0 ? String.Empty : "_" + units[sensor.Sensor.SensorType].Item1); + } + // ... or print an error message + else + { + responseStr += $"# HELP {tagHardware}_{tagSensorType}:{valueSensorName} This Sensor type is not defined in the prometheus adapter [{sensor.Sensor.SensorType}]\n"; + } + + // Creating the tag name for prometheus + string tagName = $"lhm_{tagHardware}_{tagSensorType}{tagSensorUnits}"; + tagName = tagName.ToLower(); + + // Preparing the labels for all data and uniqueness + string valueSensorId = sensor.Sensor.Identifier.ToString().Substring(valueHardwareId.Length); + string valueSensorAlias = $"{valueSensorName} ({valueSensorId})"; + + string valueHost = _root.Text; + + // Creates the tag with labels + string tagLine = $$"""{{tagName}} {"sensorName"="{{valueSensorName}}", "sensorAlias"="{{valueSensorAlias}}", "hardwareName"="{{valueHardwareName}}", "hardwareAlias"="{{valueHardwareAlias}}", "sensorId"="{{valueSensorId}}", "hardwareId"="{{valueHardwareId}}", "host"="{{valueHost}}"}"""; + + // Generates the TYPE line if we changed tagnames + if (lastTagName != tagName) + { + responseStr += $"# TYPE {tagName} gauge\n"; + lastTagName = tagName; + } + + int counter = 0; + foreach (SensorValue val in sensor.Sensor.Values.Reverse()) + { + if (counter++ >= MaxValues) break; + if (float.IsNaN(val.Value)) + { + // Print a help line saying what tag had an invalid value + responseStr += $"# HELP {tagLine} has an invalid value and was skipped.\n"; + } + else + { + // Outputs prometheus tag with labels, value, and timestamp + responseStr += $"{tagLine} {(val.Value * factor).ToString(CultureInfo.InvariantCulture)} {((DateTimeOffset)val.Time).ToUnixTimeSeconds()}\n"; + } + } + } + } + } + return responseStr; + } + + private async Task SendPrometheusAsync(HttpListenerResponse response, HttpListenerRequest request = null) + { + string responseContent = GeneratePrometheusResponse(_root); + response.AddHeader("Cache-Control", "no-cache"); + response.AddHeader("Access-Control-Allow-Origin", "*"); + await SendResponseAsync(response, responseContent, "text/plain"); + } + private async Task SendJsonSensorAsync(HttpListenerResponse response, Dictionary sensorData) { // Convert the JObject to a JSON string @@ -571,7 +708,7 @@ public class HttpServer response.AddHeader("Access-Control-Allow-Origin", "*"); await SendResponseAsync(response, responseContent, "application/json"); } - + private Dictionary GenerateJsonForNode(Node n, ref int nodeIndex) { Dictionary jsonNode = new()