Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ Notes](../../RELEASENOTES.md).

## Unreleased

* Fixed metric unit strings containing invalid Prometheus characters (e.g. `# RU`)
not being sanitized, resulting in malformed metric names.
([#6187](https://github.com/open-telemetry/opentelemetry-dotnet/issues/6187))

## 1.15.2-beta.1

Released 2026-Apr-08
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ Notes](../../RELEASENOTES.md).

## Unreleased

* Fixed metric unit strings containing invalid Prometheus characters (e.g. `# RU`)
Comment thread
Kielek marked this conversation as resolved.
not being sanitized, resulting in malformed metric names.
([#6187](https://github.com/open-telemetry/opentelemetry-dotnet/issues/6187))

## 1.15.2-beta.1

Released 2026-Apr-08
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,39 @@ public PrometheusMetric(string name, string unit, PrometheusType type, bool disa
public static PrometheusMetric Create(Metric metric, bool disableTotalNameSuffixForCounters)
=> new(metric.Name, metric.Unit, GetPrometheusType(metric.MetricType), disableTotalNameSuffixForCounters);

internal static string SanitizeMetricUnit(string metricUnit)
{
StringBuilder? sb = null;
var lastCharUnderscore = false;

for (var i = 0; i < metricUnit.Length; i++)
{
var c = metricUnit[i];

if (!char.IsLetterOrDigit(c) && c != ':')
{
if (!lastCharUnderscore)
{
lastCharUnderscore = true;
sb ??= new StringBuilder(metricUnit, 0, i, metricUnit.Length);
sb.Append('_');
}
}
else
{
if (sb != null)
{
sb.Append(c);
}

lastCharUnderscore = false;
}
}

var result = sb?.ToString() ?? metricUnit;
return result.Trim('_');
}
Comment thread
nabutabu marked this conversation as resolved.

internal static string SanitizeMetricName(string metricName)
{
StringBuilder? sb = null;
Expand Down Expand Up @@ -112,11 +145,6 @@ internal static string SanitizeMetricName(string metricName)
}

return sb?.ToString() ?? metricName;

static StringBuilder CreateStringBuilder(string name)
{
return new(name.Length);
}
}

internal static string RemoveAnnotations(string unit)
Expand Down Expand Up @@ -186,6 +214,11 @@ UpDownCounter becomes gauge
};
}

private static StringBuilder CreateStringBuilder(string value)
{
return new(value.Length);
}

private static string SanitizeOpenMetricsName(string metricName)
=> metricName.EndsWith("_total", StringComparison.Ordinal) ? metricName.Substring(0, metricName.Length - 6) : metricName;

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

return updatedUnit;
return SanitizeMetricUnit(updatedUnit);
}

private static bool TryProcessRateUnits(string updatedUnit, [NotNullWhen(true)] out string? updatedPerUnit)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,43 @@ 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

[Fact]
public void SanitizeMetricUnit_Valid()
=> AssertSanitizeMetricUnit("requests", "requests");

[Fact]
public void SanitizeMetricUnit_RemoveConsecutiveUnderscores()
=> AssertSanitizeMetricUnit("req__per__s", "req_per_s");

[Fact]
public void SanitizeMetricUnit_RemoveUnsupportedCharacters()
=> AssertSanitizeMetricUnit("# RU", "RU");

[Fact]
public void SanitizeMetricUnit_RemoveWhitespace()
=> AssertSanitizeMetricUnit("req s", "req_s");

[Fact]
public void SanitizeMetricUnit_LeadingNumberAllowed()
=> AssertSanitizeMetricUnit("2_unitname", "2_unitname");

[Fact]
public void SanitizeMetricUnit_RemoveMultipleUnsupportedCharacters()
=> AssertSanitizeMetricUnit("##/RU!", "RU");

[Fact]
public void Name_UnitWithHash_Sanitized()
=> AssertName("azure_cosmosdb_client_operation_request_charge", "# RU", PrometheusType.Histogram, false, "azure_cosmosdb_client_operation_request_charge_RU");

[Fact]
public void Name_UnitWithSpace_Sanitized()
=> AssertName("metric", "req s", PrometheusType.Gauge, false, "metric_req_s");

[Fact]
public void Name_UnitWithSpecialChars_Sanitized()
=> AssertName("metric", "req!", PrometheusType.Gauge, false, "metric_req");

[Theory]
[InlineData(PrometheusType.Counter)]
Expand All @@ -375,6 +411,12 @@ internal void Constructor_AllPrometheusTypes_Work(PrometheusType type)
Assert.Equal(type, metric.Type);
}

private static void AssertSanitizeMetricUnit(string unit, string expected)
{
var sanitizedUnit = PrometheusMetric.SanitizeMetricUnit(unit);
Assert.Equal(expected, sanitizedUnit);
}

private static void AssertName(
string name, string unit, PrometheusType type, bool disableTotalNameSuffixForCounters, string expected)
{
Expand Down
Loading