Prometheus .NET Serialization

Prometheus .NET Serialization

If you’ve ever used Prometheus or Prometheus-Net, you may have noticed that its Gauges use a JSON’ish implementation. And by “-ish” we mean that you cannot simply apply, System.Text.Json to deserialize the line. Here I’ll show you how to work with Prometheus to assist with your unit tests (aka automated tests, integration tests, etc). In this example, we’re going to extract the “Button Click” metrics sent by the app.

Prometheus is an open-source systems monitoring solution to help collect metrics and alerts in your application.

https://prometheus.io/docs/introduction/overview/

Below is an exert from the packet. Notice the key areas, “button_event_gauge“, the body{ ... }“, and the time-stamp, provided in UTC Unix Epoch time.

...
process_num_threads 44
# HELP device_event_gauge Number of times a device event occurs.
# TYPE device_event_gauge gauge
buton_event_gauge{some_name="ButtonClickedEvent",button_id="loginButton",app_page_name="User Login"} 1684439583.2730143
buton_event_gauge{some_name="ButtonClickedEvent",button_id="SignupButton",app_page_name="User Login"} 1684439565.46381
# HELP process_virtual_memory_bytes Virtual memory size in bytes.
# TYPE process_virtual_memory_bytes gauge
process_virtual_memory_bytes 28043919360
...

First, we’re going to pull back the Prometheus events. Next, parse the packet for our Gauge events. Then finally deserialize the events, converting the plain text into a C# object. In the example, we will use Reflection to match up our class’ property names to the Gauge’s information.
Let’s begin!

First, we will invoke, GetMetricsAsync(), which calls Prometheus directly. This is quicker than creating an HttpClient and accessing “http://localhost/metrics“.

public async Task<string?> GetMetricsAsync()
{
    var labels = Prometheus.Metrics.DefaultRegistry.StaticLabels;

    string? metrics;

    using (MemoryStream memory = new())
    {
        await Prometheus.Metrics.DefaultRegistry.CollectAndExportAsTextAsync(memory);
        memory.Position = 0;
        using (StreamReader reader = new(memory))
        {
            metrics = await reader.ReadToEndAsync();
        }
    }

    return metrics;
}

Next, we need to parse our packet, extracting only our Gauge events of interest (“buton_event_gauge“).

Notice the following items:

  • We’re using “.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.RemoveEmptyEntries);” instead of a RegEx, which is much slower.
/// <summary>Parse events from raw data.</summary>
/// <param name="gaugeName">Gauge name.</param>
/// <param name="rawEventData">Raw Prometheus data.</param>
/// <returns>Returns a collection of <see cref="DeviceEventGauge"/>.</returns>
public List<DeviceEvent> ParseGaugeEvents(string gaugeName, string? rawEventData)
////public List<T>? ParseGaugeEvents<T>(string gaugeName, string? rawEventData)
{
    if (rawEventData is null)
        return new();

    var events = new List<DeviceEvent>();

    // Parse gauge events from Prometheus data.
    var lines = rawEventData.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.RemoveEmptyEntries);

    foreach (var line in lines)
    {
        if (!line.StartsWith(gaugeName))
            continue;

        var json = ExtractPayload(line);

        var deviceEvent = Deserialize<DeviceEvent>(json);

        if (deviceEvent is null)
            continue;

        if (ExtractTime(line, out var eventEpoch, out var eventDttm))
        {
            deviceEvent.GaugeDttm = eventDttm;
            deviceEvent.GaugeEpochTime = eventEpoch ?? 0.0; // return ZERO if null
        }

        events.Add(deviceEvent);
    }

    var sortedList = events.OrderByDescending(o => o.GaugeEpochTime).ToList();

    return sortedList;
}