[prometheus] Fix OpenMetrics format suffixes (#5646)

Co-authored-by: Cijo Thomas <cijo.thomas@gmail.com>
Co-authored-by: Mikel Blanchard <mblanchard@macrosssoftware.com>
Co-authored-by: Reiley Yang <reyang@microsoft.com>
This commit is contained in:
Robert Coltheart 2024-05-29 03:47:02 +10:00 committed by GitHub
parent 81244d6136
commit b9d56aa64e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 151 additions and 45 deletions

View File

@ -2,6 +2,9 @@
## Unreleased
* Fixed issue with OpenMetrics suffixes for Prometheus
([#5646](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5646))
## 1.9.0-alpha.1
Released 2024-May-20

View File

@ -2,6 +2,9 @@
## Unreleased
* Fixed issue with OpenMetrics suffixes for Prometheus
([#5646](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5646))
## 1.9.0-alpha.1
Released 2024-May-20

View File

@ -27,6 +27,7 @@ internal sealed class PrometheusMetric
// consecutive `_` characters MUST be replaced with a single `_` character.
// https://github.com/open-telemetry/opentelemetry-specification/blob/b2f923fb1650dde1f061507908b834035506a796/specification/compatibility/prometheus_and_openmetrics.md#L230-L233
var sanitizedName = SanitizeMetricName(name);
var openMetricsName = SanitizeOpenMetricsName(sanitizedName);
string sanitizedUnit = null;
if (!string.IsNullOrEmpty(unit))
@ -35,38 +36,50 @@ internal sealed class PrometheusMetric
// The resulting unit SHOULD be added to the metric as
// [OpenMetrics UNIT metadata](https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#metricfamily)
// and as a suffix to the metric name unless the metric name already contains the
// unit, or the unit MUST be omitted. The unit suffix comes before any
// type-specific suffixes.
// https://github.com/open-telemetry/opentelemetry-specification/blob/b2f923fb1650dde1f061507908b834035506a796/specification/compatibility/prometheus_and_openmetrics.md#L242-L246
if (!sanitizedName.Contains(sanitizedUnit))
// and as a suffix to the metric name. The unit suffix comes before any type-specific suffixes.
// https://github.com/open-telemetry/opentelemetry-specification/blob/3dfb383fe583e3b74a2365c5a1d90256b273ee76/specification/compatibility/prometheus_and_openmetrics.md#metric-metadata-1
if (!sanitizedName.EndsWith(sanitizedUnit))
{
sanitizedName = sanitizedName + "_" + sanitizedUnit;
sanitizedName += $"_{sanitizedUnit}";
openMetricsName += $"_{sanitizedUnit}";
}
}
// 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.
// Exporters SHOULD provide a configuration option to disable the addition of `_total` suffixes.
// https://github.com/open-telemetry/opentelemetry-specification/blob/b2f923fb1650dde1f061507908b834035506a796/specification/compatibility/prometheus_and_openmetrics.md#L286
// Note that we no longer append '_ratio' for units that are '1', see: https://github.com/open-telemetry/opentelemetry-specification/issues/4058
if (type == PrometheusType.Counter && !sanitizedName.EndsWith("_total") && !disableTotalNameSuffixForCounters)
{
sanitizedName += "_total";
}
// Special case: Converting "1" to "ratio".
// https://github.com/open-telemetry/opentelemetry-specification/blob/b2f923fb1650dde1f061507908b834035506a796/specification/compatibility/prometheus_and_openmetrics.md#L239
if (type == PrometheusType.Gauge && unit == "1" && !sanitizedName.Contains("ratio"))
// For counters requested using OpenMetrics format, the MetricFamily name MUST be suffixed with '_total', regardless of the setting to disable the 'total' suffix.
// https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#counter-1
if (type == PrometheusType.Counter && !openMetricsName.EndsWith("_total"))
{
sanitizedName += "_ratio";
openMetricsName += "_total";
}
// 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.
// https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#unit
var openMetricsMetadataName = type == PrometheusType.Counter
? SanitizeOpenMetricsName(openMetricsName)
: sanitizedName;
this.Name = sanitizedName;
this.OpenMetricsName = openMetricsName;
this.OpenMetricsMetadataName = openMetricsMetadataName;
this.Unit = sanitizedUnit;
this.Type = type;
}
public string Name { get; }
public string OpenMetricsName { get; }
public string OpenMetricsMetadataName { get; }
public string Unit { get; }
public PrometheusType Type { get; }
@ -159,6 +172,16 @@ internal sealed class PrometheusMetric
return sb.ToString();
}
private static string SanitizeOpenMetricsName(string metricName)
{
if (metricName.EndsWith("_total"))
{
return metricName.Substring(0, metricName.Length - 6);
}
return metricName;
}
private static string GetUnit(string unit)
{
// 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.

View File

@ -230,12 +230,29 @@ internal static partial class PrometheusSerializer
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int WriteMetricName(byte[] buffer, int cursor, PrometheusMetric metric)
public static int WriteMetricName(byte[] buffer, int cursor, PrometheusMetric metric, bool openMetricsRequested)
{
// Metric name has already been escaped.
for (int i = 0; i < metric.Name.Length; i++)
var name = openMetricsRequested ? metric.OpenMetricsName : metric.Name;
for (int i = 0; i < name.Length; i++)
{
var ordinal = (ushort)metric.Name[i];
var ordinal = (ushort)name[i];
buffer[cursor++] = unchecked((byte)ordinal);
}
return cursor;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int WriteMetricMetadataName(byte[] buffer, int cursor, PrometheusMetric metric, bool openMetricsRequested)
{
// Metric name has already been escaped.
var name = openMetricsRequested ? metric.OpenMetricsMetadataName : metric.Name;
for (int i = 0; i < name.Length; i++)
{
var ordinal = (ushort)name[i];
buffer[cursor++] = unchecked((byte)ordinal);
}
@ -252,7 +269,7 @@ internal static partial class PrometheusSerializer
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int WriteHelpMetadata(byte[] buffer, int cursor, PrometheusMetric metric, string metricDescription)
public static int WriteHelpMetadata(byte[] buffer, int cursor, PrometheusMetric metric, string metricDescription, bool openMetricsRequested)
{
if (string.IsNullOrEmpty(metricDescription))
{
@ -260,7 +277,7 @@ internal static partial class PrometheusSerializer
}
cursor = WriteAsciiStringNoEscape(buffer, cursor, "# HELP ");
cursor = WriteMetricName(buffer, cursor, metric);
cursor = WriteMetricMetadataName(buffer, cursor, metric, openMetricsRequested);
if (!string.IsNullOrEmpty(metricDescription))
{
@ -274,14 +291,14 @@ internal static partial class PrometheusSerializer
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int WriteTypeMetadata(byte[] buffer, int cursor, PrometheusMetric metric)
public static int WriteTypeMetadata(byte[] buffer, int cursor, PrometheusMetric metric, bool openMetricsRequested)
{
var metricType = MapPrometheusType(metric.Type);
Debug.Assert(!string.IsNullOrEmpty(metricType), $"{nameof(metricType)} should not be null or empty.");
cursor = WriteAsciiStringNoEscape(buffer, cursor, "# TYPE ");
cursor = WriteMetricName(buffer, cursor, metric);
cursor = WriteMetricMetadataName(buffer, cursor, metric, openMetricsRequested);
buffer[cursor++] = unchecked((byte)' ');
cursor = WriteAsciiStringNoEscape(buffer, cursor, metricType);
@ -291,7 +308,7 @@ internal static partial class PrometheusSerializer
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int WriteUnitMetadata(byte[] buffer, int cursor, PrometheusMetric metric)
public static int WriteUnitMetadata(byte[] buffer, int cursor, PrometheusMetric metric, bool openMetricsRequested)
{
if (string.IsNullOrEmpty(metric.Unit))
{
@ -299,7 +316,7 @@ internal static partial class PrometheusSerializer
}
cursor = WriteAsciiStringNoEscape(buffer, cursor, "# UNIT ");
cursor = WriteMetricName(buffer, cursor, metric);
cursor = WriteMetricMetadataName(buffer, cursor, metric, openMetricsRequested);
buffer[cursor++] = unchecked((byte)' ');

View File

@ -24,9 +24,9 @@ internal static partial class PrometheusSerializer
public static int WriteMetric(byte[] buffer, int cursor, Metric metric, PrometheusMetric prometheusMetric, bool openMetricsRequested = false)
{
cursor = WriteTypeMetadata(buffer, cursor, prometheusMetric);
cursor = WriteUnitMetadata(buffer, cursor, prometheusMetric);
cursor = WriteHelpMetadata(buffer, cursor, prometheusMetric, metric.Description);
cursor = WriteTypeMetadata(buffer, cursor, prometheusMetric, openMetricsRequested);
cursor = WriteUnitMetadata(buffer, cursor, prometheusMetric, openMetricsRequested);
cursor = WriteHelpMetadata(buffer, cursor, prometheusMetric, metric.Description, openMetricsRequested);
if (!metric.MetricType.IsHistogram())
{
@ -35,7 +35,7 @@ internal static partial class PrometheusSerializer
var timestamp = metricPoint.EndTime.ToUnixTimeMilliseconds();
// Counter and Gauge
cursor = WriteMetricName(buffer, cursor, prometheusMetric);
cursor = WriteMetricName(buffer, cursor, prometheusMetric, openMetricsRequested);
cursor = WriteTags(buffer, cursor, metric, metricPoint.Tags);
buffer[cursor++] = unchecked((byte)' ');
@ -85,7 +85,7 @@ internal static partial class PrometheusSerializer
{
totalCount += histogramMeasurement.BucketCount;
cursor = WriteMetricName(buffer, cursor, prometheusMetric);
cursor = WriteMetricName(buffer, cursor, prometheusMetric, openMetricsRequested);
cursor = WriteAsciiStringNoEscape(buffer, cursor, "_bucket{");
cursor = WriteTags(buffer, cursor, metric, tags, writeEnclosingBraces: false);
@ -111,7 +111,7 @@ internal static partial class PrometheusSerializer
}
// Histogram sum
cursor = WriteMetricName(buffer, cursor, prometheusMetric);
cursor = WriteMetricName(buffer, cursor, prometheusMetric, openMetricsRequested);
cursor = WriteAsciiStringNoEscape(buffer, cursor, "_sum");
cursor = WriteTags(buffer, cursor, metric, metricPoint.Tags);
@ -125,7 +125,7 @@ internal static partial class PrometheusSerializer
buffer[cursor++] = ASCII_LINEFEED;
// Histogram count
cursor = WriteMetricName(buffer, cursor, prometheusMetric);
cursor = WriteMetricName(buffer, cursor, prometheusMetric, openMetricsRequested);
cursor = WriteAsciiStringNoEscape(buffer, cursor, "_count");
cursor = WriteTags(buffer, cursor, metric, metricPoint.Tags);

View File

@ -264,7 +264,7 @@ public sealed class PrometheusExporterMiddlewareTests
var beginTimestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds();
var counter = meter.CreateCounter<double>("counter_double");
var counter = meter.CreateCounter<double>("counter_double", unit: "By");
counter.Add(100.18D, tags);
counter.Add(0.99D, tags);
@ -312,7 +312,7 @@ public sealed class PrometheusExporterMiddlewareTests
var beginTimestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds();
var counter = meter.CreateCounter<double>("counter_double");
var counter = meter.CreateCounter<double>("counter_double", unit: "By");
if (!skipMetrics)
{
counter.Add(100.18D, tags);
@ -368,14 +368,16 @@ public sealed class PrometheusExporterMiddlewareTests
# TYPE otel_scope_info info
# HELP otel_scope_info Scope metadata
otel_scope_info{otel_scope_name="{{MeterName}}"} 1
# TYPE counter_double_total counter
counter_double_total{otel_scope_name="{{MeterName}}",otel_scope_version="{{MeterVersion}}",key1="value1",key2="value2"} 101.17 (\d+\.\d{3})
# TYPE counter_double_bytes counter
# UNIT counter_double_bytes bytes
counter_double_bytes_total{otel_scope_name="{{MeterName}}",otel_scope_version="{{MeterVersion}}",key1="value1",key2="value2"} 101.17 (\d+\.\d{3})
# EOF
""".ReplaceLineEndings()
: $$"""
# TYPE counter_double_total counter
counter_double_total{otel_scope_name="{{MeterName}}",otel_scope_version="{{MeterVersion}}",key1="value1",key2="value2"} 101.17 (\d+)
# TYPE counter_double_bytes_total counter
# UNIT counter_double_bytes_total bytes
counter_double_bytes_total{otel_scope_name="{{MeterName}}",otel_scope_version="{{MeterVersion}}",key1="value1",key2="value2"} 101.17 (\d+)
# EOF
""".ReplaceLineEndings();

View File

@ -202,7 +202,7 @@ public class PrometheusHttpListenerTests
new KeyValuePair<string, object>("key2", "value2"),
};
var counter = meter.CreateCounter<double>("counter_double");
var counter = meter.CreateCounter<double>("counter_double", unit: "By");
if (!skipMetrics)
{
counter.Add(100.18D, tags);
@ -241,11 +241,13 @@ public class PrometheusHttpListenerTests
+ "# TYPE otel_scope_info info\n"
+ "# HELP otel_scope_info Scope metadata\n"
+ $"otel_scope_info{{otel_scope_name='{MeterName}'}} 1\n"
+ "# TYPE counter_double_total counter\n"
+ $"counter_double_total{{otel_scope_name='{MeterName}',otel_scope_version='{MeterVersion}',key1='value1',key2='value2'}} 101.17 (\\d+\\.\\d{{3}})\n"
+ "# TYPE counter_double_bytes counter\n"
+ "# UNIT counter_double_bytes bytes\n"
+ $"counter_double_bytes_total{{otel_scope_name='{MeterName}',otel_scope_version='{MeterVersion}',key1='value1',key2='value2'}} 101.17 (\\d+\\.\\d{{3}})\n"
+ "# EOF\n"
: "# TYPE counter_double_total counter\n"
+ $"counter_double_total{{otel_scope_name='{MeterName}',otel_scope_version='{MeterVersion}',key1='value1',key2='value2'}} 101.17 (\\d+)\n"
: "# TYPE counter_double_bytes_total counter\n"
+ "# UNIT counter_double_bytes_total bytes\n"
+ $"counter_double_bytes_total{{otel_scope_name='{MeterName}',otel_scope_version='{MeterVersion}',key1='value1',key2='value2'}} 101.17 (\\d+)\n"
+ "# EOF\n";
Assert.Matches(("^" + expected + "$").Replace('\'', '"'), content);

View File

@ -26,7 +26,7 @@ public sealed class PrometheusMetricTests
}
[Fact]
public void SanitizeMetricName_RemoveUnsupportedChracters()
public void SanitizeMetricName_RemoveUnsupportedCharacters()
{
AssertSanitizeMetricName("metric_unit_$1000", "metric_unit_1000");
}
@ -116,13 +116,7 @@ public sealed class PrometheusMetricTests
}
[Fact]
public void Name_SpecialCaseGuage_AppendRatio()
{
AssertName("sample", "1", PrometheusType.Gauge, false, "sample_ratio");
}
[Fact]
public void Name_GuageWithUnit_NoAppendRatio()
public void Name_GaugeWithUnit_NoAppendRatio()
{
AssertName("sample", "unit", PrometheusType.Gauge, false, "sample_unit");
}
@ -205,6 +199,54 @@ public sealed class PrometheusMetricTests
AssertName("2_metric_name", "By", PrometheusType.Summary, false, "_metric_name_bytes");
}
[Fact]
public void OpenMetricsName_UnitAlreadyPresentInName_Appended()
{
AssertOpenMetricsName("db_bytes_written", "By", PrometheusType.Gauge, false, "db_bytes_written_bytes");
}
[Fact]
public void OpenMetricsName_SuffixedWithUnit_NotAppended()
{
AssertOpenMetricsName("db_written_bytes", "By", PrometheusType.Gauge, false, "db_written_bytes");
}
[Fact]
public void OpenMetricsName_Counter_AppendTotal()
{
AssertOpenMetricsName("db_bytes_written", "By", PrometheusType.Counter, false, "db_bytes_written_bytes_total");
}
[Fact]
public void OpenMetricsName_Counter_DisableSuffixTotal_AppendTotal()
{
AssertOpenMetricsName("db_bytes_written", "By", PrometheusType.Counter, true, "db_bytes_written_bytes_total");
}
[Fact]
public void OpenMetricsName_CounterSuffixedWithTotal_AppendUnitAndTotal()
{
AssertOpenMetricsName("db_bytes_written_total", "By", PrometheusType.Counter, false, "db_bytes_written_bytes_total");
}
[Fact]
public void OpenMetricsName_CounterSuffixedWithTotal_DisableSuffixTotal_AppendTotal()
{
AssertOpenMetricsName("db_bytes_written_total", "By", PrometheusType.Counter, false, "db_bytes_written_bytes_total");
}
[Fact]
public void OpenMetricsMetadataName_Counter_NotAppendTotal()
{
AssertOpenMetricsMetadataName("db_bytes_written", "By", PrometheusType.Counter, false, "db_bytes_written_bytes");
}
[Fact]
public void OpenMetricsMetadataName_Counter_DisableSuffixTotal_NotAppendTotal()
{
AssertOpenMetricsMetadataName("db_bytes_written", "By", PrometheusType.Counter, true, "db_bytes_written_bytes");
}
private static void AssertName(
string name, string unit, PrometheusType type, bool disableTotalNameSuffixForCounters, string expected)
{
@ -217,4 +259,18 @@ public sealed class PrometheusMetricTests
var sanatizedName = PrometheusMetric.SanitizeMetricName(name);
Assert.Equal(expected, sanatizedName);
}
private static void AssertOpenMetricsName(
string name, string unit, PrometheusType type, bool disableTotalNameSuffixForCounters, string expected)
{
var prometheusMetric = new PrometheusMetric(name, unit, type, disableTotalNameSuffixForCounters);
Assert.Equal(expected, prometheusMetric.OpenMetricsName);
}
private static void AssertOpenMetricsMetadataName(
string name, string unit, PrometheusType type, bool disableTotalNameSuffixForCounters, string expected)
{
var prometheusMetric = new PrometheusMetric(name, unit, type, disableTotalNameSuffixForCounters);
Assert.Equal(expected, prometheusMetric.OpenMetricsMetadataName);
}
}