Skip to content

Commit b9d56aa

Browse files
robertcoltheartcijothomasCodeBlanchreyang
authored
[prometheus] Fix OpenMetrics format suffixes (#5646)
Co-authored-by: Cijo Thomas <[email protected]> Co-authored-by: Mikel Blanchard <[email protected]> Co-authored-by: Reiley Yang <[email protected]>
1 parent 81244d6 commit b9d56aa

File tree

8 files changed

+151
-45
lines changed

8 files changed

+151
-45
lines changed

src/OpenTelemetry.Exporter.Prometheus.AspNetCore/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
## Unreleased
44

5+
* Fixed issue with OpenMetrics suffixes for Prometheus
6+
([#5646](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5646))
7+
58
## 1.9.0-alpha.1
69

710
Released 2024-May-20

src/OpenTelemetry.Exporter.Prometheus.HttpListener/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
## Unreleased
44

5+
* Fixed issue with OpenMetrics suffixes for Prometheus
6+
([#5646](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5646))
7+
58
## 1.9.0-alpha.1
69

710
Released 2024-May-20

src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusMetric.cs

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public PrometheusMetric(string name, string unit, PrometheusType type, bool disa
2727
// consecutive `_` characters MUST be replaced with a single `_` character.
2828
// https://github.com/open-telemetry/opentelemetry-specification/blob/b2f923fb1650dde1f061507908b834035506a796/specification/compatibility/prometheus_and_openmetrics.md#L230-L233
2929
var sanitizedName = SanitizeMetricName(name);
30+
var openMetricsName = SanitizeOpenMetricsName(sanitizedName);
3031

3132
string sanitizedUnit = null;
3233
if (!string.IsNullOrEmpty(unit))
@@ -35,38 +36,50 @@ public PrometheusMetric(string name, string unit, PrometheusType type, bool disa
3536

3637
// The resulting unit SHOULD be added to the metric as
3738
// [OpenMetrics UNIT metadata](https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#metricfamily)
38-
// and as a suffix to the metric name unless the metric name already contains the
39-
// unit, or the unit MUST be omitted. The unit suffix comes before any
40-
// type-specific suffixes.
41-
// https://github.com/open-telemetry/opentelemetry-specification/blob/b2f923fb1650dde1f061507908b834035506a796/specification/compatibility/prometheus_and_openmetrics.md#L242-L246
42-
if (!sanitizedName.Contains(sanitizedUnit))
39+
// and as a suffix to the metric name. The unit suffix comes before any type-specific suffixes.
40+
// https://github.com/open-telemetry/opentelemetry-specification/blob/3dfb383fe583e3b74a2365c5a1d90256b273ee76/specification/compatibility/prometheus_and_openmetrics.md#metric-metadata-1
41+
if (!sanitizedName.EndsWith(sanitizedUnit))
4342
{
44-
sanitizedName = sanitizedName + "_" + sanitizedUnit;
43+
sanitizedName += $"_{sanitizedUnit}";
44+
openMetricsName += $"_{sanitizedUnit}";
4545
}
4646
}
4747

4848
// If the metric name for monotonic Sum metric points does not end in a suffix of `_total` a suffix of `_total` MUST be added by default, otherwise the name MUST remain unchanged.
4949
// Exporters SHOULD provide a configuration option to disable the addition of `_total` suffixes.
5050
// https://github.com/open-telemetry/opentelemetry-specification/blob/b2f923fb1650dde1f061507908b834035506a796/specification/compatibility/prometheus_and_openmetrics.md#L286
51+
// Note that we no longer append '_ratio' for units that are '1', see: https://github.com/open-telemetry/opentelemetry-specification/issues/4058
5152
if (type == PrometheusType.Counter && !sanitizedName.EndsWith("_total") && !disableTotalNameSuffixForCounters)
5253
{
5354
sanitizedName += "_total";
5455
}
5556

56-
// Special case: Converting "1" to "ratio".
57-
// https://github.com/open-telemetry/opentelemetry-specification/blob/b2f923fb1650dde1f061507908b834035506a796/specification/compatibility/prometheus_and_openmetrics.md#L239
58-
if (type == PrometheusType.Gauge && unit == "1" && !sanitizedName.Contains("ratio"))
57+
// For counters requested using OpenMetrics format, the MetricFamily name MUST be suffixed with '_total', regardless of the setting to disable the 'total' suffix.
58+
// https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#counter-1
59+
if (type == PrometheusType.Counter && !openMetricsName.EndsWith("_total"))
5960
{
60-
sanitizedName += "_ratio";
61+
openMetricsName += "_total";
6162
}
6263

64+
// In OpenMetrics format, the UNIT, TYPE and HELP metadata must be suffixed with the unit (handled above), and not the '_total' suffix, as in the case for counters.
65+
// https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#unit
66+
var openMetricsMetadataName = type == PrometheusType.Counter
67+
? SanitizeOpenMetricsName(openMetricsName)
68+
: sanitizedName;
69+
6370
this.Name = sanitizedName;
71+
this.OpenMetricsName = openMetricsName;
72+
this.OpenMetricsMetadataName = openMetricsMetadataName;
6473
this.Unit = sanitizedUnit;
6574
this.Type = type;
6675
}
6776

6877
public string Name { get; }
6978

79+
public string OpenMetricsName { get; }
80+
81+
public string OpenMetricsMetadataName { get; }
82+
7083
public string Unit { get; }
7184

7285
public PrometheusType Type { get; }
@@ -159,6 +172,16 @@ internal static string RemoveAnnotations(string unit)
159172
return sb.ToString();
160173
}
161174

175+
private static string SanitizeOpenMetricsName(string metricName)
176+
{
177+
if (metricName.EndsWith("_total"))
178+
{
179+
return metricName.Substring(0, metricName.Length - 6);
180+
}
181+
182+
return metricName;
183+
}
184+
162185
private static string GetUnit(string unit)
163186
{
164187
// Dropping the portions of the Unit within brackets (e.g. {packet}). Brackets MUST NOT be included in the resulting unit. A "count of foo" is considered unitless in Prometheus.

src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializer.cs

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -230,12 +230,29 @@ static string GetLabelValueString(object labelValue)
230230
}
231231

232232
[MethodImpl(MethodImplOptions.AggressiveInlining)]
233-
public static int WriteMetricName(byte[] buffer, int cursor, PrometheusMetric metric)
233+
public static int WriteMetricName(byte[] buffer, int cursor, PrometheusMetric metric, bool openMetricsRequested)
234234
{
235235
// Metric name has already been escaped.
236-
for (int i = 0; i < metric.Name.Length; i++)
236+
var name = openMetricsRequested ? metric.OpenMetricsName : metric.Name;
237+
238+
for (int i = 0; i < name.Length; i++)
239+
{
240+
var ordinal = (ushort)name[i];
241+
buffer[cursor++] = unchecked((byte)ordinal);
242+
}
243+
244+
return cursor;
245+
}
246+
247+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
248+
public static int WriteMetricMetadataName(byte[] buffer, int cursor, PrometheusMetric metric, bool openMetricsRequested)
249+
{
250+
// Metric name has already been escaped.
251+
var name = openMetricsRequested ? metric.OpenMetricsMetadataName : metric.Name;
252+
253+
for (int i = 0; i < name.Length; i++)
237254
{
238-
var ordinal = (ushort)metric.Name[i];
255+
var ordinal = (ushort)name[i];
239256
buffer[cursor++] = unchecked((byte)ordinal);
240257
}
241258

@@ -252,15 +269,15 @@ public static int WriteEof(byte[] buffer, int cursor)
252269
}
253270

254271
[MethodImpl(MethodImplOptions.AggressiveInlining)]
255-
public static int WriteHelpMetadata(byte[] buffer, int cursor, PrometheusMetric metric, string metricDescription)
272+
public static int WriteHelpMetadata(byte[] buffer, int cursor, PrometheusMetric metric, string metricDescription, bool openMetricsRequested)
256273
{
257274
if (string.IsNullOrEmpty(metricDescription))
258275
{
259276
return cursor;
260277
}
261278

262279
cursor = WriteAsciiStringNoEscape(buffer, cursor, "# HELP ");
263-
cursor = WriteMetricName(buffer, cursor, metric);
280+
cursor = WriteMetricMetadataName(buffer, cursor, metric, openMetricsRequested);
264281

265282
if (!string.IsNullOrEmpty(metricDescription))
266283
{
@@ -274,14 +291,14 @@ public static int WriteHelpMetadata(byte[] buffer, int cursor, PrometheusMetric
274291
}
275292

276293
[MethodImpl(MethodImplOptions.AggressiveInlining)]
277-
public static int WriteTypeMetadata(byte[] buffer, int cursor, PrometheusMetric metric)
294+
public static int WriteTypeMetadata(byte[] buffer, int cursor, PrometheusMetric metric, bool openMetricsRequested)
278295
{
279296
var metricType = MapPrometheusType(metric.Type);
280297

281298
Debug.Assert(!string.IsNullOrEmpty(metricType), $"{nameof(metricType)} should not be null or empty.");
282299

283300
cursor = WriteAsciiStringNoEscape(buffer, cursor, "# TYPE ");
284-
cursor = WriteMetricName(buffer, cursor, metric);
301+
cursor = WriteMetricMetadataName(buffer, cursor, metric, openMetricsRequested);
285302
buffer[cursor++] = unchecked((byte)' ');
286303
cursor = WriteAsciiStringNoEscape(buffer, cursor, metricType);
287304

@@ -291,15 +308,15 @@ public static int WriteTypeMetadata(byte[] buffer, int cursor, PrometheusMetric
291308
}
292309

293310
[MethodImpl(MethodImplOptions.AggressiveInlining)]
294-
public static int WriteUnitMetadata(byte[] buffer, int cursor, PrometheusMetric metric)
311+
public static int WriteUnitMetadata(byte[] buffer, int cursor, PrometheusMetric metric, bool openMetricsRequested)
295312
{
296313
if (string.IsNullOrEmpty(metric.Unit))
297314
{
298315
return cursor;
299316
}
300317

301318
cursor = WriteAsciiStringNoEscape(buffer, cursor, "# UNIT ");
302-
cursor = WriteMetricName(buffer, cursor, metric);
319+
cursor = WriteMetricMetadataName(buffer, cursor, metric, openMetricsRequested);
303320

304321
buffer[cursor++] = unchecked((byte)' ');
305322

src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializerExt.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ public static bool CanWriteMetric(Metric metric)
2424

2525
public static int WriteMetric(byte[] buffer, int cursor, Metric metric, PrometheusMetric prometheusMetric, bool openMetricsRequested = false)
2626
{
27-
cursor = WriteTypeMetadata(buffer, cursor, prometheusMetric);
28-
cursor = WriteUnitMetadata(buffer, cursor, prometheusMetric);
29-
cursor = WriteHelpMetadata(buffer, cursor, prometheusMetric, metric.Description);
27+
cursor = WriteTypeMetadata(buffer, cursor, prometheusMetric, openMetricsRequested);
28+
cursor = WriteUnitMetadata(buffer, cursor, prometheusMetric, openMetricsRequested);
29+
cursor = WriteHelpMetadata(buffer, cursor, prometheusMetric, metric.Description, openMetricsRequested);
3030

3131
if (!metric.MetricType.IsHistogram())
3232
{
@@ -35,7 +35,7 @@ public static int WriteMetric(byte[] buffer, int cursor, Metric metric, Promethe
3535
var timestamp = metricPoint.EndTime.ToUnixTimeMilliseconds();
3636

3737
// Counter and Gauge
38-
cursor = WriteMetricName(buffer, cursor, prometheusMetric);
38+
cursor = WriteMetricName(buffer, cursor, prometheusMetric, openMetricsRequested);
3939
cursor = WriteTags(buffer, cursor, metric, metricPoint.Tags);
4040

4141
buffer[cursor++] = unchecked((byte)' ');
@@ -85,7 +85,7 @@ public static int WriteMetric(byte[] buffer, int cursor, Metric metric, Promethe
8585
{
8686
totalCount += histogramMeasurement.BucketCount;
8787

88-
cursor = WriteMetricName(buffer, cursor, prometheusMetric);
88+
cursor = WriteMetricName(buffer, cursor, prometheusMetric, openMetricsRequested);
8989
cursor = WriteAsciiStringNoEscape(buffer, cursor, "_bucket{");
9090
cursor = WriteTags(buffer, cursor, metric, tags, writeEnclosingBraces: false);
9191

@@ -111,7 +111,7 @@ public static int WriteMetric(byte[] buffer, int cursor, Metric metric, Promethe
111111
}
112112

113113
// Histogram sum
114-
cursor = WriteMetricName(buffer, cursor, prometheusMetric);
114+
cursor = WriteMetricName(buffer, cursor, prometheusMetric, openMetricsRequested);
115115
cursor = WriteAsciiStringNoEscape(buffer, cursor, "_sum");
116116
cursor = WriteTags(buffer, cursor, metric, metricPoint.Tags);
117117

@@ -125,7 +125,7 @@ public static int WriteMetric(byte[] buffer, int cursor, Metric metric, Promethe
125125
buffer[cursor++] = ASCII_LINEFEED;
126126

127127
// Histogram count
128-
cursor = WriteMetricName(buffer, cursor, prometheusMetric);
128+
cursor = WriteMetricName(buffer, cursor, prometheusMetric, openMetricsRequested);
129129
cursor = WriteAsciiStringNoEscape(buffer, cursor, "_count");
130130
cursor = WriteTags(buffer, cursor, metric, metricPoint.Tags);
131131

test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/PrometheusExporterMiddlewareTests.cs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ public async Task PrometheusExporterMiddlewareIntegration_CanServeOpenMetricsAnd
264264

265265
var beginTimestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds();
266266

267-
var counter = meter.CreateCounter<double>("counter_double");
267+
var counter = meter.CreateCounter<double>("counter_double", unit: "By");
268268
counter.Add(100.18D, tags);
269269
counter.Add(0.99D, tags);
270270

@@ -312,7 +312,7 @@ private static async Task RunPrometheusExporterMiddlewareIntegrationTest(
312312

313313
var beginTimestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds();
314314

315-
var counter = meter.CreateCounter<double>("counter_double");
315+
var counter = meter.CreateCounter<double>("counter_double", unit: "By");
316316
if (!skipMetrics)
317317
{
318318
counter.Add(100.18D, tags);
@@ -368,14 +368,16 @@ private static async Task VerifyAsync(long beginTimestamp, long endTimestamp, Ht
368368
# TYPE otel_scope_info info
369369
# HELP otel_scope_info Scope metadata
370370
otel_scope_info{otel_scope_name="{{MeterName}}"} 1
371-
# TYPE counter_double_total counter
372-
counter_double_total{otel_scope_name="{{MeterName}}",otel_scope_version="{{MeterVersion}}",key1="value1",key2="value2"} 101.17 (\d+\.\d{3})
371+
# TYPE counter_double_bytes counter
372+
# UNIT counter_double_bytes bytes
373+
counter_double_bytes_total{otel_scope_name="{{MeterName}}",otel_scope_version="{{MeterVersion}}",key1="value1",key2="value2"} 101.17 (\d+\.\d{3})
373374
# EOF
374375
375376
""".ReplaceLineEndings()
376377
: $$"""
377-
# TYPE counter_double_total counter
378-
counter_double_total{otel_scope_name="{{MeterName}}",otel_scope_version="{{MeterVersion}}",key1="value1",key2="value2"} 101.17 (\d+)
378+
# TYPE counter_double_bytes_total counter
379+
# UNIT counter_double_bytes_total bytes
380+
counter_double_bytes_total{otel_scope_name="{{MeterName}}",otel_scope_version="{{MeterVersion}}",key1="value1",key2="value2"} 101.17 (\d+)
379381
# EOF
380382
381383
""".ReplaceLineEndings();

test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerTests.cs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ private async Task RunPrometheusExporterHttpServerIntegrationTest(bool skipMetri
202202
new KeyValuePair<string, object>("key2", "value2"),
203203
};
204204

205-
var counter = meter.CreateCounter<double>("counter_double");
205+
var counter = meter.CreateCounter<double>("counter_double", unit: "By");
206206
if (!skipMetrics)
207207
{
208208
counter.Add(100.18D, tags);
@@ -241,11 +241,13 @@ private async Task RunPrometheusExporterHttpServerIntegrationTest(bool skipMetri
241241
+ "# TYPE otel_scope_info info\n"
242242
+ "# HELP otel_scope_info Scope metadata\n"
243243
+ $"otel_scope_info{{otel_scope_name='{MeterName}'}} 1\n"
244-
+ "# TYPE counter_double_total counter\n"
245-
+ $"counter_double_total{{otel_scope_name='{MeterName}',otel_scope_version='{MeterVersion}',key1='value1',key2='value2'}} 101.17 (\\d+\\.\\d{{3}})\n"
244+
+ "# TYPE counter_double_bytes counter\n"
245+
+ "# UNIT counter_double_bytes bytes\n"
246+
+ $"counter_double_bytes_total{{otel_scope_name='{MeterName}',otel_scope_version='{MeterVersion}',key1='value1',key2='value2'}} 101.17 (\\d+\\.\\d{{3}})\n"
246247
+ "# EOF\n"
247-
: "# TYPE counter_double_total counter\n"
248-
+ $"counter_double_total{{otel_scope_name='{MeterName}',otel_scope_version='{MeterVersion}',key1='value1',key2='value2'}} 101.17 (\\d+)\n"
248+
: "# TYPE counter_double_bytes_total counter\n"
249+
+ "# UNIT counter_double_bytes_total bytes\n"
250+
+ $"counter_double_bytes_total{{otel_scope_name='{MeterName}',otel_scope_version='{MeterVersion}',key1='value1',key2='value2'}} 101.17 (\\d+)\n"
249251
+ "# EOF\n";
250252

251253
Assert.Matches(("^" + expected + "$").Replace('\'', '"'), content);

0 commit comments

Comments
 (0)