Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ Notes](../../RELEASENOTES.md).
* Fix `NullReferenceException` when no bucket boundaries configured for a view.
([#6773](https://github.com/open-telemetry/opentelemetry-dotnet/pull/6773))

* Added support for `OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION`
environment variable to configure the default histogram aggregation for
histogram instruments. Valid values are `explicit_bucket_histogram` (default)
and `base2_exponential_bucket_histogram`. Explicit views configured via
`AddView` take precedence over this setting.
([#6778](https://github.com/open-telemetry/opentelemetry-dotnet/pull/6778))

## 1.14.0

Released 2025-Nov-12
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,21 @@ public static void AddOtlpExporterMetricsServices(this IServiceCollection servic
{
readerOptions.TemporalityPreference = enumValue;
}

// Parse histogram aggregation using direct string comparison instead of Enum.TryParse.
// The spec defines snake_case values (explicit_bucket_histogram, base2_exponential_bucket_histogram).
// Using direct string comparison ensures we strictly validate against spec-defined values and fail
// gracefully for invalid inputs, rather than attempting to parse arbitrary strings to enum values.
// Case-insensitive comparison is used for flexibility, though the spec uses lowercase.
var otlpDefaultHistogramAggregation = config[OtlpSpecConfigDefinitions.MetricsDefaultHistogramAggregationEnvVarName];
if (string.Equals(otlpDefaultHistogramAggregation, "base2_exponential_bucket_histogram", StringComparison.OrdinalIgnoreCase))
{
readerOptions.DefaultHistogramAggregation = MetricReaderHistogramAggregation.Base2ExponentialBucketHistogram;
}
else if (string.Equals(otlpDefaultHistogramAggregation, "explicit_bucket_histogram", StringComparison.OrdinalIgnoreCase))
{
readerOptions.DefaultHistogramAggregation = MetricReaderHistogramAggregation.ExplicitBucketHistogram;
}
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ internal static class OtlpSpecConfigDefinitions
public const string MetricsTimeoutEnvVarName = "OTEL_EXPORTER_OTLP_METRICS_TIMEOUT";
public const string MetricsProtocolEnvVarName = "OTEL_EXPORTER_OTLP_METRICS_PROTOCOL";
public const string MetricsTemporalityPreferenceEnvVarName = "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE";
public const string MetricsDefaultHistogramAggregationEnvVarName = "OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION";

public const string TracesEndpointEnvVarName = "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT";
public const string TracesHeadersEnvVarName = "OTEL_EXPORTER_OTLP_TRACES_HEADERS";
Expand Down
20 changes: 14 additions & 6 deletions src/OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -484,13 +484,21 @@ or reader

* Metrics:

The following environment variables can be used to override the default value
of the `TemporalityPreference` setting for the reader configured for metrics
when using OTLP exporter:
The following environment variables can be used to override the default values
for the reader configured for metrics when using OTLP exporter:

| Environment variable | `MetricReaderOptions` property |
| ----------------------------------------------------| ------------------------------------------------|
| `OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE` | `TemporalityPreference` |
| Environment variable | `MetricReaderOptions` property |
| ------------------------------------------------------------| ------------------------------------------------|
| `OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE` | `TemporalityPreference` |
| `OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION` | `DefaultHistogramAggregation` |

`OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION` specifies the
default aggregation to use for histogram instruments. Valid values are:
* `explicit_bucket_histogram` (default)
* `base2_exponential_bucket_histogram`

Note: Explicit views configured via `AddView` take precedence over the
default histogram aggregation.

The following environment variables can be used to override the default values
of the periodic exporting metric reader configured for metrics:
Expand Down
5 changes: 5 additions & 0 deletions src/OpenTelemetry/Metrics/Reader/MetricReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@ public MetricReaderTemporalityPreference TemporalityPreference
}
}

/// <summary>
/// Gets or sets the default histogram aggregation.
/// </summary>
internal MetricReaderHistogramAggregation? DefaultHistogramAggregation { get; set; }

/// <summary>
/// Attempts to collect the metrics, blocks the current thread until
/// metrics collection completed, shutdown signaled or timed out.
Expand Down
36 changes: 35 additions & 1 deletion src/OpenTelemetry/Metrics/Reader/MetricReaderExt.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,15 @@ internal virtual List<Metric> AddMetricWithNoViews(Instrument instrument)
Debug.Assert(instrument != null, "instrument was null");
Debug.Assert(this.metrics != null, "this.metrics was null");

var metricStreamIdentity = new MetricStreamIdentity(instrument!, metricStreamConfiguration: null);
MetricStreamConfiguration? metricStreamConfiguration = null;

// Apply default histogram aggregation if configured
if (this.DefaultHistogramAggregation is { } aggregation)
{
metricStreamConfiguration = CreateDefaultHistogramConfiguration(instrument!, aggregation);
}

var metricStreamIdentity = new MetricStreamIdentity(instrument!, metricStreamConfiguration);

var exemplarFilter = metricStreamIdentity.IsHistogram
? this.exemplarFilterForHistograms ?? this.exemplarFilter
Expand Down Expand Up @@ -120,6 +128,13 @@ internal virtual List<Metric> AddMetricWithViews(Instrument instrument, List<Met
for (int i = 0; i < maxCountMetricsToBeCreated; i++)
{
var metricStreamConfig = metricStreamConfigs[i];

// Apply default histogram aggregation if no explicit view is provided
if (metricStreamConfig == null && this.DefaultHistogramAggregation is { } aggregation)
{
metricStreamConfig = CreateDefaultHistogramConfiguration(instrument!, aggregation);
}

var metricStreamIdentity = new MetricStreamIdentity(instrument!, metricStreamConfig);

var exemplarFilter = metricStreamIdentity.IsHistogram
Expand Down Expand Up @@ -189,6 +204,25 @@ internal void ApplyParentProviderSettings(
this.exemplarFilterForHistograms = exemplarFilterForHistograms;
}

private static MetricStreamConfiguration? CreateDefaultHistogramConfiguration(Instrument instrument, MetricReaderHistogramAggregation aggregation)
{
Debug.Assert(instrument != null, "instrument was null");

var instrumentType = instrument!.GetType();
if (instrumentType.IsGenericType)
{
var genericType = instrumentType.GetGenericTypeDefinition();
if (genericType == typeof(Histogram<>))
{
return aggregation == MetricReaderHistogramAggregation.Base2ExponentialBucketHistogram
? new Base2ExponentialBucketHistogramConfiguration()
: new ExplicitBucketHistogramConfiguration();
}
}

return null;
}

private bool TryGetExistingMetric(in MetricStreamIdentity metricStreamIdentity, [NotNullWhen(true)] out Metric? existingMetric)
=> this.instrumentIdentityToMetric.TryGetValue(metricStreamIdentity, out existingMetric)
&& existingMetric != null && existingMetric.Active;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

namespace OpenTelemetry.Metrics;

/// <summary>
/// Defines the default histogram aggregation for a <see cref="MetricReader" />.
/// </summary>
internal enum MetricReaderHistogramAggregation
{
/// <summary>
/// Explicit bucket histogram aggregation.
/// </summary>
ExplicitBucketHistogram = 0,

/// <summary>
/// Base2 exponential bucket histogram aggregation.
/// </summary>
Base2ExponentialBucketHistogram = 1,
}
5 changes: 5 additions & 0 deletions src/OpenTelemetry/Metrics/Reader/MetricReaderOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,9 @@ public PeriodicExportingMetricReaderOptions PeriodicExportingMetricReaderOptions
this.periodicExportingMetricReaderOptions = value;
}
}

/// <summary>
/// Gets or sets the default histogram aggregation.
/// </summary>
internal MetricReaderHistogramAggregation? DefaultHistogramAggregation { get; set; }
}
1 change: 1 addition & 0 deletions src/Shared/PeriodicExportingMetricReaderHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ internal static PeriodicExportingMetricReader CreatePeriodicExportingMetricReade
var metricReader = new PeriodicExportingMetricReader(exporter, exportInterval, exportTimeout)
{
TemporalityPreference = options.TemporalityPreference,
DefaultHistogramAggregation = options.DefaultHistogramAggregation,
};

return metricReader;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
</ItemGroup>

<ItemGroup>
<Compile Include="$(RepoRoot)\test\OpenTelemetry.Tests\EnvironmentVariableScope.cs" Link="Includes\EnvironmentVariableScope.cs" />
<Compile Include="$(RepoRoot)\test\OpenTelemetry.Tests\Shared\DelegatingExporter.cs" Link="Includes\DelegatingExporter.cs" />
<Compile Include="$(RepoRoot)\test\OpenTelemetry.Tests\Shared\EventSourceTestHelper.cs" Link="Includes\EventSourceTestHelper.cs" />
<Compile Include="$(RepoRoot)\test\OpenTelemetry.Tests\Shared\SkipUnlessEnvVarFoundTheoryAttribute.cs" Link="Includes\SkipUnlessEnvVarFoundTheoryAttribute.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -971,6 +971,168 @@ public void Dispose()
GC.SuppressFinalize(this);
}

[Theory]
[InlineData("explicit_bucket_histogram", MetricReaderHistogramAggregation.ExplicitBucketHistogram)]
[InlineData("base2_exponential_bucket_histogram", MetricReaderHistogramAggregation.Base2ExponentialBucketHistogram)]
[InlineData("invalid", null)]
internal void TestDefaultHistogramAggregationUsingConfiguration(string configValue, MetricReaderHistogramAggregation? expectedAggregation)
{
var testExecuted = false;

var configData = new Dictionary<string, string?> { [OtlpSpecConfigDefinitions.MetricsDefaultHistogramAggregationEnvVarName] = configValue };
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(configData)
.Build();

using var meterProvider = Sdk.CreateMeterProviderBuilder()
.ConfigureServices(services =>
{
services.AddSingleton<IConfiguration>(configuration);

services.PostConfigure<MetricReaderOptions>(o =>
{
testExecuted = true;
Assert.Equal(expectedAggregation, o.DefaultHistogramAggregation);
});
})
.AddOtlpExporter()
.Build();

Assert.True(testExecuted);
}

[Theory]
[InlineData("explicit_bucket_histogram", MetricReaderHistogramAggregation.ExplicitBucketHistogram)]
[InlineData("base2_exponential_bucket_histogram", MetricReaderHistogramAggregation.Base2ExponentialBucketHistogram)]
[InlineData("invalid", null)]
internal void TestDefaultHistogramAggregationUsingEnvVar(string configValue, MetricReaderHistogramAggregation? expectedAggregation)
{
using var scope = new EnvironmentVariableScope(OtlpSpecConfigDefinitions.MetricsDefaultHistogramAggregationEnvVarName, configValue);

var testExecuted = false;

using var meterProvider = Sdk.CreateMeterProviderBuilder()
.ConfigureServices(services =>
{
services.PostConfigure<MetricReaderOptions>(o =>
{
testExecuted = true;
Assert.Equal(expectedAggregation, o.DefaultHistogramAggregation);
});
})
.AddOtlpExporter()
.Build();

Assert.True(testExecuted);
}

[Theory]
[InlineData(null, false)]
[InlineData(MetricReaderHistogramAggregation.ExplicitBucketHistogram, false)]
[InlineData(MetricReaderHistogramAggregation.Base2ExponentialBucketHistogram, true)]
internal void TestDefaultHistogramAggregationAppliedToHistograms(MetricReaderHistogramAggregation? aggregation, bool expectExponential)
{
var exportedItems = new List<Metric>();

using (var meter = new Meter(Utils.GetCurrentMethodName()))
using (var meterProvider = Sdk.CreateMeterProviderBuilder()
.AddMeter(meter.Name)
.AddInMemoryExporter(exportedItems, metricReaderOptions =>
{
metricReaderOptions.DefaultHistogramAggregation = aggregation;
})
.Build())
{
var histogram = meter.CreateHistogram<long>("test_histogram");
histogram.Record(100);
histogram.Record(200);

Assert.True(meterProvider.ForceFlush());
}

// In cumulative mode, disposal triggers a shutdown collect which exports metrics again.
// Take the first metric which was exported during ForceFlush.
var metric = exportedItems.First();
Assert.Equal("test_histogram", metric.Name);

if (expectExponential)
{
Assert.Equal(MetricType.ExponentialHistogram, metric.MetricType);
}
else
{
Assert.Equal(MetricType.Histogram, metric.MetricType);
}
}

[Fact]
internal void TestDefaultHistogramAggregationOverriddenByExplicitView()
{
var exportedItems = new List<Metric>();

using (var meter = new Meter(Utils.GetCurrentMethodName()))
using (var meterProvider = Sdk.CreateMeterProviderBuilder()
.AddMeter(meter.Name)
.AddInMemoryExporter(exportedItems, metricReaderOptions =>
{
// Set default to exponential
metricReaderOptions.DefaultHistogramAggregation = MetricReaderHistogramAggregation.Base2ExponentialBucketHistogram;
})

// Explicit view should override the default
.AddView("test_histogram", new ExplicitBucketHistogramConfiguration())
.Build())
{
var histogram = meter.CreateHistogram<long>("test_histogram");
histogram.Record(100);
histogram.Record(200);

Assert.True(meterProvider.ForceFlush());
}

// In cumulative mode, disposal triggers a shutdown collect which exports metrics again.
// Take the first metric which was exported during ForceFlush.
var metric = exportedItems.First();
Assert.Equal("test_histogram", metric.Name);

// Should use explicit bucket histogram despite default being exponential
Assert.Equal(MetricType.Histogram, metric.MetricType);
}

[Fact]
internal void TestDefaultHistogramAggregationAppliedWhenViewsConfiguredButDontMatch()
{
var exportedItems = new List<Metric>();

using (var meter = new Meter(Utils.GetCurrentMethodName()))
using (var meterProvider = Sdk.CreateMeterProviderBuilder()
.AddMeter(meter.Name)
.AddInMemoryExporter(exportedItems, metricReaderOptions =>
{
// Set default to exponential
metricReaderOptions.DefaultHistogramAggregation = MetricReaderHistogramAggregation.Base2ExponentialBucketHistogram;
})

// Add a view that doesn't match our histogram
.AddView("counter_*", new MetricStreamConfiguration())
.Build())
{
var histogram = meter.CreateHistogram<long>("test_histogram");
histogram.Record(100);
histogram.Record(200);

Assert.True(meterProvider.ForceFlush());
}

// In cumulative mode, disposal triggers a shutdown collect which exports metrics again.
// Take the first metric which was exported during ForceFlush.
var metric = exportedItems.First();
Assert.Equal("test_histogram", metric.Name);

// Should use exponential histogram from default (views configured but didn't match)
Assert.Equal(MetricType.ExponentialHistogram, metric.MetricType);
}

private static void VerifyExemplars<T>(long? longValue, double? doubleValue, bool enableExemplars, Func<T, OtlpMetrics.Exemplar?> getExemplarFunc, T state)
{
var exemplar = getExemplarFunc(state);
Expand Down
Loading