From fa7a7a50a228d5cb5593020b839598943be2351f Mon Sep 17 00:00:00 2001 From: hannahhaering Date: Sun, 31 Aug 2025 18:06:46 -0400 Subject: [PATCH 1/4] added tests for multiple readers registered to the same provider; fix: The SDK MUST NOT allow a MetricReader instance to be registered on more than one MeterProvider instance. --- .../Metrics/Reader/MetricReader.cs | 5 + .../Metrics/MultipleReadersTests.cs | 91 +++++++++++++++++++ 2 files changed, 96 insertions(+) diff --git a/src/OpenTelemetry/Metrics/Reader/MetricReader.cs b/src/OpenTelemetry/Metrics/Reader/MetricReader.cs index 25bd7ca59c4..04d267293d3 100644 --- a/src/OpenTelemetry/Metrics/Reader/MetricReader.cs +++ b/src/OpenTelemetry/Metrics/Reader/MetricReader.cs @@ -218,6 +218,11 @@ public void Dispose() internal virtual void SetParentProvider(BaseProvider parentProvider) { + if (this.parentProvider != null && this.parentProvider != parentProvider) + { + throw new NotSupportedException("A MetricReader must not be registered with multiple MeterProviders."); + } + this.parentProvider = parentProvider; } diff --git a/test/OpenTelemetry.Tests/Metrics/MultipleReadersTests.cs b/test/OpenTelemetry.Tests/Metrics/MultipleReadersTests.cs index c2a1ca5ccbb..26769fe39ab 100644 --- a/test/OpenTelemetry.Tests/Metrics/MultipleReadersTests.cs +++ b/test/OpenTelemetry.Tests/Metrics/MultipleReadersTests.cs @@ -3,6 +3,7 @@ using System.Diagnostics.Metrics; using Microsoft.Extensions.DependencyInjection; +using OpenTelemetry.Exporter; using OpenTelemetry.Tests; using Xunit; @@ -10,6 +11,96 @@ namespace OpenTelemetry.Metrics.Tests; public class MultipleReadersTests { + [Fact] + public void ReaderCannotBeRegisteredMoreThanOnce() + { + var exportedItems = new List(); + using var exporter = new InMemoryExporter(exportedItems); + using var reader = new BaseExportingMetricReader(exporter); + + using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); + + var meterProviderBuilder1 = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddReader(reader); + var meterProviderBuilder2 = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddReader(reader); + + using var meterProvider1 = meterProviderBuilder1.Build(); + Assert.Throws(() => meterProviderBuilder2.Build()); + } + + [Fact] + public void MultipleReadersOneCollectsIndependently() + { + var exportedItems1 = new List(); + var exportedItems2 = new List(); + + using var exporter1 = new InMemoryExporter(exportedItems1); + using var exporter2 = new InMemoryExporter(exportedItems2); + using var reader1 = new BaseExportingMetricReader(exporter1); + using var reader2 = new BaseExportingMetricReader(exporter2); + + using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); + var counter = meter.CreateCounter("counter"); + + var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddReader(reader1) + .AddReader(reader2); + + using var meterProvider = meterProviderBuilder.Build(); + + counter.Add(1); + + reader1.Collect(); + + Assert.Single(exportedItems1); + Assert.Empty(exportedItems2); + } + + [Fact] + public void MultipleReadersDifferentTemporality() + { + var exportedItems1 = new List(); + var exportedItems2 = new List(); + + using var exporter1 = new InMemoryExporter(exportedItems1); + using var exporter2 = new InMemoryExporter(exportedItems2); + using var reader1 = new BaseExportingMetricReader(exporter1); + using var reader2 = new BaseExportingMetricReader(exporter2); + reader1.TemporalityPreference = MetricReaderTemporalityPreference.Delta; + reader2.TemporalityPreference = MetricReaderTemporalityPreference.Cumulative; + + using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); + var counter = meter.CreateCounter("counter"); + + var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddReader(reader1) + .AddReader(reader2); + + using var meterProvider = meterProviderBuilder.Build(); + + counter.Add(1); + + reader1.Collect(); + reader2.Collect(); + + AssertLongSumValueForMetric(exportedItems1[0], 1); + AssertLongSumValueForMetric(exportedItems2[0], 1); + + exportedItems1.Clear(); + + counter.Add(10); + reader1.Collect(); + reader2.Collect(); + + AssertLongSumValueForMetric(exportedItems1[0], 10); + AssertLongSumValueForMetric(exportedItems2[0], 11); + } + [Theory] [InlineData(MetricReaderTemporalityPreference.Delta, false)] [InlineData(MetricReaderTemporalityPreference.Delta, true)] From 13ede4505996c937e34752893bd5a60eb6c89776 Mon Sep 17 00:00:00 2001 From: hannahhaering Date: Mon, 1 Sep 2025 10:53:11 -0400 Subject: [PATCH 2/4] added changelog entry --- src/OpenTelemetry/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/OpenTelemetry/CHANGELOG.md b/src/OpenTelemetry/CHANGELOG.md index 36fcccbdde4..5714b32bb2d 100644 --- a/src/OpenTelemetry/CHANGELOG.md +++ b/src/OpenTelemetry/CHANGELOG.md @@ -6,6 +6,10 @@ Notes](../../RELEASENOTES.md). ## Unreleased +* Added a verification to ensure that a MetricReader can only be registered to a +single MeterProvider, as required by the spec `The SDK MUST NOT allow a MetricReader` +`instance to be registered on more than one MeterProvider instance` + * Added `FormatMessage` configuration option to self-diagnostics feature. When set to `true` (default is false), log messages will be formatted by replacing placeholders with actual parameter values for improved readability. From efe35df9aa4049709c9f993d22d8272bf4df6865 Mon Sep 17 00:00:00 2001 From: Hannah Haering <157852144+hannahhaering@users.noreply.github.com> Date: Mon, 1 Sep 2025 11:45:15 -0400 Subject: [PATCH 3/4] Update src/OpenTelemetry/CHANGELOG.md Co-authored-by: Martin Costello --- src/OpenTelemetry/CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/OpenTelemetry/CHANGELOG.md b/src/OpenTelemetry/CHANGELOG.md index 5714b32bb2d..837a6fb316b 100644 --- a/src/OpenTelemetry/CHANGELOG.md +++ b/src/OpenTelemetry/CHANGELOG.md @@ -6,9 +6,9 @@ Notes](../../RELEASENOTES.md). ## Unreleased -* Added a verification to ensure that a MetricReader can only be registered to a -single MeterProvider, as required by the spec `The SDK MUST NOT allow a MetricReader` -`instance to be registered on more than one MeterProvider instance` +* Added a verification to ensure that a `MetricReader` can only be registered to a + single `MeterProvider`, as required by the OpenTelemetry specification. + ([#6458](https://github.com/open-telemetry/opentelemetry-dotnet/pull/6458)) * Added `FormatMessage` configuration option to self-diagnostics feature. When set to `true` (default is false), log messages will be formatted by replacing From 75d765ae843a4a45d1d109c4783dad0ee36af140 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Kie=C5=82kowicz?= Date: Tue, 9 Sep 2025 07:04:37 +0200 Subject: [PATCH 4/4] lint changelog --- src/OpenTelemetry/CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/OpenTelemetry/CHANGELOG.md b/src/OpenTelemetry/CHANGELOG.md index 837a6fb316b..8b7814ffa15 100644 --- a/src/OpenTelemetry/CHANGELOG.md +++ b/src/OpenTelemetry/CHANGELOG.md @@ -6,8 +6,8 @@ Notes](../../RELEASENOTES.md). ## Unreleased -* Added a verification to ensure that a `MetricReader` can only be registered to a - single `MeterProvider`, as required by the OpenTelemetry specification. +* Added a verification to ensure that a `MetricReader` can only be registered + to a single `MeterProvider`, as required by the OpenTelemetry specification. ([#6458](https://github.com/open-telemetry/opentelemetry-dotnet/pull/6458)) * Added `FormatMessage` configuration option to self-diagnostics feature. When