diff --git a/.github/workflows/apicompatibility.yml b/.github/workflows/apicompatibility.yml index 9ab30cf3734..70e7ada39eb 100644 --- a/.github/workflows/apicompatibility.yml +++ b/.github/workflows/apicompatibility.yml @@ -17,7 +17,7 @@ jobs: - uses: actions/checkout@v3 with: fetch-depth: 0 # fetching all - - uses: actions/setup-dotnet@v3.0.1 + - uses: actions/setup-dotnet@v3.0.2 with: dotnet-version: '7.0.x' include-prerelease: true diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index e2e8dceef06..a855357cfa0 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -27,7 +27,7 @@ jobs: - uses: actions/checkout@v3 with: fetch-depth: 0 # fetching all - - uses: actions/setup-dotnet@v3.0.1 + - uses: actions/setup-dotnet@v3.0.2 with: dotnet-version: '7.0.x' include-prerelease: true diff --git a/.github/workflows/dotnet-format.yml b/.github/workflows/dotnet-format.yml index d07b4e5ce93..37d7b78bffd 100644 --- a/.github/workflows/dotnet-format.yml +++ b/.github/workflows/dotnet-format.yml @@ -21,7 +21,7 @@ jobs: uses: actions/checkout@v3 - name: Setup .NET Core 7.0 - uses: actions/setup-dotnet@v3.0.1 + uses: actions/setup-dotnet@v3.0.2 with: dotnet-version: '7.0.x' include-prerelease: true diff --git a/.github/workflows/linux-ci.yml b/.github/workflows/linux-ci.yml index 82e30af9ff9..afe2c61192a 100644 --- a/.github/workflows/linux-ci.yml +++ b/.github/workflows/linux-ci.yml @@ -23,11 +23,11 @@ jobs: with: fetch-depth: 0 # fetching all - - uses: actions/setup-dotnet@v3.0.1 + - uses: actions/setup-dotnet@v3.0.2 with: dotnet-version: '6.0.x' - - uses: actions/setup-dotnet@v3.0.1 + - uses: actions/setup-dotnet@v3.0.2 with: dotnet-version: '7.0.x' include-prerelease: true diff --git a/.github/workflows/publish-packages-1.0.yml b/.github/workflows/publish-packages-1.0.yml index ca27a4311f2..8f6f423b58d 100644 --- a/.github/workflows/publish-packages-1.0.yml +++ b/.github/workflows/publish-packages-1.0.yml @@ -23,7 +23,7 @@ jobs: fetch-depth: 0 # fetching all ref: ${{ github.ref || 'main' }} - - uses: actions/setup-dotnet@v3.0.1 + - uses: actions/setup-dotnet@v3.0.2 with: dotnet-version: '7.0.x' include-prerelease: true diff --git a/.github/workflows/windows-ci.yml b/.github/workflows/windows-ci.yml index bc0cad6336f..7ce30a222e6 100644 --- a/.github/workflows/windows-ci.yml +++ b/.github/workflows/windows-ci.yml @@ -26,7 +26,7 @@ jobs: with: fetch-depth: 0 # fetching all - - uses: actions/setup-dotnet@v3.0.1 + - uses: actions/setup-dotnet@v3.0.2 with: dotnet-version: '7.0.x' include-prerelease: true diff --git a/build/Common.prod.props b/build/Common.prod.props index 915644b7755..90119d36dab 100644 --- a/build/Common.prod.props +++ b/build/Common.prod.props @@ -13,11 +13,17 @@ - + - - + + + diff --git a/build/PreBuild.ps1 b/build/PreBuild.ps1 index 847c4d52c70..64d51872e34 100644 --- a/build/PreBuild.ps1 +++ b/build/PreBuild.ps1 @@ -1,26 +1,31 @@ -param([string]$package, [string]$version) +param( + [string]$package, + [string]$version, + [string]$workDir = ".\LastMajorVersionBinaries" +) -$workDir = "..\LastMajorVersionBinaries" if (-Not (Test-Path $workDir)) { - Write-Host "Working directory for previous package versions not found, creating..." + Write-Host "Working directory for compatibility check packages '$workDir' not found, creating..." New-Item -Path $workDir -ItemType "directory" | Out-Null } if (Test-Path -Path "$workDir\$package.$version.zip") { - Write-Debug "Previous package version already downloaded" + Write-Debug "Previous package $package@$version already downloaded for compatibility check" } else { - Write-Host "Retrieving $package @$version for compatibility check" + Write-Host "Retrieving package $package@$version for compatibility check" Invoke-WebRequest -Uri https://www.nuget.org/api/v2/package/$package/$version -Outfile "$workDir\$package.$version.zip" } + if (Test-Path -Path "$workDir\$package\$version\lib") { - Write-Debug "Previous package version already extracted" + Write-Debug "Previous package $package@$version already extracted to '$workDir\$package\$version\lib'" } else { + Write-Host "Extracting package $package@$version from '$workDir\$package.$version.zip' to '$workDir\$package\$version' for compatibility check" Expand-Archive -LiteralPath "$workDir\$package.$version.zip" -DestinationPath "$workDir\$package\$version" -Force } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md index 3232e5d8a24..585c05c2e63 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md @@ -2,6 +2,16 @@ ## Unreleased +* OTLP histogram data points will now include `Min` and `Max` values when + they are present. + ([#2735](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2735)) + +* Adds support for limiting the length and count of attributes exported from + the OTLP log exporter. These + [Attribute Limits](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/sdk-environment-variables.md#attribute-limits) + are configured via the environment variables defined in the specification. + ([#3684](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3684)) + ## 1.4.0-beta.1 Released 2022-Sep-29 diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/LogRecordExtensions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/LogRecordExtensions.cs index 126530af082..e674b30f7ad 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/LogRecordExtensions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/LogRecordExtensions.cs @@ -18,8 +18,8 @@ using System.Collections.Generic; using System.Runtime.CompilerServices; using Google.Protobuf; -using Google.Protobuf.Collections; using Microsoft.Extensions.Logging; +using OpenTelemetry.Configuration; using OpenTelemetry.Internal; using OpenTelemetry.Logs; using OpenTelemetry.Trace; @@ -75,6 +75,11 @@ internal static OtlpLogs.LogRecord ToOtlpLog(this LogRecord logRecord) SeverityText = LogLevels[(int)logRecord.LogLevel], }; + var attributeValueLengthLimit = SdkConfiguration.Instance.AttributeValueLengthLimit; + var attributeCountLimit = SdkConfiguration.Instance.AttributeCountLimit ?? int.MaxValue; + + // First add the generic attributes like category, eventid and exception, so they are less likely being dropped because of AttributeCountLimit + if (!string.IsNullOrEmpty(logRecord.CategoryName)) { // TODO: @@ -82,7 +87,24 @@ internal static OtlpLogs.LogRecord ToOtlpLog(this LogRecord logRecord) // if it makes it to log data model. // https://github.com/open-telemetry/opentelemetry-specification/issues/2398 // 2. Confirm if this name for attribute is good. - otlpLogRecord.Attributes.AddStringAttribute("dotnet.ilogger.category", logRecord.CategoryName); + otlpLogRecord.AddStringAttribute("dotnet.ilogger.category", logRecord.CategoryName, attributeValueLengthLimit, attributeCountLimit); + } + + if (logRecord.EventId.Id != default) + { + otlpLogRecord.AddIntAttribute(nameof(logRecord.EventId.Id), logRecord.EventId.Id, attributeValueLengthLimit, attributeCountLimit); + } + + if (!string.IsNullOrEmpty(logRecord.EventId.Name)) + { + otlpLogRecord.AddStringAttribute(nameof(logRecord.EventId.Name), logRecord.EventId.Name, attributeValueLengthLimit, attributeCountLimit); + } + + if (logRecord.Exception != null) + { + otlpLogRecord.AddStringAttribute(SemanticConventions.AttributeExceptionType, logRecord.Exception.GetType().Name, attributeValueLengthLimit, attributeCountLimit); + otlpLogRecord.AddStringAttribute(SemanticConventions.AttributeExceptionMessage, logRecord.Exception.Message, attributeValueLengthLimit, attributeCountLimit); + otlpLogRecord.AddStringAttribute(SemanticConventions.AttributeExceptionStacktrace, logRecord.Exception.ToInvariantString(), attributeValueLengthLimit, attributeCountLimit); } bool bodyPopulatedFromFormattedMessage = false; @@ -103,30 +125,13 @@ internal static OtlpLogs.LogRecord ToOtlpLog(this LogRecord logRecord) { otlpLogRecord.Body = new OtlpCommon.AnyValue { StringValue = stateValue.Value as string }; } - else if (OtlpKeyValueTransformer.Instance.TryTransformTag(stateValue, out var result)) + else if (OtlpKeyValueTransformer.Instance.TryTransformTag(stateValue, out var result, attributeValueLengthLimit)) { - otlpLogRecord.Attributes.Add(result); + otlpLogRecord.AddAttribute(result, attributeCountLimit); } } } - if (logRecord.EventId.Id != default) - { - otlpLogRecord.Attributes.AddIntAttribute(nameof(logRecord.EventId.Id), logRecord.EventId.Id); - } - - if (!string.IsNullOrEmpty(logRecord.EventId.Name)) - { - otlpLogRecord.Attributes.AddStringAttribute(nameof(logRecord.EventId.Name), logRecord.EventId.Name); - } - - if (logRecord.Exception != null) - { - otlpLogRecord.Attributes.AddStringAttribute(SemanticConventions.AttributeExceptionType, logRecord.Exception.GetType().Name); - otlpLogRecord.Attributes.AddStringAttribute(SemanticConventions.AttributeExceptionMessage, logRecord.Exception.Message); - otlpLogRecord.Attributes.AddStringAttribute(SemanticConventions.AttributeExceptionStacktrace, logRecord.Exception.ToInvariantString()); - } - if (logRecord.TraceId != default && logRecord.SpanId != default) { byte[] traceIdBytes = new byte[16]; @@ -149,9 +154,9 @@ void ProcessScope(LogRecordScope scope, OtlpLogs.LogRecord otlpLog) foreach (var scopeItem in scope) { var scopeItemWithDepthInfo = new KeyValuePair($"[Scope.{scopeDepth}]:{scopeItem.Key}", scopeItem.Value); - if (OtlpKeyValueTransformer.Instance.TryTransformTag(scopeItemWithDepthInfo, out var result)) + if (OtlpKeyValueTransformer.Instance.TryTransformTag(scopeItemWithDepthInfo, out var result, attributeValueLengthLimit)) { - otlpLog.Attributes.Add(result); + otlpLog.AddAttribute(result, attributeCountLimit); } } } @@ -164,22 +169,37 @@ void ProcessScope(LogRecordScope scope, OtlpLogs.LogRecord otlpLog) return otlpLogRecord; } - private static void AddStringAttribute(this RepeatedField repeatedField, string key, string value) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void AddAttribute(this OtlpLogs.LogRecord logRecord, OtlpCommon.KeyValue attribute, int maxAttributeCount) { - repeatedField.Add(new OtlpCommon.KeyValue + if (logRecord.Attributes.Count < maxAttributeCount) + { + logRecord.Attributes.Add(attribute); + } + else { - Key = key, - Value = new OtlpCommon.AnyValue { StringValue = value }, - }); + logRecord.DroppedAttributesCount++; + } } - private static void AddIntAttribute(this RepeatedField repeatedField, string key, int value) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void AddStringAttribute(this OtlpLogs.LogRecord logRecord, string key, string value, int? maxValueLength, int maxAttributeCount) { - repeatedField.Add(new OtlpCommon.KeyValue + var attributeItem = new KeyValuePair(key, value); + if (OtlpKeyValueTransformer.Instance.TryTransformTag(attributeItem, out var result, maxValueLength)) { - Key = key, - Value = new OtlpCommon.AnyValue { IntValue = value }, - }); + logRecord.AddAttribute(result, maxAttributeCount); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void AddIntAttribute(this OtlpLogs.LogRecord logRecord, string key, int value, int? maxValueLength, int maxAttributeCount) + { + var attributeItem = new KeyValuePair(key, value); + if (OtlpKeyValueTransformer.Instance.TryTransformTag(attributeItem, out var result, maxValueLength)) + { + logRecord.AddAttribute(result, maxAttributeCount); + } } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/MetricItemExtensions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/MetricItemExtensions.cs index 4933871e7b2..50ad352ddfc 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/MetricItemExtensions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/MetricItemExtensions.cs @@ -257,6 +257,12 @@ internal static OtlpMetrics.Metric ToOtlpMetric(this Metric metric) dataPoint.Count = (ulong)metricPoint.GetHistogramCount(); dataPoint.Sum = metricPoint.GetHistogramSum(); + if (metricPoint.HasMinMax()) + { + dataPoint.Min = metricPoint.GetHistogramMin(); + dataPoint.Max = metricPoint.GetHistogramMax(); + } + foreach (var histogramMeasurement in metricPoint.GetHistogramBuckets()) { dataPoint.BucketCounts.Add((ulong)histogramMeasurement.BucketCount); diff --git a/src/OpenTelemetry/.publicApi/net462/PublicAPI.Unshipped.txt b/src/OpenTelemetry/.publicApi/net462/PublicAPI.Unshipped.txt index bf3ab6eedd6..eedd81fc086 100644 --- a/src/OpenTelemetry/.publicApi/net462/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry/.publicApi/net462/PublicAPI.Unshipped.txt @@ -10,6 +10,13 @@ OpenTelemetry.Logs.LogRecord.TraceFlags.set -> void OpenTelemetry.Logs.LogRecord.TraceId.set -> void OpenTelemetry.Logs.LogRecord.TraceState.set -> void OpenTelemetry.Logs.OpenTelemetryLoggerOptions.ConfigureResource(System.Action! configure) -> OpenTelemetry.Logs.OpenTelemetryLoggerOptions! +OpenTelemetry.Metrics.HistogramConfiguration +OpenTelemetry.Metrics.HistogramConfiguration.HistogramConfiguration() -> void +OpenTelemetry.Metrics.HistogramConfiguration.RecordMinMax.get -> bool +OpenTelemetry.Metrics.HistogramConfiguration.RecordMinMax.set -> void +OpenTelemetry.Metrics.MetricPoint.GetHistogramMax() -> double +OpenTelemetry.Metrics.MetricPoint.GetHistogramMin() -> double +OpenTelemetry.Metrics.MetricPoint.HasMinMax() -> bool static Microsoft.Extensions.DependencyInjection.MeterProviderBuilderServiceCollectionExtensions.ConfigureOpenTelemetryMetrics(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! static Microsoft.Extensions.DependencyInjection.MeterProviderBuilderServiceCollectionExtensions.ConfigureOpenTelemetryMetrics(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configure) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddInstrumentation(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder) -> OpenTelemetry.Metrics.MeterProviderBuilder! diff --git a/src/OpenTelemetry/.publicApi/net6.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry/.publicApi/net6.0/PublicAPI.Unshipped.txt index bf3ab6eedd6..eedd81fc086 100644 --- a/src/OpenTelemetry/.publicApi/net6.0/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry/.publicApi/net6.0/PublicAPI.Unshipped.txt @@ -10,6 +10,13 @@ OpenTelemetry.Logs.LogRecord.TraceFlags.set -> void OpenTelemetry.Logs.LogRecord.TraceId.set -> void OpenTelemetry.Logs.LogRecord.TraceState.set -> void OpenTelemetry.Logs.OpenTelemetryLoggerOptions.ConfigureResource(System.Action! configure) -> OpenTelemetry.Logs.OpenTelemetryLoggerOptions! +OpenTelemetry.Metrics.HistogramConfiguration +OpenTelemetry.Metrics.HistogramConfiguration.HistogramConfiguration() -> void +OpenTelemetry.Metrics.HistogramConfiguration.RecordMinMax.get -> bool +OpenTelemetry.Metrics.HistogramConfiguration.RecordMinMax.set -> void +OpenTelemetry.Metrics.MetricPoint.GetHistogramMax() -> double +OpenTelemetry.Metrics.MetricPoint.GetHistogramMin() -> double +OpenTelemetry.Metrics.MetricPoint.HasMinMax() -> bool static Microsoft.Extensions.DependencyInjection.MeterProviderBuilderServiceCollectionExtensions.ConfigureOpenTelemetryMetrics(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! static Microsoft.Extensions.DependencyInjection.MeterProviderBuilderServiceCollectionExtensions.ConfigureOpenTelemetryMetrics(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configure) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddInstrumentation(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder) -> OpenTelemetry.Metrics.MeterProviderBuilder! diff --git a/src/OpenTelemetry/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt index bf3ab6eedd6..eedd81fc086 100644 --- a/src/OpenTelemetry/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt @@ -10,6 +10,13 @@ OpenTelemetry.Logs.LogRecord.TraceFlags.set -> void OpenTelemetry.Logs.LogRecord.TraceId.set -> void OpenTelemetry.Logs.LogRecord.TraceState.set -> void OpenTelemetry.Logs.OpenTelemetryLoggerOptions.ConfigureResource(System.Action! configure) -> OpenTelemetry.Logs.OpenTelemetryLoggerOptions! +OpenTelemetry.Metrics.HistogramConfiguration +OpenTelemetry.Metrics.HistogramConfiguration.HistogramConfiguration() -> void +OpenTelemetry.Metrics.HistogramConfiguration.RecordMinMax.get -> bool +OpenTelemetry.Metrics.HistogramConfiguration.RecordMinMax.set -> void +OpenTelemetry.Metrics.MetricPoint.GetHistogramMax() -> double +OpenTelemetry.Metrics.MetricPoint.GetHistogramMin() -> double +OpenTelemetry.Metrics.MetricPoint.HasMinMax() -> bool static Microsoft.Extensions.DependencyInjection.MeterProviderBuilderServiceCollectionExtensions.ConfigureOpenTelemetryMetrics(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! static Microsoft.Extensions.DependencyInjection.MeterProviderBuilderServiceCollectionExtensions.ConfigureOpenTelemetryMetrics(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configure) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddInstrumentation(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder) -> OpenTelemetry.Metrics.MeterProviderBuilder! diff --git a/src/OpenTelemetry/.publicApi/netstandard2.1/PublicAPI.Unshipped.txt b/src/OpenTelemetry/.publicApi/netstandard2.1/PublicAPI.Unshipped.txt index bf3ab6eedd6..eedd81fc086 100644 --- a/src/OpenTelemetry/.publicApi/netstandard2.1/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry/.publicApi/netstandard2.1/PublicAPI.Unshipped.txt @@ -10,6 +10,13 @@ OpenTelemetry.Logs.LogRecord.TraceFlags.set -> void OpenTelemetry.Logs.LogRecord.TraceId.set -> void OpenTelemetry.Logs.LogRecord.TraceState.set -> void OpenTelemetry.Logs.OpenTelemetryLoggerOptions.ConfigureResource(System.Action! configure) -> OpenTelemetry.Logs.OpenTelemetryLoggerOptions! +OpenTelemetry.Metrics.HistogramConfiguration +OpenTelemetry.Metrics.HistogramConfiguration.HistogramConfiguration() -> void +OpenTelemetry.Metrics.HistogramConfiguration.RecordMinMax.get -> bool +OpenTelemetry.Metrics.HistogramConfiguration.RecordMinMax.set -> void +OpenTelemetry.Metrics.MetricPoint.GetHistogramMax() -> double +OpenTelemetry.Metrics.MetricPoint.GetHistogramMin() -> double +OpenTelemetry.Metrics.MetricPoint.HasMinMax() -> bool static Microsoft.Extensions.DependencyInjection.MeterProviderBuilderServiceCollectionExtensions.ConfigureOpenTelemetryMetrics(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! static Microsoft.Extensions.DependencyInjection.MeterProviderBuilderServiceCollectionExtensions.ConfigureOpenTelemetryMetrics(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configure) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddInstrumentation(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder) -> OpenTelemetry.Metrics.MeterProviderBuilder! diff --git a/src/OpenTelemetry/CHANGELOG.md b/src/OpenTelemetry/CHANGELOG.md index 5ea2486a319..2c6f3092d3a 100644 --- a/src/OpenTelemetry/CHANGELOG.md +++ b/src/OpenTelemetry/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +* Make recording of `Min` and `Max` for histograms configurable, enabled by + default. + ([#2735](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2735)) + * Changed default bucket boundaries for Explicit Bucket Histogram from [0, 5, 10, 25, 50, 75, 100, 250, 500, 1000] to [0, 5, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500, 5000, 7500, 10000]. diff --git a/src/OpenTelemetry/Metrics/AggregationType.cs b/src/OpenTelemetry/Metrics/AggregationType.cs index bd87c9e5b4f..eff60b2d91d 100644 --- a/src/OpenTelemetry/Metrics/AggregationType.cs +++ b/src/OpenTelemetry/Metrics/AggregationType.cs @@ -54,13 +54,23 @@ internal enum AggregationType DoubleGauge = 5, /// - /// Histogram. + /// Histogram with sum, count, buckets. /// Histogram = 6, /// - /// Histogram with sum, count only. + /// Histogram with sum, count, min, max, buckets. /// - HistogramSumCount = 7, + HistogramMinMax = 7, + + /// + /// Histogram with sum, count. + /// + HistogramSumCount = 8, + + /// + /// Histogram with sum, count, min, max. + /// + HistogramSumCountMinMax = 9, } } diff --git a/src/OpenTelemetry/Metrics/ExplicitBucketHistogramConfiguration.cs b/src/OpenTelemetry/Metrics/ExplicitBucketHistogramConfiguration.cs index 58bfc245b10..8be618748fc 100644 --- a/src/OpenTelemetry/Metrics/ExplicitBucketHistogramConfiguration.cs +++ b/src/OpenTelemetry/Metrics/ExplicitBucketHistogramConfiguration.cs @@ -21,7 +21,7 @@ namespace OpenTelemetry.Metrics /// /// Stores configuration for a histogram metric stream with explicit bucket boundaries. /// - public class ExplicitBucketHistogramConfiguration : MetricStreamConfiguration + public class ExplicitBucketHistogramConfiguration : HistogramConfiguration { /// /// Gets or sets the optional boundaries of the histogram metric stream. diff --git a/src/OpenTelemetry/Metrics/HistogramBuckets.cs b/src/OpenTelemetry/Metrics/HistogramBuckets.cs index 2a2149b31b0..c0160e534ee 100644 --- a/src/OpenTelemetry/Metrics/HistogramBuckets.cs +++ b/src/OpenTelemetry/Metrics/HistogramBuckets.cs @@ -31,13 +31,17 @@ public class HistogramBuckets internal readonly double[] ExplicitBounds; internal readonly long[] RunningBucketCounts; - internal readonly long[] SnapshotBucketCounts; internal double RunningSum; - internal double SnapshotSum; + internal double RunningMin = double.PositiveInfinity; + internal double SnapshotMin; + + internal double RunningMax = double.NegativeInfinity; + internal double SnapshotMax; + internal int IsCriticalSectionOccupied = 0; private readonly BucketLookupNode bucketLookupTreeRoot; diff --git a/src/OpenTelemetry/Metrics/HistogramConfiguration.cs b/src/OpenTelemetry/Metrics/HistogramConfiguration.cs new file mode 100644 index 00000000000..37980098937 --- /dev/null +++ b/src/OpenTelemetry/Metrics/HistogramConfiguration.cs @@ -0,0 +1,26 @@ +// +// 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.Metrics; + +public class HistogramConfiguration : MetricStreamConfiguration +{ + /// + /// Gets or sets a value indicating whether Min, Max + /// should be collected. + /// + public bool RecordMinMax { get; set; } = true; +} diff --git a/src/OpenTelemetry/Metrics/Metric.cs b/src/OpenTelemetry/Metrics/Metric.cs index c40e0f82ac7..6f5771121b7 100644 --- a/src/OpenTelemetry/Metrics/Metric.cs +++ b/src/OpenTelemetry/Metrics/Metric.cs @@ -34,7 +34,8 @@ internal Metric( AggregationTemporality temporality, int maxMetricPointsPerMetricStream, double[] histogramBounds = null, - string[] tagKeysInteresting = null) + string[] tagKeysInteresting = null, + bool histogramRecordMinMax = true) { this.InstrumentIdentity = instrumentIdentity; @@ -118,15 +119,9 @@ internal Metric( { this.MetricType = MetricType.Histogram; - if (histogramBounds != null - && histogramBounds.Length == 0) - { - aggType = AggregationType.HistogramSumCount; - } - else - { - aggType = AggregationType.Histogram; - } + aggType = histogramBounds != null && histogramBounds.Length == 0 + ? (histogramRecordMinMax ? AggregationType.HistogramSumCountMinMax : AggregationType.HistogramSumCount) + : (histogramRecordMinMax ? AggregationType.HistogramMinMax : AggregationType.Histogram); } else { diff --git a/src/OpenTelemetry/Metrics/MetricPoint.cs b/src/OpenTelemetry/Metrics/MetricPoint.cs index 55abb65b9c5..50dc46d1135 100644 --- a/src/OpenTelemetry/Metrics/MetricPoint.cs +++ b/src/OpenTelemetry/Metrics/MetricPoint.cs @@ -58,11 +58,13 @@ internal MetricPoint( this.deltaLastValue = default; this.MetricPointStatus = MetricPointStatus.NoCollectPending; - if (this.aggType == AggregationType.Histogram) + if (this.aggType == AggregationType.Histogram || + this.aggType == AggregationType.HistogramMinMax) { this.histogramBuckets = new HistogramBuckets(histogramExplicitBounds); } - else if (this.aggType == AggregationType.HistogramSumCount) + else if (this.aggType == AggregationType.HistogramSumCount || + this.aggType == AggregationType.HistogramSumCountMinMax) { this.histogramBuckets = new HistogramBuckets(null); } @@ -187,7 +189,10 @@ public readonly double GetGaugeLastValueDouble() [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly long GetHistogramCount() { - if (this.aggType != AggregationType.Histogram && this.aggType != AggregationType.HistogramSumCount) + if (this.aggType != AggregationType.Histogram && + this.aggType != AggregationType.HistogramSumCount && + this.aggType != AggregationType.HistogramMinMax && + this.aggType != AggregationType.HistogramSumCountMinMax) { this.ThrowNotSupportedMetricTypeException(nameof(this.GetHistogramCount)); } @@ -205,7 +210,10 @@ public readonly long GetHistogramCount() [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly double GetHistogramSum() { - if (this.aggType != AggregationType.Histogram && this.aggType != AggregationType.HistogramSumCount) + if (this.aggType != AggregationType.Histogram && + this.aggType != AggregationType.HistogramSumCount && + this.aggType != AggregationType.HistogramMinMax && + this.aggType != AggregationType.HistogramSumCountMinMax) { this.ThrowNotSupportedMetricTypeException(nameof(this.GetHistogramSum)); } @@ -223,7 +231,10 @@ public readonly double GetHistogramSum() [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly HistogramBuckets GetHistogramBuckets() { - if (this.aggType != AggregationType.Histogram && this.aggType != AggregationType.HistogramSumCount) + if (this.aggType != AggregationType.Histogram && + this.aggType != AggregationType.HistogramSumCount && + this.aggType != AggregationType.HistogramMinMax && + this.aggType != AggregationType.HistogramSumCountMinMax) { this.ThrowNotSupportedMetricTypeException(nameof(this.GetHistogramBuckets)); } @@ -231,6 +242,47 @@ public readonly HistogramBuckets GetHistogramBuckets() return this.histogramBuckets; } + /// + /// Gets the minimum value of the histogram associated with the metric + /// point if present. Check by using . + /// + /// + /// Applies to metric type. + /// + /// A histogram's maximum value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public double GetHistogramMin() + { + return this.histogramBuckets.SnapshotMin; + } + + /// + /// Gets the maximum value of the histogram associated with the metric + /// point if present. Check by using . + /// + /// + /// Applies to metric type. + /// + /// A histogram's maximum value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public double GetHistogramMax() + { + return this.histogramBuckets.SnapshotMax; + } + + /// + /// Gets whether the histogram has a valid Min and Max value. + /// Should be used before + /// and to + /// ensure a valid value is returned. + /// + /// A minimum and maximum value exist. + public bool HasMinMax() + { + return this.aggType == AggregationType.HistogramMinMax || + this.aggType == AggregationType.HistogramSumCountMinMax; + } + internal readonly MetricPoint Copy() { MetricPoint copy = this; @@ -261,7 +313,9 @@ internal void Update(long number) } case AggregationType.Histogram: + case AggregationType.HistogramMinMax: case AggregationType.HistogramSumCount: + case AggregationType.HistogramSumCountMinMax: { this.Update((double)number); @@ -374,6 +428,63 @@ internal void Update(double number) sw.SpinOnce(); } + break; + } + + case AggregationType.HistogramMinMax: + { + int i = this.histogramBuckets.FindBucketIndex(number); + + var sw = default(SpinWait); + while (true) + { + if (Interlocked.Exchange(ref this.histogramBuckets.IsCriticalSectionOccupied, 1) == 0) + { + // Lock acquired + unchecked + { + this.runningValue.AsLong++; + this.histogramBuckets.RunningSum += number; + this.histogramBuckets.RunningBucketCounts[i]++; + this.histogramBuckets.RunningMin = Math.Min(this.histogramBuckets.RunningMin, number); + this.histogramBuckets.RunningMax = Math.Max(this.histogramBuckets.RunningMax, number); + } + + // Release lock + Interlocked.Exchange(ref this.histogramBuckets.IsCriticalSectionOccupied, 0); + break; + } + + sw.SpinOnce(); + } + + break; + } + + case AggregationType.HistogramSumCountMinMax: + { + var sw = default(SpinWait); + while (true) + { + if (Interlocked.Exchange(ref this.histogramBuckets.IsCriticalSectionOccupied, 1) == 0) + { + // Lock acquired + unchecked + { + this.runningValue.AsLong++; + this.histogramBuckets.RunningSum += number; + this.histogramBuckets.RunningMin = Math.Min(this.histogramBuckets.RunningMin, number); + this.histogramBuckets.RunningMax = Math.Max(this.histogramBuckets.RunningMax, number); + } + + // Release lock + Interlocked.Exchange(ref this.histogramBuckets.IsCriticalSectionOccupied, 0); + break; + } + + sw.SpinOnce(); + } + break; } } @@ -501,6 +612,7 @@ internal void TakeSnapshot(bool outputDelta) // Lock acquired this.snapshotValue.AsLong = this.runningValue.AsLong; this.histogramBuckets.SnapshotSum = this.histogramBuckets.RunningSum; + if (outputDelta) { this.runningValue.AsLong = 0; @@ -539,10 +651,88 @@ internal void TakeSnapshot(bool outputDelta) // Lock acquired this.snapshotValue.AsLong = this.runningValue.AsLong; this.histogramBuckets.SnapshotSum = this.histogramBuckets.RunningSum; + + if (outputDelta) + { + this.runningValue.AsLong = 0; + this.histogramBuckets.RunningSum = 0; + } + + this.MetricPointStatus = MetricPointStatus.NoCollectPending; + + // Release lock + Interlocked.Exchange(ref this.histogramBuckets.IsCriticalSectionOccupied, 0); + break; + } + + sw.SpinOnce(); + } + + break; + } + + case AggregationType.HistogramMinMax: + { + var sw = default(SpinWait); + while (true) + { + if (Interlocked.Exchange(ref this.histogramBuckets.IsCriticalSectionOccupied, 1) == 0) + { + // Lock acquired + this.snapshotValue.AsLong = this.runningValue.AsLong; + this.histogramBuckets.SnapshotSum = this.histogramBuckets.RunningSum; + this.histogramBuckets.SnapshotMin = this.histogramBuckets.RunningMin; + this.histogramBuckets.SnapshotMax = this.histogramBuckets.RunningMax; + + if (outputDelta) + { + this.runningValue.AsLong = 0; + this.histogramBuckets.RunningSum = 0; + this.histogramBuckets.RunningMin = double.PositiveInfinity; + this.histogramBuckets.RunningMax = double.NegativeInfinity; + } + + for (int i = 0; i < this.histogramBuckets.RunningBucketCounts.Length; i++) + { + this.histogramBuckets.SnapshotBucketCounts[i] = this.histogramBuckets.RunningBucketCounts[i]; + if (outputDelta) + { + this.histogramBuckets.RunningBucketCounts[i] = 0; + } + } + + this.MetricPointStatus = MetricPointStatus.NoCollectPending; + + // Release lock + Interlocked.Exchange(ref this.histogramBuckets.IsCriticalSectionOccupied, 0); + break; + } + + sw.SpinOnce(); + } + + break; + } + + case AggregationType.HistogramSumCountMinMax: + { + var sw = default(SpinWait); + while (true) + { + if (Interlocked.Exchange(ref this.histogramBuckets.IsCriticalSectionOccupied, 1) == 0) + { + // Lock acquired + this.snapshotValue.AsLong = this.runningValue.AsLong; + this.histogramBuckets.SnapshotSum = this.histogramBuckets.RunningSum; + this.histogramBuckets.SnapshotMin = this.histogramBuckets.RunningMin; + this.histogramBuckets.SnapshotMax = this.histogramBuckets.RunningMax; + if (outputDelta) { this.runningValue.AsLong = 0; this.histogramBuckets.RunningSum = 0; + this.histogramBuckets.RunningMin = double.PositiveInfinity; + this.histogramBuckets.RunningMax = double.NegativeInfinity; } this.MetricPointStatus = MetricPointStatus.NoCollectPending; diff --git a/src/OpenTelemetry/Metrics/MetricReaderExt.cs b/src/OpenTelemetry/Metrics/MetricReaderExt.cs index d2863d55d6e..4aa6769f346 100644 --- a/src/OpenTelemetry/Metrics/MetricReaderExt.cs +++ b/src/OpenTelemetry/Metrics/MetricReaderExt.cs @@ -156,8 +156,7 @@ internal List AddMetricsListWithViews(Instrument instrument, List metricIdentity1.Equals(metricIdentity2); public static bool operator !=(MetricStreamIdentity metricIdentity1, MetricStreamIdentity metricIdentity2) => !metricIdentity1.Equals(metricIdentity2); @@ -99,6 +103,7 @@ public bool Equals(MetricStreamIdentity other) && this.Unit == other.Unit && this.Description == other.Description && this.ViewId == other.ViewId + && this.HistogramRecordMinMax == other.HistogramRecordMinMax && StringArrayComparer.Equals(this.TagKeys, other.TagKeys) && HistogramBoundsEqual(this.HistogramBucketBounds, other.HistogramBucketBounds); } diff --git a/test/OpenTelemetry.Tests/Metrics/MetricTestData.cs b/test/OpenTelemetry.Tests/Metrics/MetricTestData.cs index 0664dc18c00..c4b8fdf944a 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricTestData.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricTestData.cs @@ -49,5 +49,22 @@ public static IEnumerable InvalidHistogramBoundaries new object[] { new double[] { 0, 1, 1, 2 } }, new object[] { new double[] { 0, 1, 2, -1 } }, }; + + public static IEnumerable ValidHistogramMinMax + => new List + { + new object[] { new double[] { -10, 0, 1, 9, 10, 11, 19 }, new HistogramConfiguration(), -10, 19 }, + new object[] { new double[] { double.NegativeInfinity }, new HistogramConfiguration(), double.NegativeInfinity, double.NegativeInfinity }, + new object[] { new double[] { double.NegativeInfinity, 0, double.PositiveInfinity }, new HistogramConfiguration(), double.NegativeInfinity, double.PositiveInfinity }, + new object[] { new double[] { 1 }, new HistogramConfiguration(), 1, 1 }, + new object[] { new double[] { 5, 100, 4, 101, -2, 97 }, new ExplicitBucketHistogramConfiguration() { Boundaries = new double[] { 10, 20 } }, -2, 101 }, + }; + + public static IEnumerable InvalidHistogramMinMax + => new List + { + new object[] { new double[] { 1 }, new HistogramConfiguration() { RecordMinMax = false } }, + new object[] { new double[] { 1 }, new ExplicitBucketHistogramConfiguration() { Boundaries = new double[] { 10, 20 }, RecordMinMax = false } }, + }; } } diff --git a/test/OpenTelemetry.Tests/Metrics/MetricViewTests.cs b/test/OpenTelemetry.Tests/Metrics/MetricViewTests.cs index 125290c2b4c..1dda7eb6fa5 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricViewTests.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricViewTests.cs @@ -572,6 +572,74 @@ public void ViewToProduceCustomHistogramBound() Assert.Equal(boundaries.Length + 1, actualCount); } + [Theory] + [MemberData(nameof(MetricTestData.ValidHistogramMinMax), MemberType = typeof(MetricTestData))] + public void HistogramMinMax(double[] values, HistogramConfiguration histogramConfiguration, double expectedMin, double expectedMax) + { + using var meter = new Meter(Utils.GetCurrentMethodName()); + var histogram = meter.CreateHistogram("MyHistogram"); + var exportedItems = new List(); + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddView(histogram.Name, histogramConfiguration) + .AddInMemoryExporter(exportedItems) + .Build(); + + for (var i = 0; i < values.Length; i++) + { + histogram.Record(values[i]); + } + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + + var metricPoints = new List(); + foreach (ref readonly var mp in exportedItems[0].GetMetricPoints()) + { + metricPoints.Add(mp); + } + + var histogramPoint = metricPoints[0]; + var hasMinMax = histogramPoint.HasMinMax(); + Assert.True(hasMinMax); + + var min = histogramPoint.GetHistogramMin(); + var max = histogramPoint.GetHistogramMax(); + + Assert.Equal(expectedMin, min); + Assert.Equal(expectedMax, max); + } + + [Theory] + [MemberData(nameof(MetricTestData.InvalidHistogramMinMax), MemberType = typeof(MetricTestData))] + public void HistogramMinMaxNotPresent(double[] values, HistogramConfiguration histogramConfiguration) + { + using var meter = new Meter(Utils.GetCurrentMethodName()); + var histogram = meter.CreateHistogram("MyHistogram"); + var exportedItems = new List(); + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddView(histogram.Name, histogramConfiguration) + .AddInMemoryExporter(exportedItems) + .Build(); + + for (var i = 0; i < values.Length; i++) + { + histogram.Record(values[i]); + } + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + + var metricPoints = new List(); + foreach (ref readonly var mp in exportedItems[0].GetMetricPoints()) + { + metricPoints.Add(mp); + } + + var histogramPoint = metricPoints[0]; + + Assert.False(histogramPoint.HasMinMax()); + } + [Fact] public void ViewToSelectTagKeys() {