diff --git a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/CHANGELOG.md b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/CHANGELOG.md index 151ee398408..f2fd26ccd5a 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/CHANGELOG.md @@ -5,11 +5,12 @@ * Bug fix for Prometheus Exporter reporting StatusCode 204 instead of 200, when no metrics are collected ([#3643](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3643)) - * Added overloads which accept a name to the `MeterProviderBuilder` `AddPrometheusExporter` extension to allow for more fine-grained options management ([#3648](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3648)) +* Added support for OpenMetrics UNIT metadata + ([#3651](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3651)) ## 1.4.0-alpha.2 @@ -21,7 +22,6 @@ Released 2022-Aug-18 ([#3430](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3430) [#3503](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3503) [#3507](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3507)) - * Added `IEndpointRouteBuilder` extension methods to help with Prometheus middleware configuration on ASP.NET Core ([#3295](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3295)) @@ -41,10 +41,8 @@ Released 2022-Apr-15 * Added `IApplicationBuilder` extension methods to help with Prometheus middleware configuration on ASP.NET Core ([#3029](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3029)) - * Changed Prometheus exporter to return 204 No Content and log a warning event if there are no metrics to collect. - * Removes .NET Framework 4.6.1. The minimum .NET Framework version supported is .NET 4.6.2. ([#3190](https://github.com/open-telemetry/opentelemetry-dotnet/issues/3190)) diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/CHANGELOG.md b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/CHANGELOG.md index f46954288a1..9ab03e4de81 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/CHANGELOG.md @@ -5,11 +5,12 @@ * Bug fix for Prometheus Exporter reporting StatusCode 204 instead of 200, when no metrics are collected ([#3643](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3643)) - * Added overloads which accept a name to the `MeterProviderBuilder` `AddPrometheusHttpListener` extension to allow for more fine-grained options management ([#3648](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3648)) +* Added support for OpenMetrics UNIT metadata + ([#3651](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3651)) ## 1.4.0-alpha.2 @@ -41,10 +42,8 @@ Released 2022-Apr-15 * Added `IApplicationBuilder` extension methods to help with Prometheus middleware configuration on ASP.NET Core ([#3029](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3029)) - * Changed Prometheus exporter to return 204 No Content and log a warning event if there are no metrics to collect. - * Removes .NET Framework 4.6.1. The minimum .NET Framework version supported is .NET 4.6.2. ([#3190](https://github.com/open-telemetry/opentelemetry-dotnet/issues/3190)) diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializer.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializer.cs index 18a22caaec2..5c38be07a77 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializer.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializer.cs @@ -271,8 +271,13 @@ public static int WriteMetricName(byte[] buffer, int cursor, string metricName, } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int WriteHelpText(byte[] buffer, int cursor, string metricName, string metricUnit = null, string metricDescription = null) + public static int WriteHelpMetadata(byte[] buffer, int cursor, string metricName, string metricUnit, string metricDescription) { + if (string.IsNullOrEmpty(metricDescription)) + { + return cursor; + } + cursor = WriteAsciiStringNoEscape(buffer, cursor, "# HELP "); cursor = WriteMetricName(buffer, cursor, metricName, metricUnit); @@ -288,7 +293,7 @@ public static int WriteHelpText(byte[] buffer, int cursor, string metricName, st } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int WriteTypeInfo(byte[] buffer, int cursor, string metricName, string metricUnit, string metricType) + public static int WriteTypeMetadata(byte[] buffer, int cursor, string metricName, string metricUnit, string metricType) { Debug.Assert(!string.IsNullOrEmpty(metricType), $"{nameof(metricType)} should not be null or empty."); @@ -301,5 +306,39 @@ public static int WriteTypeInfo(byte[] buffer, int cursor, string metricName, st return cursor; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int WriteUnitMetadata(byte[] buffer, int cursor, string metricName, string metricUnit) + { + if (string.IsNullOrEmpty(metricUnit)) + { + return cursor; + } + + cursor = WriteAsciiStringNoEscape(buffer, cursor, "# UNIT "); + cursor = WriteMetricName(buffer, cursor, metricName, metricUnit); + + 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)'_'); + } + } + + buffer[cursor++] = ASCII_LINEFEED; + + return cursor; + } } } diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializerExt.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializerExt.cs index 8ae48797dfa..445e0410c81 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializerExt.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializerExt.cs @@ -36,13 +36,10 @@ UpDownCounter becomes gauge public static int WriteMetric(byte[] buffer, int cursor, Metric metric) { - if (!string.IsNullOrWhiteSpace(metric.Description)) - { - cursor = WriteHelpText(buffer, cursor, metric.Name, metric.Unit, metric.Description); - } - int metricType = (int)metric.MetricType >> 4; - cursor = WriteTypeInfo(buffer, cursor, metric.Name, metric.Unit, MetricTypes[metricType]); + 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); if (!metric.MetricType.IsHistogram()) { diff --git a/test/OpenTelemetry.Exporter.Prometheus.Tests/PrometheusSerializerTests.cs b/test/OpenTelemetry.Exporter.Prometheus.Tests/PrometheusSerializerTests.cs index ba57d0bb511..24472e0e645 100644 --- a/test/OpenTelemetry.Exporter.Prometheus.Tests/PrometheusSerializerTests.cs +++ b/test/OpenTelemetry.Exporter.Prometheus.Tests/PrometheusSerializerTests.cs @@ -69,8 +69,8 @@ public void GaugeZeroDimensionWithDescription() var cursor = PrometheusSerializer.WriteMetric(buffer, 0, metrics[0]); Assert.Matches( ("^" - + "# HELP test_gauge Hello, world!\n" + "# TYPE test_gauge gauge\n" + + "# HELP test_gauge Hello, world!\n" + "test_gauge 123 \\d+\n" + "$").Replace('\'', '"'), Encoding.UTF8.GetString(buffer, 0, cursor)); @@ -96,6 +96,34 @@ public void GaugeZeroDimensionWithUnit() Assert.Matches( ("^" + "# TYPE test_gauge_seconds gauge\n" + + "# UNIT test_gauge_seconds seconds\n" + + "test_gauge_seconds 123 \\d+\n" + + "$").Replace('\'', '"'), + Encoding.UTF8.GetString(buffer, 0, cursor)); + } + + [Fact] + public void GaugeZeroDimensionWithDescriptionAndUnit() + { + var buffer = new byte[85000]; + var metrics = new List(); + + using var meter = new Meter(Utils.GetCurrentMethodName()); + using var provider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddInMemoryExporter(metrics) + .Build(); + + meter.CreateObservableGauge("test_gauge", () => 123, unit: "seconds", description: "Hello, world!"); + + provider.ForceFlush(); + + var cursor = PrometheusSerializer.WriteMetric(buffer, 0, metrics[0]); + Assert.Matches( + ("^" + + "# TYPE test_gauge_seconds gauge\n" + + "# UNIT test_gauge_seconds seconds\n" + + "# HELP test_gauge_seconds Hello, world!\n" + "test_gauge_seconds 123 \\d+\n" + "$").Replace('\'', '"'), Encoding.UTF8.GetString(buffer, 0, cursor));