Skip to content

Commit cb20e48

Browse files
nabutabumartincostelloCopilot
authored
[Exporter.Prometheus] Sanitize metric unit (#7033)
Co-authored-by: Martin Costello <martin@martincostello.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent ef66839 commit cb20e48

4 files changed

Lines changed: 90 additions & 7 deletions

File tree

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ Notes](../../RELEASENOTES.md).
77

88
## Unreleased
99

10+
* Fixed metric unit strings containing invalid Prometheus characters (e.g. `# RU`)
11+
not being sanitized, resulting in malformed metric names.
12+
([#6187](https://github.com/open-telemetry/opentelemetry-dotnet/issues/6187))
13+
1014
## 1.15.2-beta.1
1115

1216
Released 2026-Apr-08

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ Notes](../../RELEASENOTES.md).
77

88
## Unreleased
99

10+
* Fixed metric unit strings containing invalid Prometheus characters (e.g. `# RU`)
11+
not being sanitized, resulting in malformed metric names.
12+
([#6187](https://github.com/open-telemetry/opentelemetry-dotnet/issues/6187))
13+
1014
## 1.15.2-beta.1
1115

1216
Released 2026-Apr-08

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

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,39 @@ public PrometheusMetric(string name, string unit, PrometheusType type, bool disa
7777
public static PrometheusMetric Create(Metric metric, bool disableTotalNameSuffixForCounters)
7878
=> new(metric.Name, metric.Unit, GetPrometheusType(metric.MetricType), disableTotalNameSuffixForCounters);
7979

80+
internal static string SanitizeMetricUnit(string metricUnit)
81+
{
82+
StringBuilder? sb = null;
83+
var lastCharUnderscore = false;
84+
85+
for (var i = 0; i < metricUnit.Length; i++)
86+
{
87+
var c = metricUnit[i];
88+
89+
if (!char.IsLetterOrDigit(c) && c != ':')
90+
{
91+
if (!lastCharUnderscore)
92+
{
93+
lastCharUnderscore = true;
94+
sb ??= new StringBuilder(metricUnit, 0, i, metricUnit.Length);
95+
sb.Append('_');
96+
}
97+
}
98+
else
99+
{
100+
if (sb != null)
101+
{
102+
sb.Append(c);
103+
}
104+
105+
lastCharUnderscore = false;
106+
}
107+
}
108+
109+
var result = sb?.ToString() ?? metricUnit;
110+
return result.Trim('_');
111+
}
112+
80113
internal static string SanitizeMetricName(string metricName)
81114
{
82115
StringBuilder? sb = null;
@@ -112,11 +145,6 @@ internal static string SanitizeMetricName(string metricName)
112145
}
113146

114147
return sb?.ToString() ?? metricName;
115-
116-
static StringBuilder CreateStringBuilder(string name)
117-
{
118-
return new(name.Length);
119-
}
120148
}
121149

122150
internal static string RemoveAnnotations(string unit)
@@ -186,6 +214,11 @@ UpDownCounter becomes gauge
186214
};
187215
}
188216

217+
private static StringBuilder CreateStringBuilder(string value)
218+
{
219+
return new(value.Length);
220+
}
221+
189222
private static string SanitizeOpenMetricsName(string metricName)
190223
=> metricName.EndsWith("_total", StringComparison.Ordinal) ? metricName.Substring(0, metricName.Length - 6) : metricName;
191224

@@ -208,7 +241,7 @@ private static string GetUnit(string unit)
208241
updatedUnit = MapUnit(updatedUnit.AsSpan());
209242
}
210243

211-
return updatedUnit;
244+
return SanitizeMetricUnit(updatedUnit);
212245
}
213246

214247
private static bool TryProcessRateUnits(string updatedUnit, [NotNullWhen(true)] out string? updatedPerUnit)

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

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,43 @@ public void GetPrometheusType_HistogramVariants_ReturnsHistogram(int metricTypeV
361361

362362
[Fact]
363363
public void Name_MultipleSlashesInUnit_FirstSlashProcessed()
364-
=> AssertName("metric", "req/s/extra", PrometheusType.Gauge, false, "metric_req_per_s/extra"); // // Multiple slashes
364+
=> AssertName("metric", "req/s/extra", PrometheusType.Gauge, false, "metric_req_per_s_extra"); // // Multiple slashes
365+
366+
[Fact]
367+
public void SanitizeMetricUnit_Valid()
368+
=> AssertSanitizeMetricUnit("requests", "requests");
369+
370+
[Fact]
371+
public void SanitizeMetricUnit_RemoveConsecutiveUnderscores()
372+
=> AssertSanitizeMetricUnit("req__per__s", "req_per_s");
373+
374+
[Fact]
375+
public void SanitizeMetricUnit_RemoveUnsupportedCharacters()
376+
=> AssertSanitizeMetricUnit("# RU", "RU");
377+
378+
[Fact]
379+
public void SanitizeMetricUnit_RemoveWhitespace()
380+
=> AssertSanitizeMetricUnit("req s", "req_s");
381+
382+
[Fact]
383+
public void SanitizeMetricUnit_LeadingNumberAllowed()
384+
=> AssertSanitizeMetricUnit("2_unitname", "2_unitname");
385+
386+
[Fact]
387+
public void SanitizeMetricUnit_RemoveMultipleUnsupportedCharacters()
388+
=> AssertSanitizeMetricUnit("##/RU!", "RU");
389+
390+
[Fact]
391+
public void Name_UnitWithHash_Sanitized()
392+
=> AssertName("azure_cosmosdb_client_operation_request_charge", "# RU", PrometheusType.Histogram, false, "azure_cosmosdb_client_operation_request_charge_RU");
393+
394+
[Fact]
395+
public void Name_UnitWithSpace_Sanitized()
396+
=> AssertName("metric", "req s", PrometheusType.Gauge, false, "metric_req_s");
397+
398+
[Fact]
399+
public void Name_UnitWithSpecialChars_Sanitized()
400+
=> AssertName("metric", "req!", PrometheusType.Gauge, false, "metric_req");
365401

366402
[Theory]
367403
[InlineData(PrometheusType.Counter)]
@@ -375,6 +411,12 @@ internal void Constructor_AllPrometheusTypes_Work(PrometheusType type)
375411
Assert.Equal(type, metric.Type);
376412
}
377413

414+
private static void AssertSanitizeMetricUnit(string unit, string expected)
415+
{
416+
var sanitizedUnit = PrometheusMetric.SanitizeMetricUnit(unit);
417+
Assert.Equal(expected, sanitizedUnit);
418+
}
419+
378420
private static void AssertName(
379421
string name, string unit, PrometheusType type, bool disableTotalNameSuffixForCounters, string expected)
380422
{

0 commit comments

Comments
 (0)