Experimental Prometheus support (#1943)

* Experimental Prometheus support
Refactors text response to accept responseType
Generates a plaintext output in prometheus format using standard units and factors
Only called if and when endpoint (/metrics) is accessed
Exceptions are reported in the text output
This tries to minimize changes and uses no extra dependencies

* Changed strings to interpolation and raw literals to improve readability

* Fixed a typo

* Adding the Async HttpServer from Master

* Added back the http headers to SendResponseAsync

* Adding no-cache to sensor and prometheus
This commit is contained in:
Cristobal E.
2025-10-19 13:36:41 +02:00
committed by GitHub
parent 7063f5e0ae
commit 1ce022562f
+95 -18
View File
@@ -370,6 +370,12 @@ public class HttpServer
return;
}
if (requestedFile == "metrics")
{
await SendPrometheusAsync(context.Response, request);
return;
}
if (requestedFile.Contains("images_icon"))
{
await ServeResourceImageAsync(context.Response, requestedFile.Replace("images_icon/", string.Empty));
@@ -563,31 +569,102 @@ public class HttpServer
response.Close();
}
private string GeneratePrometheusResponse(Node node)
{
string responseStr = "";
string lastTagName = "";
/// Dictionary to convert all data to base units for OpenMetrics
/// suffix, 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, ("rpms", 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", 1000) }, //originally KB
{ 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 prometheusHost = node.Parent.Text;
foreach (SensorNode sensor in node.Nodes[i].Nodes)
{
string tagHardware = (((HardwareNode)node).Hardware.Parent != null ? ((HardwareNode)node).Hardware.Parent.HardwareType.ToString() : ((HardwareNode)node).Hardware.HardwareType.ToString());
string tagSensorType = sensor.Sensor.SensorType.ToString();
string tagSensorUnits = (units[sensor.Sensor.SensorType].Item1.Length == 0 ? "" : "_" + units[sensor.Sensor.SensorType].Item1);
string tagName = $"lhm_{tagHardware}_{tagSensorType}{tagSensorUnits}";
tagName = tagName.ToLower();
string valueSensor = sensor.Text.Replace("#", "");
string valueHardware = node.Text;
string valueId = sensor.Sensor.Identifier.ToString().Split('/').Last();
string valueFamily = node.Nodes[i].Text;
string valueHost = _root.Text;
try
{
if (lastTagName != tagName)
{
responseStr += $"# TYPE {tagName} gauge\n";
lastTagName = tagName;
}
double? tagValue = units[sensor.Sensor.SensorType].Item2 * sensor.Sensor.Value;
responseStr += $$"""{{tagName}} {"sensor"="{{valueSensor}}" "hardware"="{{valueHardware}}" "id"="{{valueId}}" "family"="{{valueFamily}}" "host"="{{valueHost}}"} {{tagValue}}""" + "\n";
}
catch (Exception)
{
responseStr += $"# HELP {lastTagName} This Sensor type is not defined in the prometheus adapter [{sensor.Sensor.SensorType}]\n";
responseStr += $$"""{{tagName}} {"sensor"="{{valueSensor}}" "hardware"="{{valueHardware}}" "id"="{{valueId}}" "family"="{{valueFamily}}" "host"="{{valueHost}}"} {{sensor.Sensor.Value}}""" + "\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
string responseContent = System.Text.Json.JsonSerializer.Serialize(sensorData);
byte[] buffer = Encoding.UTF8.GetBytes(responseContent);
// Add headers and set content type
response.AddHeader("Cache-Control", "no-cache");
response.AddHeader("Access-Control-Allow-Origin", "*");
response.ContentType = "application/json";
// Write the response content to the output stream
try
{
response.ContentLength64 = buffer.Length;
await response.OutputStream.WriteAsync(buffer, 0, buffer.Length);
response.OutputStream.Close();
}
catch (HttpListenerException)
{ }
// Close the response
response.Close();
await SendResponseAsync(response, responseContent, "application/json");
}
private Dictionary<string, object> GenerateJsonForNode(Node n, ref int nodeIndex)
{
Dictionary<string, object> jsonNode = new()