From 1f7f9315dc90e367ed38fcab411904e64536a802 Mon Sep 17 00:00:00 2001 From: Mikel Blanchard Date: Tue, 21 Nov 2023 16:17:40 -0800 Subject: [PATCH] [hosting] Support .NET 8 IMetricsBuilder API (#4958) --- Directory.Packages.props | 1 + .../.publicApi/Stable/PublicAPI.Unshipped.txt | 1 + .../CHANGELOG.md | 6 + .../OpenTelemetryMetricsListener.cs | 120 ++++++ .../OpenTelemetry.Extensions.Hosting.csproj | 1 + .../OpenTelemetryBuilder.cs | 25 +- .../OpenTelemetryMetricsBuilderExtensions.cs | 81 ++++ .../Internal/OpenTelemetrySdkEventSource.cs | 12 + .../ILogger/OpenTelemetryLoggingExtensions.cs | 57 ++- .../Builder/MeterProviderBuilderExtensions.cs | 4 + src/OpenTelemetry/Metrics/MeterProviderSdk.cs | 342 +++++++-------- src/OpenTelemetry/Metrics/Metric.cs | 3 +- src/OpenTelemetry/Metrics/MetricReaderExt.cs | 109 +++-- ...nTelemetry.Extensions.Hosting.Tests.csproj | 15 + .../OpenTelemetryBuilderTests.cs | 37 ++ ...nTelemetryMetricsBuilderExtensionsTests.cs | 259 +++++++++++ .../Metrics/MeterProviderSdkTest.cs | 76 ++++ .../Metrics/MetricApiTestsBase.cs | 403 +++++++----------- .../Metrics/MetricExemplarTests.cs | 18 +- .../Metrics/MetricSnapshotTestsBase.cs | 18 +- .../Metrics/MetricTestsBase.cs | 176 ++++++++ .../Metrics/MetricViewTests.cs | 223 +++++----- 22 files changed, 1345 insertions(+), 642 deletions(-) create mode 100644 src/OpenTelemetry.Extensions.Hosting/Implementation/OpenTelemetryMetricsListener.cs create mode 100644 src/OpenTelemetry.Extensions.Hosting/OpenTelemetryMetricsBuilderExtensions.cs create mode 100644 test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetryMetricsBuilderExtensionsTests.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index af1039e95..13fb63b9f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -29,6 +29,7 @@ + diff --git a/src/OpenTelemetry.Extensions.Hosting/.publicApi/Stable/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Extensions.Hosting/.publicApi/Stable/PublicAPI.Unshipped.txt index e69de29bb..8b1378917 100644 --- a/src/OpenTelemetry.Extensions.Hosting/.publicApi/Stable/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Extensions.Hosting/.publicApi/Stable/PublicAPI.Unshipped.txt @@ -0,0 +1 @@ + diff --git a/src/OpenTelemetry.Extensions.Hosting/CHANGELOG.md b/src/OpenTelemetry.Extensions.Hosting/CHANGELOG.md index f9ab8b5d8..6b801fc61 100644 --- a/src/OpenTelemetry.Extensions.Hosting/CHANGELOG.md +++ b/src/OpenTelemetry.Extensions.Hosting/CHANGELOG.md @@ -6,6 +6,12 @@ version to `8.0.0`. ([#5051](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5051)) +* The `OpenTelemetryBuilder.WithMetrics` method will now register an + `IMetricsListener` named 'OpenTelemetry' into the `IServiceCollection` to + enable metric management via the new `Microsoft.Extensions.Diagnostics` .NET 8 + APIs. + ([#4958](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4958)) + ## 1.7.0-alpha.1 Released 2023-Oct-16 diff --git a/src/OpenTelemetry.Extensions.Hosting/Implementation/OpenTelemetryMetricsListener.cs b/src/OpenTelemetry.Extensions.Hosting/Implementation/OpenTelemetryMetricsListener.cs new file mode 100644 index 000000000..51c8c0d18 --- /dev/null +++ b/src/OpenTelemetry.Extensions.Hosting/Implementation/OpenTelemetryMetricsListener.cs @@ -0,0 +1,120 @@ +// +// 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.Diagnostics; +using System.Diagnostics.Metrics; +using Microsoft.Extensions.Diagnostics.Metrics; + +namespace OpenTelemetry.Metrics; + +internal sealed class OpenTelemetryMetricsListener : IMetricsListener, IDisposable +{ + private readonly MeterProviderSdk meterProviderSdk; + private IObservableInstrumentsSource? observableInstrumentsSource; + + public OpenTelemetryMetricsListener(MeterProvider meterProvider) + { + var meterProviderSdk = meterProvider as MeterProviderSdk; + + Debug.Assert(meterProviderSdk != null, "meterProvider was not MeterProviderSdk"); + + this.meterProviderSdk = meterProviderSdk!; + + this.meterProviderSdk.OnCollectObservableInstruments += this.OnCollectObservableInstruments; + } + + public string Name => "OpenTelemetry"; + + public void Dispose() + { + this.meterProviderSdk.OnCollectObservableInstruments -= this.OnCollectObservableInstruments; + } + + public MeasurementHandlers GetMeasurementHandlers() + { + return new MeasurementHandlers() + { + ByteHandler = (instrument, value, tags, state) + => this.MeasurementRecordedLong(instrument, value, tags, state), + ShortHandler = (instrument, value, tags, state) + => this.MeasurementRecordedLong(instrument, value, tags, state), + IntHandler = (instrument, value, tags, state) + => this.MeasurementRecordedLong(instrument, value, tags, state), + LongHandler = this.MeasurementRecordedLong, + FloatHandler = (instrument, value, tags, state) + => this.MeasurementRecordedDouble(instrument, value, tags, state), + DoubleHandler = this.MeasurementRecordedDouble, + }; + } + + public bool InstrumentPublished(Instrument instrument, out object? userState) + { + userState = this.meterProviderSdk.InstrumentPublished(instrument, listeningIsManagedExternally: true); + return userState != null; + } + + public void MeasurementsCompleted(Instrument instrument, object? userState) + { + var meterProvider = this.meterProviderSdk; + + if (meterProvider.ViewCount > 0) + { + meterProvider.MeasurementsCompleted(instrument, userState); + } + else + { + meterProvider.MeasurementsCompletedSingleStream(instrument, userState); + } + } + + public void Initialize(IObservableInstrumentsSource source) + { + this.observableInstrumentsSource = source; + } + + private void OnCollectObservableInstruments() + { + this.observableInstrumentsSource?.RecordObservableInstruments(); + } + + private void MeasurementRecordedDouble(Instrument instrument, double value, ReadOnlySpan> tagsRos, object? userState) + { + var meterProvider = this.meterProviderSdk; + + if (meterProvider.ViewCount > 0) + { + meterProvider.MeasurementRecordedDouble(instrument, value, tagsRos, userState); + } + else + { + meterProvider.MeasurementRecordedDoubleSingleStream(instrument, value, tagsRos, userState); + } + } + + private void MeasurementRecordedLong(Instrument instrument, long value, ReadOnlySpan> tagsRos, object? userState) + { + var meterProvider = this.meterProviderSdk; + + if (meterProvider.ViewCount > 0) + { + meterProvider.MeasurementRecordedLong(instrument, value, tagsRos, userState); + } + else + { + meterProvider.MeasurementRecordedLongSingleStream(instrument, value, tagsRos, userState); + } + } +} diff --git a/src/OpenTelemetry.Extensions.Hosting/OpenTelemetry.Extensions.Hosting.csproj b/src/OpenTelemetry.Extensions.Hosting/OpenTelemetry.Extensions.Hosting.csproj index 0d589aae7..cc6b1b078 100644 --- a/src/OpenTelemetry.Extensions.Hosting/OpenTelemetry.Extensions.Hosting.csproj +++ b/src/OpenTelemetry.Extensions.Hosting/OpenTelemetry.Extensions.Hosting.csproj @@ -10,6 +10,7 @@ + diff --git a/src/OpenTelemetry.Extensions.Hosting/OpenTelemetryBuilder.cs b/src/OpenTelemetry.Extensions.Hosting/OpenTelemetryBuilder.cs index 805808488..00af83db1 100644 --- a/src/OpenTelemetry.Extensions.Hosting/OpenTelemetryBuilder.cs +++ b/src/OpenTelemetry.Extensions.Hosting/OpenTelemetryBuilder.cs @@ -15,6 +15,7 @@ // using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.Metrics; using OpenTelemetry.Internal; using OpenTelemetry.Logs; using OpenTelemetry.Metrics; @@ -61,13 +62,13 @@ public sealed class OpenTelemetryBuilder Guard.ThrowIfNull(configure); this.Services.ConfigureOpenTelemetryMeterProvider( - (sp, builder) => builder.ConfigureResource(configure)); + builder => builder.ConfigureResource(configure)); this.Services.ConfigureOpenTelemetryTracerProvider( - (sp, builder) => builder.ConfigureResource(configure)); + builder => builder.ConfigureResource(configure)); this.Services.ConfigureOpenTelemetryLoggerProvider( - (sp, builder) => builder.ConfigureResource(configure)); + builder => builder.ConfigureResource(configure)); return this; } @@ -76,9 +77,15 @@ public sealed class OpenTelemetryBuilder /// Adds metric services into the builder. /// /// - /// Note: This is safe to be called multiple times and by library authors. + /// Notes: + /// + /// This is safe to be called multiple times and by library authors. /// Only a single will be created for a given - /// . + /// . + /// This method automatically registers an named 'OpenTelemetry' into the . + /// /// /// The supplied for chaining /// calls. @@ -95,11 +102,9 @@ public sealed class OpenTelemetryBuilder /// calls. public OpenTelemetryBuilder WithMetrics(Action configure) { - Guard.ThrowIfNull(configure); - - var builder = new MeterProviderBuilderBase(this.Services); - - configure(builder); + OpenTelemetryMetricsBuilderExtensions.RegisterMetricsListener( + this.Services, + configure); return this; } diff --git a/src/OpenTelemetry.Extensions.Hosting/OpenTelemetryMetricsBuilderExtensions.cs b/src/OpenTelemetry.Extensions.Hosting/OpenTelemetryMetricsBuilderExtensions.cs new file mode 100644 index 000000000..a34cea3ea --- /dev/null +++ b/src/OpenTelemetry.Extensions.Hosting/OpenTelemetryMetricsBuilderExtensions.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.Diagnostics; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using OpenTelemetry.Internal; +using OpenTelemetry.Metrics; + +namespace Microsoft.Extensions.Diagnostics.Metrics; + +/// +/// Contains extension methods for registering OpenTelemetry metrics with an +/// instance. +/// +internal static class OpenTelemetryMetricsBuilderExtensions +{ + /// + /// Adds an OpenTelemetry named 'OpenTelemetry' to the . + /// + /// + /// Note: This is safe to be called multiple times and by library authors. + /// Only a single will be created for a given + /// . + /// + /// . + /// The supplied for chaining + /// calls. + public static IMetricsBuilder UseOpenTelemetry( + this IMetricsBuilder metricsBuilder) + => UseOpenTelemetry(metricsBuilder, b => { }); + + /// + /// Adds an OpenTelemetry named 'OpenTelemetry' to the . + /// + /// + /// . + /// + /// configuration callback. + /// The supplied for chaining + /// calls. + public static IMetricsBuilder UseOpenTelemetry( + this IMetricsBuilder metricsBuilder, + Action configure) + { + Guard.ThrowIfNull(metricsBuilder); + + RegisterMetricsListener(metricsBuilder.Services, configure); + + return metricsBuilder; + } + + internal static void RegisterMetricsListener( + IServiceCollection services, + Action configure) + { + Debug.Assert(services != null, "services was null"); + + Guard.ThrowIfNull(configure); + + var builder = new MeterProviderBuilderBase(services!); + + services!.TryAddEnumerable( + ServiceDescriptor.Singleton()); + + configure(builder); + } +} diff --git a/src/OpenTelemetry/Internal/OpenTelemetrySdkEventSource.cs b/src/OpenTelemetry/Internal/OpenTelemetrySdkEventSource.cs index 1cbba26f9..67a668cf3 100644 --- a/src/OpenTelemetry/Internal/OpenTelemetrySdkEventSource.cs +++ b/src/OpenTelemetry/Internal/OpenTelemetrySdkEventSource.cs @@ -347,6 +347,18 @@ internal sealed class OpenTelemetrySdkEventSource : EventSource this.WriteEvent(51, type, reason); } + [Event(52, Message = "Instrument '{0}', Meter '{1}' has been deactivated.", Level = EventLevel.Informational)] + public void MetricInstrumentDeactivated(string instrumentName, string meterName) + { + this.WriteEvent(52, instrumentName, meterName); + } + + [Event(53, Message = "Instrument '{0}', Meter '{1}' has been removed.", Level = EventLevel.Informational)] + public void MetricInstrumentRemoved(string instrumentName, string meterName) + { + this.WriteEvent(53, instrumentName, meterName); + } + #if DEBUG public class OpenTelemetryEventListener : EventListener { diff --git a/src/OpenTelemetry/Logs/ILogger/OpenTelemetryLoggingExtensions.cs b/src/OpenTelemetry/Logs/ILogger/OpenTelemetryLoggingExtensions.cs index 577624144..e4cf260aa 100644 --- a/src/OpenTelemetry/Logs/ILogger/OpenTelemetryLoggingExtensions.cs +++ b/src/OpenTelemetry/Logs/ILogger/OpenTelemetryLoggingExtensions.cs @@ -50,15 +50,43 @@ public static class OpenTelemetryLoggingExtensions /// The supplied for call chaining. public static ILoggingBuilder AddOpenTelemetry( this ILoggingBuilder builder) + => AddOpenTelemetryInternal(builder, configureBuilder: null, configureOptions: null); + + /// + /// Adds an OpenTelemetry logger named 'OpenTelemetry' to the . + /// + /// + /// The to use. + /// Optional configuration action. + /// The supplied for call chaining. + public static ILoggingBuilder AddOpenTelemetry( + this ILoggingBuilder builder, + Action? configure) + => AddOpenTelemetryInternal(builder, configureBuilder: null, configureOptions: configure); + + private static ILoggingBuilder AddOpenTelemetryInternal( + ILoggingBuilder builder, + Action? configureBuilder, + Action? configureOptions) { Guard.ThrowIfNull(builder); builder.AddConfiguration(); - // Note: This will bind logger options element (eg "Logging:OpenTelemetry") to OpenTelemetryLoggerOptions - RegisterLoggerProviderOptions(builder.Services); + var services = builder.Services; - new LoggerProviderBuilderBase(builder.Services).ConfigureBuilder( + if (configureOptions != null) + { + // TODO: Move this below the RegisterLoggerProviderOptions call so + // that user-supplied delegate fires AFTER the options are bound to + // Logging:OpenTelemetry configuration. + services.Configure(configureOptions); + } + + // Note: This will bind logger options element (eg "Logging:OpenTelemetry") to OpenTelemetryLoggerOptions + RegisterLoggerProviderOptions(services); + + var loggingBuilder = new LoggerProviderBuilderBase(services).ConfigureBuilder( (sp, logging) => { var options = sp.GetRequiredService>().CurrentValue; @@ -78,7 +106,9 @@ public static class OpenTelemetryLoggingExtensions options.Processors.Clear(); }); - builder.Services.TryAddEnumerable( + configureBuilder?.Invoke(loggingBuilder); + + services.TryAddEnumerable( ServiceDescriptor.Singleton( sp => new OpenTelemetryLoggerProvider( sp.GetRequiredService(), @@ -107,23 +137,4 @@ public static class OpenTelemetryLoggingExtensions LoggerProviderOptions.RegisterProviderOptions(services); } } - - /// - /// Adds an OpenTelemetry logger named 'OpenTelemetry' to the . - /// - /// - /// The to use. - /// Optional configuration action. - /// The supplied for call chaining. - public static ILoggingBuilder AddOpenTelemetry( - this ILoggingBuilder builder, - Action? configure) - { - if (configure != null) - { - builder.Services.Configure(configure); - } - - return AddOpenTelemetry(builder); - } } diff --git a/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderExtensions.cs b/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderExtensions.cs index d5c979604..767cf3071 100644 --- a/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderExtensions.cs +++ b/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderExtensions.cs @@ -277,6 +277,8 @@ public static class MeterProviderBuilderExtensions /// The supplied for chaining. public static MeterProviderBuilder SetResourceBuilder(this MeterProviderBuilder meterProviderBuilder, ResourceBuilder resourceBuilder) { + Guard.ThrowIfNull(resourceBuilder); + meterProviderBuilder.ConfigureBuilder((sp, builder) => { if (builder is MeterProviderBuilderSdk meterProviderBuilderSdk) @@ -297,6 +299,8 @@ public static class MeterProviderBuilderExtensions /// The supplied for chaining. public static MeterProviderBuilder ConfigureResource(this MeterProviderBuilder meterProviderBuilder, Action configure) { + Guard.ThrowIfNull(configure); + meterProviderBuilder.ConfigureBuilder((sp, builder) => { if (builder is MeterProviderBuilderSdk meterProviderBuilderSdk) diff --git a/src/OpenTelemetry/Metrics/MeterProviderSdk.cs b/src/OpenTelemetry/Metrics/MeterProviderSdk.cs index 73880837d..74405b02b 100644 --- a/src/OpenTelemetry/Metrics/MeterProviderSdk.cs +++ b/src/OpenTelemetry/Metrics/MeterProviderSdk.cs @@ -31,6 +31,7 @@ internal sealed class MeterProviderSdk : MeterProvider internal int ShutdownCount; internal bool Disposed; internal bool ShouldReclaimUnusedMetricPoints; + internal Action? OnCollectObservableInstruments; private const string EmitOverFlowAttributeConfigKey = "OTEL_DOTNET_EXPERIMENTAL_METRICS_EMIT_OVERFLOW_ATTRIBUTE"; private const string ReclaimUnusedMetricPointsConfigKey = "OTEL_DOTNET_EXPERIMENTAL_METRICS_RECLAIM_UNUSED_METRIC_POINTS"; @@ -41,6 +42,7 @@ internal sealed class MeterProviderSdk : MeterProvider private readonly MeterListener listener; private readonly MetricReader? reader; private readonly CompositeMetricReader? compositeMetricReader; + private readonly Func shouldListenTo = instrument => false; internal MeterProviderSdk( IServiceProvider serviceProvider, @@ -149,16 +151,15 @@ internal sealed class MeterProviderSdk : MeterProvider } // Setup Listener - Func shouldListenTo = instrument => false; if (state.MeterSources.Any(s => WildcardHelper.ContainsWildcard(s))) { var regex = WildcardHelper.GetWildcardRegex(state.MeterSources); - shouldListenTo = instrument => regex.IsMatch(instrument.Meter.Name); + this.shouldListenTo = instrument => regex.IsMatch(instrument.Meter.Name); } else if (state.MeterSources.Any()) { var meterSourcesToSubscribe = new HashSet(state.MeterSources, StringComparer.OrdinalIgnoreCase); - shouldListenTo = instrument => meterSourcesToSubscribe.Contains(instrument.Meter.Name); + this.shouldListenTo = instrument => meterSourcesToSubscribe.Contains(instrument.Meter.Name); } OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent($"Listening to following meters = \"{string.Join(";", state.MeterSources)}\"."); @@ -168,116 +169,19 @@ internal sealed class MeterProviderSdk : MeterProvider OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent($"Number of views configured = {viewConfigCount}."); + this.listener.InstrumentPublished = (instrument, listener) => + { + object? state = this.InstrumentPublished(instrument, listeningIsManagedExternally: false); + if (state != null) + { + listener.EnableMeasurementEvents(instrument, state); + } + }; + // We expect that all the readers to be added are provided before MeterProviderSdk is built. // If there are no readers added, we do not enable measurements for the instruments. if (viewConfigCount > 0) { - this.listener.InstrumentPublished = (instrument, listener) => - { - bool enabledMeasurements = false; - - if (!shouldListenTo(instrument)) - { - OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored(instrument.Name, instrument.Meter.Name, "Instrument belongs to a Meter not subscribed by the provider.", "Use AddMeter to add the Meter to the provider."); - return; - } - - try - { - OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent($"Started publishing Instrument = \"{instrument.Name}\" of Meter = \"{instrument.Meter.Name}\"."); - - // Creating list with initial capacity as the maximum - // possible size, to avoid any array resize/copy internally. - // There may be excess space wasted, but it'll eligible for - // GC right after this method. - var metricStreamConfigs = new List(viewConfigCount); - for (var i = 0; i < viewConfigCount; ++i) - { - var viewConfig = this.viewConfigs[i]; - MetricStreamConfiguration? metricStreamConfig = null; - - try - { - metricStreamConfig = viewConfig(instrument); - - // The SDK provides some static MetricStreamConfigurations. - // For example, the Drop configuration. The static ViewId - // should not be changed for these configurations. - if (metricStreamConfig != null && !metricStreamConfig.ViewId.HasValue) - { - metricStreamConfig.ViewId = i; - } - - if (metricStreamConfig is HistogramConfiguration - && instrument.GetType().GetGenericTypeDefinition() != typeof(Histogram<>)) - { - metricStreamConfig = null; - - OpenTelemetrySdkEventSource.Log.MetricViewIgnored( - instrument.Name, - instrument.Meter.Name, - "The current SDK does not allow aggregating non-Histogram instruments as Histograms.", - "Fix the view configuration."); - } - } - catch (Exception ex) - { - OpenTelemetrySdkEventSource.Log.MetricViewIgnored(instrument.Name, instrument.Meter.Name, ex.Message, "Fix the view configuration."); - } - - if (metricStreamConfig != null) - { - metricStreamConfigs.Add(metricStreamConfig); - } - } - - if (metricStreamConfigs.Count == 0) - { - // No views matched. Add null - // which will apply defaults. - // Users can turn off this default - // by adding a view like below as the last view. - // .AddView(instrumentName: "*", MetricStreamConfiguration.Drop) - metricStreamConfigs.Add(null); - } - - if (this.reader != null) - { - if (this.compositeMetricReader == null) - { - var metrics = this.reader.AddMetricsListWithViews(instrument, metricStreamConfigs); - if (metrics.Count > 0) - { - listener.EnableMeasurementEvents(instrument, metrics); - enabledMeasurements = true; - } - } - else - { - var metricsSuperList = this.compositeMetricReader.AddMetricsSuperListWithViews(instrument, metricStreamConfigs); - if (metricsSuperList.Any(metrics => metrics.Count > 0)) - { - listener.EnableMeasurementEvents(instrument, metricsSuperList); - enabledMeasurements = true; - } - } - } - - if (enabledMeasurements) - { - OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent($"Measurements for Instrument = \"{instrument.Name}\" of Meter = \"{instrument.Meter.Name}\" will be processed and aggregated by the SDK."); - } - else - { - OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent($"Measurements for Instrument = \"{instrument.Name}\" of Meter = \"{instrument.Meter.Name}\" will be dropped by the SDK."); - } - } - catch (Exception) - { - OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored(instrument.Name, instrument.Meter.Name, "SDK internal error occurred.", "Contact SDK owners."); - } - }; - // Everything double this.listener.SetMeasurementEventCallback(this.MeasurementRecordedDouble); this.listener.SetMeasurementEventCallback((instrument, value, tags, state) => this.MeasurementRecordedDouble(instrument, value, tags, state)); @@ -292,68 +196,6 @@ internal sealed class MeterProviderSdk : MeterProvider } else { - this.listener.InstrumentPublished = (instrument, listener) => - { - bool enabledMeasurements = false; - - if (!shouldListenTo(instrument)) - { - OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored(instrument.Name, instrument.Meter.Name, "Instrument belongs to a Meter not subscribed by the provider.", "Use AddMeter to add the Meter to the provider."); - return; - } - - try - { - OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent($"Started publishing Instrument = \"{instrument.Name}\" of Meter = \"{instrument.Meter.Name}\"."); - - if (!MeterProviderBuilderSdk.IsValidInstrumentName(instrument.Name)) - { - OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored( - instrument.Name, - instrument.Meter.Name, - "Instrument name is invalid.", - "The name must comply with the OpenTelemetry specification"); - - return; - } - - if (this.reader != null) - { - if (this.compositeMetricReader == null) - { - var metric = this.reader.AddMetricWithNoViews(instrument); - if (metric != null) - { - listener.EnableMeasurementEvents(instrument, metric); - enabledMeasurements = true; - } - } - else - { - var metrics = this.compositeMetricReader.AddMetricsWithNoViews(instrument); - if (metrics.Any(metric => metric != null)) - { - listener.EnableMeasurementEvents(instrument, metrics); - enabledMeasurements = true; - } - } - } - - if (enabledMeasurements) - { - OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent($"Measurements for Instrument = \"{instrument.Name}\" of Meter = \"{instrument.Meter.Name}\" will be processed and aggregated by the SDK."); - } - else - { - OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent($"Measurements for Instrument = \"{instrument.Name}\" of Meter = \"{instrument.Meter.Name}\" will be dropped by the SDK."); - } - } - catch (Exception) - { - OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored(instrument.Name, instrument.Meter.Name, "SDK internal error occurred.", "Contact SDK owners."); - } - }; - // Everything double this.listener.SetMeasurementEventCallback(this.MeasurementRecordedDoubleSingleStream); this.listener.SetMeasurementEventCallback((instrument, value, tags, state) => this.MeasurementRecordedDoubleSingleStream(instrument, value, tags, state)); @@ -378,6 +220,162 @@ internal sealed class MeterProviderSdk : MeterProvider internal MetricReader? Reader => this.reader; + internal int ViewCount => this.viewConfigs.Count; + + internal object? InstrumentPublished(Instrument instrument, bool listeningIsManagedExternally) + { + var listenToInstrumentUsingSdkConfiguration = this.shouldListenTo(instrument); + + if (listeningIsManagedExternally && listenToInstrumentUsingSdkConfiguration) + { + OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored( + instrument.Name, + instrument.Meter.Name, + "Instrument belongs to a Meter which has been enabled both externally and via a subscription on the provider. External subscription will be ignored in favor of the provider subscription.", + "Programmatic calls adding meters to the SDK (either by calling AddMeter directly or indirectly through helpers such as 'AddInstrumentation' extensions) are always favored over external registrations. When also using external management (typically IMetricsBuilder or IMetricsListener) remove programmatic calls to the SDK to allow registrations to be added and removed dynamically."); + return null; + } + else if (!listenToInstrumentUsingSdkConfiguration && !listeningIsManagedExternally) + { + OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored( + instrument.Name, + instrument.Meter.Name, + "Instrument belongs to a Meter not subscribed by the provider.", + "Use AddMeter to add the Meter to the provider."); + return null; + } + + object? state = null; + var viewConfigCount = this.viewConfigs.Count; + + try + { + OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent($"Started publishing Instrument = \"{instrument.Name}\" of Meter = \"{instrument.Meter.Name}\"."); + + if (viewConfigCount <= 0) + { + if (!MeterProviderBuilderSdk.IsValidInstrumentName(instrument.Name)) + { + OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored( + instrument.Name, + instrument.Meter.Name, + "Instrument name is invalid.", + "The name must comply with the OpenTelemetry specification"); + return null; + } + + if (this.reader != null) + { + if (this.compositeMetricReader == null) + { + state = this.reader.AddMetricWithNoViews(instrument); + } + else + { + var metrics = this.compositeMetricReader.AddMetricsWithNoViews(instrument); + if (metrics.Any(metric => metric != null)) + { + state = metrics; + } + } + } + } + else + { + // Creating list with initial capacity as the maximum + // possible size, to avoid any array resize/copy internally. + // There may be excess space wasted, but it'll eligible for + // GC right after this method. + var metricStreamConfigs = new List(viewConfigCount); + for (var i = 0; i < viewConfigCount; ++i) + { + var viewConfig = this.viewConfigs[i]; + MetricStreamConfiguration? metricStreamConfig = null; + + try + { + metricStreamConfig = viewConfig(instrument); + + // The SDK provides some static MetricStreamConfigurations. + // For example, the Drop configuration. The static ViewId + // should not be changed for these configurations. + if (metricStreamConfig != null && !metricStreamConfig.ViewId.HasValue) + { + metricStreamConfig.ViewId = i; + } + + if (metricStreamConfig is HistogramConfiguration + && instrument.GetType().GetGenericTypeDefinition() != typeof(Histogram<>)) + { + metricStreamConfig = null; + + OpenTelemetrySdkEventSource.Log.MetricViewIgnored( + instrument.Name, + instrument.Meter.Name, + "The current SDK does not allow aggregating non-Histogram instruments as Histograms.", + "Fix the view configuration."); + } + } + catch (Exception ex) + { + OpenTelemetrySdkEventSource.Log.MetricViewIgnored(instrument.Name, instrument.Meter.Name, ex.Message, "Fix the view configuration."); + } + + if (metricStreamConfig != null) + { + metricStreamConfigs.Add(metricStreamConfig); + } + } + + if (metricStreamConfigs.Count == 0) + { + // No views matched. Add null + // which will apply defaults. + // Users can turn off this default + // by adding a view like below as the last view. + // .AddView(instrumentName: "*", MetricStreamConfiguration.Drop) + metricStreamConfigs.Add(null); + } + + if (this.reader != null) + { + if (this.compositeMetricReader == null) + { + var metrics = this.reader.AddMetricsListWithViews(instrument, metricStreamConfigs); + if (metrics.Count > 0) + { + state = metrics; + } + } + else + { + var metricsSuperList = this.compositeMetricReader.AddMetricsSuperListWithViews(instrument, metricStreamConfigs); + if (metricsSuperList.Any(metrics => metrics.Count > 0)) + { + state = metricsSuperList; + } + } + } + } + + if (state != null) + { + OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent($"Measurements for Instrument = \"{instrument.Name}\" of Meter = \"{instrument.Meter.Name}\" will be processed and aggregated by the SDK."); + return state; + } + else + { + OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent($"Measurements for Instrument = \"{instrument.Name}\" of Meter = \"{instrument.Meter.Name}\" will be dropped by the SDK."); + return null; + } + } + catch (Exception) + { + OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored(instrument.Name, instrument.Meter.Name, "SDK internal error occurred.", "Contact SDK owners."); + return null; + } + } + internal void MeasurementsCompletedSingleStream(Instrument instrument, object? state) { Debug.Assert(instrument != null, "instrument must be non-null."); @@ -542,6 +540,8 @@ internal sealed class MeterProviderSdk : MeterProvider try { this.listener.RecordObservableInstruments(); + + this.OnCollectObservableInstruments?.Invoke(); } catch (Exception exception) { diff --git a/src/OpenTelemetry/Metrics/Metric.cs b/src/OpenTelemetry/Metrics/Metric.cs index 554d20fe3..fc9d2bb90 100644 --- a/src/OpenTelemetry/Metrics/Metric.cs +++ b/src/OpenTelemetry/Metrics/Metric.cs @@ -170,7 +170,6 @@ public sealed class Metric this.aggStore = new AggregatorStore(instrumentIdentity, aggType, temporality, maxMetricPointsPerMetricStream, emitOverflowAttribute, shouldReclaimUnusedMetricPoints, exemplarFilter); this.Temporality = temporality; - this.InstrumentDisposed = false; } /// @@ -213,7 +212,7 @@ public sealed class Metric /// internal MetricStreamIdentity InstrumentIdentity { get; private set; } - internal bool InstrumentDisposed { get; set; } + internal bool Active { get; set; } = true; /// /// Get the metric points for the metric stream. diff --git a/src/OpenTelemetry/Metrics/MetricReaderExt.cs b/src/OpenTelemetry/Metrics/MetricReaderExt.cs index 16762f97d..0fede2ab6 100644 --- a/src/OpenTelemetry/Metrics/MetricReaderExt.cs +++ b/src/OpenTelemetry/Metrics/MetricReaderExt.cs @@ -16,6 +16,7 @@ using System.Collections.Concurrent; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Metrics; using OpenTelemetry.Internal; @@ -50,20 +51,11 @@ public abstract partial class MetricReader var metricStreamIdentity = new MetricStreamIdentity(instrument, metricStreamConfiguration: null); lock (this.instrumentCreationLock) { - if (this.instrumentIdentityToMetric.TryGetValue(metricStreamIdentity, out var existingMetric)) + if (this.TryGetExistingMetric(in metricStreamIdentity, out var existingMetric)) { return existingMetric; } - if (this.metricStreamNames.Contains(metricStreamIdentity.MetricStreamName)) - { - OpenTelemetrySdkEventSource.Log.DuplicateMetricInstrument( - metricStreamIdentity.InstrumentName, - metricStreamIdentity.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; if (index >= this.maxMetricStreams) { @@ -90,7 +82,9 @@ public abstract partial class MetricReader this.instrumentIdentityToMetric[metricStreamIdentity] = metric; this.metrics![index] = metric; - this.metricStreamNames.Add(metricStreamIdentity.MetricStreamName); + + this.CreateOrUpdateMetricStreamRegistration(in metricStreamIdentity); + return metric; } } @@ -135,21 +129,12 @@ public abstract partial class MetricReader continue; } - if (this.instrumentIdentityToMetric.TryGetValue(metricStreamIdentity, out var existingMetric)) + if (this.TryGetExistingMetric(in metricStreamIdentity, out var existingMetric)) { metrics.Add(existingMetric); continue; } - if (this.metricStreamNames.Contains(metricStreamIdentity.MetricStreamName)) - { - OpenTelemetrySdkEventSource.Log.DuplicateMetricInstrument( - metricStreamIdentity.InstrumentName, - metricStreamIdentity.MeterName, - "Metric instrument has the same name as an existing one but differs by description, unit, instrument type, or aggregation configuration (like histogram bounds, tag keys etc. ). 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."); - } - if (metricStreamConfig == MetricStreamConfiguration.Drop) { OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored(metricStreamIdentity.InstrumentName, metricStreamIdentity.MeterName, "View configuration asks to drop this instrument.", "Modify view configuration to allow this instrument, if desired."); @@ -169,7 +154,8 @@ public abstract partial class MetricReader this.instrumentIdentityToMetric[metricStreamIdentity] = metric; this.metrics![index] = metric; metrics.Add(metric); - this.metricStreamNames.Add(metricStreamIdentity.MetricStreamName); + + this.CreateOrUpdateMetricStreamRegistration(in metricStreamIdentity); } } @@ -215,14 +201,14 @@ public abstract partial class MetricReader internal void CompleteSingleStreamMeasurement(Metric metric) { - metric.InstrumentDisposed = true; + DeactivateMetric(metric); } internal void CompleteMeasurement(List metrics) { foreach (var metric in metrics) { - metric.InstrumentDisposed = true; + DeactivateMetric(metric); } } @@ -252,6 +238,41 @@ public abstract partial class MetricReader } } + private static void DeactivateMetric(Metric metric) + { + if (metric.Active) + { + // TODO: This will cause the metric to be removed from the storage + // array during the next collect/export. If this happens often we + // will run out of storage. Would it be better instead to set the + // end time on the metric and keep it around so it can be + // reactivated? + metric.Active = false; + + OpenTelemetrySdkEventSource.Log.MetricInstrumentDeactivated( + metric.Name, + metric.MeterName); + } + } + + private bool TryGetExistingMetric(in MetricStreamIdentity metricStreamIdentity, [NotNullWhen(true)] out Metric? existingMetric) + => this.instrumentIdentityToMetric.TryGetValue(metricStreamIdentity, out existingMetric) + && existingMetric.Active; + + private void CreateOrUpdateMetricStreamRegistration(in MetricStreamIdentity metricStreamIdentity) + { + if (!this.metricStreamNames.Add(metricStreamIdentity.MetricStreamName)) + { + // TODO: If a metric is deactivated and then reactivated we log the + // same warning as if it was a duplicate. + OpenTelemetrySdkEventSource.Log.DuplicateMetricInstrument( + metricStreamIdentity.InstrumentName, + metricStreamIdentity.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."); + } + } + private Batch GetMetricsBatch() { Debug.Assert(this.metrics != null, "this.metrics was null"); @@ -264,25 +285,20 @@ public abstract partial class MetricReader int metricCountCurrentBatch = 0; for (int i = 0; i < target; i++) { - var metric = this.metrics![i]; - int metricPointSize = 0; + ref var metric = ref this.metrics![i]; if (metric != null) { - if (metric.InstrumentDisposed) - { - metricPointSize = metric.Snapshot(); - this.instrumentIdentityToMetric.TryRemove(metric.InstrumentIdentity, out var _); - this.metrics[i] = null; - } - else - { - metricPointSize = metric.Snapshot(); - } + int metricPointSize = metric.Snapshot(); if (metricPointSize > 0) { this.metricsCurrentBatch![metricCountCurrentBatch++] = metric; } + + if (!metric.Active) + { + this.RemoveMetric(ref metric); + } } } @@ -294,4 +310,25 @@ public abstract partial class MetricReader return default; } } + + private void RemoveMetric(ref Metric? metric) + { + Debug.Assert(metric != null, "metric was null"); + + // TODO: This logic removes the metric. If the same + // metric is published again we will create a new metric + // for it. If this happens often we will run out of + // storage. Instead, should we keep the metric around + // and set a new start time + reset its data if it comes + // back? + + OpenTelemetrySdkEventSource.Log.MetricInstrumentRemoved(metric!.Name, metric.MeterName); + + var result = this.instrumentIdentityToMetric.TryRemove(metric.InstrumentIdentity, out var _); + Debug.Assert(result, "result was false"); + + // Note: metric is a reference to the array storage so + // this clears the metric out of the array. + metric = null; + } } diff --git a/test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetry.Extensions.Hosting.Tests.csproj b/test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetry.Extensions.Hosting.Tests.csproj index c7b5e9c2e..e1d6d4fb4 100644 --- a/test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetry.Extensions.Hosting.Tests.csproj +++ b/test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetry.Extensions.Hosting.Tests.csproj @@ -4,6 +4,7 @@ $(TargetFrameworksForTests) disable + $(DefineConstants);BUILDING_HOSTING_TESTS @@ -17,6 +18,20 @@ + + + + + + + + + + + + diff --git a/test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetryBuilderTests.cs b/test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetryBuilderTests.cs index 4d31768c2..43da25a08 100644 --- a/test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetryBuilderTests.cs +++ b/test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetryBuilderTests.cs @@ -75,4 +75,41 @@ public class OpenTelemetryBuilderTests loggerProvider.Resource.Attributes, kvp => kvp.Key == "l_key1" && (string)kvp.Value == "l_value1"); } + + [Fact] + public void ConfigureResourceServiceProviderTest() + { + var services = new ServiceCollection(); + + services.AddSingleton(); + + services.AddOpenTelemetry() + .ConfigureResource(r => r.AddDetector(sp => sp.GetRequiredService())) + .WithLogging() + .WithMetrics() + .WithTracing(); + + using var sp = services.BuildServiceProvider(); + + var tracerProvider = sp.GetRequiredService() as TracerProviderSdk; + var meterProvider = sp.GetRequiredService() as MeterProviderSdk; + var loggerProvider = sp.GetRequiredService() as LoggerProviderSdk; + + Assert.NotNull(tracerProvider); + Assert.NotNull(meterProvider); + Assert.NotNull(loggerProvider); + + Assert.Single(tracerProvider.Resource.Attributes, kvp => kvp.Key == "key1" && (string)kvp.Value == "value1"); + Assert.Single(meterProvider.Resource.Attributes, kvp => kvp.Key == "key1" && (string)kvp.Value == "value1"); + Assert.Single(loggerProvider.Resource.Attributes, kvp => kvp.Key == "key1" && (string)kvp.Value == "value1"); + } + + private sealed class TestResourceDetector : IResourceDetector + { + public Resource Detect() => ResourceBuilder.CreateEmpty().AddAttributes( + new Dictionary + { + ["key1"] = "value1", + }).Build(); + } } diff --git a/test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetryMetricsBuilderExtensionsTests.cs b/test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetryMetricsBuilderExtensionsTests.cs new file mode 100644 index 000000000..66f2c11ee --- /dev/null +++ b/test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetryMetricsBuilderExtensionsTests.cs @@ -0,0 +1,259 @@ +// +// 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.Diagnostics.Metrics; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration.Memory; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.Metrics; +using Microsoft.Extensions.Options; +using OpenTelemetry.Internal; +using OpenTelemetry.Logs; +using OpenTelemetry.Metrics; +using OpenTelemetry.Metrics.Tests; +using OpenTelemetry.Tests; +using OpenTelemetry.Trace; +using Xunit; + +namespace OpenTelemetry.Extensions.Hosting.Tests; + +public class OpenTelemetryMetricsBuilderExtensionsTests +{ + [Theory] + [InlineData(false)] + [InlineData(true)] + public void EnableMetricsTest(bool useWithMetricsStyle) + { + using var meter = new Meter(Utils.GetCurrentMethodName()); + List exportedItems = new(); + + using (var host = MetricTestsBase.BuildHost( + useWithMetricsStyle, + configureMetricsBuilder: builder => builder.EnableMetrics(meter.Name), + configureMeterProviderBuilder: builder => builder.AddInMemoryExporter(exportedItems))) + { + var counter = meter.CreateCounter("TestCounter"); + counter.Add(1); + } + + AssertSingleMetricWithLongSum(exportedItems); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void EnableMetricsWithAddMeterTest(bool useWithMetricsStyle) + { + using var meter = new Meter(Utils.GetCurrentMethodName()); + List exportedItems = new(); + + using (var host = MetricTestsBase.BuildHost( + useWithMetricsStyle, + configureMetricsBuilder: builder => builder.EnableMetrics(meter.Name), + configureMeterProviderBuilder: builder => builder + .AddSdkMeter(meter.Name) + .AddInMemoryExporter(exportedItems))) + { + var counter = meter.CreateCounter("TestCounter"); + counter.Add(1); + } + + AssertSingleMetricWithLongSum(exportedItems); + } + + [Theory] + [InlineData(false, MetricReaderTemporalityPreference.Delta)] + [InlineData(true, MetricReaderTemporalityPreference.Delta)] + [InlineData(false, MetricReaderTemporalityPreference.Cumulative)] + [InlineData(true, MetricReaderTemporalityPreference.Cumulative)] + public void ReloadOfMetricsViaIConfigurationWithExportCleanupTest(bool useWithMetricsStyle, MetricReaderTemporalityPreference temporalityPreference) + { + using var inMemoryEventListener = new InMemoryEventListener(OpenTelemetrySdkEventSource.Log); + + using var meter = new Meter(Utils.GetCurrentMethodName()); + List exportedItems = new(); + + var source = new MemoryConfigurationSource(); + var memory = new MemoryConfigurationProvider(source); + var configuration = new ConfigurationRoot(new[] { memory }); + + using var host = MetricTestsBase.BuildHost( + useWithMetricsStyle, + configureAppConfiguration: (context, builder) => builder.AddConfiguration(configuration), + configureMeterProviderBuilder: builder => builder + .AddInMemoryExporter(exportedItems, reader => reader.TemporalityPreference = temporalityPreference)); + + var meterProvider = host.Services.GetRequiredService(); + var options = host.Services.GetRequiredService>(); + + var counter = meter.CreateCounter("TestCounter"); + counter.Add(1); + + meterProvider.ForceFlush(); + + Assert.Empty(exportedItems); + + memory.Set($"Metrics:EnabledMetrics:{meter.Name}:Default", "true"); + + configuration.Reload(); + + counter.Add(1); + + meterProvider.ForceFlush(); + + AssertSingleMetricWithLongSum(exportedItems); + + exportedItems.Clear(); + + memory.Set($"Metrics:EnabledMetrics:{meter.Name}:Default", "false"); + + configuration.Reload(); + + counter.Add(1); + + meterProvider.ForceFlush(); + + if (temporalityPreference == MetricReaderTemporalityPreference.Cumulative) + { + // Note: When in Cumulative the metric shows up on the export + // immediately after being deactivated and then is ignored. + AssertSingleMetricWithLongSum(exportedItems); + + meterProvider.ForceFlush(); + exportedItems.Clear(); + Assert.Empty(exportedItems); + } + else + { + Assert.Empty(exportedItems); + } + + memory.Set($"Metrics:OpenTelemetry:EnabledMetrics:{meter.Name}:Default", "true"); + + configuration.Reload(); + + counter.Add(1); + + meterProvider.ForceFlush(); + + AssertSingleMetricWithLongSum(exportedItems); + + var duplicateMetricInstrumentEvents = inMemoryEventListener.Events.Where((e) => e.EventId == 38); + + // Note: We currently log a duplicate warning anytime a metric is reactivated. + Assert.Single(duplicateMetricInstrumentEvents); + + var metricInstrumentDeactivatedEvents = inMemoryEventListener.Events.Where((e) => e.EventId == 52); + + Assert.Single(metricInstrumentDeactivatedEvents); + + var metricInstrumentRemovedEvents = inMemoryEventListener.Events.Where((e) => e.EventId == 53); + + Assert.Single(metricInstrumentRemovedEvents); + } + + [Theory] + [InlineData(false, MetricReaderTemporalityPreference.Delta)] + [InlineData(true, MetricReaderTemporalityPreference.Delta)] + [InlineData(false, MetricReaderTemporalityPreference.Cumulative)] + [InlineData(true, MetricReaderTemporalityPreference.Cumulative)] + public void ReloadOfMetricsViaIConfigurationWithoutExportCleanupTest(bool useWithMetricsStyle, MetricReaderTemporalityPreference temporalityPreference) + { + using var inMemoryEventListener = new InMemoryEventListener(OpenTelemetrySdkEventSource.Log); + + using var meter = new Meter(Utils.GetCurrentMethodName()); + List exportedItems = new(); + + var source = new MemoryConfigurationSource(); + var memory = new MemoryConfigurationProvider(source); + memory.Set($"Metrics:EnabledMetrics:{meter.Name}:Default", "true"); + var configuration = new ConfigurationRoot(new[] { memory }); + + using var host = MetricTestsBase.BuildHost( + useWithMetricsStyle, + configureAppConfiguration: (context, builder) => builder.AddConfiguration(configuration), + configureMeterProviderBuilder: builder => builder + .AddInMemoryExporter(exportedItems, reader => reader.TemporalityPreference = temporalityPreference)); + + var meterProvider = host.Services.GetRequiredService(); + var options = host.Services.GetRequiredService>(); + + var counter = meter.CreateCounter("TestCounter"); + counter.Add(1); + + memory.Set($"Metrics:EnabledMetrics:{meter.Name}:Default", "false"); + configuration.Reload(); + counter.Add(1); + + memory.Set($"Metrics:EnabledMetrics:{meter.Name}:Default", "true"); + configuration.Reload(); + counter.Add(1); + + meterProvider.ForceFlush(); + + // Note: We end up with 2 of the same metric being exported. This is + // because the current behavior when something is deactivated is to + // remove the metric. The next publish creates a new metric. + Assert.Equal(2, exportedItems.Count); + + AssertMetricWithLongSum(exportedItems[0]); + AssertMetricWithLongSum(exportedItems[1]); + + exportedItems.Clear(); + + counter.Add(1); + + meterProvider.ForceFlush(); + + AssertSingleMetricWithLongSum( + exportedItems, + expectedValue: temporalityPreference == MetricReaderTemporalityPreference.Delta ? 1 : 2); + + var duplicateMetricInstrumentEvents = inMemoryEventListener.Events.Where((e) => e.EventId == 38); + + // Note: We currently log a duplicate warning anytime a metric is reactivated. + Assert.Single(duplicateMetricInstrumentEvents); + + var metricInstrumentDeactivatedEvents = inMemoryEventListener.Events.Where((e) => e.EventId == 52); + + Assert.Single(metricInstrumentDeactivatedEvents); + + var metricInstrumentRemovedEvents = inMemoryEventListener.Events.Where((e) => e.EventId == 53); + + Assert.Single(metricInstrumentRemovedEvents); + } + + private static void AssertSingleMetricWithLongSum(List exportedItems, long expectedValue = 1) + { + Assert.Single(exportedItems); + + AssertMetricWithLongSum(exportedItems[0], expectedValue); + } + + private static void AssertMetricWithLongSum(Metric metric, long expectedValue = 1) + { + List metricPoints = new(); + foreach (ref readonly var mp in metric.GetMetricPoints()) + { + metricPoints.Add(mp); + } + + Assert.Single(metricPoints); + + var metricPoint = metricPoints[0]; + Assert.Equal(expectedValue, metricPoint.GetSumLong()); + } +} diff --git a/test/OpenTelemetry.Tests/Metrics/MeterProviderSdkTest.cs b/test/OpenTelemetry.Tests/Metrics/MeterProviderSdkTest.cs index 9a980e12e..be2e564a3 100644 --- a/test/OpenTelemetry.Tests/Metrics/MeterProviderSdkTest.cs +++ b/test/OpenTelemetry.Tests/Metrics/MeterProviderSdkTest.cs @@ -14,6 +14,9 @@ // limitations under the License. // +using System.Diagnostics.Metrics; +using OpenTelemetry.Internal; +using OpenTelemetry.Tests; using Xunit; namespace OpenTelemetry.Metrics.Tests; @@ -45,4 +48,77 @@ public class MeterProviderSdkTest Assert.NotNull(provider); } + + [Theory] + [InlineData(false, true)] + [InlineData(true, true)] + [InlineData(false, false)] + [InlineData(true, false)] + public void TransientMeterExhaustsMetricStorageTest(bool withView, bool forceFlushAfterEachTest) + { + using var inMemoryEventListener = new InMemoryEventListener(OpenTelemetrySdkEventSource.Log); + + var meterName = Utils.GetCurrentMethodName(); + var exportedItems = new List(); + + var builder = Sdk.CreateMeterProviderBuilder() + .SetMaxMetricStreams(1) + .AddMeter(meterName) + .AddInMemoryExporter(exportedItems); + + if (withView) + { + builder.AddView(i => null); + } + + using var meterProvider = builder + .Build() as MeterProviderSdk; + + Assert.NotNull(meterProvider); + + RunTest(); + + if (forceFlushAfterEachTest) + { + Assert.Single(exportedItems); + } + + RunTest(); + + if (forceFlushAfterEachTest) + { + Assert.Empty(exportedItems); + } + else + { + meterProvider.ForceFlush(); + + Assert.Single(exportedItems); + } + +#if DEBUG + // Note: This is inside a debug block because when running in CI the + // event source sees events from other tests running in parallel. + var metricInstrumentIgnoredEvents = inMemoryEventListener.Events.Where((e) => e.EventId == 33); + + Assert.Single(metricInstrumentIgnoredEvents); +#endif + + void RunTest() + { + exportedItems.Clear(); + + var meter = new Meter(meterName); + + var counter = meter.CreateCounter("Counter"); + counter.Add(1); + + meter.Dispose(); + + if (forceFlushAfterEachTest) + { + meterProvider.ForceFlush(); + } + } + } } diff --git a/test/OpenTelemetry.Tests/Metrics/MetricApiTestsBase.cs b/test/OpenTelemetry.Tests/Metrics/MetricApiTestsBase.cs index ff226b8e3..8eb3a124d 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricApiTestsBase.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricApiTestsBase.cs @@ -17,7 +17,6 @@ using System.Diagnostics; using System.Diagnostics.Metrics; using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; using OpenTelemetry.Exporter; using OpenTelemetry.Internal; using OpenTelemetry.Tests; @@ -36,27 +35,11 @@ public abstract class MetricApiTestsBase : MetricTestsBase private static readonly double DeltaDoubleValueUpdatedByEachCall = 11.987; private static readonly int NumberOfMetricUpdateByEachThread = 100000; private readonly ITestOutputHelper output; - private readonly IConfiguration configuration; protected MetricApiTestsBase(ITestOutputHelper output, bool emitOverflowAttribute, bool shouldReclaimUnusedMetricPoints) + : base(BuildConfiguration(emitOverflowAttribute, shouldReclaimUnusedMetricPoints)) { this.output = output; - - var configurationData = new Dictionary(); - - if (emitOverflowAttribute) - { - configurationData[EmitOverFlowAttributeConfigKey] = "true"; - } - - if (shouldReclaimUnusedMetricPoints) - { - configurationData[ReclaimUnusedMetricPointsConfigKey] = "true"; - } - - this.configuration = new ConfigurationBuilder() - .AddInMemoryCollection(configurationData) - .Build(); } [Fact] @@ -64,14 +47,10 @@ public abstract class MetricApiTestsBase : MetricTestsBase { using var meter = new Meter(Utils.GetCurrentMethodName()); var exportedItems = new List(); - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .ConfigureServices(services => - { - services.AddSingleton(this.configuration); - }) + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) - .AddInMemoryExporter(exportedItems) - .Build(); + .AddInMemoryExporter(exportedItems)); var counter = meter.CreateCounter("myCounter"); counter.Add(100, new KeyValuePair("tagWithNullValue", null)); @@ -101,14 +80,10 @@ public abstract class MetricApiTestsBase : MetricTestsBase { using var meter = new Meter(Utils.GetCurrentMethodName()); var exportedItems = new List(); - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .ConfigureServices(services => - { - services.AddSingleton(this.configuration); - }) + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) - .AddInMemoryExporter(exportedItems) - .Build(); + .AddInMemoryExporter(exportedItems)); var measurement = new Measurement(100, new("name", "apple"), new("color", "red")); meter.CreateObservableGauge("myGauge", () => measurement); @@ -134,14 +109,10 @@ public abstract class MetricApiTestsBase : MetricTestsBase { using var meter = new Meter(Utils.GetCurrentMethodName()); var exportedItems = new List(); - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .ConfigureServices(services => - { - services.AddSingleton(this.configuration); - }) + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) - .AddInMemoryExporter(exportedItems) - .Build(); + .AddInMemoryExporter(exportedItems)); var measurement = new Measurement(100, new("name", "apple"), new("color", "red")); meter.CreateObservableGauge("myGauge", () => measurement); @@ -172,15 +143,10 @@ public abstract class MetricApiTestsBase : MetricTestsBase var exportedItems = new List(); using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); - var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() - .ConfigureServices(services => - { - services.AddSingleton(this.configuration); - }) - .AddMeter(meter.Name) - .AddInMemoryExporter(exportedItems); - using var meterProvider = meterProviderBuilder.Build(); + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedItems)); var counter = meter.CreateCounter("name1", unit); counter.Add(10); @@ -199,15 +165,10 @@ public abstract class MetricApiTestsBase : MetricTestsBase var exportedItems = new List(); using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); - var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() - .ConfigureServices(services => - { - services.AddSingleton(this.configuration); - }) - .AddMeter(meter.Name) - .AddInMemoryExporter(exportedItems); - using var meterProvider = meterProviderBuilder.Build(); + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedItems)); var counter = meter.CreateCounter("name1", null, description); counter.Add(10); @@ -223,15 +184,10 @@ public abstract class MetricApiTestsBase : MetricTestsBase var exportedItems = new List(); using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); - var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() - .ConfigureServices(services => - { - services.AddSingleton(this.configuration); - }) - .AddMeter(meter.Name) - .AddInMemoryExporter(exportedItems); - using var meterProvider = meterProviderBuilder.Build(); + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedItems)); var instrument = meter.CreateCounter("instrumentName", "instrumentUnit", "instrumentDescription"); var duplicateInstrument = meter.CreateCounter("instrumentName", "instrumentUnit", "instrumentDescription"); @@ -261,15 +217,10 @@ public abstract class MetricApiTestsBase : MetricTestsBase var exportedItems = new List(); using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); - var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() - .ConfigureServices(services => - { - services.AddSingleton(this.configuration); - }) - .AddMeter(meter.Name) - .AddInMemoryExporter(exportedItems); - using var meterProvider = meterProviderBuilder.Build(); + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedItems)); var instrument = meter.CreateCounter("instrumentName", "instrumentUnit", "instrumentDescription1"); var duplicateInstrument = meter.CreateCounter("instrumentName", "instrumentUnit", "instrumentDescription2"); @@ -312,15 +263,10 @@ public abstract class MetricApiTestsBase : MetricTestsBase var exportedItems = new List(); using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); - var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() - .ConfigureServices(services => - { - services.AddSingleton(this.configuration); - }) - .AddMeter(meter.Name) - .AddInMemoryExporter(exportedItems); - using var meterProvider = meterProviderBuilder.Build(); + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedItems)); var instrument = meter.CreateCounter("instrumentName", "instrumentUnit1", "instrumentDescription"); var duplicateInstrument = meter.CreateCounter("instrumentName", "instrumentUnit2", "instrumentDescription"); @@ -363,15 +309,10 @@ public abstract class MetricApiTestsBase : MetricTestsBase var exportedItems = new List(); using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); - var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() - .ConfigureServices(services => - { - services.AddSingleton(this.configuration); - }) - .AddMeter(meter.Name) - .AddInMemoryExporter(exportedItems); - using var meterProvider = meterProviderBuilder.Build(); + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedItems)); var instrument = meter.CreateCounter("instrumentName", "instrumentUnit", "instrumentDescription"); var duplicateInstrument = meter.CreateCounter("instrumentName", "instrumentUnit", "instrumentDescription"); @@ -412,15 +353,10 @@ public abstract class MetricApiTestsBase : MetricTestsBase var exportedItems = new List(); using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); - var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() - .ConfigureServices(services => - { - services.AddSingleton(this.configuration); - }) - .AddMeter(meter.Name) - .AddInMemoryExporter(exportedItems); - using var meterProvider = meterProviderBuilder.Build(); + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedItems)); var instrument = meter.CreateCounter("instrumentName", "instrumentUnit", "instrumentDescription"); var duplicateInstrument = meter.CreateHistogram("instrumentName", "instrumentUnit", "instrumentDescription"); @@ -463,16 +399,11 @@ public abstract class MetricApiTestsBase : MetricTestsBase using var meter1 = new Meter($"{Utils.GetCurrentMethodName()}", "1.0"); using var meter2 = new Meter($"{Utils.GetCurrentMethodName()}", "2.0"); - var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() - .ConfigureServices(services => - { - services.AddSingleton(this.configuration); - }) + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter1.Name) .AddMeter(meter2.Name) - .AddInMemoryExporter(exportedItems); - - using var meterProvider = meterProviderBuilder.Build(); + .AddInMemoryExporter(exportedItems)); // Expecting one metric stream. var counterLong = meter1.CreateCounter("name1"); @@ -500,24 +431,22 @@ public abstract class MetricApiTestsBase : MetricTestsBase using var meter1 = new Meter($"{Utils.GetCurrentMethodName()}.1.{temporality}"); using var meter2 = new Meter($"{Utils.GetCurrentMethodName()}.2.{temporality}"); - var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() - .ConfigureServices(services => - { - services.AddSingleton(this.configuration); - }) - .AddMeter(meter1.Name) - .AddMeter(meter2.Name) - .AddInMemoryExporter(exportedItems, metricReaderOptions => - { - metricReaderOptions.TemporalityPreference = temporality; - }); - if (hasView) + using var container = this.BuildMeterProvider(out var meterProvider, builder => { - meterProviderBuilder.AddView("name1", new MetricStreamConfiguration() { Description = "description" }); - } + builder + .AddMeter(meter1.Name) + .AddMeter(meter2.Name) + .AddInMemoryExporter(exportedItems, metricReaderOptions => + { + metricReaderOptions.TemporalityPreference = temporality; + }); - using var meterProvider = meterProviderBuilder.Build(); + if (hasView) + { + builder.AddView("name1", new MetricStreamConfiguration() { Description = "description" }); + } + }); // Expecting one metric stream. var counterLong = meter1.CreateCounter("name1"); @@ -535,6 +464,7 @@ public abstract class MetricApiTestsBase : MetricTestsBase Assert.Equal(2, exportedItems.Count); } +#if !BUILDING_HOSTING_TESTS [Theory] [InlineData(true)] [InlineData(false)] @@ -548,22 +478,20 @@ public abstract class MetricApiTestsBase : MetricTestsBase using var meter6 = new Meter("SomeCompany.SomeProduct.SomeComponent"); var exportedItems = new List(); - var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() - .ConfigureServices(services => - { - services.AddSingleton(this.configuration); - }) - .AddMeter("AbcCompany.XyzProduct.Component?") - .AddMeter("DefCompany.*.ComponentC") - .AddMeter("GhiCompany.qweProduct.ComponentN") // Mixing of non-wildcard meter name and wildcard meter name. - .AddInMemoryExporter(exportedItems); - if (hasView) + using var container = this.BuildMeterProvider(out var meterProvider, builder => { - meterProviderBuilder.AddView("myGauge1", "newName"); - } + builder + .AddMeter("AbcCompany.XyzProduct.Component?") + .AddMeter("DefCompany.*.ComponentC") + .AddMeter("GhiCompany.qweProduct.ComponentN") // Mixing of non-wildcard meter name and wildcard meter name. + .AddInMemoryExporter(exportedItems); - using var meterProvider = meterProviderBuilder.Build(); + if (hasView) + { + builder.AddView("myGauge1", "newName"); + } + }); var measurement = new Measurement(100, new("name", "apple"), new("color", "red")); meter1.CreateObservableGauge("myGauge1", () => measurement); @@ -591,6 +519,7 @@ public abstract class MetricApiTestsBase : MetricTestsBase Assert.Equal("myGauge4", exportedItems[3].Name); Assert.Equal("myGauge5", exportedItems[4].Name); } +#endif [Theory] [InlineData(true)] @@ -601,19 +530,18 @@ public abstract class MetricApiTestsBase : MetricTestsBase using var meter2 = new Meter($"abcCompany.xYzProduct.componentC.{hasView}"); var exportedItems = new List(); - var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() - .ConfigureServices(services => - { - services.AddSingleton(this.configuration); - }) - .AddInMemoryExporter(exportedItems); - if (hasView) + using var container = this.BuildMeterProvider(out var meterProvider, builder => { - meterProviderBuilder.AddView("gauge1", "renamed"); - } + builder + .AddInMemoryExporter(exportedItems); + + if (hasView) + { + builder.AddView("gauge1", "renamed"); + } + }); - using var meterProvider = meterProviderBuilder.Build(); var measurement = new Measurement(100, new("name", "apple"), new("color", "red")); meter1.CreateObservableGauge("myGauge1", () => measurement); @@ -634,17 +562,13 @@ public abstract class MetricApiTestsBase : MetricTestsBase using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{exportDelta}"); var counterLong = meter.CreateCounter("mycounter"); - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .ConfigureServices(services => - { - services.AddSingleton(this.configuration); - }) + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems, metricReaderOptions => { metricReaderOptions.TemporalityPreference = exportDelta ? MetricReaderTemporalityPreference.Delta : MetricReaderTemporalityPreference.Cumulative; - }) - .Build(); + })); counterLong.Add(10); counterLong.Add(10); @@ -740,17 +664,12 @@ public abstract class MetricApiTestsBase : MetricTestsBase }; }); - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .ConfigureServices(services => - { - services.AddSingleton(this.configuration); - }) + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems, metricReaderOptions => { metricReaderOptions.TemporalityPreference = exportDelta ? MetricReaderTemporalityPreference.Delta : MetricReaderTemporalityPreference.Cumulative; - }) - .Build(); + })); meterProvider.ForceFlush(MaxTimeToAllowForFlush); long sumReceived = GetLongSum(exportedItems); @@ -818,17 +737,12 @@ public abstract class MetricApiTestsBase : MetricTestsBase }; }); - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .ConfigureServices(services => - { - services.AddSingleton(this.configuration); - }) + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems, metricReaderOptions => { metricReaderOptions.TemporalityPreference = exportDelta ? MetricReaderTemporalityPreference.Delta : MetricReaderTemporalityPreference.Cumulative; - }) - .Build(); + })); // Export 1 meterProvider.ForceFlush(MaxTimeToAllowForFlush); @@ -919,18 +833,13 @@ public abstract class MetricApiTestsBase : MetricTestsBase }; }); - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .ConfigureServices(services => - { - services.AddSingleton(this.configuration); - }) + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems, metricReaderOptions => { metricReaderOptions.TemporalityPreference = exportDelta ? MetricReaderTemporalityPreference.Delta : MetricReaderTemporalityPreference.Cumulative; }) - .AddView("requestCount", new MetricStreamConfiguration() { TagKeys = Array.Empty() }) - .Build(); + .AddView("requestCount", new MetricStreamConfiguration() { TagKeys = Array.Empty() })); meterProvider.ForceFlush(MaxTimeToAllowForFlush); Assert.Single(exportedItems); @@ -964,17 +873,13 @@ public abstract class MetricApiTestsBase : MetricTestsBase using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{exportDelta}"); var counterLong = meter.CreateUpDownCounter("mycounter"); - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .ConfigureServices(services => - { - services.AddSingleton(this.configuration); - }) + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems, metricReaderOptions => { metricReaderOptions.TemporalityPreference = exportDelta ? MetricReaderTemporalityPreference.Delta : MetricReaderTemporalityPreference.Cumulative; - }) - .Build(); + })); counterLong.Add(10); counterLong.Add(-5); @@ -1050,17 +955,12 @@ public abstract class MetricApiTestsBase : MetricTestsBase }; }); - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .ConfigureServices(services => - { - services.AddSingleton(this.configuration); - }) + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems, metricReaderOptions => { metricReaderOptions.TemporalityPreference = exportDelta ? MetricReaderTemporalityPreference.Delta : MetricReaderTemporalityPreference.Cumulative; - }) - .Build(); + })); meterProvider.ForceFlush(MaxTimeToAllowForFlush); long sumReceived = GetLongSum(exportedItems); @@ -1118,17 +1018,12 @@ public abstract class MetricApiTestsBase : MetricTestsBase }; }); - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .ConfigureServices(services => - { - services.AddSingleton(this.configuration); - }) + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems, metricReaderOptions => { metricReaderOptions.TemporalityPreference = exportDelta ? MetricReaderTemporalityPreference.Delta : MetricReaderTemporalityPreference.Cumulative; - }) - .Build(); + })); // Export 1 meterProvider.ForceFlush(MaxTimeToAllowForFlush); @@ -1192,17 +1087,13 @@ public abstract class MetricApiTestsBase : MetricTestsBase using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{exportDelta}"); var counterLong = meter.CreateCounter("Counter"); - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .ConfigureServices(services => - { - services.AddSingleton(this.configuration); - }) + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems, metricReaderOptions => { metricReaderOptions.TemporalityPreference = exportDelta ? MetricReaderTemporalityPreference.Delta : MetricReaderTemporalityPreference.Cumulative; - }) - .Build(); + })); // Emit the first metric with the sorted order of tag keys counterLong.Add(5, new("Key1", "Value1"), new("Key2", "Value2"), new("Key3", "Value3")); @@ -1287,17 +1178,13 @@ public abstract class MetricApiTestsBase : MetricTestsBase using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{exportDelta}"); var counterLong = meter.CreateCounter("Counter"); - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .ConfigureServices(services => - { - services.AddSingleton(this.configuration); - }) + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems, metricReaderOptions => { metricReaderOptions.TemporalityPreference = exportDelta ? MetricReaderTemporalityPreference.Delta : MetricReaderTemporalityPreference.Cumulative; - }) - .Build(); + })); // Emit the first metric with the unsorted order of tag keys counterLong.Add(5, new("Key1", "Value1"), new("Key3", "Value3"), new("Key2", "Value2")); @@ -1384,18 +1271,14 @@ public abstract class MetricApiTestsBase : MetricTestsBase var meter2 = new Meter($"{Utils.GetCurrentMethodName()}.{temporality}.2"); var counter1 = meter1.CreateCounter("counterFromMeter1"); var counter2 = meter2.CreateCounter("counterFromMeter2"); - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .ConfigureServices(services => - { - services.AddSingleton(this.configuration); - }) + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter1.Name) .AddMeter(meter2.Name) .AddInMemoryExporter(exportedItems, metricReaderOptions => { metricReaderOptions.TemporalityPreference = temporality; - }) - .Build(); + })); counter1.Add(10, new KeyValuePair("key", "value")); counter2.Add(10, new KeyValuePair("key", "value")); @@ -1456,17 +1339,13 @@ public abstract class MetricApiTestsBase : MetricTestsBase using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{temporality}"); var counterLong = meter.CreateCounter("mycounterCapTest"); - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .ConfigureServices(services => - { - services.AddSingleton(this.configuration); - }) + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems, metricReaderOptions => { metricReaderOptions.TemporalityPreference = temporality; - }) - .Build(); + })); // Make one Add with no tags. // as currently we reserve 0th index @@ -1556,14 +1435,9 @@ public abstract class MetricApiTestsBase : MetricTestsBase using var meter = new Meter("InstrumentWithInvalidNameIsIgnoredTest"); - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .ConfigureServices(services => - { - services.AddSingleton(this.configuration); - }) + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) - .AddInMemoryExporter(exportedItems) - .Build(); + .AddInMemoryExporter(exportedItems)); var counterLong = meter.CreateCounter(instrumentName); counterLong.Add(10); @@ -1582,14 +1456,9 @@ public abstract class MetricApiTestsBase : MetricTestsBase using var meter = new Meter("InstrumentValidNameIsExportedTest"); - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .ConfigureServices(services => - { - services.AddSingleton(this.configuration); - }) + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) - .AddInMemoryExporter(exportedItems) - .Build(); + .AddInMemoryExporter(exportedItems)); var counterLong = meter.CreateCounter(name); counterLong.Add(10); @@ -1608,19 +1477,17 @@ public abstract class MetricApiTestsBase : MetricTestsBase { // This test ensures that MeterProviderSdk can be set up without any reader using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{hasViews}"); - var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() - .ConfigureServices(services => - { - services.AddSingleton(this.configuration); - }) - .AddMeter(meter.Name); - if (hasViews) + using var container = this.BuildMeterProvider(out var meterProvider, builder => { - meterProviderBuilder.AddView("counter", "renamedCounter"); - } + builder + .AddMeter(meter.Name); - using var meterProvider = meterProviderBuilder.Build(); + if (hasViews) + { + builder.AddView("counter", "renamedCounter"); + } + }); var counter = meter.CreateCounter("counter"); @@ -1632,14 +1499,10 @@ public abstract class MetricApiTestsBase : MetricTestsBase { using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); var exportedItems = new List(); - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .ConfigureServices(services => - { - services.AddSingleton(this.configuration); - }) + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) - .AddInMemoryExporter(exportedItems) - .Build(); + .AddInMemoryExporter(exportedItems)); using (var inMemoryEventListener = new InMemoryEventListener(OpenTelemetrySdkEventSource.Log)) { @@ -1648,13 +1511,41 @@ public abstract class MetricApiTestsBase : MetricTestsBase // This validates that we log InstrumentIgnored event // and not something else. - Assert.Single(inMemoryEventListener.Events.Where((e) => e.EventId == 33)); + var instrumentIgnoredEvents = inMemoryEventListener.Events.Where((e) => e.EventId == 33); +#if BUILDING_HOSTING_TESTS + // Note: When using IMetricsListener this event is fired twice. Once + // for the SDK listener ignoring it because it isn't listening to + // the meter and then once for IMetricsListener ignoring it because + // decimal is not supported. + Assert.Equal(2, instrumentIgnoredEvents.Count()); +#else + Assert.Single(instrumentIgnoredEvents); +#endif } meterProvider.ForceFlush(MaxTimeToAllowForFlush); Assert.Empty(exportedItems); } + internal static IConfiguration BuildConfiguration(bool emitOverflowAttribute, bool shouldReclaimUnusedMetricPoints) + { + var configurationData = new Dictionary(); + + if (emitOverflowAttribute) + { + configurationData[EmitOverFlowAttributeConfigKey] = "true"; + } + + if (shouldReclaimUnusedMetricPoints) + { + configurationData[ReclaimUnusedMetricPointsConfigKey] = "true"; + } + + return new ConfigurationBuilder() + .AddInMemoryCollection(configurationData) + .Build(); + } + private static void CounterUpdateThread(object obj) where T : struct, IComparable { @@ -1716,10 +1607,10 @@ public abstract class MetricApiTestsBase : MetricTestsBase var metricItems = new List(); using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{typeof(T).Name}.{deltaValueUpdatedByEachCall}"); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) - .AddInMemoryExporter(metricItems) - .Build(); + .AddInMemoryExporter(metricItems)); var argToThread = new UpdateThreadArguments { @@ -1772,10 +1663,10 @@ public abstract class MetricApiTestsBase : MetricTestsBase var metricReader = new BaseExportingMetricReader(new InMemoryExporter(metrics)); using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{typeof(T).Name}"); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) - .AddReader(metricReader) - .Build(); + .AddReader(metricReader)); var argsToThread = new UpdateThreadArguments { diff --git a/test/OpenTelemetry.Tests/Metrics/MetricExemplarTests.cs b/test/OpenTelemetry.Tests/Metrics/MetricExemplarTests.cs index d306e618d..6529b4ec2 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricExemplarTests.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricExemplarTests.cs @@ -40,14 +40,14 @@ public class MetricExemplarTests : MetricTestsBase using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); var counter = meter.CreateCounter("testCounter"); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .SetExemplarFilter(new AlwaysOnExemplarFilter()) .AddInMemoryExporter(exportedItems, metricReaderOptions => { metricReaderOptions.TemporalityPreference = MetricReaderTemporalityPreference.Delta; - }) - .Build(); + })); var measurementValues = GenerateRandomValues(10); foreach (var value in measurementValues) @@ -94,14 +94,14 @@ public class MetricExemplarTests : MetricTestsBase using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); var histogram = meter.CreateHistogram("testHistogram"); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .SetExemplarFilter(new AlwaysOnExemplarFilter()) .AddInMemoryExporter(exportedItems, metricReaderOptions => { metricReaderOptions.TemporalityPreference = MetricReaderTemporalityPreference.Delta; - }) - .Build(); + })); var measurementValues = GenerateRandomValues(10); foreach (var value in measurementValues) @@ -147,15 +147,15 @@ public class MetricExemplarTests : MetricTestsBase using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); var histogram = meter.CreateHistogram("testHistogram"); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .SetExemplarFilter(new AlwaysOnExemplarFilter()) .AddView(histogram.Name, new MetricStreamConfiguration() { TagKeys = new string[] { "key1" } }) .AddInMemoryExporter(exportedItems, metricReaderOptions => { metricReaderOptions.TemporalityPreference = MetricReaderTemporalityPreference.Delta; - }) - .Build(); + })); var measurementValues = GenerateRandomValues(10); foreach (var value in measurementValues) diff --git a/test/OpenTelemetry.Tests/Metrics/MetricSnapshotTestsBase.cs b/test/OpenTelemetry.Tests/Metrics/MetricSnapshotTestsBase.cs index fad3663f7..9a22f7b67 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricSnapshotTestsBase.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricSnapshotTestsBase.cs @@ -31,21 +31,9 @@ public abstract class MetricSnapshotTestsBase protected MetricSnapshotTestsBase(bool emitOverflowAttribute, bool shouldReclaimUnusedMetricPoints) { - var configurationData = new Dictionary(); - - if (emitOverflowAttribute) - { - configurationData[MetricTestsBase.EmitOverFlowAttributeConfigKey] = "true"; - } - - if (shouldReclaimUnusedMetricPoints) - { - configurationData[MetricTestsBase.ReclaimUnusedMetricPointsConfigKey] = "true"; - } - - this.configuration = new ConfigurationBuilder() - .AddInMemoryCollection(configurationData) - .Build(); + this.configuration = MetricApiTestsBase.BuildConfiguration( + emitOverflowAttribute, + shouldReclaimUnusedMetricPoints); } [Fact] diff --git a/test/OpenTelemetry.Tests/Metrics/MetricTestsBase.cs b/test/OpenTelemetry.Tests/Metrics/MetricTestsBase.cs index 193280257..df8d4ecac 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricTestsBase.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricTestsBase.cs @@ -14,6 +14,15 @@ // limitations under the License. // +#if BUILDING_HOSTING_TESTS +using System.Diagnostics; +#endif +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +#if BUILDING_HOSTING_TESTS +using Microsoft.Extensions.Diagnostics.Metrics; +using Microsoft.Extensions.Hosting; +#endif using Xunit; namespace OpenTelemetry.Metrics.Tests; @@ -23,6 +32,75 @@ public class MetricTestsBase public const string EmitOverFlowAttributeConfigKey = "OTEL_DOTNET_EXPERIMENTAL_METRICS_EMIT_OVERFLOW_ATTRIBUTE"; public const string ReclaimUnusedMetricPointsConfigKey = "OTEL_DOTNET_EXPERIMENTAL_METRICS_RECLAIM_UNUSED_METRIC_POINTS"; + protected readonly IConfiguration configuration; + + protected MetricTestsBase() + { + } + + protected MetricTestsBase(IConfiguration configuration) + { + this.configuration = configuration; + } + +#if BUILDING_HOSTING_TESTS + public static IHost BuildHost( + bool useWithMetricsStyle, + Action configureAppConfiguration = null, + Action configureServices = null, + Action configureMetricsBuilder = null, + Action configureMeterProviderBuilder = null) + { + var hostBuilder = new HostBuilder() + .ConfigureDefaults(null) + .ConfigureAppConfiguration((context, builder) => + { + configureAppConfiguration?.Invoke(context, builder); + }) + .ConfigureServices(services => + { + configureServices?.Invoke(services); + + services.AddMetrics(builder => + { + configureMetricsBuilder?.Invoke(builder); + + if (!useWithMetricsStyle) + { + builder.UseOpenTelemetry(metricsBuilder => ConfigureBuilder(metricsBuilder, configureMeterProviderBuilder)); + } + }); + + if (useWithMetricsStyle) + { + services + .AddOpenTelemetry() + .WithMetrics(metricsBuilder => ConfigureBuilder(metricsBuilder, configureMeterProviderBuilder)); + } + + services.AddHostedService(); + }); + + var host = hostBuilder.Build(); + + host.Start(); + + return host; + + static void ConfigureBuilder(MeterProviderBuilder builder, Action configureMeterProviderBuilder) + { + IServiceCollection localServices = null; + + builder.ConfigureServices(services => localServices = services); + + Debug.Assert(localServices != null, "localServices was null"); + + var testBuilder = new HostingMeterProviderBuilder(localServices); + configureMeterProviderBuilder?.Invoke(testBuilder); + } + } +#endif + // This method relies on the assumption that MetricPoints are exported in the order in which they are emitted. // For Delta AggregationTemporality, this holds true only until the AggregatorStore has not begun recaliming the MetricPoints. public static void ValidateMetricPointTags(List> expectedTags, ReadOnlyTagCollection actualTags) @@ -130,8 +208,106 @@ public class MetricTestsBase } } + public IDisposable BuildMeterProvider( + out MeterProvider meterProvider, + Action configure) + { + if (configure == null) + { + throw new ArgumentNullException(nameof(configure)); + } + +#if BUILDING_HOSTING_TESTS + var host = BuildHost( + useWithMetricsStyle: false, + configureMeterProviderBuilder: configure, + configureServices: services => + { + if (this.configuration != null) + { + services.AddSingleton(this.configuration); + } + }); + + meterProvider = host.Services.GetService(); + + return host; +#else + var builder = Sdk.CreateMeterProviderBuilder(); + + if (this.configuration != null) + { + builder.ConfigureServices(services => services.AddSingleton(this.configuration)); + } + + configure(builder); + + return meterProvider = builder.Build(); +#endif + } + internal static Exemplar[] GetExemplars(MetricPoint mp) { return mp.GetExemplars().Where(exemplar => exemplar.Timestamp != default).ToArray(); } + +#if BUILDING_HOSTING_TESTS + public sealed class HostingMeterProviderBuilder : MeterProviderBuilderBase + { + public HostingMeterProviderBuilder(IServiceCollection services) + : base(services) + { + } + + public override MeterProviderBuilder AddMeter(params string[] names) + { + return this.ConfigureServices(services => + { + foreach (var name in names) + { + // Note: The entire purpose of this class is to use the + // IMetricsBuilder API to enable Metrics and NOT the + // traditional AddMeter API. + services.AddMetrics(builder => builder.EnableMetrics(name)); + } + }); + } + + public MeterProviderBuilder AddSdkMeter(params string[] names) + { + return base.AddMeter(names); + } + } + + private sealed class MetricsSubscriptionManagerCleanupHostedService : IHostedService, IDisposable + { + private readonly object metricsSubscriptionManager; + + public MetricsSubscriptionManagerCleanupHostedService(IServiceProvider serviceProvider) + { + this.metricsSubscriptionManager = serviceProvider.GetService( + typeof(ConsoleMetrics).Assembly.GetType("Microsoft.Extensions.Diagnostics.Metrics.MetricsSubscriptionManager")); + + if (this.metricsSubscriptionManager == null) + { + throw new InvalidOperationException("MetricsSubscriptionManager could not be found reflectively."); + } + } + + public void Dispose() + { + // Note: The current version of MetricsSubscriptionManager seems to + // be bugged in that it doesn't implement IDisposable. This hack + // manually invokes Dispose so that tests don't clobber each other. + // See: https://github.com/dotnet/runtime/issues/94434. + this.metricsSubscriptionManager.GetType().GetMethod("Dispose").Invoke(this.metricsSubscriptionManager, null); + } + + public Task StartAsync(CancellationToken cancellationToken) + => Task.CompletedTask; + + public Task StopAsync(CancellationToken cancellationToken) + => Task.CompletedTask; + } +#endif } diff --git a/test/OpenTelemetry.Tests/Metrics/MetricViewTests.cs b/test/OpenTelemetry.Tests/Metrics/MetricViewTests.cs index df98fc6cc..049c972fd 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricViewTests.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricViewTests.cs @@ -30,11 +30,11 @@ public class MetricViewTests : MetricTestsBase { using var meter = new Meter(Utils.GetCurrentMethodName()); var exportedItems = new List(); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView("name1", "renamed") - .AddInMemoryExporter(exportedItems) - .Build(); + .AddInMemoryExporter(exportedItems)); // Expecting one metric stream. var counterLong = meter.CreateCounter("name1"); @@ -53,19 +53,17 @@ public class MetricViewTests : MetricTestsBase using var meter1 = new Meter("AddViewWithInvalidNameThrowsArgumentException"); - var ex = Assert.Throws(() => Sdk.CreateMeterProviderBuilder() + var ex = Assert.Throws(() => this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter1.Name) .AddView("name1", viewNewName) - .AddInMemoryExporter(exportedItems) - .Build()); + .AddInMemoryExporter(exportedItems))); Assert.Contains($"Custom view name {viewNewName} is invalid.", ex.Message); - ex = Assert.Throws(() => Sdk.CreateMeterProviderBuilder() + ex = Assert.Throws(() => this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter1.Name) .AddView("name1", new MetricStreamConfiguration() { Name = viewNewName }) - .AddInMemoryExporter(exportedItems) - .Build()); + .AddInMemoryExporter(exportedItems))); Assert.Contains($"Custom view name {viewNewName} is invalid.", ex.Message); } @@ -77,11 +75,10 @@ public class MetricViewTests : MetricTestsBase using var meter1 = new Meter("AddViewWithInvalidNameThrowsArgumentException"); - Assert.Throws(() => Sdk.CreateMeterProviderBuilder() + Assert.Throws(() => this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter1.Name) .AddView("name1", (MetricStreamConfiguration)null) - .AddInMemoryExporter(exportedItems) - .Build()); + .AddInMemoryExporter(exportedItems))); } [Fact] @@ -91,11 +88,10 @@ public class MetricViewTests : MetricTestsBase using var meter1 = new Meter("AddViewWithGuaranteedConflictThrowsInvalidArgumentException"); - Assert.Throws(() => Sdk.CreateMeterProviderBuilder() + Assert.Throws(() => this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter1.Name) .AddView("instrumenta.*", name: "newname") - .AddInMemoryExporter(exportedItems) - .Build()); + .AddInMemoryExporter(exportedItems))); } [Fact] @@ -105,11 +101,10 @@ public class MetricViewTests : MetricTestsBase using var meter1 = new Meter("AddViewWithGuaranteedConflictThrowsInvalidArgumentException"); - Assert.Throws(() => Sdk.CreateMeterProviderBuilder() + Assert.Throws(() => this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter1.Name) .AddView("instrumenta.*", new MetricStreamConfiguration() { Name = "newname" }) - .AddInMemoryExporter(exportedItems) - .Build()); + .AddInMemoryExporter(exportedItems))); } [Fact] @@ -118,17 +113,18 @@ public class MetricViewTests : MetricTestsBase var exportedItems = new List(); using var meter1 = new Meter("AddViewWithExceptionInUserCallback"); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter1.Name) .AddView((instrument) => { throw new Exception("bad"); }) - .AddInMemoryExporter(exportedItems) - .Build(); + .AddInMemoryExporter(exportedItems)); using (var inMemoryEventListener = new InMemoryEventListener(OpenTelemetrySdkEventSource.Log)) { var counter1 = meter1.CreateCounter("counter1"); counter1.Add(1); - Assert.Single(inMemoryEventListener.Events.Where((e) => e.EventId == 41)); + + var metricViewIgnoredEvents = inMemoryEventListener.Events.Where((e) => e.EventId == 41); + Assert.Single(metricViewIgnoredEvents); } meterProvider.ForceFlush(MaxTimeToAllowForFlush); @@ -144,12 +140,11 @@ public class MetricViewTests : MetricTestsBase var exportedItems = new List(); using var meter1 = new Meter("AddViewWithExceptionInUserCallback"); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter1.Name) .AddView((instrument) => { throw new Exception("bad"); }) .AddView("*", MetricStreamConfiguration.Drop) - .AddInMemoryExporter(exportedItems) - .Build(); + .AddInMemoryExporter(exportedItems)); using (var inMemoryEventListener = new InMemoryEventListener(OpenTelemetrySdkEventSource.Log)) { @@ -172,18 +167,19 @@ public class MetricViewTests : MetricTestsBase var exportedItems = new List(); using var meter1 = new Meter("AddViewWithExceptionInUserCallback"); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter1.Name) .AddView((instrument) => { throw new Exception("bad"); }) .AddView((instrument) => { return new MetricStreamConfiguration() { Name = "newname" }; }) - .AddInMemoryExporter(exportedItems) - .Build(); + .AddInMemoryExporter(exportedItems)); using (var inMemoryEventListener = new InMemoryEventListener(OpenTelemetrySdkEventSource.Log)) { var counter1 = meter1.CreateCounter("counter1"); counter1.Add(1); - Assert.Single(inMemoryEventListener.Events.Where((e) => e.EventId == 41)); + + var metricViewIgnoredEvents = inMemoryEventListener.Events.Where((e) => e.EventId == 41); + Assert.Single(metricViewIgnoredEvents); } meterProvider.ForceFlush(MaxTimeToAllowForFlush); @@ -198,8 +194,8 @@ public class MetricViewTests : MetricTestsBase [MemberData(nameof(MetricTestData.InvalidHistogramBoundaries), MemberType = typeof(MetricTestData))] public void AddViewWithInvalidHistogramBoundsThrowsArgumentException(double[] boundaries) { - var ex = Assert.Throws(() => Sdk.CreateMeterProviderBuilder() - .AddView("name1", new ExplicitBucketHistogramConfiguration { Boundaries = boundaries })); + var ex = Assert.Throws(() => this.BuildMeterProvider(out var meterProvider, builder => builder + .AddView("name1", new ExplicitBucketHistogramConfiguration { Boundaries = boundaries }))); Assert.Contains("Histogram boundaries must be in ascending order with distinct values", ex.Message); } @@ -210,8 +206,8 @@ public class MetricViewTests : MetricTestsBase [InlineData(1)] public void AddViewWithInvalidExponentialHistogramMaxSizeConfigThrowsArgumentException(int maxSize) { - var ex = Assert.Throws(() => Sdk.CreateMeterProviderBuilder() - .AddView("name1", new Base2ExponentialBucketHistogramConfiguration { MaxSize = maxSize })); + var ex = Assert.Throws(() => this.BuildMeterProvider(out var meterProvider, builder => builder + .AddView("name1", new Base2ExponentialBucketHistogramConfiguration { MaxSize = maxSize }))); Assert.Contains("Histogram max size is invalid", ex.Message); } @@ -221,8 +217,8 @@ public class MetricViewTests : MetricTestsBase [InlineData(21)] public void AddViewWithInvalidExponentialHistogramMaxScaleConfigThrowsArgumentException(int maxScale) { - var ex = Assert.Throws(() => Sdk.CreateMeterProviderBuilder() - .AddView("name1", new Base2ExponentialBucketHistogramConfiguration { MaxScale = maxScale })); + var ex = Assert.Throws(() => this.BuildMeterProvider(out var meterProvider, builder => builder + .AddView("name1", new Base2ExponentialBucketHistogramConfiguration { MaxScale = maxScale }))); Assert.Contains("Histogram max scale is invalid", ex.Message); } @@ -237,7 +233,7 @@ public class MetricViewTests : MetricTestsBase var counter1 = meter1.CreateCounter("counter1"); - using (var provider = Sdk.CreateMeterProviderBuilder() + using (var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter1.Name) .AddView((instrument) => { @@ -245,8 +241,7 @@ public class MetricViewTests : MetricTestsBase ? new ExplicitBucketHistogramConfiguration() { Boundaries = boundaries } : null; }) - .AddInMemoryExporter(exportedItems) - .Build()) + .AddInMemoryExporter(exportedItems))) { counter1.Add(1); } @@ -263,11 +258,10 @@ public class MetricViewTests : MetricTestsBase var exportedItems = new List(); using var meter1 = new Meter("ViewWithInvalidNameIgnoredTest"); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter1.Name) .AddView("name1", viewNewName) - .AddInMemoryExporter(exportedItems) - .Build(); + .AddInMemoryExporter(exportedItems)); var counterLong = meter1.CreateCounter("name1"); counterLong.Add(10); @@ -287,7 +281,7 @@ public class MetricViewTests : MetricTestsBase var exportedItems = new List(); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter1.Name) .AddMeter(meter2.Name) .AddView((instrument) => @@ -302,8 +296,7 @@ public class MetricViewTests : MetricTestsBase return null; } }) - .AddInMemoryExporter(exportedItems) - .Build(); + .AddInMemoryExporter(exportedItems)); // Without views only 1 stream would be // exported (the 2nd one gets dropped due to @@ -327,7 +320,7 @@ public class MetricViewTests : MetricTestsBase { using var meter1 = new Meter("ViewToRenameMetricConditionallyTest"); var exportedItems = new List(); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter1.Name) // since here it's a func, we can't validate the name right away @@ -345,8 +338,7 @@ public class MetricViewTests : MetricTestsBase return null; } }) - .AddInMemoryExporter(exportedItems) - .Build(); + .AddInMemoryExporter(exportedItems)); // Because the MetricStreamName passed is invalid, the view is ignored, // and default aggregation is used. @@ -364,7 +356,7 @@ public class MetricViewTests : MetricTestsBase { using var meter1 = new Meter("ViewToRenameMetricConditionallyTest"); var exportedItems = new List(); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter1.Name) .AddView((instrument) => { @@ -379,8 +371,7 @@ public class MetricViewTests : MetricTestsBase return null; } }) - .AddInMemoryExporter(exportedItems) - .Build(); + .AddInMemoryExporter(exportedItems)); // Expecting one metric stream. var counter1 = meter1.CreateCounter("name1", "unit", "original_description"); @@ -401,7 +392,7 @@ public class MetricViewTests : MetricTestsBase using var meter = new Meter("ViewToRenameMetricConditionallyTest"); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView((instrument) => { @@ -415,8 +406,7 @@ public class MetricViewTests : MetricTestsBase return null; } }) - .AddInMemoryExporter(exportedItems) - .Build(); + .AddInMemoryExporter(exportedItems)); // Expecting one metric stream. // Since the View name was null, the instrument name was used instead @@ -436,12 +426,12 @@ public class MetricViewTests : MetricTestsBase { using var meter = new Meter(Utils.GetCurrentMethodName()); var exportedItems = new List(); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView("name1", "renamedStream1") .AddView("name1", "renamedStream2") - .AddInMemoryExporter(exportedItems) - .Build(); + .AddInMemoryExporter(exportedItems)); // Expecting two metric stream. var counterLong = meter.CreateCounter("name1"); @@ -457,13 +447,13 @@ public class MetricViewTests : MetricTestsBase { using var meter = new Meter(Utils.GetCurrentMethodName()); var exportedItems = new List(); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView("name1", "renamedStream1") .AddView("name1", "renamedStream2") .AddView("name1", "renamedStream2") - .AddInMemoryExporter(exportedItems) - .Build(); + .AddInMemoryExporter(exportedItems)); // Expecting three metric stream. // the second .AddView("name1", "renamedStream2") @@ -482,12 +472,12 @@ public class MetricViewTests : MetricTestsBase { using var meter = new Meter(Utils.GetCurrentMethodName()); var exportedItems = new List(); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView("NotAHistogram", new ExplicitBucketHistogramConfiguration() { Name = "ImAnExplicitBoundsHistogram" }) .AddView("NotAHistogram", new Base2ExponentialBucketHistogramConfiguration() { Name = "ImAnExponentialHistogram" }) - .AddInMemoryExporter(exportedItems) - .Build(); + .AddInMemoryExporter(exportedItems)); var counter = meter.CreateCounter("NotAHistogram"); counter.Add(10); @@ -515,12 +505,12 @@ public class MetricViewTests : MetricTestsBase using var meter = new Meter(Utils.GetCurrentMethodName()); var exportedItems = new List(); var boundaries = new double[] { 10, 20 }; - using var meterProvider = Sdk.CreateMeterProviderBuilder() + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView("MyHistogram", new ExplicitBucketHistogramConfiguration() { Name = "MyHistogramDefaultBound" }) .AddView("MyHistogram", new ExplicitBucketHistogramConfiguration() { Boundaries = boundaries }) - .AddInMemoryExporter(exportedItems) - .Build(); + .AddInMemoryExporter(exportedItems)); var histogram = meter.CreateHistogram("MyHistogram"); histogram.Record(-10); @@ -600,11 +590,11 @@ public class MetricViewTests : MetricTestsBase using var meter = new Meter(Utils.GetCurrentMethodName()); var exportedItems = new List(); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView("MyHistogram", new Base2ExponentialBucketHistogramConfiguration()) - .AddInMemoryExporter(exportedItems) - .Build(); + .AddInMemoryExporter(exportedItems)); var histogram = meter.CreateHistogram("MyHistogram"); var expectedHistogram = new Base2ExponentialBucketHistogram(); @@ -648,11 +638,11 @@ public class MetricViewTests : MetricTestsBase using var meter = new Meter(Utils.GetCurrentMethodName()); var histogram = meter.CreateHistogram("MyHistogram"); var exportedItems = new List(); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView(histogram.Name, histogramConfiguration) - .AddInMemoryExporter(exportedItems) - .Build(); + .AddInMemoryExporter(exportedItems)); for (var i = 0; i < values.Length; i++) { @@ -686,11 +676,11 @@ public class MetricViewTests : MetricTestsBase using var meter = new Meter(Utils.GetCurrentMethodName()); var histogram = meter.CreateHistogram("MyHistogram"); var exportedItems = new List(); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView(histogram.Name, histogramConfiguration) - .AddInMemoryExporter(exportedItems) - .Build(); + .AddInMemoryExporter(exportedItems)); for (var i = 0; i < values.Length; i++) { @@ -714,7 +704,8 @@ public class MetricViewTests : MetricTestsBase { using var meter = new Meter(Utils.GetCurrentMethodName()); var exportedItems = new List(); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView("FruitCounter", new MetricStreamConfiguration() { @@ -731,8 +722,7 @@ public class MetricViewTests : MetricTestsBase TagKeys = Array.Empty(), Name = "NoTags", }) - .AddInMemoryExporter(exportedItems) - .Build(); + .AddInMemoryExporter(exportedItems)); var counter = meter.CreateCounter("FruitCounter"); counter.Add(10, new("name", "apple"), new("color", "red"), new("size", "small")); @@ -785,11 +775,11 @@ public class MetricViewTests : MetricTestsBase { using var meter = new Meter(Utils.GetCurrentMethodName()); var exportedItems = new List(); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView("counterNotInteresting", MetricStreamConfiguration.Drop) - .AddInMemoryExporter(exportedItems) - .Build(); + .AddInMemoryExporter(exportedItems)); // Expecting one metric stream. var counterInteresting = meter.CreateCounter("counterInteresting"); @@ -808,11 +798,11 @@ public class MetricViewTests : MetricTestsBase { using var meter = new Meter(Utils.GetCurrentMethodName()); var exportedItems = new List(); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView("observableCounterNotInteresting", MetricStreamConfiguration.Drop) - .AddInMemoryExporter(exportedItems) - .Build(); + .AddInMemoryExporter(exportedItems)); // Expecting one metric stream. meter.CreateObservableCounter("observableCounterNotInteresting", () => { return 10; }, "ms"); @@ -829,11 +819,11 @@ public class MetricViewTests : MetricTestsBase { using var meter = new Meter(Utils.GetCurrentMethodName()); var exportedItems = new List(); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView("observableGaugeNotInteresting", MetricStreamConfiguration.Drop) - .AddInMemoryExporter(exportedItems) - .Build(); + .AddInMemoryExporter(exportedItems)); // Expecting one metric stream. meter.CreateObservableGauge("observableGaugeNotInteresting", () => { return 10; }, "ms"); @@ -850,11 +840,11 @@ public class MetricViewTests : MetricTestsBase { using var meter = new Meter(Utils.GetCurrentMethodName()); var exportedItems = new List(); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView("server*", MetricStreamConfiguration.Drop) - .AddInMemoryExporter(exportedItems) - .Build(); + .AddInMemoryExporter(exportedItems)); // Expecting two client metric streams as both server* are dropped. var serverRequests = meter.CreateCounter("server.requests"); @@ -877,12 +867,12 @@ public class MetricViewTests : MetricTestsBase { using var meter = new Meter(Utils.GetCurrentMethodName()); var exportedItems = new List(); - using var meterProvider = Sdk.CreateMeterProviderBuilder() + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView("server.requests", MetricStreamConfiguration.Drop) .AddView("server.requests", "server.request_renamed") - .AddInMemoryExporter(exportedItems) - .Build(); + .AddInMemoryExporter(exportedItems)); // Expecting one metric stream even though a View is asking // to drop the instrument, because another View is matching @@ -902,13 +892,12 @@ public class MetricViewTests : MetricTestsBase var exportedItems = new List(); using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); - var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView("instrumentName", new MetricStreamConfiguration() { Description = "newDescription1" }) .AddView("instrumentName", new MetricStreamConfiguration() { Description = "newDescription2" }) - .AddInMemoryExporter(exportedItems); - - using var meterProvider = meterProviderBuilder.Build(); + .AddInMemoryExporter(exportedItems)); var instrument = meter.CreateCounter("instrumentName", "instrumentUnit", "instrumentDescription"); @@ -949,7 +938,8 @@ public class MetricViewTests : MetricTestsBase var exportedItems = new List(); using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); - var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView((instrument) => { @@ -961,9 +951,7 @@ public class MetricViewTests : MetricTestsBase ? new MetricStreamConfiguration() { Name = "MetricStreamB" } : new MetricStreamConfiguration() { Name = "MetricStreamC" }; }) - .AddInMemoryExporter(exportedItems); - - using var meterProvider = meterProviderBuilder.Build(); + .AddInMemoryExporter(exportedItems)); var instrument1 = meter.CreateCounter("name", "unit", "description1"); var instrument2 = meter.CreateCounter("name", "unit", "description2"); @@ -1006,7 +994,8 @@ public class MetricViewTests : MetricTestsBase var exportedItems = new List(); using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); - var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView((instrument) => { @@ -1016,9 +1005,7 @@ public class MetricViewTests : MetricTestsBase { return new MetricStreamConfiguration { TagKeys = new[] { "key2" } }; }) - .AddInMemoryExporter(exportedItems); - - using var meterProvider = meterProviderBuilder.Build(); + .AddInMemoryExporter(exportedItems)); var instrument1 = meter.CreateCounter("name"); var instrument2 = meter.CreateCounter("name"); @@ -1054,7 +1041,8 @@ public class MetricViewTests : MetricTestsBase var exportedItems = new List(); using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); - var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView((instrument) => { @@ -1064,9 +1052,7 @@ public class MetricViewTests : MetricTestsBase { return new MetricStreamConfiguration { TagKeys = new[] { "key1" } }; }) - .AddInMemoryExporter(exportedItems); - - using var meterProvider = meterProviderBuilder.Build(); + .AddInMemoryExporter(exportedItems)); var instrument1 = meter.CreateCounter("name"); var instrument2 = meter.CreateCounter("name"); @@ -1103,7 +1089,8 @@ public class MetricViewTests : MetricTestsBase var exportedItems = new List(); using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); - var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView((instrument) => { @@ -1113,9 +1100,7 @@ public class MetricViewTests : MetricTestsBase { return new ExplicitBucketHistogramConfiguration { Boundaries = new[] { 10.0, 20.0 } }; }) - .AddInMemoryExporter(exportedItems); - - using var meterProvider = meterProviderBuilder.Build(); + .AddInMemoryExporter(exportedItems)); var instrument1 = meter.CreateHistogram("name"); var instrument2 = meter.CreateHistogram("name"); @@ -1181,7 +1166,8 @@ public class MetricViewTests : MetricTestsBase var exportedItems = new List(); using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); - var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView((instrument) => { @@ -1194,9 +1180,7 @@ public class MetricViewTests : MetricTestsBase return null; } }) - .AddInMemoryExporter(exportedItems); - - using var meterProvider = meterProviderBuilder.Build(); + .AddInMemoryExporter(exportedItems)); var instrument1 = meter.CreateCounter("name"); var instrument2 = meter.CreateCounter("othername"); @@ -1235,7 +1219,8 @@ public class MetricViewTests : MetricTestsBase var exportedItems = new List(); using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); - var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView((instrument) => { @@ -1248,9 +1233,7 @@ public class MetricViewTests : MetricTestsBase return MetricStreamConfiguration.Drop; } }) - .AddInMemoryExporter(exportedItems); - - using var meterProvider = meterProviderBuilder.Build(); + .AddInMemoryExporter(exportedItems)); var instrument1 = meter.CreateCounter("name"); var instrument2 = meter.CreateCounter("othername");