Skip to content
This repository was archived by the owner on Feb 19, 2024. It is now read-only.

Commit db4dad8

Browse files
authored
feat: Provide support for optional timestamp & labels (#19)
Signed-off-by: Tom Kerkhove <[email protected]>
1 parent 0894bf2 commit db4dad8

File tree

5 files changed

+217
-16
lines changed

5 files changed

+217
-16
lines changed

src/Promitor.Parsers.Prometheus.Core/PrometheusMetricsParser.cs

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System;
44
using System.Collections.Generic;
55
using System.IO;
6+
using System.Linq;
67
using System.Text.RegularExpressions;
78
using System.Threading.Tasks;
89

@@ -11,7 +12,7 @@ namespace Promitor.Parsers.Prometheus.Core
1112
public class PrometheusMetricsParser
1213
{
1314
const string MetricInfoRegex = @"# (\w+) (\w*) (.*)";
14-
const string MeasurementRegex = @"(.+){(.*)} ((?:-?\d+(?:\.\d*)*)*(?:NaN)*) (\d*)";
15+
const string MeasurementRegex = "([^{\\ ]+)({.+})* ((?:-?\\d+(?:\\.\\d*)*)*(?:NaN)*)+ *(\\d*)*";
1516

1617
public static async Task<List<IMetric>> ParseAsync(Stream rawMetricsStream)
1718
{
@@ -119,7 +120,7 @@ private static GaugeMeasurement ParseMeasurement(string line)
119120
var regexOutcome = Regex.Match(line, MeasurementRegex);
120121
if (regexOutcome.Success == false)
121122
{
122-
throw new Exception($"Measurement doesn't follow the required Regex statement ({MeasurementRegex})");
123+
throw new Exception($"Measurement doesn't follow the required Regex statement '{MeasurementRegex}' for entry '{line}'");
123124
}
124125

125126
// Assign value
@@ -136,31 +137,43 @@ private static GaugeMeasurement ParseMeasurement(string line)
136137

137138
private static double ParseMetricValue(Match regexOutcome)
138139
{
139-
if (regexOutcome.Groups.Count < 4 || regexOutcome.Groups[3].Value == null)
140+
var rawMetricValue = regexOutcome.Groups[3].Captures.FirstOrDefault()?.Value;
141+
if (regexOutcome.Groups.Count < 4 || string.IsNullOrWhiteSpace(rawMetricValue))
140142
{
141143
throw new Exception("No metric value was found");
142144
}
143-
144-
return double.Parse(regexOutcome.Groups[3].Value);
145+
146+
return double.Parse(rawMetricValue);
145147
}
146148

147149
private static DateTimeOffset? ParseMetricTimestamp(Match regexOutcome)
148-
{
149-
if (regexOutcome.Groups.Count < 5 || regexOutcome.Groups[4].Value == null)
150+
{
151+
var rawUnixTimeInSeconds = regexOutcome.Groups[4].Captures.FirstOrDefault()?.Value;
152+
if (regexOutcome.Groups.Count < 5 || string.IsNullOrWhiteSpace(rawUnixTimeInSeconds))
150153
{
151154
return null;
152155
}
153156

154-
var unixTimeInSeconds = long.Parse(regexOutcome.Groups[4].Value);
157+
var unixTimeInSeconds = long.Parse(rawUnixTimeInSeconds);
155158
return DateTimeOffset.FromUnixTimeMilliseconds(unixTimeInSeconds);
156159
}
157160

158161
private static void ParseMetricLabels(Match regexOutcome, GaugeMeasurement measurement)
159162
{
160-
var labels = regexOutcome.Groups[2].Value;
163+
var rawLabels = regexOutcome.Groups[2].Value;
161164

165+
// When there are no labels, return
166+
if (string.IsNullOrWhiteSpace(rawLabels))
167+
{
168+
return;
169+
}
170+
171+
// Our capture group includes the leading { and trailing }, so we have to remove it
172+
rawLabels = rawLabels.Remove(0, 1);
173+
rawLabels = rawLabels.Remove(rawLabels.Length - 1);
174+
162175
// Get every individual raw label
163-
foreach (var rawLabel in labels.Split(','))
176+
foreach (var rawLabel in rawLabels.Split(','))
164177
{
165178
// Split label into information
166179
var splitLabelInfo = rawLabel.Split('=');

src/Promitor.Parsers.Prometheus.Tests/PrometheusMetricsParserTests.cs

Lines changed: 158 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
using Promitor.Parsers.Prometheus.Core;
1+
using System;
2+
using Promitor.Parsers.Prometheus.Core;
23
using Promitor.Parsers.Prometheus.Core.Models;
34
using Promitor.Parsers.Prometheus.Core.Models.Interfaces;
45
using System.Collections.Generic;
56
using System.ComponentModel;
67
using System.IO;
8+
using System.Linq;
79
using System.Threading.Tasks;
810
using Xunit;
911

@@ -12,11 +14,132 @@ namespace Promitor.Parsers.Prometheus.Tests
1214
[Category("Unit")]
1315
public class PrometheusMetricsParserTests
1416
{
17+
[Theory]
18+
[InlineData(52977208)]
19+
[InlineData(153.1351)]
20+
[InlineData(-52977208)]
21+
[InlineData(-153.1351)]
22+
[InlineData(-1)]
23+
[InlineData(double.NaN)]
24+
public async Task Parse_RawMetricWithTimestampAndLabels_ReturnCorrectInfo(double metricValue)
25+
{
26+
// Arrange
27+
var metricName = "azure_container_registry_total_pull_count_discovered";
28+
var metricDescription = "Amount of images that were pulled from the container registry";
29+
var resourceGroupName = "promitor";
30+
var subscriptionId = "0f9d7fea-99e8-4768-8672-06a28514f77e";
31+
var resourceUri = "subscriptions/0f9d7fea-99e8-4768-8672-06a28514f77e/resourceGroups/promitor/providers/Microsoft.ContainerRegistry/registries/promitor";
32+
var instanceName = "promitor";
33+
var timestamp = DateTimeOffset.UtcNow;
34+
var rawMetric = $@"# HELP {metricName} {metricDescription}
35+
# TYPE {metricName} gauge
36+
{metricName}{{resource_group=""{resourceGroupName}"",subscription_id=""{subscriptionId}"",resource_uri=""{resourceUri}"",instance_name=""{instanceName}""}} {metricValue} {timestamp.ToUnixTimeMilliseconds()}";
37+
var rawMetricsStream = GenerateStream(rawMetric);
38+
39+
// Act
40+
var metrics = await PrometheusMetricsParser.ParseAsync(rawMetricsStream);
41+
42+
// Assert
43+
Assert.True(rawMetricsStream.CanRead);
44+
Assert.True(rawMetricsStream.CanSeek);
45+
Assert.NotNull(metrics);
46+
Assert.Single(metrics);
47+
var testMetric = metrics.FirstOrDefault();
48+
Assert.NotNull(testMetric);
49+
Assert.Equal(metricName, testMetric.Name);
50+
Assert.Equal(metricDescription, testMetric.Description);
51+
Assert.Equal(MetricTypes.Gauge, testMetric.Type);
52+
var testGauge = testMetric as Gauge;
53+
Assert.Single(testGauge.Measurements);
54+
var testMeasurement = testGauge.Measurements.First();
55+
Assert.Equal(metricValue, testMeasurement.Value);
56+
Assert.Equal(timestamp.ToString("yyyy-MM-ddTHH:mm:ss.zzz"), testMeasurement.Timestamp?.ToString("yyyy-MM-ddTHH:mm:ss.zzz"));
57+
Assert.NotNull(testMeasurement.Labels);
58+
Assert.Equal(4, testMeasurement.Labels.Count);
59+
Assert.True(testMeasurement.Labels.ContainsKey("resource_group"));
60+
Assert.True(testMeasurement.Labels.ContainsKey("subscription_id"));
61+
Assert.True(testMeasurement.Labels.ContainsKey("resource_uri"));
62+
Assert.True(testMeasurement.Labels.ContainsKey("instance_name"));
63+
Assert.Equal(resourceGroupName, testMeasurement.Labels["resource_group"]);
64+
Assert.Equal(subscriptionId, testMeasurement.Labels["subscription_id"]);
65+
Assert.Equal(resourceUri, testMeasurement.Labels["resource_uri"]);
66+
Assert.Equal(instanceName, testMeasurement.Labels["instance_name"]);
67+
}
68+
69+
[Fact]
70+
public async Task Parse_RawMetricWithTimestampButWithoutLabels_ReturnCorrectInfo()
71+
{
72+
// Arrange
73+
var metricName = "promitor_runtime_dotnet_totalmemory";
74+
var metricDescription = "Total known allocated memory";
75+
double metricValue = 52977208;
76+
var timestamp = DateTimeOffset.UtcNow;
77+
var rawMetric = $@"# HELP {metricName} {metricDescription}
78+
# TYPE {metricName} gauge
79+
{metricName} {metricValue} {timestamp.ToUnixTimeMilliseconds()}";
80+
var rawMetricsStream = GenerateStream(rawMetric);
81+
82+
// Act
83+
var metrics = await PrometheusMetricsParser.ParseAsync(rawMetricsStream);
84+
85+
// Assert
86+
Assert.True(rawMetricsStream.CanRead);
87+
Assert.True(rawMetricsStream.CanSeek);
88+
Assert.NotNull(metrics);
89+
Assert.Single(metrics);
90+
var testMetric = metrics.FirstOrDefault();
91+
Assert.NotNull(testMetric);
92+
Assert.Equal(metricName, testMetric.Name);
93+
Assert.Equal(metricDescription, testMetric.Description);
94+
Assert.Equal(MetricTypes.Gauge, testMetric.Type);
95+
var testGauge = testMetric as Gauge;
96+
Assert.Single(testGauge.Measurements);
97+
var testMeasurement = testGauge.Measurements.First();
98+
Assert.Equal(metricValue, testMeasurement.Value);
99+
Assert.Equal(timestamp.ToString("yyyy-MM-ddTHH:mm:ss.zzz"), testMeasurement.Timestamp?.ToString("yyyy-MM-ddTHH:mm:ss.zzz"));
100+
Assert.NotNull(testMeasurement.Labels);
101+
Assert.Empty(testMeasurement.Labels);
102+
}
103+
104+
[Fact]
105+
public async Task Parse_RawMetricWithoutLabelsAndTimestamp_ReturnCorrectInfo()
106+
{
107+
// Arrange
108+
var metricName = "promitor_runtime_dotnet_totalmemory";
109+
var metricDescription = "Total known allocated memory";
110+
double metricValue = 52977208;
111+
var rawMetric = $@"# HELP {metricName} {metricDescription}
112+
# TYPE {metricName} gauge
113+
{metricName} {metricValue}";
114+
var rawMetricsStream = GenerateStream(rawMetric);
115+
116+
// Act
117+
var metrics = await PrometheusMetricsParser.ParseAsync(rawMetricsStream);
118+
119+
// Assert
120+
Assert.True(rawMetricsStream.CanRead);
121+
Assert.True(rawMetricsStream.CanSeek);
122+
Assert.NotNull(metrics);
123+
Assert.Single(metrics);
124+
var testMetric = metrics.FirstOrDefault();
125+
Assert.NotNull(testMetric);
126+
Assert.Equal(metricName, testMetric.Name);
127+
Assert.Equal(metricDescription, testMetric.Description);
128+
Assert.Equal(MetricTypes.Gauge, testMetric.Type);
129+
var testGauge = testMetric as Gauge;
130+
Assert.Single(testGauge.Measurements);
131+
var testMeasurement = testGauge.Measurements.First();
132+
Assert.Null(testMeasurement.Timestamp);
133+
Assert.Equal(metricValue, testMeasurement.Value);
134+
Assert.NotNull(testMeasurement.Labels);
135+
Assert.Empty(testMeasurement.Labels);
136+
}
137+
15138
[Fact]
16-
public async Task Parse_ValidInput_ReturnsMetrics()
139+
public async Task Parse_ValidInputWithLabels_ReturnsMetrics()
17140
{
18141
// Arrange
19-
var filePath = Path.Combine(Directory.GetCurrentDirectory(), "Samples", "raw-metrics.txt");
142+
var filePath = Path.Combine(Directory.GetCurrentDirectory(), "Samples", "raw-metrics-with-labels.txt");
20143
var rawMetricsStream = File.OpenRead(filePath);
21144

22145
// Act
@@ -29,6 +152,22 @@ public async Task Parse_ValidInput_ReturnsMetrics()
29152
AssertAzureContainerRegistryPullCount(metrics);
30153
}
31154

155+
[Fact]
156+
public async Task Parse_ValidInputWithoutLabels_ReturnsMetrics()
157+
{
158+
// Arrange
159+
var filePath = Path.Combine(Directory.GetCurrentDirectory(), "Samples", "raw-metrics-without-labels.txt");
160+
var rawMetricsStream = File.OpenRead(filePath);
161+
162+
// Act
163+
var metrics = await PrometheusMetricsParser.ParseAsync(rawMetricsStream);
164+
165+
// Assert
166+
Assert.True(rawMetricsStream.CanRead);
167+
Assert.True(rawMetricsStream.CanSeek);
168+
Assert.NotNull(metrics);
169+
}
170+
32171
private void AssertAzureContainerRegistryPullCount(List<IMetric> metrics)
33172
{
34173
var acrMetric = metrics.Find(f => f.Name == "azure_container_registry_total_pull_count_discovered");
@@ -37,6 +176,22 @@ private void AssertAzureContainerRegistryPullCount(List<IMetric> metrics)
37176
Assert.Equal("Amount of images that were pulled from the container registry", acrMetric.Description);
38177
var acrGauge = acrMetric as Gauge;
39178
Assert.Equal(2, acrGauge.Measurements.Count);
179+
var firstMeasurement = acrGauge.Measurements.First();
180+
Assert.NotNull(firstMeasurement.Labels);
181+
Assert.Equal(4, firstMeasurement.Labels.Count);
182+
Assert.True(firstMeasurement.Labels.ContainsKey("resource_group"));
183+
Assert.True(firstMeasurement.Labels.ContainsKey("subscription_id"));
184+
Assert.True(firstMeasurement.Labels.ContainsKey("resource_uri"));
185+
Assert.True(firstMeasurement.Labels.ContainsKey("instance_name"));
186+
}
187+
public static Stream GenerateStream(string s)
188+
{
189+
var stream = new MemoryStream();
190+
var writer = new StreamWriter(stream);
191+
writer.Write(s);
192+
writer.Flush();
193+
stream.Position = 0;
194+
return stream;
40195
}
41196
}
42197
}

src/Promitor.Parsers.Prometheus.Tests/Promitor.Parsers.Prometheus.Tests.csproj

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,15 @@
66
</PropertyGroup>
77

88
<ItemGroup>
9-
<None Remove="Samples\raw-metrics.txt" />
9+
<None Remove="Samples\raw-metrics-with-labels.txt" />
10+
<None Remove="Samples\raw-metrics-without-labels.txt" />
1011
</ItemGroup>
1112

1213
<ItemGroup>
13-
<Content Include="Samples\raw-metrics.txt">
14+
<Content Include="Samples\raw-metrics-without-labels.txt">
15+
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
16+
</Content>
17+
<Content Include="Samples\raw-metrics-with-labels.txt">
1418
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
1519
</Content>
1620
</ItemGroup>

src/Promitor.Parsers.Prometheus.Tests/Samples/raw-metrics.txt renamed to src/Promitor.Parsers.Prometheus.Tests/Samples/raw-metrics-with-labels.txt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,4 +182,9 @@ azure_storage_account_capacity_discovery{tenant_id="c8819874-9e56-4e3f-b1a8-1c03
182182
azure_storage_account_capacity_discovery{tenant_id="c8819874-9e56-4e3f-b1a8-1c0325138f27",subscription_id="0f9d7fea-99e8-4768-8672-06a28514f77e",resource_uri="subscriptions/0f9d7fea-99e8-4768-8672-06a28514f77e/resourceGroups/promitor/providers/Microsoft.Storage/storageAccounts/sqlvah2u4xyvba5u4i",resource_group="promitor",instance_name="sqlvah2u4xyvba5u4i",geo="china",environment="dev"} 15984560 1626853387707
183183
azure_storage_account_capacity_discovery{tenant_id="c8819874-9e56-4e3f-b1a8-1c0325138f27",subscription_id="0f9d7fea-99e8-4768-8672-06a28514f77e",resource_uri="subscriptions/0f9d7fea-99e8-4768-8672-06a28514f77e/resourceGroups/azure-deprecation/providers/Microsoft.Storage/storageAccounts/storageaccountazure8896",resource_group="azure-deprecation",instance_name="storageaccountazure8896",geo="china",environment="dev"} 6604977 1626853387756
184184
azure_storage_account_capacity_discovery{tenant_id="c8819874-9e56-4e3f-b1a8-1c0325138f27",subscription_id="0f9d7fea-99e8-4768-8672-06a28514f77e",resource_uri="subscriptions/0f9d7fea-99e8-4768-8672-06a28514f77e/resourceGroups/promitor-sources/providers/Microsoft.Storage/storageAccounts/promitordatalake",resource_group="promitor-sources",instance_name="promitordatalake",geo="china",environment="dev"} 6768488 1626853388206
185-
azure_storage_account_capacity_discovery{tenant_id="c8819874-9e56-4e3f-b1a8-1c0325138f27",subscription_id="0f9d7fea-99e8-4768-8672-06a28514f77e",resource_uri="subscriptions/0f9d7fea-99e8-4768-8672-06a28514f77e/resourceGroups/azure-deprecation/providers/Microsoft.Storage/storageAccounts/storageaccountazurea0a1",resource_group="azure-deprecation",instance_name="storageaccountazurea0a1",geo="china",environment="dev"} NaN 1626853387766
185+
azure_storage_account_capacity_discovery{tenant_id="c8819874-9e56-4e3f-b1a8-1c0325138f27",subscription_id="0f9d7fea-99e8-4768-8672-06a28514f77e",resource_uri="subscriptions/0f9d7fea-99e8-4768-8672-06a28514f77e/resourceGroups/azure-deprecation/providers/Microsoft.Storage/storageAccounts/storageaccountazurea0a1",resource_group="azure-deprecation",instance_name="storageaccountazurea0a1",geo="china",environment="dev"} NaN 1626853387766
186+
# HELP promitor_runtime_dotnet_collection_count_total GC collection count
187+
# TYPE promitor_runtime_dotnet_collection_count_total counter
188+
promitor_runtime_dotnet_collection_count_total{generation="0"} 1
189+
promitor_runtime_dotnet_collection_count_total{generation="1"} 0
190+
promitor_runtime_dotnet_collection_count_total{generation="2"} 0
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# HELP promitor_runtime_dotnet_totalmemory Total known allocated memory
2+
# TYPE promitor_runtime_dotnet_totalmemory gauge
3+
promitor_runtime_dotnet_totalmemory 52977208
4+
# HELP promitor_runtime_process_cpu_seconds_total Total user and system CPU time spent in seconds
5+
# TYPE promitor_runtime_process_cpu_seconds_total counter
6+
promitor_runtime_process_cpu_seconds_total 10.33
7+
# HELP promitor_runtime_process_virtual_bytes Process virtual memory size
8+
# TYPE promitor_runtime_process_virtual_bytes gauge
9+
promitor_runtime_process_virtual_bytes 18348056576
10+
# HELP promitor_runtime_process_working_set Process working set
11+
# TYPE promitor_runtime_process_working_set gauge
12+
promitor_runtime_process_working_set 217669632
13+
# HELP promitor_runtime_process_private_bytes Process private memory size
14+
# TYPE promitor_runtime_process_private_bytes gauge
15+
promitor_runtime_process_private_bytes 211578880
16+
# HELP promitor_runtime_process_num_threads Total number of threads
17+
# TYPE promitor_runtime_process_num_threads gauge
18+
promitor_runtime_process_num_threads 37
19+
# HELP promitor_runtime_process_processid Process ID
20+
# TYPE promitor_runtime_process_processid gauge
21+
promitor_runtime_process_processid 28
22+
# HELP promitor_runtime_process_start_time_seconds Start time of the process since unix epoch in seconds
23+
# TYPE promitor_runtime_process_start_time_seconds gauge
24+
promitor_runtime_process_start_time_seconds 1628486652

0 commit comments

Comments
 (0)