From ffffe5cc5ad82ea97420118dfbf0c1b68834a7f4 Mon Sep 17 00:00:00 2001 From: Alan West <3676547+alanwest@users.noreply.github.com> Date: Fri, 4 Mar 2022 17:16:14 -0800 Subject: [PATCH] Allow duplicate instrument registration (#2916) --- .../ConsoleMetricExporter.cs | 8 +- .../Implementation/MetricItemExtensions.cs | 4 +- .../.publicApi/net461/PublicAPI.Unshipped.txt | 3 +- .../netstandard2.0/PublicAPI.Unshipped.txt | 3 +- src/OpenTelemetry/CHANGELOG.md | 21 +- .../Internal/OpenTelemetrySdkEventSource.cs | 6 + .../Metrics/InstrumentIdentity.cs | 81 ++++ src/OpenTelemetry/Metrics/Metric.cs | 73 ++-- src/OpenTelemetry/Metrics/MetricReaderExt.cs | 44 ++- .../Metrics/MetricAPITest.cs | 352 ++++++++++++++++-- 10 files changed, 514 insertions(+), 81 deletions(-) create mode 100644 src/OpenTelemetry/Metrics/InstrumentIdentity.cs diff --git a/src/OpenTelemetry.Exporter.Console/ConsoleMetricExporter.cs b/src/OpenTelemetry.Exporter.Console/ConsoleMetricExporter.cs index 88dc077eff9..ca4cc0e1199 100644 --- a/src/OpenTelemetry.Exporter.Console/ConsoleMetricExporter.cs +++ b/src/OpenTelemetry.Exporter.Console/ConsoleMetricExporter.cs @@ -62,13 +62,13 @@ public override ExportResult Export(in Batch batch) msg.Append($", Unit: {metric.Unit}"); } - if (!string.IsNullOrEmpty(metric.Meter.Name)) + if (!string.IsNullOrEmpty(metric.MeterName)) { - msg.Append($", Meter: {metric.Meter.Name}"); + msg.Append($", Meter: {metric.MeterName}"); - if (!string.IsNullOrEmpty(metric.Meter.Version)) + if (!string.IsNullOrEmpty(metric.MeterVersion)) { - msg.Append($"/{metric.Meter.Version}"); + msg.Append($"/{metric.MeterVersion}"); } } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/MetricItemExtensions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/MetricItemExtensions.cs index eafacb2f979..eead48ca400 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/MetricItemExtensions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/MetricItemExtensions.cs @@ -60,10 +60,10 @@ internal static void AddMetrics( continue; } - var meterName = metric.Meter.Name; + var meterName = metric.MeterName; if (!metricsByLibrary.TryGetValue(meterName, out var instrumentationLibraryMetrics)) { - instrumentationLibraryMetrics = GetMetricListFromPool(meterName, metric.Meter.Version); + instrumentationLibraryMetrics = GetMetricListFromPool(meterName, metric.MeterVersion); metricsByLibrary.Add(meterName, instrumentationLibraryMetrics); resourceMetrics.InstrumentationLibraryMetrics.Add(instrumentationLibraryMetrics); diff --git a/src/OpenTelemetry/.publicApi/net461/PublicAPI.Unshipped.txt b/src/OpenTelemetry/.publicApi/net461/PublicAPI.Unshipped.txt index 3e246c44335..4305ae25fd5 100644 --- a/src/OpenTelemetry/.publicApi/net461/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry/.publicApi/net461/PublicAPI.Unshipped.txt @@ -63,7 +63,8 @@ OpenTelemetry.Metrics.MeterProviderExtensions OpenTelemetry.Metrics.Metric OpenTelemetry.Metrics.Metric.Description.get -> string OpenTelemetry.Metrics.Metric.GetMetricPoints() -> OpenTelemetry.Metrics.MetricPointsAccessor -OpenTelemetry.Metrics.Metric.Meter.get -> System.Diagnostics.Metrics.Meter +OpenTelemetry.Metrics.Metric.MeterName.get -> string +OpenTelemetry.Metrics.Metric.MeterVersion.get -> string OpenTelemetry.Metrics.Metric.MetricType.get -> OpenTelemetry.Metrics.MetricType OpenTelemetry.Metrics.Metric.Name.get -> string OpenTelemetry.Metrics.Metric.Temporality.get -> OpenTelemetry.Metrics.AggregationTemporality diff --git a/src/OpenTelemetry/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt index 3e246c44335..4305ae25fd5 100644 --- a/src/OpenTelemetry/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt @@ -63,7 +63,8 @@ OpenTelemetry.Metrics.MeterProviderExtensions OpenTelemetry.Metrics.Metric OpenTelemetry.Metrics.Metric.Description.get -> string OpenTelemetry.Metrics.Metric.GetMetricPoints() -> OpenTelemetry.Metrics.MetricPointsAccessor -OpenTelemetry.Metrics.Metric.Meter.get -> System.Diagnostics.Metrics.Meter +OpenTelemetry.Metrics.Metric.MeterName.get -> string +OpenTelemetry.Metrics.Metric.MeterVersion.get -> string OpenTelemetry.Metrics.Metric.MetricType.get -> OpenTelemetry.Metrics.MetricType OpenTelemetry.Metrics.Metric.Name.get -> string OpenTelemetry.Metrics.Metric.Temporality.get -> OpenTelemetry.Metrics.AggregationTemporality diff --git a/src/OpenTelemetry/CHANGELOG.md b/src/OpenTelemetry/CHANGELOG.md index c023e6ba628..2cfd316b6ea 100644 --- a/src/OpenTelemetry/CHANGELOG.md +++ b/src/OpenTelemetry/CHANGELOG.md @@ -2,14 +2,29 @@ ## Unreleased +* Instantiating multiple metric instruments with the same name and also + identical in all other respects - same type, description, and unit - result + in a single metric stream aggregating measurements from all the identical + instruments. + + Instantiating multiple metric instruments with the same name but differ in + some respect - different type, description, or unit - will result in a + separate metric stream for each distinct instrument. + + ([#2916](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2916)) + +* The `Meter` property on `OpenTelemetry.Metrics.Metric` has been removed. + It now has `MeterName` and `MeterVersion` properties. + ([#2916](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2916)) + * Added support for implementing custom `ResourceDetector`. ([#2949](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2949/) [#2897](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2897)) * Perf improvement for Histogram and HistogramSumCount by implementing lock-free updates. - ([2951](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2951)) - ([2961](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2961)) + ([#2951](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2951) + [#2961](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2961)) ## 1.2.0-rc2 @@ -27,7 +42,7 @@ Released 2022-Feb-02 * Performance improvement: when emitting metrics, users are strongly advised to provide tags with same Key order, to achieve maximum performance. - ([#2805](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2805/files)) + ([#2805](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2805)) ## 1.2.0-rc1 diff --git a/src/OpenTelemetry/Internal/OpenTelemetrySdkEventSource.cs b/src/OpenTelemetry/Internal/OpenTelemetrySdkEventSource.cs index 01f600a5734..ef76e10a8d0 100644 --- a/src/OpenTelemetry/Internal/OpenTelemetrySdkEventSource.cs +++ b/src/OpenTelemetry/Internal/OpenTelemetrySdkEventSource.cs @@ -360,6 +360,12 @@ public void ProviderDisposed(string providerName) this.WriteEvent(37, providerName); } + [Event(38, Message = "Duplicate Instrument '{0}', Meter '{1}' encountered. Reason: '{2}'. Suggested action: '{3}'", Level = EventLevel.Warning)] + public void DuplicateMetricInstrument(string instrumentName, string meterName, string reason, string fix) + { + this.WriteEvent(38, instrumentName, meterName, reason, fix); + } + #if DEBUG public class OpenTelemetryEventListener : EventListener { diff --git a/src/OpenTelemetry/Metrics/InstrumentIdentity.cs b/src/OpenTelemetry/Metrics/InstrumentIdentity.cs new file mode 100644 index 00000000000..52445d725cf --- /dev/null +++ b/src/OpenTelemetry/Metrics/InstrumentIdentity.cs @@ -0,0 +1,81 @@ +// +// 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; +using System.Diagnostics.Metrics; + +namespace OpenTelemetry.Metrics +{ + internal readonly struct InstrumentIdentity : IEquatable + { + private readonly int hashCode; + + public InstrumentIdentity(Meter meter, string instrumentName, string unit, string description, Type instrumentType) + { + this.MeterName = meter.Name; + this.MeterVersion = meter.Version ?? string.Empty; + this.InstrumentName = instrumentName; + this.Unit = unit ?? string.Empty; + this.Description = description ?? string.Empty; + this.InstrumentType = instrumentType; + + unchecked + { + var hash = 17; + hash = (hash * 31) + this.InstrumentType.GetHashCode(); + hash = (hash * 31) + this.MeterName.GetHashCode(); + hash = (hash * 31) + this.MeterVersion.GetHashCode(); + hash = (hash * 31) + this.InstrumentName.GetHashCode(); + hash = this.Unit == null ? hash : (hash * 31) + this.Unit.GetHashCode(); + hash = this.Description == null ? hash : (hash * 31) + this.Description.GetHashCode(); + this.hashCode = hash; + } + } + + public readonly string MeterName { get; } + + public readonly string MeterVersion { get; } + + public readonly string InstrumentName { get; } + + public readonly string Unit { get; } + + public readonly string Description { get; } + + public readonly Type InstrumentType { get; } + + public static bool operator ==(InstrumentIdentity metricIdentity1, InstrumentIdentity metricIdentity2) => metricIdentity1.Equals(metricIdentity2); + + public static bool operator !=(InstrumentIdentity metricIdentity1, InstrumentIdentity metricIdentity2) => !metricIdentity1.Equals(metricIdentity2); + + public readonly override bool Equals(object obj) + { + return obj is InstrumentIdentity other && this.Equals(other); + } + + public bool Equals(InstrumentIdentity other) + { + return this.InstrumentType == other.InstrumentType + && this.MeterName == other.MeterName + && this.MeterVersion == other.MeterVersion + && this.InstrumentName == other.InstrumentName + && this.Unit == other.Unit + && this.Description == other.Description; + } + + public readonly override int GetHashCode() => this.hashCode; + } +} diff --git a/src/OpenTelemetry/Metrics/Metric.cs b/src/OpenTelemetry/Metrics/Metric.cs index 5a567a06756..f266d12a00e 100644 --- a/src/OpenTelemetry/Metrics/Metric.cs +++ b/src/OpenTelemetry/Metrics/Metric.cs @@ -30,68 +30,63 @@ public sealed class Metric private readonly AggregatorStore aggStore; internal Metric( - Instrument instrument, + InstrumentIdentity instrumentIdentity, AggregationTemporality temporality, - string metricName, - string metricDescription, int maxMetricPointsPerMetricStream, double[] histogramBounds = null, string[] tagKeysInteresting = null) { - this.Name = metricName; - this.Description = metricDescription ?? string.Empty; - this.Unit = instrument.Unit ?? string.Empty; - this.Meter = instrument.Meter; + this.InstrumentIdentity = instrumentIdentity; AggregationType aggType; - if (instrument.GetType() == typeof(ObservableCounter) - || instrument.GetType() == typeof(ObservableCounter) - || instrument.GetType() == typeof(ObservableCounter) - || instrument.GetType() == typeof(ObservableCounter)) + if (instrumentIdentity.InstrumentType == typeof(ObservableCounter) + || instrumentIdentity.InstrumentType == typeof(ObservableCounter) + || instrumentIdentity.InstrumentType == typeof(ObservableCounter) + || instrumentIdentity.InstrumentType == typeof(ObservableCounter)) { aggType = AggregationType.LongSumIncomingCumulative; this.MetricType = MetricType.LongSum; } - else if (instrument.GetType() == typeof(Counter) - || instrument.GetType() == typeof(Counter) - || instrument.GetType() == typeof(Counter) - || instrument.GetType() == typeof(Counter)) + else if (instrumentIdentity.InstrumentType == typeof(Counter) + || instrumentIdentity.InstrumentType == typeof(Counter) + || instrumentIdentity.InstrumentType == typeof(Counter) + || instrumentIdentity.InstrumentType == typeof(Counter)) { aggType = AggregationType.LongSumIncomingDelta; this.MetricType = MetricType.LongSum; } - else if (instrument.GetType() == typeof(Counter) - || instrument.GetType() == typeof(Counter)) + else if (instrumentIdentity.InstrumentType == typeof(Counter) + || instrumentIdentity.InstrumentType == typeof(Counter)) { aggType = AggregationType.DoubleSumIncomingDelta; this.MetricType = MetricType.DoubleSum; } - else if (instrument.GetType() == typeof(ObservableCounter) - || instrument.GetType() == typeof(ObservableCounter)) + else if (instrumentIdentity.InstrumentType == typeof(ObservableCounter) + || instrumentIdentity.InstrumentType == typeof(ObservableCounter)) { aggType = AggregationType.DoubleSumIncomingCumulative; this.MetricType = MetricType.DoubleSum; } - else if (instrument.GetType() == typeof(ObservableGauge) - || instrument.GetType() == typeof(ObservableGauge)) + else if (instrumentIdentity.InstrumentType == typeof(ObservableGauge) + || instrumentIdentity.InstrumentType == typeof(ObservableGauge)) { aggType = AggregationType.DoubleGauge; this.MetricType = MetricType.DoubleGauge; } - else if (instrument.GetType() == typeof(ObservableGauge) - || instrument.GetType() == typeof(ObservableGauge) - || instrument.GetType() == typeof(ObservableGauge) - || instrument.GetType() == typeof(ObservableGauge)) + else if (instrumentIdentity.InstrumentType == typeof(ObservableGauge) + || instrumentIdentity.InstrumentType == typeof(ObservableGauge) + || instrumentIdentity.InstrumentType == typeof(ObservableGauge) + || instrumentIdentity.InstrumentType == typeof(ObservableGauge)) { aggType = AggregationType.LongGauge; this.MetricType = MetricType.LongGauge; } - else if (instrument.GetType() == typeof(Histogram) - || instrument.GetType() == typeof(Histogram) - || instrument.GetType() == typeof(Histogram) - || instrument.GetType() == typeof(Histogram) - || instrument.GetType() == typeof(Histogram) - || instrument.GetType() == typeof(Histogram)) + else if (instrumentIdentity.InstrumentType == typeof(Histogram) + || instrumentIdentity.InstrumentType == typeof(Histogram) + || instrumentIdentity.InstrumentType == typeof(Histogram) + || instrumentIdentity.InstrumentType == typeof(Histogram) + || instrumentIdentity.InstrumentType == typeof(Histogram) + || instrumentIdentity.InstrumentType == typeof(Histogram)) { this.MetricType = MetricType.Histogram; @@ -107,10 +102,10 @@ internal Metric( } else { - throw new NotSupportedException($"Unsupported Instrument Type: {instrument.GetType().FullName}"); + throw new NotSupportedException($"Unsupported Instrument Type: {instrumentIdentity.InstrumentType.FullName}"); } - this.aggStore = new AggregatorStore(metricName, aggType, temporality, maxMetricPointsPerMetricStream, histogramBounds ?? DefaultHistogramBounds, tagKeysInteresting); + this.aggStore = new AggregatorStore(instrumentIdentity.InstrumentName, aggType, temporality, maxMetricPointsPerMetricStream, histogramBounds ?? DefaultHistogramBounds, tagKeysInteresting); this.Temporality = temporality; this.InstrumentDisposed = false; } @@ -119,13 +114,17 @@ internal Metric( public AggregationTemporality Temporality { get; private set; } - public string Name { get; private set; } + public string Name => this.InstrumentIdentity.InstrumentName; - public string Description { get; private set; } + public string Description => this.InstrumentIdentity.Description; - public string Unit { get; private set; } + public string Unit => this.InstrumentIdentity.Unit; - public Meter Meter { get; private set; } + public string MeterName => this.InstrumentIdentity.MeterName; + + public string MeterVersion => this.InstrumentIdentity.MeterVersion; + + internal InstrumentIdentity InstrumentIdentity { get; private set; } internal bool InstrumentDisposed { get; set; } diff --git a/src/OpenTelemetry/Metrics/MetricReaderExt.cs b/src/OpenTelemetry/Metrics/MetricReaderExt.cs index a77b108e01c..74c4b3cfe4f 100644 --- a/src/OpenTelemetry/Metrics/MetricReaderExt.cs +++ b/src/OpenTelemetry/Metrics/MetricReaderExt.cs @@ -15,6 +15,7 @@ // using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics.Metrics; using OpenTelemetry.Internal; @@ -27,6 +28,7 @@ namespace OpenTelemetry.Metrics public abstract partial class MetricReader { private readonly HashSet metricStreamNames = new HashSet(StringComparer.OrdinalIgnoreCase); + private readonly ConcurrentDictionary instrumentIdentityToMetric = new ConcurrentDictionary(); private readonly object instrumentCreationLock = new object(); private int maxMetricStreams; private int maxMetricPointsPerMetricStream; @@ -37,14 +39,24 @@ public abstract partial class MetricReader internal Metric AddMetricWithNoViews(Instrument instrument) { var meterName = instrument.Meter.Name; + var meterVersion = instrument.Meter.Version; var metricName = instrument.Name; - var metricStreamName = $"{meterName}.{metricName}"; + var metricStreamName = $"{meterName}.{meterVersion}.{metricName}"; + var instrumentIdentity = new InstrumentIdentity(instrument.Meter, metricName, instrument.Unit, instrument.Description, instrument.GetType()); lock (this.instrumentCreationLock) { + if (this.instrumentIdentityToMetric.TryGetValue(instrumentIdentity, out var existingMetric)) + { + return existingMetric; + } + if (this.metricStreamNames.Contains(metricStreamName)) { - OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored(metricName, instrument.Meter.Name, "Metric name conflicting with existing name.", "Either change the name of the instrument or change name using View."); - return null; + OpenTelemetrySdkEventSource.Log.DuplicateMetricInstrument( + metricName, + meterName, + "Metric instrument has the same name as an existing one but differs by description, unit, or instrument type. Measurements from this instrument will still be exported but may result in conflicts.", + "Either change the name of the instrument or use MeterProviderBuilder.AddView to resolve the conflict."); } var index = ++this.metricIndex; @@ -55,7 +67,8 @@ internal Metric AddMetricWithNoViews(Instrument instrument) } else { - var metric = new Metric(instrument, this.Temporality, metricName, instrument.Description, this.maxMetricPointsPerMetricStream); + var metric = new Metric(instrumentIdentity, this.Temporality, this.maxMetricPointsPerMetricStream); + this.instrumentIdentityToMetric[instrumentIdentity] = metric; this.metrics[index] = metric; this.metricStreamNames.Add(metricStreamName); return metric; @@ -88,8 +101,11 @@ internal List AddMetricsListWithViews(Instrument instrument, List AddMetricsListWithViews(Instrument instrument, List AddMetricsListWithViews(Instrument instrument, List GetMetricsBatch() if (metric.InstrumentDisposed) { metricPointSize = metric.Snapshot(); + this.instrumentIdentityToMetric.TryRemove(metric.InstrumentIdentity, out var _); this.metrics[i] = null; } else diff --git a/test/OpenTelemetry.Tests/Metrics/MetricAPITest.cs b/test/OpenTelemetry.Tests/Metrics/MetricAPITest.cs index ba24ef84014..218ba648ab0 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricAPITest.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricAPITest.cs @@ -144,43 +144,29 @@ public void MetricDescriptionIsExportedCorrectly(string description) Assert.Equal(description ?? string.Empty, metric.Description); } - [Theory] - [InlineData(AggregationTemporality.Cumulative, true)] - [InlineData(AggregationTemporality.Cumulative, false)] - [InlineData(AggregationTemporality.Delta, true)] - [InlineData(AggregationTemporality.Delta, false)] - public void DuplicateInstrumentNamesFromSameMeterAreNotAllowed(AggregationTemporality temporality, bool hasView) + [Fact] + public void DuplicateInstrumentRegistration_NoViews_IdenticalInstruments() { var exportedItems = new List(); - using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{temporality}"); + using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() .AddMeter(meter.Name) - .AddInMemoryExporter(exportedItems, metricReaderOptions => - { - metricReaderOptions.Temporality = temporality; - }); - - if (hasView) - { - meterProviderBuilder.AddView("name1", new MetricStreamConfiguration() { Description = "description" }); - } + .AddInMemoryExporter(exportedItems); using var meterProvider = meterProviderBuilder.Build(); - var counterLong = meter.CreateCounter("name1"); - var anotherCounterSameName = meter.CreateCounter("name1"); + var instrument = meter.CreateCounter("instrumentName", "instrumentUnit", "instrumentDescription"); + var duplicateInstrument = meter.CreateCounter("instrumentName", "instrumentUnit", "instrumentDescription"); - counterLong.Add(10); - anotherCounterSameName.Add(20); - counterLong.Add(10); - anotherCounterSameName.Add(20); + instrument.Add(10); + duplicateInstrument.Add(20); meterProvider.ForceFlush(MaxTimeToAllowForFlush); Assert.Single(exportedItems); var metric = exportedItems[0]; - Assert.Equal("name1", metric.Name); + Assert.Equal("instrumentName", metric.Name); List metricPoints = new List(); foreach (ref readonly var mp in metric.GetMetricPoints()) { @@ -189,7 +175,325 @@ public void DuplicateInstrumentNamesFromSameMeterAreNotAllowed(AggregationTempor Assert.Single(metricPoints); var metricPoint1 = metricPoints[0]; - Assert.Equal(20, metricPoint1.GetSumLong()); + Assert.Equal(30, metricPoint1.GetSumLong()); + } + + [Fact] + public void DuplicateInstrumentRegistration_NoViews_DuplicateInstruments_DifferentDescription() + { + var exportedItems = new List(); + + using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); + var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedItems); + + using var meterProvider = meterProviderBuilder.Build(); + + var instrument = meter.CreateCounter("instrumentName", "instrumentUnit", "instrumentDescription1"); + var duplicateInstrument = meter.CreateCounter("instrumentName", "instrumentUnit", "instrumentDescription2"); + + instrument.Add(10); + duplicateInstrument.Add(20); + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Equal(2, exportedItems.Count); + + var metric1 = exportedItems[0]; + var metric2 = exportedItems[1]; + Assert.Equal("instrumentDescription1", metric1.Description); + Assert.Equal("instrumentDescription2", metric2.Description); + + List metric1MetricPoints = new List(); + foreach (ref readonly var mp in metric1.GetMetricPoints()) + { + metric1MetricPoints.Add(mp); + } + + Assert.Single(metric1MetricPoints); + var metricPoint1 = metric1MetricPoints[0]; + Assert.Equal(10, metricPoint1.GetSumLong()); + + List metric2MetricPoints = new List(); + foreach (ref readonly var mp in metric2.GetMetricPoints()) + { + metric2MetricPoints.Add(mp); + } + + Assert.Single(metric2MetricPoints); + var metricPoint2 = metric2MetricPoints[0]; + Assert.Equal(20, metricPoint2.GetSumLong()); + } + + [Fact] + public void DuplicateInstrumentRegistration_NoViews_DuplicateInstruments_DifferentUnit() + { + var exportedItems = new List(); + + using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); + var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedItems); + + using var meterProvider = meterProviderBuilder.Build(); + + var instrument = meter.CreateCounter("instrumentName", "instrumentUnit1", "instrumentDescription"); + var duplicateInstrument = meter.CreateCounter("instrumentName", "instrumentUnit2", "instrumentDescription"); + + instrument.Add(10); + duplicateInstrument.Add(20); + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Equal(2, exportedItems.Count); + + var metric1 = exportedItems[0]; + var metric2 = exportedItems[1]; + Assert.Equal("instrumentUnit1", metric1.Unit); + Assert.Equal("instrumentUnit2", metric2.Unit); + + List metric1MetricPoints = new List(); + foreach (ref readonly var mp in metric1.GetMetricPoints()) + { + metric1MetricPoints.Add(mp); + } + + Assert.Single(metric1MetricPoints); + var metricPoint1 = metric1MetricPoints[0]; + Assert.Equal(10, metricPoint1.GetSumLong()); + + List metric2MetricPoints = new List(); + foreach (ref readonly var mp in metric2.GetMetricPoints()) + { + metric2MetricPoints.Add(mp); + } + + Assert.Single(metric2MetricPoints); + var metricPoint2 = metric2MetricPoints[0]; + Assert.Equal(20, metricPoint2.GetSumLong()); + } + + [Fact] + public void DuplicateInstrumentRegistration_NoViews_DuplicateInstruments_DifferentDataType() + { + var exportedItems = new List(); + + using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); + var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedItems); + + using var meterProvider = meterProviderBuilder.Build(); + + var instrument = meter.CreateCounter("instrumentName", "instrumentUnit", "instrumentDescription"); + var duplicateInstrument = meter.CreateCounter("instrumentName", "instrumentUnit", "instrumentDescription"); + + instrument.Add(10); + duplicateInstrument.Add(20); + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Equal(2, exportedItems.Count); + + var metric1 = exportedItems[0]; + var metric2 = exportedItems[1]; + + List metric1MetricPoints = new List(); + foreach (ref readonly var mp in metric1.GetMetricPoints()) + { + metric1MetricPoints.Add(mp); + } + + Assert.Single(metric1MetricPoints); + var metricPoint1 = metric1MetricPoints[0]; + Assert.Equal(10, metricPoint1.GetSumLong()); + + List metric2MetricPoints = new List(); + foreach (ref readonly var mp in metric2.GetMetricPoints()) + { + metric2MetricPoints.Add(mp); + } + + Assert.Single(metric2MetricPoints); + var metricPoint2 = metric2MetricPoints[0]; + Assert.Equal(20D, metricPoint2.GetSumDouble()); + } + + [Fact] + public void DuplicateInstrumentRegistration_NoViews_DuplicateInstruments_DifferentInstrumentType() + { + var exportedItems = new List(); + + using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); + var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedItems); + + using var meterProvider = meterProviderBuilder.Build(); + + var instrument = meter.CreateCounter("instrumentName", "instrumentUnit", "instrumentDescription"); + var duplicateInstrument = meter.CreateHistogram("instrumentName", "instrumentUnit", "instrumentDescription"); + + instrument.Add(10); + duplicateInstrument.Record(20); + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Equal(2, exportedItems.Count); + + var metric1 = exportedItems[0]; + var metric2 = exportedItems[1]; + + List metric1MetricPoints = new List(); + foreach (ref readonly var mp in metric1.GetMetricPoints()) + { + metric1MetricPoints.Add(mp); + } + + Assert.Single(metric1MetricPoints); + var metricPoint1 = metric1MetricPoints[0]; + Assert.Equal(10, metricPoint1.GetSumLong()); + + List metric2MetricPoints = new List(); + foreach (ref readonly var mp in metric2.GetMetricPoints()) + { + metric2MetricPoints.Add(mp); + } + + Assert.Single(metric2MetricPoints); + var metricPoint2 = metric2MetricPoints[0]; + Assert.Equal(1, metricPoint2.GetHistogramCount()); + Assert.Equal(20D, metricPoint2.GetHistogramSum()); + } + + [Fact] + public void DuplicateInstrumentRegistration_WithViews_DuplicateInstruments_DifferentDescription() + { + var exportedItems = new List(); + + using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); + var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddView("instrumentName", new MetricStreamConfiguration { Description = "newDescription1" }) + .AddView("instrumentName", new MetricStreamConfiguration { Description = "newDescription2" }) + .AddInMemoryExporter(exportedItems); + + using var meterProvider = meterProviderBuilder.Build(); + + var instrument = meter.CreateCounter("instrumentName", "instrumentUnit", "instrumentDescription"); + + instrument.Add(10); + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Equal(2, exportedItems.Count); + + var metric1 = exportedItems[0]; + var metric2 = exportedItems[1]; + Assert.Equal("newDescription1", metric1.Description); + Assert.Equal("newDescription2", metric2.Description); + + List metric1MetricPoints = new List(); + foreach (ref readonly var mp in metric1.GetMetricPoints()) + { + metric1MetricPoints.Add(mp); + } + + Assert.Single(metric1MetricPoints); + var metricPoint1 = metric1MetricPoints[0]; + Assert.Equal(10, metricPoint1.GetSumLong()); + + List metric2MetricPoints = new List(); + foreach (ref readonly var mp in metric2.GetMetricPoints()) + { + metric2MetricPoints.Add(mp); + } + + Assert.Single(metric2MetricPoints); + var metricPoint2 = metric2MetricPoints[0]; + Assert.Equal(10, metricPoint2.GetSumLong()); + } + + [Fact] + public void DuplicateInstrumentRegistration_WithViews_TwoInstruments_ThreeStreams() + { + var exportedItems = new List(); + + using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); + var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddView((instrument) => + { + return new MetricStreamConfiguration { Name = "MetricStreamA", Description = "description" }; + }) + .AddView((instrument) => + { + return instrument.Description == "description1" + ? new MetricStreamConfiguration { Name = "MetricStreamB" } + : new MetricStreamConfiguration { Name = "MetricStreamC" }; + }) + .AddInMemoryExporter(exportedItems); + + using var meterProvider = meterProviderBuilder.Build(); + + var instrument1 = meter.CreateCounter("name", "unit", "description1"); + var instrument2 = meter.CreateCounter("name", "unit", "description2"); + + instrument1.Add(10); + instrument2.Add(10); + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Equal(3, exportedItems.Count); + + var metricA = exportedItems[0]; + var metricB = exportedItems[1]; + var metricC = exportedItems[2]; + + Assert.Equal("MetricStreamA", metricA.Name); + Assert.Equal(20, GetAggregatedValue(metricA)); + + Assert.Equal("MetricStreamB", metricB.Name); + Assert.Equal(10, GetAggregatedValue(metricB)); + + Assert.Equal("MetricStreamC", metricC.Name); + Assert.Equal(10, GetAggregatedValue(metricC)); + + long GetAggregatedValue(Metric metric) + { + var metricPoints = new List(); + foreach (ref readonly var mp in metric.GetMetricPoints()) + { + metricPoints.Add(mp); + } + + Assert.Single(metricPoints); + return metricPoints[0].GetSumLong(); + } + } + + [Fact] + public void DuplicateInstrumentNamesFromDifferentMetersWithSameNameDifferentVersion() + { + var exportedItems = new List(); + + using var meter1 = new Meter($"{Utils.GetCurrentMethodName()}", "1.0"); + using var meter2 = new Meter($"{Utils.GetCurrentMethodName()}", "2.0"); + var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter1.Name) + .AddMeter(meter2.Name) + .AddInMemoryExporter(exportedItems); + + using var meterProvider = meterProviderBuilder.Build(); + + // Expecting one metric stream. + var counterLong = meter1.CreateCounter("name1"); + counterLong.Add(10); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Single(exportedItems); + + // Expeecting another metric stream since the meter differs by version + var anotherCounterSameNameDiffMeter = meter2.CreateCounter("name1"); + anotherCounterSameNameDiffMeter.Add(10); + counterLong.Add(10); + exportedItems.Clear(); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Equal(2, exportedItems.Count); } [Theory]