diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusMetric.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusMetric.cs index df1632ca467..81d0a1da42a 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusMetric.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusMetric.cs @@ -119,6 +119,41 @@ static StringBuilder CreateStringBuilder(string name) } } + internal static string SanitizeUnitName(string unit) + { + if (string.IsNullOrEmpty(unit)) + { + return string.Empty; + } + + var sb = new StringBuilder(unit.Length); + var lastCharUnderscore = false; + + for (var i = 0; i < unit.Length; i++) + { + var c = unit[i]; + + if (char.IsLetterOrDigit(c)) + { + sb.Append(c); + lastCharUnderscore = false; + } + else if (!lastCharUnderscore && sb.Length > 0) + { + sb.Append('_'); + lastCharUnderscore = true; + } + } + + // Strip trailing underscore + if (sb.Length > 0 && sb[sb.Length - 1] == '_') + { + sb.Length--; + } + + return sb.Length == 0 ? string.Empty : sb.ToString(); + } + internal static string RemoveAnnotations(string unit) { // UCUM standard says the curly braces shouldn't be nested: @@ -208,6 +243,7 @@ private static string GetUnit(string unit) updatedUnit = MapUnit(updatedUnit.AsSpan()); } + updatedUnit = SanitizeUnitName(updatedUnit); return updatedUnit; } diff --git a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusMetricTests.cs b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusMetricTests.cs index 5e8856f4843..1b3ef129b5f 100644 --- a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusMetricTests.cs +++ b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusMetricTests.cs @@ -263,6 +263,42 @@ public void GetPrometheusType_MapsOpenTelemetryMetricsTypeToPrometheus(MetricsMa [Fact] public void Name_EmptyUnit_NoSuffixAdded() => AssertName("metric", string.Empty, PrometheusType.Gauge, false, "metric"); + [Fact] + public void Name_MalformedUnit_HashRU_Sanitized() + => AssertName("metric", "# RU", PrometheusType.Gauge, false, "metric_RU"); + + [Fact] + public void Name_MalformedUnit_HashOnly_Unitless() + => AssertName("metric", "#", PrometheusType.Gauge, false, "metric"); + + [Fact] + public void Name_MalformedUnit_HashSpaceHash_Unitless() + => AssertName("metric", "# #", PrometheusType.Gauge, false, "metric"); + + [Fact] + public void Name_MalformedUnit_HashUnderscoreRDotUDot_Sanitized() + => AssertName("metric", "#_R.U.", PrometheusType.Gauge, false, "metric_R_U"); + + [Fact] + public void SanitizeUnitName_Valid() + => Assert.Equal("bytes", PrometheusMetric.SanitizeUnitName("bytes")); + + [Fact] + public void SanitizeUnitName_WithInvalidChars() + => Assert.Equal("RU", PrometheusMetric.SanitizeUnitName("# RU")); + + [Fact] + public void SanitizeUnitName_OnlyInvalidChars_ReturnsEmpty() + => Assert.Equal(string.Empty, PrometheusMetric.SanitizeUnitName("#")); + + [Fact] + public void SanitizeUnitName_Empty_ReturnsEmpty() + => Assert.Equal(string.Empty, PrometheusMetric.SanitizeUnitName(string.Empty)); + + [Fact] + public void SanitizeUnitName_DotsCollapsed() + => Assert.Equal("R_U", PrometheusMetric.SanitizeUnitName("R.U.")); + [Fact] public void Name_NullUnit_NoSuffixAdded() { @@ -361,7 +397,7 @@ public void GetPrometheusType_HistogramVariants_ReturnsHistogram(int metricTypeV [Fact] public void Name_MultipleSlashesInUnit_FirstSlashProcessed() - => AssertName("metric", "req/s/extra", PrometheusType.Gauge, false, "metric_req_per_s/extra"); // // Multiple slashes + => AssertName("metric", "req/s/extra", PrometheusType.Gauge, false, "metric_req_per_s_extra"); // Multiple slashes [Theory] [InlineData(PrometheusType.Counter)]