From ac79c1ce1785b64cf5cdac7929d10c6e9df6377c Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Tue, 8 Aug 2023 20:31:48 +0800 Subject: [PATCH 01/10] Update Prometheus exporter name and unit processing --- ...etry.Exporter.Prometheus.AspNetCore.csproj | 3 + .../Internal/PrometheusMetric.cs | 263 ++++++++++++++++++ .../Internal/PrometheusSerializer.cs | 65 +---- .../Internal/PrometheusSerializerExt.cs | 67 ++++- .../Internal/PrometheusType.cs | 45 +++ ...ry.Exporter.Prometheus.HttpListener.csproj | 1 + .../PrometheusExporterMiddlewareTests.cs | 4 +- .../PrometheusHttpListenerTests.cs | 2 +- .../PrometheusMetricTests.cs | 220 +++++++++++++++ .../PrometheusSerializerTests.cs | 4 +- 10 files changed, 608 insertions(+), 66 deletions(-) create mode 100644 src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusMetric.cs create mode 100644 src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusType.cs create mode 100644 test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusMetricTests.cs diff --git a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/OpenTelemetry.Exporter.Prometheus.AspNetCore.csproj b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/OpenTelemetry.Exporter.Prometheus.AspNetCore.csproj index 7fa02cfdb93..bbc2c66c5e8 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/OpenTelemetry.Exporter.Prometheus.AspNetCore.csproj +++ b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/OpenTelemetry.Exporter.Prometheus.AspNetCore.csproj @@ -16,6 +16,7 @@ Remove this property once we have released a stable version.--> false + 11.0 @@ -26,6 +27,8 @@ + + diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusMetric.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusMetric.cs new file mode 100644 index 00000000000..2e0e6be45ac --- /dev/null +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusMetric.cs @@ -0,0 +1,263 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Text; + +namespace OpenTelemetry.Exporter.Prometheus; + +internal class PrometheusMetric +{ + public PrometheusMetric(string name, string unit, PrometheusType type) + { + // The metric name is + // required to match the regex: `[a-zA-Z_:]([a-zA-Z0-9_:])*`. Invalid characters + // in the metric name MUST be replaced with the `_` character. Multiple + // consecutive `_` characters MUST be replaced with a single `_` character. + // https://github.com/open-telemetry/opentelemetry-specification/blob/b2f923fb1650dde1f061507908b834035506a796/specification/compatibility/prometheus_and_openmetrics.md#L230-L233 + var sanitizedName = SanitizeMetricName(name); + + string sanitizedUnit = null; + if (!string.IsNullOrEmpty(unit)) + { + sanitizedUnit = GetUnit(unit); + + // The resulting unit SHOULD be added to the metric as + // [OpenMetrics UNIT metadata](https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#metricfamily) + // and as a suffix to the metric name unless the metric name already contains the + // unit, or the unit MUST be omitted. The unit suffix comes before any + // type-specific suffixes. + // https://github.com/open-telemetry/opentelemetry-specification/blob/b2f923fb1650dde1f061507908b834035506a796/specification/compatibility/prometheus_and_openmetrics.md#L242-L246 + if (!sanitizedName.Contains(sanitizedUnit)) + { + sanitizedName = sanitizedName + "_" + sanitizedUnit; + } + } + + // If the metric name for monotonic Sum metric points does not end in a suffix of `_total` a suffix of `_total` MUST be added by default, otherwise the name MUST remain unchanged. + // Exporters SHOULD provide a configuration option to disable the addition of `_total` suffixes. + // https://github.com/open-telemetry/opentelemetry-specification/blob/b2f923fb1650dde1f061507908b834035506a796/specification/compatibility/prometheus_and_openmetrics.md#L286 + if (type == PrometheusType.Counter && !sanitizedName.Contains("total")) + { + sanitizedName += "_total"; + } + + // Special case: Converting "1" to "ratio". + // https://github.com/open-telemetry/opentelemetry-specification/blob/b2f923fb1650dde1f061507908b834035506a796/specification/compatibility/prometheus_and_openmetrics.md#L239 + if (type == PrometheusType.Gauge && unit == "1" && !sanitizedName.Contains("ratio")) + { + sanitizedName += "_ratio"; + } + + this.Name = sanitizedName; + this.Unit = sanitizedUnit; + } + + public string Name { get; } + + public string Unit { get; } + + internal static string SanitizeMetricName(string metricName) + { + StringBuilder sb = null; + var lastCharUnderscore = false; + + for (var i = 0; i < metricName.Length; i++) + { + var c = metricName[i]; + + if (i == 0 && char.IsNumber(c)) + { + sb ??= CreateStringBuilder(metricName); + sb.Append('_'); + lastCharUnderscore = true; + continue; + } + + if (!char.IsLetterOrDigit(c) && c != ':') + { + if (!lastCharUnderscore) + { + lastCharUnderscore = true; + sb ??= CreateStringBuilder(metricName); + sb.Append('_'); + } + } + else + { + sb ??= CreateStringBuilder(metricName); + sb.Append(c); + lastCharUnderscore = false; + } + } + + return sb?.ToString() ?? metricName; + + static StringBuilder CreateStringBuilder(string name) => new StringBuilder(name.Length); + } + + internal static string RemoveAnnotations(string unit) + { + StringBuilder sb = null; + + var hasOpenBrace = false; + var startOpenBraceIndex = 0; + var lastWriteIndex = 0; + + for (var i = 0; i < unit.Length; i++) + { + var c = unit[i]; + if (c == '{') + { + if (!hasOpenBrace) + { + hasOpenBrace = true; + startOpenBraceIndex = i; + } + } + else if (c == '}') + { + if (hasOpenBrace) + { + sb ??= new StringBuilder(); + sb.Append(unit, lastWriteIndex, startOpenBraceIndex - lastWriteIndex); + hasOpenBrace = false; + lastWriteIndex = i + 1; + } + } + } + + if (lastWriteIndex == 0) + { + return unit; + } + + sb.Append(unit, lastWriteIndex, unit.Length - lastWriteIndex); + return sb.ToString(); + } + + private static string GetUnit(string unit) + { + // Dropping the portions of the Unit within brackets (e.g. {packet}). Brackets MUST NOT be included in the resulting unit. A "count of foo" is considered unitless in Prometheus. + // https://github.com/open-telemetry/opentelemetry-specification/blob/b2f923fb1650dde1f061507908b834035506a796/specification/compatibility/prometheus_and_openmetrics.md#L238 + var updatedUnit = RemoveAnnotations(unit); + + // Converting "foo/bar" to "foo_per_bar". + // https://github.com/open-telemetry/opentelemetry-specification/blob/b2f923fb1650dde1f061507908b834035506a796/specification/compatibility/prometheus_and_openmetrics.md#L240C3-L240C41 + if (TryProcessRateUnits(updatedUnit, out var updatedPerUnit)) + { + updatedUnit = updatedPerUnit; + } + else + { + // Converting from abbreviations to full words (e.g. "ms" to "milliseconds"). + // https://github.com/open-telemetry/opentelemetry-specification/blob/b2f923fb1650dde1f061507908b834035506a796/specification/compatibility/prometheus_and_openmetrics.md#L237 + updatedUnit = MapUnit(updatedUnit.AsSpan()); + } + + return updatedUnit; + } + + private static bool TryProcessRateUnits(string updatedUnit, out string updatedPerUnit) + { + updatedPerUnit = null; + + for (int i = 0; i < updatedUnit.Length; i++) + { + if (updatedUnit[i] == '/') + { + // Only convert rate expressed units if it's a valid expression. + if (i == updatedUnit.Length - 1) + { + return false; + } + + updatedPerUnit = MapUnit(updatedUnit.AsSpan(0, i)) + "_per_" + MapPerUnit(updatedUnit.AsSpan(i + 1, updatedUnit.Length - i - 1)); + return true; + } + } + + return false; + } + + // The map to translate OTLP units to Prometheus units + // OTLP metrics use the c/s notation as specified at https://ucum.org/ucum.html + // (See also https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/semantic_conventions/README.md#instrument-units) + // Prometheus best practices for units: https://prometheus.io/docs/practices/naming/#base-units + // OpenMetrics specification for units: https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#units-and-base-units + private static string MapUnit(ReadOnlySpan unit) + { + return unit switch + { + // Time + "d" => "days", + "h" => "hours", + "min" => "minutes", + "s" => "seconds", + "ms" => "milliseconds", + "us" => "microseconds", + "ns" => "nanoseconds", + + // Bytes + "By" => "bytes", + "KiBy" => "kibibytes", + "MiBy" => "mebibytes", + "GiBy" => "gibibytes", + "TiBy" => "tibibytes", + "KBy" => "kilobytes", + "MBy" => "megabytes", + "GBy" => "gigabytes", + "TBy" => "terabytes", + "B" => "bytes", + "KB" => "kilobytes", + "MB" => "megabytes", + "GB" => "gigabytes", + "TB" => "terabytes", + + // SI + "m" => "meters", + "V" => "volts", + "A" => "amperes", + "J" => "joules", + "W" => "watts", + "g" => "grams", + + // Misc + "Cel" => "celsius", + "Hz" => "hertz", + "1" => string.Empty, + "%" => "percent", + "$" => "dollars", + _ => unit.ToString(), + }; + } + + // The map that translates the "per" unit + // Example: s => per second (singular) + private static string MapPerUnit(ReadOnlySpan perUnit) + { + return perUnit switch + { + "s" => "second", + "m" => "minute", + "h" => "hour", + "d" => "day", + "w" => "week", + "mo" => "month", + "y" => "year", + _ => perUnit.ToString(), + }; + } +} diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializer.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializer.cs index 611da872b64..c370feabf9e 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializer.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializer.cs @@ -229,39 +229,12 @@ public static int WriteLabel(byte[] buffer, int cursor, string labelKey, object } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int WriteMetricName(byte[] buffer, int cursor, string metricName, string metricUnit = null) + public static int WriteMetricName(byte[] buffer, int cursor, PrometheusMetric metric) { - Debug.Assert(!string.IsNullOrEmpty(metricName), $"{nameof(metricName)} should not be null or empty."); - - for (int i = 0; i < metricName.Length; i++) - { - var ordinal = (ushort)metricName[i]; - buffer[cursor++] = ordinal switch - { - ASCII_FULL_STOP or ASCII_HYPHEN_MINUS => unchecked((byte)'_'), - _ => unchecked((byte)ordinal), - }; - } - - if (!string.IsNullOrEmpty(metricUnit)) + for (int i = 0; i < metric.Name.Length; i++) { - buffer[cursor++] = unchecked((byte)'_'); - - for (int i = 0; i < metricUnit.Length; i++) - { - var ordinal = (ushort)metricUnit[i]; - - if ((ordinal >= (ushort)'A' && ordinal <= (ushort)'Z') || - (ordinal >= (ushort)'a' && ordinal <= (ushort)'z') || - (ordinal >= (ushort)'0' && ordinal <= (ushort)'9')) - { - buffer[cursor++] = unchecked((byte)ordinal); - } - else - { - buffer[cursor++] = unchecked((byte)'_'); - } - } + var ordinal = (ushort)metric.Name[i]; + buffer[cursor++] = unchecked((byte)ordinal); } return cursor; @@ -277,7 +250,7 @@ public static int WriteEof(byte[] buffer, int cursor) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int WriteHelpMetadata(byte[] buffer, int cursor, string metricName, string metricUnit, string metricDescription) + public static int WriteHelpMetadata(byte[] buffer, int cursor, PrometheusMetric metric, string metricDescription) { if (string.IsNullOrEmpty(metricDescription)) { @@ -285,7 +258,7 @@ public static int WriteHelpMetadata(byte[] buffer, int cursor, string metricName } cursor = WriteAsciiStringNoEscape(buffer, cursor, "# HELP "); - cursor = WriteMetricName(buffer, cursor, metricName, metricUnit); + cursor = WriteMetricName(buffer, cursor, metric); if (!string.IsNullOrEmpty(metricDescription)) { @@ -299,12 +272,12 @@ public static int WriteHelpMetadata(byte[] buffer, int cursor, string metricName } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int WriteTypeMetadata(byte[] buffer, int cursor, string metricName, string metricUnit, string metricType) + public static int WriteTypeMetadata(byte[] buffer, int cursor, PrometheusMetric metric, string metricType) { Debug.Assert(!string.IsNullOrEmpty(metricType), $"{nameof(metricType)} should not be null or empty."); cursor = WriteAsciiStringNoEscape(buffer, cursor, "# TYPE "); - cursor = WriteMetricName(buffer, cursor, metricName, metricUnit); + cursor = WriteMetricName(buffer, cursor, metric); buffer[cursor++] = unchecked((byte)' '); cursor = WriteAsciiStringNoEscape(buffer, cursor, metricType); @@ -314,32 +287,22 @@ public static int WriteTypeMetadata(byte[] buffer, int cursor, string metricName } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int WriteUnitMetadata(byte[] buffer, int cursor, string metricName, string metricUnit) + public static int WriteUnitMetadata(byte[] buffer, int cursor, PrometheusMetric metric) { - if (string.IsNullOrEmpty(metricUnit)) + if (string.IsNullOrEmpty(metric.Unit)) { return cursor; } cursor = WriteAsciiStringNoEscape(buffer, cursor, "# UNIT "); - cursor = WriteMetricName(buffer, cursor, metricName, metricUnit); + cursor = WriteMetricName(buffer, cursor, metric); buffer[cursor++] = unchecked((byte)' '); - for (int i = 0; i < metricUnit.Length; i++) + for (int i = 0; i < metric.Unit.Length; i++) { - var ordinal = (ushort)metricUnit[i]; - - if ((ordinal >= (ushort)'A' && ordinal <= (ushort)'Z') || - (ordinal >= (ushort)'a' && ordinal <= (ushort)'z') || - (ordinal >= (ushort)'0' && ordinal <= (ushort)'9')) - { - buffer[cursor++] = unchecked((byte)ordinal); - } - else - { - buffer[cursor++] = unchecked((byte)'_'); - } + var ordinal = (ushort)metric.Unit[i]; + buffer[cursor++] = unchecked((byte)ordinal); } buffer[cursor++] = ASCII_LINEFEED; diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializerExt.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializerExt.cs index e1fea7093bb..71a5b07c8c6 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializerExt.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializerExt.cs @@ -14,6 +14,7 @@ // limitations under the License. // +using System.Collections.Concurrent; using OpenTelemetry.Metrics; namespace OpenTelemetry.Exporter.Prometheus; @@ -23,17 +24,40 @@ namespace OpenTelemetry.Exporter.Prometheus; /// internal static partial class PrometheusSerializer { + private const int MaxCachedMetrics = 1024; + /* Counter becomes counter Gauge becomes gauge Histogram becomes histogram UpDownCounter becomes gauge * https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/data-model.md#otlp-metric-points-to-prometheus */ - private static readonly string[] MetricTypes = new string[] + private static readonly PrometheusType[] MetricTypes = new PrometheusType[] { - "untyped", "counter", "gauge", "summary", "histogram", "histogram", "histogram", "histogram", "gauge", + PrometheusType.Untyped, PrometheusType.Counter, PrometheusType.Gauge, PrometheusType.Summary, PrometheusType.Histogram, PrometheusType.Histogram, PrometheusType.Histogram, PrometheusType.Histogram, PrometheusType.Gauge, }; + private static string MapPrometheusType(PrometheusType type) + { + return type switch + { + PrometheusType.Gauge => "gauge", + PrometheusType.Counter => "counter", + PrometheusType.Summary => "summary", + PrometheusType.Histogram => "histogram", + _ => "untyped", + }; + } + + private static PrometheusType GetPrometheusType(Metric metric) + { + int metricType = (int)metric.MetricType >> 4; + return MetricTypes[metricType]; + } + + private static readonly ConcurrentDictionary MetricsCache = new ConcurrentDictionary(); + private static int metricsCacheCount; + public static int WriteMetric(byte[] buffer, int cursor, Metric metric) { if (metric.MetricType == MetricType.ExponentialHistogram) @@ -43,10 +67,12 @@ public static int WriteMetric(byte[] buffer, int cursor, Metric metric) return cursor; } - int metricType = (int)metric.MetricType >> 4; - cursor = WriteTypeMetadata(buffer, cursor, metric.Name, metric.Unit, MetricTypes[metricType]); - cursor = WriteUnitMetadata(buffer, cursor, metric.Name, metric.Unit); - cursor = WriteHelpMetadata(buffer, cursor, metric.Name, metric.Unit, metric.Description); + PrometheusType prometheusType = GetPrometheusType(metric); + var prometheusMetric = GetPrometheusMetric(metric); + + cursor = WriteTypeMetadata(buffer, cursor, prometheusMetric, MapPrometheusType(prometheusType)); + cursor = WriteUnitMetadata(buffer, cursor, prometheusMetric); + cursor = WriteHelpMetadata(buffer, cursor, prometheusMetric, metric.Description); if (!metric.MetricType.IsHistogram()) { @@ -56,7 +82,7 @@ public static int WriteMetric(byte[] buffer, int cursor, Metric metric) var timestamp = metricPoint.EndTime.ToUnixTimeMilliseconds(); // Counter and Gauge - cursor = WriteMetricName(buffer, cursor, metric.Name, metric.Unit); + cursor = WriteMetricName(buffer, cursor, prometheusMetric); if (tags.Count > 0) { @@ -118,7 +144,7 @@ public static int WriteMetric(byte[] buffer, int cursor, Metric metric) { totalCount += histogramMeasurement.BucketCount; - cursor = WriteMetricName(buffer, cursor, metric.Name, metric.Unit); + cursor = WriteMetricName(buffer, cursor, prometheusMetric); cursor = WriteAsciiStringNoEscape(buffer, cursor, "_bucket{"); foreach (var tag in tags) @@ -149,7 +175,7 @@ public static int WriteMetric(byte[] buffer, int cursor, Metric metric) } // Histogram sum - cursor = WriteMetricName(buffer, cursor, metric.Name, metric.Unit); + cursor = WriteMetricName(buffer, cursor, prometheusMetric); cursor = WriteAsciiStringNoEscape(buffer, cursor, "_sum"); if (tags.Count > 0) @@ -175,7 +201,7 @@ public static int WriteMetric(byte[] buffer, int cursor, Metric metric) buffer[cursor++] = ASCII_LINEFEED; // Histogram count - cursor = WriteMetricName(buffer, cursor, metric.Name, metric.Unit); + cursor = WriteMetricName(buffer, cursor, prometheusMetric); cursor = WriteAsciiStringNoEscape(buffer, cursor, "_count"); if (tags.Count > 0) @@ -206,4 +232,25 @@ public static int WriteMetric(byte[] buffer, int cursor, Metric metric) return cursor; } + + private static PrometheusMetric GetPrometheusMetric(Metric metric) + { + if (metricsCacheCount >= MaxCachedMetrics) + { + if (!MetricsCache.TryGetValue(metric, out var formatter)) + { + formatter = new PrometheusMetric(metric.Name, metric.Unit, GetPrometheusType(metric)); + } + + return formatter; + } + else + { + return MetricsCache.GetOrAdd(metric, m => + { + Interlocked.Increment(ref metricsCacheCount); + return new PrometheusMetric(m.Name, m.Unit, GetPrometheusType(metric)); + }); + } + } } diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusType.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusType.cs new file mode 100644 index 00000000000..22aeb32ee0c --- /dev/null +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusType.cs @@ -0,0 +1,45 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace OpenTelemetry.Exporter.Prometheus; + +internal enum PrometheusType +{ + /// + /// Not mapped. + /// + Untyped, + + /// + /// Mapped from Guage and UpDownCounter. + /// + Gauge, + + /// + /// Mapped from Counter. + /// + Counter, + + /// + /// Not mapped. + /// + Summary, + + /// + /// Mapped from Histogram. + /// + Histogram, +} diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/OpenTelemetry.Exporter.Prometheus.HttpListener.csproj b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/OpenTelemetry.Exporter.Prometheus.HttpListener.csproj index 08e4db4cd66..3e258a637dc 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/OpenTelemetry.Exporter.Prometheus.HttpListener.csproj +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/OpenTelemetry.Exporter.Prometheus.HttpListener.csproj @@ -5,6 +5,7 @@ Stand-alone HttpListener for hosting OpenTelemetry .NET Prometheus Exporter $(PackageTags);prometheus;metrics core- + 11.0 disable diff --git a/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/PrometheusExporterMiddlewareTests.cs b/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/PrometheusExporterMiddlewareTests.cs index 171b26500ee..c863b08ed78 100644 --- a/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/PrometheusExporterMiddlewareTests.cs +++ b/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/PrometheusExporterMiddlewareTests.cs @@ -298,8 +298,8 @@ private static async Task RunPrometheusExporterMiddlewareIntegrationTest( var matches = Regex.Matches( content, ("^" - + "# TYPE counter_double counter\n" - + "counter_double{key1='value1',key2='value2'} 101.17 (\\d+)\n" + + "# TYPE counter_double_total counter\n" + + "counter_double_total{key1='value1',key2='value2'} 101.17 (\\d+)\n" + "\n" + "# EOF\n" + "$").Replace('\'', '"')); diff --git a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerTests.cs b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerTests.cs index 7f7889af4e5..1d2969f9b19 100644 --- a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerTests.cs +++ b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerTests.cs @@ -132,7 +132,7 @@ private async Task RunPrometheusExporterHttpServerIntegrationTest(bool skipMetri Assert.Equal("text/plain; charset=utf-8; version=0.0.4", response.Content.Headers.ContentType.ToString()); Assert.Matches( - "^# TYPE counter_double counter\ncounter_double{key1='value1',key2='value2'} 101.17 \\d+\n\n# EOF\n$".Replace('\'', '"'), + "^# TYPE counter_double_total counter\ncounter_double_total{key1='value1',key2='value2'} 101.17 \\d+\n\n# EOF\n$".Replace('\'', '"'), await response.Content.ReadAsStringAsync().ConfigureAwait(false)); } else diff --git a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusMetricTests.cs b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusMetricTests.cs new file mode 100644 index 00000000000..f1004ab9adb --- /dev/null +++ b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusMetricTests.cs @@ -0,0 +1,220 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using Xunit; + +namespace OpenTelemetry.Exporter.Prometheus.Tests; + +public sealed class PrometheusMetricTests +{ + [Fact] + public void SanitizeMetricName_Valid() + { + AssertSanitizeMetricName("active_directory_ds_replication_network_io", "active_directory_ds_replication_network_io"); + } + + [Fact] + public void SanitizeMetricName_RemoveConsecutiveUnderscores() + { + AssertSanitizeMetricName("cpu_sp__d_hertz", "cpu_sp_d_hertz"); + } + + [Fact] + public void SanitizeMetricName_SupportLeadingAndTrailingUnderscores() + { + AssertSanitizeMetricName("_cpu_speed_hertz_", "_cpu_speed_hertz_"); + } + + [Fact] + public void SanitizeMetricName_RemoveUnsupportedChracters() + { + AssertSanitizeMetricName("metric_unit_$1000", "metric_unit_1000"); + } + + [Fact] + public void SanitizeMetricName_RemoveWhitespace() + { + AssertSanitizeMetricName("unit include", "unit_include"); + } + + [Fact] + public void SanitizeMetricName_RemoveMultipleUnsupportedChracters() + { + AssertSanitizeMetricName("sample_me%%$$$_count_ !!@unit include", "sample_me_count_unit_include"); + } + + [Fact] + public void SanitizeMetricName_RemoveStartingNumber() + { + AssertSanitizeMetricName("1_some_metric_name", "_some_metric_name"); + } + + [Fact] + public void SanitizeMetricName_SupportColon() + { + AssertSanitizeMetricName("sample_metric_name__:_per_meter", "sample_metric_name_:_per_meter"); + } + + [Fact] + public void Unit_Annotation_None() + { + Assert.Equal("Test", PrometheusMetric.RemoveAnnotations("Test")); + } + + [Fact] + public void Unit_Annotation_RemoveLeading() + { + Assert.Equal("%", PrometheusMetric.RemoveAnnotations("%{percentage}")); + } + + [Fact] + public void Unit_Annotation_RemoveTrailing() + { + Assert.Equal("%", PrometheusMetric.RemoveAnnotations("{percentage}%")); + } + + [Fact] + public void Unit_Annotation_RemoveLeadingAndTrailing() + { + Assert.Equal("%", PrometheusMetric.RemoveAnnotations("{percentage}%{percentage}")); + } + + [Fact] + public void Unit_Annotation_RemoveMiddle() + { + Assert.Equal("startend", PrometheusMetric.RemoveAnnotations("start{percentage}end")); + } + + [Fact] + public void Unit_Annotation_RemoveEverything() + { + Assert.Equal(string.Empty, PrometheusMetric.RemoveAnnotations("{percentage}")); + } + + [Fact] + public void Unit_Annotation_Multiple_RemoveEverything() + { + Assert.Equal(string.Empty, PrometheusMetric.RemoveAnnotations("{one}{two}")); + } + + [Fact] + public void Unit_Annotation_NoClose() + { + Assert.Equal("{one", PrometheusMetric.RemoveAnnotations("{one")); + } + + [Fact] + public void Unit_AnnotationMismatch_NoClose() + { + Assert.Equal("}", PrometheusMetric.RemoveAnnotations("{{one}}")); + } + + [Fact] + public void Unit_AnnotationMismatch_Close() + { + Assert.Equal(string.Empty, PrometheusMetric.RemoveAnnotations("{{one}")); + } + + [Fact] + public void Name_SpecialCaseGuage_AppendRatio() + { + AssertName("sample", "1", PrometheusType.Gauge, "sample_ratio"); + } + + [Fact] + public void Name_GuageWithUnit_NoAppendRatio() + { + AssertName("sample", "unit", PrometheusType.Gauge, "sample_unit"); + } + + [Fact] + public void Name_SpecialCaseCounter_AppendTotal() + { + AssertName("sample", "unit", PrometheusType.Counter, "sample_unit_total"); + } + + [Fact] + public void Name_SpecialCaseCounterWithoutUnit_DropUnitAppendTotal() + { + AssertName("sample", "1", PrometheusType.Counter, "sample_total"); + } + + [Fact] + public void Name_SpecialCaseCounterWithNumber_AppendTotal() + { + AssertName("sample", "2", PrometheusType.Counter, "sample_2_total"); + } + + [Fact] + public void Name_UnsupportedMetricNameChars_Drop() + { + AssertName("s%%ple", "%/m", PrometheusType.Summary, "s_ple_percent_per_minute"); + } + + [Fact] + public void Name_UnitOtherThanOne_Normal() + { + AssertName("metric_name", "2", PrometheusType.Summary, "metric_name_2"); + } + + [Fact] + public void Name_UnitAlreadyPresentInName_NotAppended() + { + AssertName("metric_name_total", "total", PrometheusType.Counter, "metric_name_total"); + } + + [Fact] + public void Name_UnitAlreadyPresentInName_TotalNonCounterType_NotAppended() + { + AssertName("metric_name_total", "total", PrometheusType.Summary, "metric_name_total"); + } + + [Fact] + public void Name_UnitAlreadyPresentInName_CustomGauge_NotAppended() + { + AssertName("metric_hertz", "hertz", PrometheusType.Gauge, "metric_hertz"); + } + + [Fact] + public void Name_UnitAlreadyPresentInName_CustomCounter_NotAppended() + { + AssertName("metric_hertz_total", "hertz_total", PrometheusType.Counter, "metric_hertz_total"); + } + + [Fact] + public void Name_UnitAlreadyPresentInName_OrderMatters_Appended() + { + AssertName("metric_total_hertz", "hertz_total", PrometheusType.Counter, "metric_total_hertz_hertz_total"); + } + + [Fact] + public void Name_StartWithNumber_UnderscoreStart() + { + AssertName("2_metric_name", "By", PrometheusType.Summary, "_metric_name_bytes"); + } + + private static void AssertName(string name, string unit, PrometheusType type, string expected) + { + var prometheusMetric = new PrometheusMetric(name, unit, type); + Assert.Equal(expected, prometheusMetric.Name); + } + + private static void AssertSanitizeMetricName(string name, string expected) + { + var sanatizedName = PrometheusMetric.SanitizeMetricName(name); + Assert.Equal(expected, sanatizedName); + } +} diff --git a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusSerializerTests.cs b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusSerializerTests.cs index e2c201caac2..d60f979a844 100644 --- a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusSerializerTests.cs +++ b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusSerializerTests.cs @@ -208,8 +208,8 @@ public void SumDoubleInfinities() var cursor = PrometheusSerializer.WriteMetric(buffer, 0, metrics[0]); Assert.Matches( ("^" - + "# TYPE test_counter counter\n" - + "test_counter \\+Inf \\d+\n" + + "# TYPE test_counter_total counter\n" + + "test_counter_total \\+Inf \\d+\n" + "$").Replace('\'', '"'), Encoding.UTF8.GetString(buffer, 0, cursor)); } From 1b041cc7a6b667e821d894f7cc806a07381f8160 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Tue, 8 Aug 2023 20:37:28 +0800 Subject: [PATCH 02/10] Clean up --- .../Internal/PrometheusMetric.cs | 3 ++ .../Internal/PrometheusSerializer.cs | 6 ++- .../Internal/PrometheusSerializerExt.cs | 39 +++++++++---------- 3 files changed, 27 insertions(+), 21 deletions(-) diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusMetric.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusMetric.cs index 2e0e6be45ac..806171aeca8 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusMetric.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusMetric.cs @@ -63,12 +63,15 @@ public PrometheusMetric(string name, string unit, PrometheusType type) this.Name = sanitizedName; this.Unit = sanitizedUnit; + this.Type = type; } public string Name { get; } public string Unit { get; } + public PrometheusType Type { get; } + internal static string SanitizeMetricName(string metricName) { StringBuilder sb = null; diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializer.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializer.cs index c370feabf9e..41651e99096 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializer.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializer.cs @@ -231,6 +231,7 @@ public static int WriteLabel(byte[] buffer, int cursor, string labelKey, object [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int WriteMetricName(byte[] buffer, int cursor, PrometheusMetric metric) { + // Metric name has already been escaped. for (int i = 0; i < metric.Name.Length; i++) { var ordinal = (ushort)metric.Name[i]; @@ -272,8 +273,10 @@ public static int WriteHelpMetadata(byte[] buffer, int cursor, PrometheusMetric } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int WriteTypeMetadata(byte[] buffer, int cursor, PrometheusMetric metric, string metricType) + public static int WriteTypeMetadata(byte[] buffer, int cursor, PrometheusMetric metric) { + var metricType = MapPrometheusType(metric.Type); + Debug.Assert(!string.IsNullOrEmpty(metricType), $"{nameof(metricType)} should not be null or empty."); cursor = WriteAsciiStringNoEscape(buffer, cursor, "# TYPE "); @@ -299,6 +302,7 @@ public static int WriteUnitMetadata(byte[] buffer, int cursor, PrometheusMetric buffer[cursor++] = unchecked((byte)' '); + // Unit name has already been escaped. for (int i = 0; i < metric.Unit.Length; i++) { var ordinal = (ushort)metric.Unit[i]; diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializerExt.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializerExt.cs index 71a5b07c8c6..ee963ce343b 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializerExt.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializerExt.cs @@ -37,24 +37,6 @@ UpDownCounter becomes gauge PrometheusType.Untyped, PrometheusType.Counter, PrometheusType.Gauge, PrometheusType.Summary, PrometheusType.Histogram, PrometheusType.Histogram, PrometheusType.Histogram, PrometheusType.Histogram, PrometheusType.Gauge, }; - private static string MapPrometheusType(PrometheusType type) - { - return type switch - { - PrometheusType.Gauge => "gauge", - PrometheusType.Counter => "counter", - PrometheusType.Summary => "summary", - PrometheusType.Histogram => "histogram", - _ => "untyped", - }; - } - - private static PrometheusType GetPrometheusType(Metric metric) - { - int metricType = (int)metric.MetricType >> 4; - return MetricTypes[metricType]; - } - private static readonly ConcurrentDictionary MetricsCache = new ConcurrentDictionary(); private static int metricsCacheCount; @@ -67,10 +49,9 @@ public static int WriteMetric(byte[] buffer, int cursor, Metric metric) return cursor; } - PrometheusType prometheusType = GetPrometheusType(metric); var prometheusMetric = GetPrometheusMetric(metric); - cursor = WriteTypeMetadata(buffer, cursor, prometheusMetric, MapPrometheusType(prometheusType)); + cursor = WriteTypeMetadata(buffer, cursor, prometheusMetric); cursor = WriteUnitMetadata(buffer, cursor, prometheusMetric); cursor = WriteHelpMetadata(buffer, cursor, prometheusMetric, metric.Description); @@ -253,4 +234,22 @@ private static PrometheusMetric GetPrometheusMetric(Metric metric) }); } } + + private static string MapPrometheusType(PrometheusType type) + { + return type switch + { + PrometheusType.Gauge => "gauge", + PrometheusType.Counter => "counter", + PrometheusType.Summary => "summary", + PrometheusType.Histogram => "histogram", + _ => "untyped", + }; + } + + private static PrometheusType GetPrometheusType(Metric metric) + { + int metricType = (int)metric.MetricType >> 4; + return MetricTypes[metricType]; + } } From 0077081e161f83a44f4b069223dc8261d3f6349e Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Tue, 8 Aug 2023 20:45:18 +0800 Subject: [PATCH 03/10] Fix build --- .../Internal/PrometheusMetric.cs | 2 +- .../Internal/PrometheusType.cs | 4 ++-- .../PrometheusMetricTests.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusMetric.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusMetric.cs index 806171aeca8..b7bc83ad6db 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusMetric.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusMetric.cs @@ -1,4 +1,4 @@ -// +// // Copyright The OpenTelemetry Authors // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusType.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusType.cs index 22aeb32ee0c..0b5acfa8724 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusType.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusType.cs @@ -1,4 +1,4 @@ -// +// // Copyright The OpenTelemetry Authors // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -24,7 +24,7 @@ internal enum PrometheusType Untyped, /// - /// Mapped from Guage and UpDownCounter. + /// Mapped from Gauge and UpDownCounter. /// Gauge, diff --git a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusMetricTests.cs b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusMetricTests.cs index f1004ab9adb..a699d9dfea7 100644 --- a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusMetricTests.cs +++ b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusMetricTests.cs @@ -1,4 +1,4 @@ -// +// // Copyright The OpenTelemetry Authors // // Licensed under the Apache License, Version 2.0 (the "License"); From d6fc93fadbb6ff318399cadd9c7e2cdb0ae7bda2 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Wed, 9 Aug 2023 11:50:16 +0800 Subject: [PATCH 04/10] PR feedback and comments --- build/Common.props | 2 +- ...etry.Exporter.Prometheus.AspNetCore.csproj | 1 - .../Internal/PrometheusSerializer.cs | 12 ++++++++++ .../Internal/PrometheusSerializerExt.cs | 22 +++++-------------- ...ry.Exporter.Prometheus.HttpListener.csproj | 1 - 5 files changed, 19 insertions(+), 19 deletions(-) diff --git a/build/Common.props b/build/Common.props index d8de43e5213..3c200c40347 100644 --- a/build/Common.props +++ b/build/Common.props @@ -1,6 +1,6 @@ - 10.0 + 11.0 true $([System.IO.Directory]::GetParent($(MSBuildThisFileDirectory)).Parent.FullName) $(MSBuildThisFileDirectory)debug.snk diff --git a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/OpenTelemetry.Exporter.Prometheus.AspNetCore.csproj b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/OpenTelemetry.Exporter.Prometheus.AspNetCore.csproj index bbc2c66c5e8..9471eeda251 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/OpenTelemetry.Exporter.Prometheus.AspNetCore.csproj +++ b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/OpenTelemetry.Exporter.Prometheus.AspNetCore.csproj @@ -16,7 +16,6 @@ Remove this property once we have released a stable version.--> false - 11.0 diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializer.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializer.cs index 41651e99096..1ca91a5ee7f 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializer.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializer.cs @@ -313,4 +313,16 @@ public static int WriteUnitMetadata(byte[] buffer, int cursor, PrometheusMetric return cursor; } + + private static string MapPrometheusType(PrometheusType type) + { + return type switch + { + PrometheusType.Gauge => "gauge", + PrometheusType.Counter => "counter", + PrometheusType.Summary => "summary", + PrometheusType.Histogram => "histogram", + _ => "untyped", + }; + } } diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializerExt.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializerExt.cs index ee963ce343b..2cd6b5d24d6 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializerExt.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializerExt.cs @@ -216,10 +216,12 @@ public static int WriteMetric(byte[] buffer, int cursor, Metric metric) private static PrometheusMetric GetPrometheusMetric(Metric metric) { + // Optimize writing metrics with bounded cache that has pre-calculated Prometheus names. if (metricsCacheCount >= MaxCachedMetrics) { if (!MetricsCache.TryGetValue(metric, out var formatter)) { + // The cache is full and the metric isn't in it. formatter = new PrometheusMetric(metric.Name, metric.Unit, GetPrometheusType(metric)); } @@ -233,23 +235,11 @@ private static PrometheusMetric GetPrometheusMetric(Metric metric) return new PrometheusMetric(m.Name, m.Unit, GetPrometheusType(metric)); }); } - } - private static string MapPrometheusType(PrometheusType type) - { - return type switch + static PrometheusType GetPrometheusType(Metric metric) { - PrometheusType.Gauge => "gauge", - PrometheusType.Counter => "counter", - PrometheusType.Summary => "summary", - PrometheusType.Histogram => "histogram", - _ => "untyped", - }; - } - - private static PrometheusType GetPrometheusType(Metric metric) - { - int metricType = (int)metric.MetricType >> 4; - return MetricTypes[metricType]; + int metricType = (int)metric.MetricType >> 4; + return MetricTypes[metricType]; + } } } diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/OpenTelemetry.Exporter.Prometheus.HttpListener.csproj b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/OpenTelemetry.Exporter.Prometheus.HttpListener.csproj index 3e258a637dc..08e4db4cd66 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/OpenTelemetry.Exporter.Prometheus.HttpListener.csproj +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/OpenTelemetry.Exporter.Prometheus.HttpListener.csproj @@ -5,7 +5,6 @@ Stand-alone HttpListener for hosting OpenTelemetry .NET Prometheus Exporter $(PackageTags);prometheus;metrics core- - 11.0 disable From eab4bea5cdd12d298f0f24200b1a471fb1d84955 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Wed, 9 Aug 2023 11:50:46 +0800 Subject: [PATCH 05/10] seal --- .../Internal/PrometheusMetric.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusMetric.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusMetric.cs index b7bc83ad6db..179fcd195ba 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusMetric.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusMetric.cs @@ -18,7 +18,7 @@ namespace OpenTelemetry.Exporter.Prometheus; -internal class PrometheusMetric +internal sealed class PrometheusMetric { public PrometheusMetric(string name, string unit, PrometheusType type) { From b2529656b66a039e3d90ce023e5484b878103c73 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Thu, 10 Aug 2023 08:01:20 +0800 Subject: [PATCH 06/10] PR feedback --- build/Common.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/Common.props b/build/Common.props index 3c200c40347..db5f54b3319 100644 --- a/build/Common.props +++ b/build/Common.props @@ -1,6 +1,6 @@ - 11.0 + latest true $([System.IO.Directory]::GetParent($(MSBuildThisFileDirectory)).Parent.FullName) $(MSBuildThisFileDirectory)debug.snk From 84d0f6e63df57ec44416840829774cc4daf40a3b Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Thu, 10 Aug 2023 10:15:34 +0800 Subject: [PATCH 07/10] Update src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusMetric.cs Co-authored-by: Utkarsh Umesan Pillai <66651184+utpilla@users.noreply.github.com> --- .../Internal/PrometheusMetric.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusMetric.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusMetric.cs index 179fcd195ba..8813300a665 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusMetric.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusMetric.cs @@ -49,7 +49,7 @@ public PrometheusMetric(string name, string unit, PrometheusType type) // If the metric name for monotonic Sum metric points does not end in a suffix of `_total` a suffix of `_total` MUST be added by default, otherwise the name MUST remain unchanged. // Exporters SHOULD provide a configuration option to disable the addition of `_total` suffixes. // https://github.com/open-telemetry/opentelemetry-specification/blob/b2f923fb1650dde1f061507908b834035506a796/specification/compatibility/prometheus_and_openmetrics.md#L286 - if (type == PrometheusType.Counter && !sanitizedName.Contains("total")) + if (type == PrometheusType.Counter && !sanitizedName.EndsWith("_total")) { sanitizedName += "_total"; } From ad25e7f7170d2c7616a37c93323ef052a074e6cb Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Thu, 10 Aug 2023 11:25:46 +0800 Subject: [PATCH 08/10] Refactor cache to belong to manager --- .../Internal/PrometheusCollectionManager.cs | 30 +++++++++- .../Internal/PrometheusMetric.cs | 23 ++++++++ .../Internal/PrometheusSerializerExt.cs | 55 ++----------------- .../PrometheusSerializerBenchmarks.cs | 14 ++++- .../PrometheusSerializerTests.cs | 36 ++++++------ 5 files changed, 90 insertions(+), 68 deletions(-) diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusCollectionManager.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusCollectionManager.cs index aef92a6b4d3..8b91aaa5af4 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusCollectionManager.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusCollectionManager.cs @@ -21,9 +21,13 @@ namespace OpenTelemetry.Exporter.Prometheus; internal sealed class PrometheusCollectionManager { + private const int MaxCachedMetrics = 1024; + private readonly PrometheusExporter exporter; private readonly int scrapeResponseCacheDurationMilliseconds; private readonly Func, ExportResult> onCollectRef; + private readonly Dictionary metricsCache; + private int metricsCacheCount; private byte[] buffer = new byte[85000]; // encourage the object to live in LOH (large object heap) private int globalLockState; private ArraySegment previousDataView; @@ -37,6 +41,7 @@ public PrometheusCollectionManager(PrometheusExporter exporter) this.exporter = exporter; this.scrapeResponseCacheDurationMilliseconds = this.exporter.ScrapeResponseCacheDurationMilliseconds; this.onCollectRef = this.OnCollect; + this.metricsCache = new Dictionary(); } #if NET6_0_OR_GREATER @@ -179,11 +184,16 @@ private ExportResult OnCollect(Batch metrics) { foreach (var metric in metrics) { + if (!PrometheusSerializer.CanWriteMetric(metric)) + { + continue; + } + while (true) { try { - cursor = PrometheusSerializer.WriteMetric(this.buffer, cursor, metric); + cursor = PrometheusSerializer.WriteMetric(this.buffer, cursor, metric, this.GetPrometheusMetric(metric)); break; } catch (IndexOutOfRangeException) @@ -244,6 +254,24 @@ private bool IncreaseBufferSize() return true; } + private PrometheusMetric GetPrometheusMetric(Metric metric) + { + // Optimize writing metrics with bounded cache that has pre-calculated Prometheus names. + if (!this.metricsCache.TryGetValue(metric, out var prometheusMetric)) + { + prometheusMetric = PrometheusMetric.Create(metric); + + // Add to the cache if there is space. + if (this.metricsCacheCount < MaxCachedMetrics) + { + this.metricsCache[metric] = prometheusMetric; + this.metricsCacheCount++; + } + } + + return prometheusMetric; + } + public readonly struct CollectionResponse { public CollectionResponse(ArraySegment view, DateTime generatedAtUtc, bool fromCache) diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusMetric.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusMetric.cs index 8813300a665..3247db35882 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusMetric.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusMetric.cs @@ -15,11 +15,23 @@ // using System.Text; +using OpenTelemetry.Metrics; namespace OpenTelemetry.Exporter.Prometheus; internal sealed class PrometheusMetric { + /* Counter becomes counter + Gauge becomes gauge + Histogram becomes histogram + UpDownCounter becomes gauge + * https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/data-model.md#otlp-metric-points-to-prometheus + */ + private static readonly PrometheusType[] MetricTypes = new PrometheusType[] + { + PrometheusType.Untyped, PrometheusType.Counter, PrometheusType.Gauge, PrometheusType.Summary, PrometheusType.Histogram, PrometheusType.Histogram, PrometheusType.Histogram, PrometheusType.Histogram, PrometheusType.Gauge, + }; + public PrometheusMetric(string name, string unit, PrometheusType type) { // The metric name is @@ -72,6 +84,11 @@ public PrometheusMetric(string name, string unit, PrometheusType type) public PrometheusType Type { get; } + public static PrometheusMetric Create(Metric metric) + { + return new PrometheusMetric(metric.Name, metric.Unit, GetPrometheusType(metric)); + } + internal static string SanitizeMetricName(string metricName) { StringBuilder sb = null; @@ -195,6 +212,12 @@ private static bool TryProcessRateUnits(string updatedUnit, out string updatedPe return false; } + private static PrometheusType GetPrometheusType(Metric metric) + { + int metricType = (int)metric.MetricType >> 4; + return MetricTypes[metricType]; + } + // The map to translate OTLP units to Prometheus units // OTLP metrics use the c/s notation as specified at https://ucum.org/ucum.html // (See also https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/semantic_conventions/README.md#instrument-units) diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializerExt.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializerExt.cs index 2cd6b5d24d6..0eb068b185d 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializerExt.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializerExt.cs @@ -14,7 +14,6 @@ // limitations under the License. // -using System.Collections.Concurrent; using OpenTelemetry.Metrics; namespace OpenTelemetry.Exporter.Prometheus; @@ -24,33 +23,20 @@ namespace OpenTelemetry.Exporter.Prometheus; /// internal static partial class PrometheusSerializer { - private const int MaxCachedMetrics = 1024; - - /* Counter becomes counter - Gauge becomes gauge - Histogram becomes histogram - UpDownCounter becomes gauge - * https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/data-model.md#otlp-metric-points-to-prometheus - */ - private static readonly PrometheusType[] MetricTypes = new PrometheusType[] - { - PrometheusType.Untyped, PrometheusType.Counter, PrometheusType.Gauge, PrometheusType.Summary, PrometheusType.Histogram, PrometheusType.Histogram, PrometheusType.Histogram, PrometheusType.Histogram, PrometheusType.Gauge, - }; - - private static readonly ConcurrentDictionary MetricsCache = new ConcurrentDictionary(); - private static int metricsCacheCount; - - public static int WriteMetric(byte[] buffer, int cursor, Metric metric) + public static bool CanWriteMetric(Metric metric) { if (metric.MetricType == MetricType.ExponentialHistogram) { // Exponential histograms are not yet support by Prometheus. // They are ignored for now. - return cursor; + return false; } - var prometheusMetric = GetPrometheusMetric(metric); + return true; + } + public static int WriteMetric(byte[] buffer, int cursor, Metric metric, PrometheusMetric prometheusMetric) + { cursor = WriteTypeMetadata(buffer, cursor, prometheusMetric); cursor = WriteUnitMetadata(buffer, cursor, prometheusMetric); cursor = WriteHelpMetadata(buffer, cursor, prometheusMetric, metric.Description); @@ -213,33 +199,4 @@ public static int WriteMetric(byte[] buffer, int cursor, Metric metric) return cursor; } - - private static PrometheusMetric GetPrometheusMetric(Metric metric) - { - // Optimize writing metrics with bounded cache that has pre-calculated Prometheus names. - if (metricsCacheCount >= MaxCachedMetrics) - { - if (!MetricsCache.TryGetValue(metric, out var formatter)) - { - // The cache is full and the metric isn't in it. - formatter = new PrometheusMetric(metric.Name, metric.Unit, GetPrometheusType(metric)); - } - - return formatter; - } - else - { - return MetricsCache.GetOrAdd(metric, m => - { - Interlocked.Increment(ref metricsCacheCount); - return new PrometheusMetric(m.Name, m.Unit, GetPrometheusType(metric)); - }); - } - - static PrometheusType GetPrometheusType(Metric metric) - { - int metricType = (int)metric.MetricType >> 4; - return MetricTypes[metricType]; - } - } } diff --git a/test/Benchmarks/Exporter/PrometheusSerializerBenchmarks.cs b/test/Benchmarks/Exporter/PrometheusSerializerBenchmarks.cs index 11258c8f0b7..0629593aa81 100644 --- a/test/Benchmarks/Exporter/PrometheusSerializerBenchmarks.cs +++ b/test/Benchmarks/Exporter/PrometheusSerializerBenchmarks.cs @@ -29,6 +29,7 @@ public class PrometheusSerializerBenchmarks private readonly byte[] buffer = new byte[85000]; private Meter meter; private MeterProvider meterProvider; + private Dictionary cache = new Dictionary(); [Params(1, 1000, 10000)] public int NumberOfSerializeCalls { get; set; } @@ -69,8 +70,19 @@ public void WriteMetric() int cursor = 0; foreach (var metric in this.metrics) { - cursor = PrometheusSerializer.WriteMetric(this.buffer, cursor, metric); + cursor = PrometheusSerializer.WriteMetric(this.buffer, cursor, metric, this.GetPrometheusMetric(metric)); } } } + + private PrometheusMetric GetPrometheusMetric(Metric metric) + { + if (!this.cache.TryGetValue(metric, out var prometheusMetric)) + { + prometheusMetric = PrometheusMetric.Create(metric); + this.cache[metric] = prometheusMetric; + } + + return prometheusMetric; + } } diff --git a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusSerializerTests.cs b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusSerializerTests.cs index d60f979a844..069a2e0c810 100644 --- a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusSerializerTests.cs +++ b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusSerializerTests.cs @@ -40,7 +40,7 @@ public void GaugeZeroDimension() provider.ForceFlush(); - var cursor = PrometheusSerializer.WriteMetric(buffer, 0, metrics[0]); + var cursor = WriteMetric(buffer, 0, metrics[0]); Assert.Matches( ("^" + "# TYPE test_gauge gauge\n" @@ -65,7 +65,7 @@ public void GaugeZeroDimensionWithDescription() provider.ForceFlush(); - var cursor = PrometheusSerializer.WriteMetric(buffer, 0, metrics[0]); + var cursor = WriteMetric(buffer, 0, metrics[0]); Assert.Matches( ("^" + "# TYPE test_gauge gauge\n" @@ -91,7 +91,7 @@ public void GaugeZeroDimensionWithUnit() provider.ForceFlush(); - var cursor = PrometheusSerializer.WriteMetric(buffer, 0, metrics[0]); + var cursor = WriteMetric(buffer, 0, metrics[0]); Assert.Matches( ("^" + "# TYPE test_gauge_seconds gauge\n" @@ -117,7 +117,7 @@ public void GaugeZeroDimensionWithDescriptionAndUnit() provider.ForceFlush(); - var cursor = PrometheusSerializer.WriteMetric(buffer, 0, metrics[0]); + var cursor = WriteMetric(buffer, 0, metrics[0]); Assert.Matches( ("^" + "# TYPE test_gauge_seconds gauge\n" @@ -146,7 +146,7 @@ public void GaugeOneDimension() provider.ForceFlush(); - var cursor = PrometheusSerializer.WriteMetric(buffer, 0, metrics[0]); + var cursor = WriteMetric(buffer, 0, metrics[0]); Assert.Matches( ("^" + "# TYPE test_gauge gauge\n" @@ -176,7 +176,7 @@ public void GaugeDoubleSubnormal() provider.ForceFlush(); - var cursor = PrometheusSerializer.WriteMetric(buffer, 0, metrics[0]); + var cursor = WriteMetric(buffer, 0, metrics[0]); Assert.Matches( ("^" + "# TYPE test_gauge gauge\n" @@ -205,7 +205,7 @@ public void SumDoubleInfinities() provider.ForceFlush(); - var cursor = PrometheusSerializer.WriteMetric(buffer, 0, metrics[0]); + var cursor = WriteMetric(buffer, 0, metrics[0]); Assert.Matches( ("^" + "# TYPE test_counter_total counter\n" @@ -232,7 +232,7 @@ public void SumNonMonotonicDouble() provider.ForceFlush(); - var cursor = PrometheusSerializer.WriteMetric(buffer, 0, metrics[0]); + var cursor = WriteMetric(buffer, 0, metrics[0]); Assert.Matches( ("^" + "# TYPE test_updown_counter gauge\n" @@ -259,7 +259,7 @@ public void HistogramZeroDimension() provider.ForceFlush(); - var cursor = PrometheusSerializer.WriteMetric(buffer, 0, metrics[0]); + var cursor = WriteMetric(buffer, 0, metrics[0]); Assert.Matches( ("^" + "# TYPE test_histogram histogram\n" @@ -303,7 +303,7 @@ public void HistogramOneDimension() provider.ForceFlush(); - var cursor = PrometheusSerializer.WriteMetric(buffer, 0, metrics[0]); + var cursor = WriteMetric(buffer, 0, metrics[0]); Assert.Matches( ("^" + "# TYPE test_histogram histogram\n" @@ -347,7 +347,7 @@ public void HistogramTwoDimensions() provider.ForceFlush(); - var cursor = PrometheusSerializer.WriteMetric(buffer, 0, metrics[0]); + var cursor = WriteMetric(buffer, 0, metrics[0]); Assert.Matches( ("^" + "# TYPE test_histogram histogram\n" @@ -392,7 +392,7 @@ public void HistogramInfinities() provider.ForceFlush(); - var cursor = PrometheusSerializer.WriteMetric(buffer, 0, metrics[0]); + var cursor = WriteMetric(buffer, 0, metrics[0]); Assert.Matches( ("^" + "# TYPE test_histogram histogram\n" @@ -437,7 +437,7 @@ public void HistogramNaN() provider.ForceFlush(); - var cursor = PrometheusSerializer.WriteMetric(buffer, 0, metrics[0]); + var cursor = WriteMetric(buffer, 0, metrics[0]); Assert.Matches( ("^" + "# TYPE test_histogram histogram\n" @@ -482,9 +482,11 @@ public void ExponentialHistogramIsIgnoredForNow() provider.ForceFlush(); - var cursor = PrometheusSerializer.WriteMetric(buffer, 0, metrics[0]); - Assert.Matches( - "^$", - Encoding.UTF8.GetString(buffer, 0, cursor)); + Assert.False(PrometheusSerializer.CanWriteMetric(metrics[0])); + } + + private static int WriteMetric(byte[] buffer, int cursor, Metric metric) + { + return PrometheusSerializer.WriteMetric(buffer, cursor, metric, PrometheusMetric.Create(metric)); } } From 6357abfcf7c7a9bf6d3b448cb492a31399018be7 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Thu, 10 Aug 2023 11:37:26 +0800 Subject: [PATCH 09/10] Comment --- .../Internal/PrometheusMetric.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusMetric.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusMetric.cs index 3247db35882..2c500562fed 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusMetric.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusMetric.cs @@ -130,6 +130,10 @@ internal static string SanitizeMetricName(string metricName) internal static string RemoveAnnotations(string unit) { + // UCUM standard says the curly braces shouldn't be nested: + // https://ucum.org/ucum#section-Character-Set-and-Lexical-Rules + // What should happen if they are nested isn't defined. + // Right now the remove annotations code doesn't attempt to balance multiple start and end braces. StringBuilder sb = null; var hasOpenBrace = false; From 4797cea5b88c2a56f0332426e1aa28af95eefd3c Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Wed, 16 Aug 2023 09:27:40 +0800 Subject: [PATCH 10/10] Update readme files --- src/OpenTelemetry.Exporter.Prometheus.AspNetCore/CHANGELOG.md | 3 +++ .../CHANGELOG.md | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/CHANGELOG.md b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/CHANGELOG.md index d75871cdbe0..33653049bbc 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +* Added support for unit and name conversion following the [OpenTelemetry Specification](https://github.com/open-telemetry/opentelemetry-specification/blob/065b25024549120800da7cda6ccd9717658ff0df/specification/compatibility/prometheus_and_openmetrics.md?plain=1#L235-L240) + ([#4753](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4753)) + ## 1.6.0-alpha.1 Released 2023-Jul-12 diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/CHANGELOG.md b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/CHANGELOG.md index a7a8d3b09c9..df46fc8dc84 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +* Added support for unit and name conversion following the [OpenTelemetry Specification](https://github.com/open-telemetry/opentelemetry-specification/blob/065b25024549120800da7cda6ccd9717658ff0df/specification/compatibility/prometheus_and_openmetrics.md?plain=1#L235-L240) + ([#4753](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4753)) + ## 1.6.0-alpha.1 Released 2023-Jul-12