Skip to content

[Exporter.Prometheus] Add dots and values escaping#7439

Merged
martincostello merged 19 commits into
open-telemetry:mainfrom
martincostello:gh-7246
Jun 30, 2026
Merged

[Exporter.Prometheus] Add dots and values escaping#7439
martincostello merged 19 commits into
open-telemetry:mainfrom
martincostello:gh-7246

Conversation

@martincostello

Copy link
Copy Markdown
Member

Contributes to #7246.

Changes

  • Add support for the dots and values escaping schemes.
  • Add missing in modifiers on methods accepting parameters of type PrometheusProtocol.

Benchmark Results

Summary (generated by Claude 🐙)

Method: ./benchmark.ps1 @("*PrometheusSerializerBenchmarks*") -EnableMemoryDiagnoser (BenchmarkDotNet Default job, .NET 10.0). Ratio = target ÷ baseline (< 1.0 = improvement).

Benchmark Calls Duration ratio Memory ratio
WriteMetric 1 0.93 (2766 vs 2989 ns) 0.87 (4.12 vs 4.74 KB)
WriteMetric 1000 0.87 (2.654 vs 3.038 ms) 0.87 (4117 vs 4742 KB)
WriteMetric 10000 0.94 (28.2 vs 30.2 ms) 0.87 (41172 vs 47422 KB)
WriteHistogramMetric 1 1.03 (1550 vs 1502 ns) 0.89 (1.26 vs 1.41 KB)
WriteHistogramMetric 1000 0.96 (1.487 vs 1.545 ms) 0.89 (1258 vs 1414 KB)
WriteHistogramMetric 10000 0.94 (15.04 vs 16.01 ms) 0.89 (12578 vs 14141 KB)
WriteMetricWithTypedLabels 1 0.87 (467 vs 540 ns) 0.89 (1.16 vs 1.31 KB)
WriteMetricWithTypedLabels 1000 0.92 (485 vs 526 µs) 0.88 (1156 vs 1313 KB)
WriteMetricWithTypedLabels 10000 0.71* (4.83 vs 6.82 ms) 0.88 (11563 vs 13125 KB)

Conclusion: net improvement, no regressions.

  • Memory: ~11–13% lower allocation on every case (deterministic).
  • Throughput: faster in 8 of 9 cases (−4% to −13% in the stable @1000 cases).

Root cause of the improvement: the label-writing fast path previously re-sanitized every label key — including otel_scope_* scope labels — via GetSanitizedLabelKey (a StringBuilder+string allocation per label). The change writes scope labels' already-formed output key directly and writes all output keys with a plain byte copy instead of re-running the normalization. Since every metric carries at least otel_scope_name, this removes a string allocation per scope label and does less per-character work.

Notes (not regressions):

  • WriteHistogramMetric @1 = 1.03 is a single-invocation artifact (~47 ns, within ~1.5× the error bars) and is contradicted by the same path being faster at @1000 (0.96) and @10000 (0.94).
  • *WriteMetricWithTypedLabels @10000 = 0.71 is flattered by a noisy main measurement (StdDev ±14%); the representative figure is the @1000 case (0.92).

Detailed Results

Expand to view

main

BenchmarkDotNet v0.15.8, Windows 11 (10.0.26200.8655/25H2/2025Update/HudsonValley2)
13th Gen Intel Core i7-13700H 2.90GHz, 1 CPU, 20 logical and 14 physical cores
.NET SDK 10.0.301
  [Host]    : .NET 10.0.9 (10.0.9, 10.0.926.27113), X64 RyuJIT x86-64-v3
  .NET 10.0 : .NET 10.0.9 (10.0.9, 10.0.926.27113), X64 RyuJIT x86-64-v3

Job=.NET 10.0  Runtime=.NET 10.0  Toolchain=net10.0  
Method NumberOfSerializeCalls Mean Error StdDev Gen0 Allocated
WriteMetric 1 2,988.9 ns 54.44 ns 50.93 ns 0.3853 4.74 KB
WriteHistogramMetric 1 1,502.3 ns 28.93 ns 27.06 ns 0.1144 1.41 KB
WriteMetricWithTypedLabels 1 539.5 ns 7.20 ns 6.01 ns 0.1068 1.31 KB
WriteMetric 1000 3,037,818.6 ns 60,600.39 ns 67,357.18 ns 386.7188 4742.19 KB
WriteHistogramMetric 1000 1,544,807.7 ns 22,855.81 ns 19,085.63 ns 115.2344 1414.06 KB
WriteMetricWithTypedLabels 1000 526,373.1 ns 9,069.13 ns 8,039.55 ns 106.4453 1312.5 KB
WriteMetric 10000 30,152,397.7 ns 483,575.69 ns 452,337.00 ns 3843.7500 47421.88 KB
WriteHistogramMetric 10000 16,011,151.8 ns 315,025.04 ns 591,693.43 ns 1125.0000 14140.65 KB
WriteMetricWithTypedLabels 10000 6,823,377.0 ns 325,675.74 ns 944,844.44 ns 1070.3125 13125 KB

