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>
This commit is contained in:
Cristobal E.
2025-12-08 17:07:47 +01:00
committed by GitHub
co-authored by PhyxionNL
parent fc15d76600
commit 2ee84226ce
+138 -1
View File
@@ -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<string, object>();
@@ -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, (string, double)>
{
{ 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<string, object> 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<string, object> GenerateJsonForNode(Node n, ref int nodeIndex)
{
Dictionary<string, object> jsonNode = new()