diff --git a/Directory.Packages.props b/Directory.Packages.props index af1039e95ef..13fb63b9f08 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 e69de29bb2d..8b137891791 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 f9ab8b5d8ed..6b801fc615c 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 00000000000..51c8c0d1826 --- /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 0d589aae7da..cc6b1b078a1 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 805808488a1..00af83db167 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 OpenTelemetryBuilder ConfigureResource( 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 OpenTelemetryBuilder ConfigureResource( /// 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 OpenTelemetryBuilder WithMetrics() /// 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 00000000000..a34cea3ea3e --- /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 1cbba26f9f8..67a668cf3d5 100644 --- a/src/OpenTelemetry/Internal/OpenTelemetrySdkEventSource.cs +++ b/src/OpenTelemetry/Internal/OpenTelemetrySdkEventSource.cs @@ -347,6 +347,18 @@ public void LoggerProcessStateSkipped(string type, string reason) 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 5776241441d..e4cf260aa16 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(); + var services = builder.Services; + + 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(builder.Services); + RegisterLoggerProviderOptions(services); - new LoggerProviderBuilderBase(builder.Services).ConfigureBuilder( + var loggingBuilder = new LoggerProviderBuilderBase(services).ConfigureBuilder( (sp, logging) => { var options = sp.GetRequiredService>().CurrentValue; @@ -78,7 +106,9 @@ public static ILoggingBuilder AddOpenTelemetry( options.Processors.Clear(); }); - builder.Services.TryAddEnumerable( + configureBuilder?.Invoke(loggingBuilder); + + services.TryAddEnumerable( ServiceDescriptor.Singleton( sp => new OpenTelemetryLoggerProvider( sp.GetRequiredService(), @@ -107,23 +137,4 @@ static void RegisterLoggerProviderOptions(IServiceCollection services) 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 d5c979604bf..767cf30714b 100644 --- a/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderExtensions.cs +++ b/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderExtensions.cs @@ -277,6 +277,8 @@ public static MeterProviderBuilder SetMaxMetricPointsPerMetricStream(this MeterP /// 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 MeterProviderBuilder SetResourceBuilder(this MeterProviderBuilder /// 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 73880837dce..74405b02be2 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 MeterProviderSdk( } // 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 MeterProviderSdk( 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,92 +196,186 @@ internal MeterProviderSdk( } else { - this.listener.InstrumentPublished = (instrument, listener) => - { - bool enabledMeasurements = false; + // Everything double + this.listener.SetMeasurementEventCallback(this.MeasurementRecordedDoubleSingleStream); + this.listener.SetMeasurementEventCallback((instrument, value, tags, state) => this.MeasurementRecordedDoubleSingleStream(instrument, value, tags, state)); + + // Everything long + this.listener.SetMeasurementEventCallback(this.MeasurementRecordedLongSingleStream); + this.listener.SetMeasurementEventCallback((instrument, value, tags, state) => this.MeasurementRecordedLongSingleStream(instrument, value, tags, state)); + this.listener.SetMeasurementEventCallback((instrument, value, tags, state) => this.MeasurementRecordedLongSingleStream(instrument, value, tags, state)); + this.listener.SetMeasurementEventCallback((instrument, value, tags, state) => this.MeasurementRecordedLongSingleStream(instrument, value, tags, state)); + + this.listener.MeasurementsCompleted = (instrument, state) => this.MeasurementsCompletedSingleStream(instrument, state); + } + + this.listener.Start(); + + OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent("MeterProvider built successfully."); + } + + internal Resource Resource { get; } + + internal List Instrumentations => this.instrumentations; + + 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; + } - if (!shouldListenTo(instrument)) + 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 belongs to a Meter not subscribed by the provider.", "Use AddMeter to add the Meter to the provider."); - return; + OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored( + instrument.Name, + instrument.Meter.Name, + "Instrument name is invalid.", + "The name must comply with the OpenTelemetry specification"); + return null; } - try + if (this.reader != null) { - OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent($"Started publishing Instrument = \"{instrument.Name}\" of Meter = \"{instrument.Meter.Name}\"."); - - if (!MeterProviderBuilderSdk.IsValidInstrumentName(instrument.Name)) + if (this.compositeMetricReader == null) { - OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored( - instrument.Name, - instrument.Meter.Name, - "Instrument name is invalid.", - "The name must comply with the OpenTelemetry specification"); - - return; + 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; - if (this.reader != null) + try { - if (this.compositeMetricReader == null) + 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) { - var metric = this.reader.AddMetricWithNoViews(instrument); - if (metric != null) - { - listener.EnableMeasurementEvents(instrument, metric); - enabledMeasurements = true; - } + metricStreamConfig.ViewId = i; } - else + + if (metricStreamConfig is HistogramConfiguration + && instrument.GetType().GetGenericTypeDefinition() != typeof(Histogram<>)) { - var metrics = this.compositeMetricReader.AddMetricsWithNoViews(instrument); - if (metrics.Any(metric => metric != null)) - { - listener.EnableMeasurementEvents(instrument, metrics); - enabledMeasurements = true; - } + 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."); } } - - if (enabledMeasurements) + catch (Exception ex) { - OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent($"Measurements for Instrument = \"{instrument.Name}\" of Meter = \"{instrument.Meter.Name}\" will be processed and aggregated by the SDK."); + OpenTelemetrySdkEventSource.Log.MetricViewIgnored(instrument.Name, instrument.Meter.Name, ex.Message, "Fix the view configuration."); } - else + + if (metricStreamConfig != null) { - OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent($"Measurements for Instrument = \"{instrument.Name}\" of Meter = \"{instrument.Meter.Name}\" will be dropped by the SDK."); + metricStreamConfigs.Add(metricStreamConfig); } } - catch (Exception) + + if (metricStreamConfigs.Count == 0) { - OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored(instrument.Name, instrument.Meter.Name, "SDK internal error occurred.", "Contact SDK owners."); + // 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); } - }; - - // Everything double - this.listener.SetMeasurementEventCallback(this.MeasurementRecordedDoubleSingleStream); - this.listener.SetMeasurementEventCallback((instrument, value, tags, state) => this.MeasurementRecordedDoubleSingleStream(instrument, value, tags, state)); - // Everything long - this.listener.SetMeasurementEventCallback(this.MeasurementRecordedLongSingleStream); - this.listener.SetMeasurementEventCallback((instrument, value, tags, state) => this.MeasurementRecordedLongSingleStream(instrument, value, tags, state)); - this.listener.SetMeasurementEventCallback((instrument, value, tags, state) => this.MeasurementRecordedLongSingleStream(instrument, value, tags, state)); - this.listener.SetMeasurementEventCallback((instrument, value, tags, state) => this.MeasurementRecordedLongSingleStream(instrument, value, tags, state)); + 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; + } + } + } + } - this.listener.MeasurementsCompleted = (instrument, state) => this.MeasurementsCompletedSingleStream(instrument, state); + 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; } - - this.listener.Start(); - - OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent("MeterProvider built successfully."); } - internal Resource Resource { get; } - - internal List Instrumentations => this.instrumentations; - - internal MetricReader? Reader => this.reader; - internal void MeasurementsCompletedSingleStream(Instrument instrument, object? state) { Debug.Assert(instrument != null, "instrument must be non-null."); @@ -542,6 +540,8 @@ internal void CollectObservableInstruments() 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 554d20fe3b5..fc9d2bb9099 100644 --- a/src/OpenTelemetry/Metrics/Metric.cs +++ b/src/OpenTelemetry/Metrics/Metric.cs @@ -170,7 +170,6 @@ internal Metric( this.aggStore = new AggregatorStore(instrumentIdentity, aggType, temporality, maxMetricPointsPerMetricStream, emitOverflowAttribute, shouldReclaimUnusedMetricPoints, exemplarFilter); this.Temporality = temporality; - this.InstrumentDisposed = false; } /// @@ -213,7 +212,7 @@ internal 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 16762f97da6..0fede2ab672 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 @@ internal AggregationTemporality GetAggregationTemporality(Type instrumentType) 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 @@ internal AggregationTemporality GetAggregationTemporality(Type instrumentType) this.instrumentIdentityToMetric[metricStreamIdentity] = metric; this.metrics![index] = metric; - this.metricStreamNames.Add(metricStreamIdentity.MetricStreamName); + + this.CreateOrUpdateMetricStreamRegistration(in metricStreamIdentity); + return metric; } } @@ -135,21 +129,12 @@ internal List AddMetricsListWithViews(Instrument instrument, List AddMetricsListWithViews(Instrument instrument, List metrics, double value, ReadOn 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 @@ internal void SetMaxMetricPointsPerMetricStream(int maxMetricPointsPerMetricStre } } + 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 @@ private Batch GetMetricsBatch() 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 @@ private Batch GetMetricsBatch() 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 c7b5e9c2ea9..e1d6d4fb496 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 4d31768c2ff..43da25a08b4 100644 --- a/test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetryBuilderTests.cs +++ b/test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetryBuilderTests.cs @@ -75,4 +75,41 @@ public void ConfigureResourceTest() 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 00000000000..66f2c11ee97 --- /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 9a980e12ea4..be2e564a3dd 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 void BuilderTypeDoesNotChangeTest() 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 ff226b8e360..8eb3a124dbf 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 void MeasurementWithNullValuedTag() { 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 void ObserverCallbackTest() { 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 void ObserverCallbackExceptionTest() { 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 void MetricUnitIsExportedCorrectly(string unit) 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 void MetricDescriptionIsExportedCorrectly(string description) 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 void DuplicateInstrumentRegistration_NoViews_IdenticalInstruments() 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 void DuplicateInstrumentRegistration_NoViews_DuplicateInstruments_Differe 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 void DuplicateInstrumentRegistration_NoViews_DuplicateInstruments_Differe 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 void DuplicateInstrumentRegistration_NoViews_DuplicateInstruments_Differe 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 void DuplicateInstrumentRegistration_NoViews_DuplicateInstruments_Differe 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 void DuplicateInstrumentNamesFromDifferentMetersWithSameNameDifferentVers 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 void DuplicateInstrumentNamesFromDifferentMetersAreAllowed(MetricReaderTe 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 void DuplicateInstrumentNamesFromDifferentMetersAreAllowed(MetricReaderTe Assert.Equal(2, exportedItems.Count); } +#if !BUILDING_HOSTING_TESTS [Theory] [InlineData(true)] [InlineData(false)] @@ -548,22 +478,20 @@ public void MeterSourcesWildcardSupportMatchTest(bool hasView) 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 void MeterSourcesWildcardSupportMatchTest(bool hasView) Assert.Equal("myGauge4", exportedItems[3].Name); Assert.Equal("myGauge5", exportedItems[4].Name); } +#endif [Theory] [InlineData(true)] @@ -601,19 +530,18 @@ public void MeterSourcesWildcardSupportNegativeTestNoMeterAdded(bool hasView) 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 void CounterAggregationTest(bool exportDelta) 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 void ObservableCounterAggregationTest(bool exportDelta) }; }); - 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 void ObservableCounterWithTagsAggregationTest(bool exportDelta) }; }); - 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 void ObservableCounterSpatialAggregationTest(bool exportDelta) }; }); - 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 void UpDownCounterAggregationTest(bool exportDelta) 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 void ObservableUpDownCounterAggregationTest(bool exportDelta) }; }); - 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 void ObservableUpDownCounterWithTagsAggregationTest(bool exportDelta) }; }); - 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 void DimensionsAreOrderInsensitiveWithSortedKeysFirst(bool exportDelta) 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 void DimensionsAreOrderInsensitiveWithUnsortedKeysFirst(bool exportDelta) 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 void TestInstrumentDisposal(MetricReaderTemporalityPreference temporality 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 @@ int MetricPointCount() 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 void InstrumentWithInvalidNameIsIgnoredTest(string instrumentName) 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 void InstrumentWithValidNameIsExportedTest(string name) 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 void SetupSdkProviderWithNoReader(bool hasViews) { // 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 void UnsupportedMetricInstrument() { 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 void UnsupportedMetricInstrument() // 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 @@ private void MultithreadedCounterTest(T deltaValueUpdatedByEachCall) 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 @@ private void MultithreadedHistogramTest(long[] expected, T[] values) 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 d306e618de3..6529b4ec2c6 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricExemplarTests.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricExemplarTests.cs @@ -40,14 +40,14 @@ public void TestExemplarsCounter() 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 void TestExemplarsHistogram() 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 void TestExemplarsFilterTags() 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 fad3663f773..9a22f7b6795 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 19328025704..df8d4ecac73 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 static void CheckTagsForNthMetricPoint(List metrics, List 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 df98fc6cc2a..049c972fd90 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricViewTests.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricViewTests.cs @@ -30,11 +30,11 @@ public void ViewToRenameMetric() { 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 void AddViewWithInvalidNameThrowsArgumentException(string viewNewName) 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 void AddViewWithNullMetricStreamConfigurationThrowsArgumentnullException( 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 void AddViewWithNameThrowsInvalidArgumentExceptionWhenConflict() 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 void AddViewWithNameInMetricStreamConfigurationThrowsInvalidArgumentExcep 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 void AddViewWithExceptionInUserCallbackAppliedDefault() 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 void AddViewWithExceptionInUserCallbackNoDefault() 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 void AddViewsWithAndWithoutExceptionInUserCallback() 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 void AddViewsWithAndWithoutExceptionInUserCallback() [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 void AddViewWithInvalidHistogramBoundsThrowsArgumentException(double[] bo [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 void AddViewWithInvalidExponentialHistogramMaxSizeConfigThrowsArgumentExc [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 void AddViewWithInvalidHistogramBoundsIgnored(double[] boundaries) 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 void AddViewWithInvalidHistogramBoundsIgnored(double[] boundaries) ? new ExplicitBucketHistogramConfiguration() { Boundaries = boundaries } : null; }) - .AddInMemoryExporter(exportedItems) - .Build()) + .AddInMemoryExporter(exportedItems))) { counter1.Add(1); } @@ -263,11 +258,10 @@ public void ViewWithValidNameExported(string viewNewName) 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 void ViewToRenameMetricConditionally() 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 void ViewToRenameMetricConditionally() 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 void ViewWithInvalidNameIgnoredConditionally(string viewNewName) { 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 void ViewWithInvalidNameIgnoredConditionally(string viewNewName) 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 void ViewWithValidNameConditionally(string viewNewName) { 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 void ViewWithValidNameConditionally(string viewNewName) return null; } }) - .AddInMemoryExporter(exportedItems) - .Build(); + .AddInMemoryExporter(exportedItems)); // Expecting one metric stream. var counter1 = meter1.CreateCounter("name1", "unit", "original_description"); @@ -401,7 +392,7 @@ public void ViewWithNullCustomNameTakesInstrumentName() 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 void ViewWithNullCustomNameTakesInstrumentName() 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 void ViewToProduceMultipleStreamsFromInstrument() { 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 void ViewToProduceMultipleStreamsWithDuplicatesFromInstrument() { 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 void ViewWithHistogramConfigurationIgnoredWhenAppliedToNonHistogram() { 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 void ViewToProduceCustomHistogramBound() 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 void ViewToProduceExponentialHistogram() 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 void HistogramMinMax(double[] values, HistogramConfiguration histogramCon 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 void HistogramMinMaxNotPresent(double[] values, HistogramConfiguration hi 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 void ViewToSelectTagKeys() { 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 void ViewToSelectTagKeys() 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 void ViewToDropSingleInstrument() { 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 void ViewToDropSingleInstrumentObservableCounter() { 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 void ViewToDropSingleInstrumentObservableGauge() { 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 void ViewToDropMultipleInstruments() { 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 void ViewToDropAndRetainInstrument() { 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 void ViewConflict_OneInstrument_DifferentDescription() 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 void ViewConflict_TwoDistinctInstruments_ThreeStreams() 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 void ViewConflict_TwoDistinctInstruments_ThreeStreams() ? 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 void ViewConflict_TwoIdenticalInstruments_TwoViews_DifferentTags() 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 void ViewConflict_TwoIdenticalInstruments_TwoViews_DifferentTags() { 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 void ViewConflict_TwoIdenticalInstruments_TwoViews_SameTags() 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 void ViewConflict_TwoIdenticalInstruments_TwoViews_SameTags() { 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 void ViewConflict_TwoIdenticalInstruments_TwoViews_DifferentHistogramBoun 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 void ViewConflict_TwoIdenticalInstruments_TwoViews_DifferentHistogramBoun { 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 void ViewConflict_TwoInstruments_OneMatchesView() 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 void ViewConflict_TwoInstruments_OneMatchesView() 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 void ViewConflict_TwoInstruments_ConflictAvoidedBecauseSecondInstrumentIs 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 void ViewConflict_TwoInstruments_ConflictAvoidedBecauseSecondInstrumentIs return MetricStreamConfiguration.Drop; } }) - .AddInMemoryExporter(exportedItems); - - using var meterProvider = meterProviderBuilder.Build(); + .AddInMemoryExporter(exportedItems)); var instrument1 = meter.CreateCounter("name"); var instrument2 = meter.CreateCounter("othername");