This PR

BenchmarkDotNet v0.15.8, Windows 11 (10.0.26200.8655/25H2/2025Update/HudsonValley2)
13th Gen Intel Core i7-13700H 2.90GHz, 1 CPU, 20 logical and 14 physical cores
.NET SDK 10.0.301
  [Host]    : .NET 10.0.9 (10.0.9, 10.0.926.27113), X64 RyuJIT x86-64-v3
  .NET 10.0 : .NET 10.0.9 (10.0.9, 10.0.926.27113), X64 RyuJIT x86-64-v3

Job=.NET 10.0  Runtime=.NET 10.0  Toolchain=net10.0  
Method NumberOfSerializeCalls Mean Error StdDev Gen0 Allocated
WriteMetric 1 2,766.0 ns 40.73 ns 38.10 ns 0.3357 4.12 KB
WriteHistogramMetric 1 1,549.5 ns 30.36 ns 33.74 ns 0.1011 1.26 KB
WriteMetricWithTypedLabels 1 467.1 ns 9.03 ns 8.45 ns 0.0939 1.16 KB
WriteMetric 1000 2,653,619.2 ns 52,321.04 ns 88,845.10 ns 335.9375 4117.19 KB
WriteHistogramMetric 1000 1,486,984.9 ns 21,520.17 ns 19,077.07 ns 101.5625 1257.81 KB
WriteMetricWithTypedLabels 1000 485,448.2 ns 9,686.10 ns 17,953.81 ns 94.2383 1156.25 KB
WriteMetric 10000 28,200,055.3 ns 554,182.37 ns 720,593.68 ns 3343.7500 41171.88 KB
WriteHistogramMetric 10000 15,037,502.2 ns 280,918.31 ns 288,482.42 ns 1015.6250 12578.13 KB
WriteMetricWithTypedLabels 10000 4,832,012.7 ns 96,157.43 ns 185,262.75 ns 937.5000 11562.5 KB

Merge requirement checklist

  • CONTRIBUTING guidelines followed (license requirements, nullable enabled, static analysis, etc.)
  • Unit tests added/updated
  • Appropriate CHANGELOG.md files updated for non-trivial changes
  • Changes in public API reviewed (if applicable)

Add missing `in` modifiers on methods accepting parameters of type `PrometheusProtocol`.
Add support for the `dots` and `values` escaping schemes.

Contributes to open-telemetry#7246.
Fix incorrectly sorted `[InlineData]` attributes.
@github-actions github-actions Bot added pkg:OpenTelemetry.Exporter.Prometheus.AspNetCore Issues related to OpenTelemetry.Exporter.Prometheus.AspNetCore NuGet package pkg:OpenTelemetry.Exporter.Prometheus.HttpListener Issues related to OpenTelemetry.Exporter.Prometheus.HttpListener NuGet package labels Jun 21, 2026
Comment thread src/OpenTelemetry.Exporter.Prometheus.AspNetCore/CHANGELOG.md Outdated
Comment thread src/OpenTelemetry.Exporter.Prometheus.HttpListener/CHANGELOG.md Outdated
@codecov

codecov Bot commented Jun 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.57265% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 90.22%. Comparing base (816ccc1) to head (ffaee57).
⚠️ Report is 2 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
...HttpListener/Internal/Shared/PrometheusEscaping.cs 98.78% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #7439      +/-   ##
==========================================
+ Coverage   90.05%   90.22%   +0.16%     
==========================================
  Files         284      279       -5     
  Lines       15230    15082     -148     
