-
Notifications
You must be signed in to change notification settings - Fork 888
[Exporter.Prometheus] Add dots and values escaping #7439
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 9 commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
4883775
[Exporter.Prometheus] Avoid struct copies
martincostello 3cd1eba
[Exporter.Prometheus] Add dots and values escaping
martincostello 977cac7
[Exporter.Prometheus] Fix sorting
martincostello 8549b36
[Exporter.Prometheus] Address feedback
martincostello f42506e
[Exporter.Prometheus] Update test coverage
martincostello 190a68b
[Exporter.Prometheus] Extend tests
martincostello d434d3b
Merge branch 'main' into gh-7246
martincostello f79b46b
Merge branch 'main' into gh-7246
martincostello dbe44d4
Merge remote-tracking branch 'origin/main' into gh-7246
martincostello 8c8a028
[Exporter.Prometheus] Fix CHANGELOGs
martincostello e4bf711
[Exporter.Prometheus] Address feedback
martincostello f86a8c0
Merge branch 'main' into gh-7246
martincostello c7bbf26
[Exporter.Prometheus] Fix merge
martincostello 3fb26e8
[Exporter.Prometheus] Address feedback
martincostello de68faf
[Exporter.Prometheus] Fix typo
martincostello 6351342
[Exporter.Prometheus] Fix dots escaping collisions
Kielek fc9f83e
[Exporter.Prometheus] Fix values escaping prefix collision
Kielek 82450e9
[Exporter.Prometheus] Fix escaping negotiation without version
Kielek ffaee57
[Exporter.Prometheus] Remove redundant code
martincostello File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
30 changes: 30 additions & 0 deletions
30
src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/Shared/EscapingScheme.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| // Copyright The OpenTelemetry Authors | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| namespace OpenTelemetry.Exporter.Prometheus; | ||
|
|
||
| /// <summary> | ||
| /// The Prometheus UTF-8 name escaping scheme used when rendering metric and label names. | ||
| /// See https://prometheus.io/docs/instrumenting/escaping_schemes/. | ||
| /// </summary> | ||
| internal enum EscapingScheme | ||
| { | ||
| /// <summary> | ||
| /// Discouraged characters are replaced with the <c>_</c> character. This is the | ||
| /// OpenTelemetry default and also covers the legacy (pre-1.0.0) text formats which | ||
| /// have no negotiated escaping scheme. | ||
| /// </summary> | ||
| Underscores = 0, | ||
|
|
||
| /// <summary> | ||
| /// Dots are replaced with <c>_dot_</c> and underscores are doubled, allowing the | ||
| /// original name to be recovered by a Prometheus client. | ||
| /// </summary> | ||
| Dots, | ||
|
|
||
| /// <summary> | ||
| /// Names that are not valid legacy names are prefixed with <c>U__</c> and discouraged | ||
| /// characters are encoded as their hexadecimal Unicode code point. | ||
| /// </summary> | ||
| Values, | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
231 changes: 231 additions & 0 deletions
231
src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/Shared/PrometheusEscaping.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,231 @@ | ||
| // Copyright The OpenTelemetry Authors | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| using System.Buffers; | ||
| using System.Diagnostics; | ||
| using System.Text; | ||
|
|
||
| namespace OpenTelemetry.Exporter.Prometheus; | ||
|
|
||
| /// <summary> | ||
| /// Implements the Prometheus name escaping schemes. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// The transformations are faithful to <c>EscapeName</c> in | ||
| /// https://github.com/prometheus/common/blob/43de10cc658055c6b5e4d619edc917d5af493409/model/metric.go | ||
| /// so that a Prometheus client can reverse them. | ||
| /// </remarks> | ||
| internal static class PrometheusEscaping | ||
| { | ||
| // Upper bound on the number of bytes a single UTF-16 character can expand to. "_dot_" is five | ||
| // bytes for the dots scheme and a value-encoded basic-multilingual-plane character ("_FFFD_" | ||
| // or "_xxxx_") is at most six bytes; a surrogate pair expands to at most eight bytes across | ||
| // two characters, so six per character remains a safe bound. | ||
| private const int MaxBytesPerCharacter = 6; | ||
|
|
||
| public static EscapingScheme FromString(string? escaping) => escaping switch | ||
| { | ||
| PrometheusProtocol.DotsEscaping => EscapingScheme.Dots, | ||
| PrometheusProtocol.ValuesEscaping => EscapingScheme.Values, | ||
| _ => EscapingScheme.Underscores, | ||
| }; | ||
|
|
||
| /// <summary> | ||
| /// Escapes a fully-constructed metric or label name (including any unit and type-specific | ||
| /// suffixes) according to the supplied scheme. The name MUST be escaped as a single unit | ||
| /// so that the structural underscores introduced when building the name are doubled and | ||
| /// can be reversed by a client. | ||
| /// </summary> | ||
| /// <param name="name">The name to escape.</param> | ||
| /// <param name="scheme">The escaping scheme to apply.</param> | ||
| /// <returns>The escaped name. Always a valid legacy (ASCII) name for the dots and values schemes.</returns> | ||
| public static string EscapeName(string name, EscapingScheme scheme) | ||
| { | ||
| if (string.IsNullOrEmpty(name) || scheme == EscapingScheme.Underscores) | ||
| { | ||
| return name; | ||
| } | ||
|
|
||
| if (scheme == EscapingScheme.Values && IsValidLegacyName(name)) | ||
| { | ||
| return name; | ||
| } | ||
|
|
||
| var rented = ArrayPool<byte>.Shared.Rent((scheme == EscapingScheme.Values ? 3 : 0) + (name.Length * MaxBytesPerCharacter)); | ||
|
|
||
| try | ||
| { | ||
| var length = EscapeName(rented, 0, name, scheme); | ||
| #if NET | ||
| return Encoding.ASCII.GetString(rented.AsSpan(0, length)); | ||
| #else | ||
| return Encoding.ASCII.GetString(rented, 0, length); | ||
| #endif | ||
| } | ||
| finally | ||
| { | ||
| ArrayPool<byte>.Shared.Return(rented); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Escapes a fully-constructed metric or label name directly into <paramref name="buffer"/> | ||
| /// according to the supplied scheme, avoiding an intermediate string allocation. The scheme | ||
| /// MUST be <see cref="EscapingScheme.Dots"/> or <see cref="EscapingScheme.Values"/>; the | ||
| /// underscores scheme is handled using the pre-computed names. | ||
| /// </summary> | ||
| /// <param name="buffer">The buffer to write to.</param> | ||
| /// <param name="cursor">The current position in the buffer.</param> | ||
| /// <param name="name">The name to escape.</param> | ||
| /// <param name="scheme">The escaping scheme to apply.</param> | ||
| /// <returns>The new cursor position after writing the (ASCII) escaped name.</returns> | ||
| public static int EscapeName(byte[] buffer, int cursor, string name, EscapingScheme scheme) | ||
| { | ||
| Debug.Assert(scheme is EscapingScheme.Dots or EscapingScheme.Values, $"Unexpected escaping scheme: {scheme}"); | ||
|
|
||
| if (string.IsNullOrEmpty(name)) | ||
| { | ||
| return cursor; | ||
| } | ||
|
|
||
| if (scheme == EscapingScheme.Values && IsValidLegacyName(name)) | ||
| { | ||
| return WriteAscii(buffer, cursor, name); | ||
| } | ||
|
|
||
| if (scheme == EscapingScheme.Values) | ||
| { | ||
| cursor = WriteAscii(buffer, cursor, "U__"); | ||
| } | ||
|
|
||
| var index = 0; | ||
|
|
||
| while (index < name.Length) | ||
| { | ||
| var codePoint = GetCodePoint(name, index, out var charsConsumed, out var isValidRune); | ||
|
|
||
| if (codePoint == '_') | ||
| { | ||
| cursor = WriteAscii(buffer, cursor, "__"); | ||
| } | ||
| else if (scheme == EscapingScheme.Dots && codePoint == '.') | ||
| { | ||
| cursor = WriteAscii(buffer, cursor, "_dot_"); | ||
| } | ||
| else if (IsValidLegacyRune(codePoint, index == 0)) | ||
| { | ||
| buffer[cursor++] = unchecked((byte)codePoint); | ||
| } | ||
| else if (scheme == EscapingScheme.Dots) | ||
| { | ||
| cursor = WriteAscii(buffer, cursor, "__"); | ||
| } | ||
| else if (!isValidRune) | ||
| { | ||
| cursor = WriteAscii(buffer, cursor, "_FFFD_"); | ||
| } | ||
| else | ||
| { | ||
| cursor = WriteHexCodePoint(buffer, cursor, codePoint); | ||
| } | ||
|
|
||
| index += charsConsumed; | ||
| } | ||
|
|
||
| return cursor; | ||
| } | ||
|
|
||
| private static bool IsValidLegacyName(string name) | ||
| { | ||
| var index = 0; | ||
|
|
||
| while (index < name.Length) | ||
| { | ||
| var codePoint = GetCodePoint(name, index, out var charsConsumed, out _); | ||
|
|
||
| if (!IsValidLegacyRune(codePoint, index == 0)) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| index += charsConsumed; | ||
| } | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| private static bool IsValidLegacyRune(int codePoint, bool isFirst) => | ||
| codePoint is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z') or '_' or ':' || | ||
| (!isFirst && codePoint is >= '0' and <= '9'); | ||
|
|
||
| private static int GetCodePoint(string value, int index, out int charsConsumed, out bool isValidRune) | ||
| { | ||
| #if NET | ||
| var status = Rune.DecodeFromUtf16(value.AsSpan(index), out var rune, out charsConsumed); | ||
|
|
||
| isValidRune = status == OperationStatus.Done; | ||
|
|
||
| return rune.Value; | ||
| #else | ||
| const int UnicodeReplacementCharacter = 0xFFFD; | ||
|
|
||
| var character = value[index]; | ||
|
|
||
| if (char.IsHighSurrogate(character) && | ||
| index + 1 < value.Length && | ||
| char.IsLowSurrogate(value[index + 1])) | ||
| { | ||
| charsConsumed = 2; | ||
| isValidRune = true; | ||
| return char.ConvertToUtf32(character, value[index + 1]); | ||
| } | ||
|
|
||
| if (char.IsSurrogate(character)) | ||
| { | ||
| // An unpaired surrogate is not a valid Unicode scalar value. | ||
| charsConsumed = 1; | ||
| isValidRune = false; | ||
| return UnicodeReplacementCharacter; | ||
| } | ||
|
|
||
| charsConsumed = 1; | ||
| isValidRune = true; | ||
|
|
||
| return character; | ||
| #endif | ||
| } | ||
|
|
||
| private static int WriteAscii(byte[] buffer, int cursor, string value) | ||
| { | ||
| for (var i = 0; i < value.Length; i++) | ||
| { | ||
| buffer[cursor++] = unchecked((byte)value[i]); | ||
| } | ||
|
|
||
| return cursor; | ||
| } | ||
|
|
||
| private static int WriteHexCodePoint(byte[] buffer, int cursor, int codePoint) | ||
| { | ||
| // Writes "_<lowercase-hex>_" with no leading zeros, matching the reference implementation's | ||
| // use of strconv.FormatInt(value, 16). Code points are at most 0x10FFFF (six hex digits). | ||
| buffer[cursor++] = unchecked((byte)'_'); | ||
|
|
||
| var started = false; | ||
|
|
||
| for (var shift = 20; shift >= 0; shift -= 4) | ||
| { | ||
| var nibble = (codePoint >> shift) & 0xF; | ||
|
|
||
| if (nibble != 0 || started || shift == 0) | ||
| { | ||
| buffer[cursor++] = unchecked((byte)(nibble < 10 ? '0' + nibble : 'a' + (nibble - 10))); | ||
| started = true; | ||
| } | ||
| } | ||
|
|
||
| buffer[cursor++] = unchecked((byte)'_'); | ||
|
|
||
| return cursor; | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.