==========================================
- Hits        13716    13607     -109     
+ Misses       1514     1475      -39     
Flag Coverage Δ
unittests-PowerShellScripts ?
unittests-Project-Experimental 90.11% <99.57%> (+0.02%) ⬆️
unittests-Project-Stable 90.04% <99.57%> (-0.03%) ⬇️
unittests-UnstableCoreLibraries-Experimental 49.69% <99.57%> (+0.83%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...metheus.AspNetCore/PrometheusExporterMiddleware.cs 100.00% <100.00%> (ø)
...s.HttpListener/Internal/PrometheusHeadersParser.cs 98.85% <100.00%> (ø)
...ner/Internal/Shared/PrometheusCollectionManager.cs 90.35% <100.00%> (ø)
...s.HttpListener/Internal/Shared/PrometheusMetric.cs 99.54% <100.00%> (+0.07%) ⬆️
...HttpListener/Internal/Shared/PrometheusProtocol.cs 98.03% <100.00%> (+0.21%) ⬆️
...rnal/Shared/Serialization/OpenMetricsSerializer.cs 100.00% <100.00%> (ø)
...l/Shared/Serialization/PrometheusTextSerializer.cs 100.00% <100.00%> (ø)
...ernal/Shared/Serialization/TextFormatSerializer.cs 100.00% <100.00%> (ø)
...HttpListener/Internal/Shared/PrometheusEscaping.cs 98.78% <98.78%> (ø)

... and 13 files with indirect coverage changes

- Update comments.
- Add `Debug.Assert()`.
- Add PR number to CHANGELOGs.
@martincostello
martincostello marked this pull request as ready for review June 21, 2026 16:07
@martincostello
martincostello requested a review from a team as a code owner June 21, 2026 16:07
Copilot AI review requested due to automatic review settings June 24, 2026 09:35
Move entries to the right place.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds support in the Prometheus exporters’ text/OpenMetrics serialization pipeline for the dots and values UTF-8 name escaping schemes (as negotiated via the Accept header). This extends the existing escaping negotiation work to cover additional Prometheus-supported schemes, while also tightening some protocol- and serializer-related APIs.

Changes:

  • Implement dots and values escaping for metric and label names, including reversible escaping logic and scheme negotiation.
  • Update serialization to use escaping-aware metric name sets and avoid re-sanitizing already-final output keys (notably scope labels).
  • Expand unit/integration/fuzz tests and snapshots to validate correct negotiation and output for the new schemes.

Reviewed changes

Copilot reviewed 21 out of 21 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/snapshots/PrometheusSerializer.WriteMetric_EscapesMetricAndLabelNamesUsingNegotiatedScheme_escaping=values.verified.txt Adds snapshot for values escaping output.
test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/snapshots/PrometheusSerializer.WriteMetric_EscapesMetricAndLabelNamesUsingNegotiatedScheme_escaping=underscores.verified.txt Adds snapshot for underscores escaping output.
test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/snapshots/PrometheusSerializer.WriteMetric_EscapesMetricAndLabelNamesUsingNegotiatedScheme_escaping=dots.verified.txt Adds snapshot for dots escaping output.
test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusSerializerTests.cs Adds serializer test covering negotiated escaping schemes with snapshot verification.
test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusMetricTests.cs Validates counter naming/suffix behavior under dots/values vs default.
test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusIntegrationTests.cs Extends integration tests to include escaping=dots and escaping=values.
test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusEscapingTests.cs New unit tests for escaping transformations and scheme parsing.
test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusAcceptHeaders.cs Extends Accept-header negotiation test cases for dots/values.
test/OpenTelemetry.Exporter.Prometheus.HttpListener.FuzzTests/PrometheusSerializerFuzzTests.cs Adds property tests to ensure escaping matches reference implementation and buffer overload matches string overload.
test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/PrometheusIntegrationTests.cs Extends ASP.NET Core integration tests to include escaping=dots and escaping=values.
src/OpenTelemetry.Exporter.Prometheus.HttpListener/OpenTelemetry.Exporter.Prometheus.HttpListener.csproj Links IsExternalInit shim into the project.
src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/Shared/Serialization/TextFormatSerializer.cs Adds escaping awareness to serialization; updates tag-writing to use output keys and structural suffix helpers.
src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/Shared/Serialization/PrometheusTextSerializer.cs Uses escaping-specific name sets for metric naming/bytes.
src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/Shared/Serialization/OpenMetricsSerializer.cs Uses escaping-specific name sets for OpenMetrics naming/bytes; updates created metric naming.
src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/Shared/PrometheusProtocol.cs Adds dots/values scheme constants and stores parsed EscapingScheme in the protocol.
src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/Shared/PrometheusMetric.cs Refactors metric naming to compute/caches per-escaping NameSet (underscores eagerly; others lazily).
src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/Shared/PrometheusEscaping.cs New escaping implementation for reversible dots and values schemes.
src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/Shared/PrometheusCollectionManager.cs Adds in modifiers to reduce copying and threads escaping through response generation.
src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/Shared/EscapingScheme.cs New enum describing supported escaping schemes.
src/OpenTelemetry.Exporter.Prometheus.HttpListener/CHANGELOG.md Documents added dots/values negotiation support.
src/OpenTelemetry.Exporter.Prometheus.AspNetCore/CHANGELOG.md Documents added dots/values negotiation support.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@martincostello martincostello added the keep-open Prevents issues and pull requests being closed as stale label Jun 28, 2026
@martincostello martincostello removed the keep-open Prevents issues and pull requests being closed as stale label Jun 29, 2026
- Handle label vs. metric name escaping correctly.
- Fix colliding exemplars with different escapings.
- Fix scope labels not being escaped.
This was referenced Jul 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pkg:OpenTelemetry.Exporter.Prometheus.AspNetCore Issues related to OpenTelemetry.Exporter.Prometheus.AspNetCore NuGet package pkg:OpenTelemetry.Exporter.Prometheus.HttpListener Issues related to OpenTelemetry.Exporter.Prometheus.HttpListener NuGet package spec-compliance Issues related to compliance with the OpenTelemetry Specification

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants