[hosting] Support .NET 8 IMetricsBuilder API (#4958)

This commit is contained in:
Mikel Blanchard 2023-11-21 16:17:40 -08:00 committed by GitHub
parent 0c4f065484
commit 1f7f9315dc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 1345 additions and 642 deletions

View File

@ -29,6 +29,7 @@
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Configuration" Version="8.0.0" />

View File

@ -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

View File

@ -0,0 +1,120 @@
// <copyright file="OpenTelemetryMetricsListener.cs" company="OpenTelemetry Authors">
// 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.
// </copyright>
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<KeyValuePair<string, object?>> 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<KeyValuePair<string, object?>> tagsRos, object? userState)
{
var meterProvider = this.meterProviderSdk;
if (meterProvider.ViewCount > 0)
{
meterProvider.MeasurementRecordedLong(instrument, value, tagsRos, userState);
}
else
{
meterProvider.MeasurementRecordedLongSingleStream(instrument, value, tagsRos, userState);
}
}
}

View File

@ -10,6 +10,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.Abstractions" />
</ItemGroup>
<ItemGroup>

View File

@ -15,6 +15,7 @@
// </copyright>
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.Metrics;
using OpenTelemetry.Internal;
using OpenTelemetry.Logs;
using OpenTelemetry.Metrics;
@ -61,13 +62,13 @@ public sealed class OpenTelemetryBuilder
Guard.ThrowIfNull(configure);
this.Services.ConfigureOpenTelemetryMeterProvider(
(sp, builder) => builder.ConfigureResource(configure));
builder => builder.ConfigureResource(configure));
this.Services.ConfigureOpenTelemetryTracerProvider(
(sp, builder) => builder.ConfigureResource(configure));
builder => builder.ConfigureResource(configure));
this.Services.ConfigureOpenTelemetryLoggerProvider(
(sp, builder) => builder.ConfigureResource(configure));
builder => builder.ConfigureResource(configure));
return this;
}
@ -76,9 +77,15 @@ public sealed class OpenTelemetryBuilder
/// Adds metric services into the builder.
/// </summary>
/// <remarks>
/// Note: This is safe to be called multiple times and by library authors.
/// Notes:
/// <list type="bullet">
/// <item>This is safe to be called multiple times and by library authors.
/// Only a single <see cref="MeterProvider"/> will be created for a given
/// <see cref="IServiceCollection"/>.
/// <see cref="IServiceCollection"/>.</item>
/// <item>This method automatically registers an <see
/// cref="IMetricsListener"/> named 'OpenTelemetry' into the <see
/// cref="IServiceCollection"/>.</item>
/// </list>
/// </remarks>
/// <returns>The supplied <see cref="OpenTelemetryBuilder"/> for chaining
/// calls.</returns>
@ -95,11 +102,9 @@ public sealed class OpenTelemetryBuilder
/// calls.</returns>
public OpenTelemetryBuilder WithMetrics(Action<MeterProviderBuilder> configure)
{
Guard.ThrowIfNull(configure);
var builder = new MeterProviderBuilderBase(this.Services);
configure(builder);
OpenTelemetryMetricsBuilderExtensions.RegisterMetricsListener(
this.Services,
configure);
return this;
}

View File

@ -0,0 +1,81 @@
// <copyright file="OpenTelemetryMetricsBuilderExtensions.cs" company="OpenTelemetry Authors">
// 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.
// </copyright>
using System.Diagnostics;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using OpenTelemetry.Internal;
using OpenTelemetry.Metrics;
namespace Microsoft.Extensions.Diagnostics.Metrics;
/// <summary>
/// Contains extension methods for registering OpenTelemetry metrics with an
/// <see cref="IMetricsBuilder"/> instance.
/// </summary>
internal static class OpenTelemetryMetricsBuilderExtensions
{
/// <summary>
/// Adds an OpenTelemetry <see cref="IMetricsListener"/> named 'OpenTelemetry' to the <see cref="IMetricsBuilder"/>.
/// </summary>
/// <remarks>
/// Note: This is safe to be called multiple times and by library authors.
/// Only a single <see cref="MeterProvider"/> will be created for a given
/// <see cref="IServiceCollection"/>.
/// </remarks>
/// <param name="metricsBuilder"><see cref="IMetricsBuilder"/>.</param>
/// <returns>The supplied <see cref="IMetricsBuilder"/> for chaining
/// calls.</returns>
public static IMetricsBuilder UseOpenTelemetry(
this IMetricsBuilder metricsBuilder)
=> UseOpenTelemetry(metricsBuilder, b => { });
/// <summary>
/// Adds an OpenTelemetry <see cref="IMetricsListener"/> named 'OpenTelemetry' to the <see cref="IMetricsBuilder"/>.
/// </summary>
/// <remarks><inheritdoc cref="UseOpenTelemetry(IMetricsBuilder)" path="/remarks"/></remarks>
/// <param name="metricsBuilder"><see cref="IMetricsBuilder"/>.</param>
/// <param name="configure"><see cref="MeterProviderBuilder"/>
/// configuration callback.</param>
/// <returns>The supplied <see cref="IMetricsBuilder"/> for chaining
/// calls.</returns>
public static IMetricsBuilder UseOpenTelemetry(
this IMetricsBuilder metricsBuilder,
Action<MeterProviderBuilder> configure)
{
Guard.ThrowIfNull(metricsBuilder);
RegisterMetricsListener(metricsBuilder.Services, configure);
return metricsBuilder;
}
internal static void RegisterMetricsListener(
IServiceCollection services,
Action<MeterProviderBuilder> configure)
{
Debug.Assert(services != null, "services was null");
Guard.ThrowIfNull(configure);
var builder = new MeterProviderBuilderBase(services!);
services!.TryAddEnumerable(
ServiceDescriptor.Singleton<IMetricsListener, OpenTelemetryMetricsListener>());
configure(builder);
}
}

View File

@ -347,6 +347,18 @@ internal sealed class OpenTelemetrySdkEventSource : EventSource
this.WriteEvent(51, type, reason);
}
[Event(52, Message = "Instrument '{0}', Meter '{1}' has been deactivated.", Level = EventLevel.Informational)]
public void MetricInstrumentDeactivated(string instrumentName, string meterName)
{
this.WriteEvent(52, instrumentName, meterName);
}
[Event(53, Message = "Instrument '{0}', Meter '{1}' has been removed.", Level = EventLevel.Informational)]
public void MetricInstrumentRemoved(string instrumentName, string meterName)
{
this.WriteEvent(53, instrumentName, meterName);
}
#if DEBUG
public class OpenTelemetryEventListener : EventListener
{

View File

@ -50,15 +50,43 @@ public static class OpenTelemetryLoggingExtensions
/// <returns>The supplied <see cref="ILoggingBuilder"/> for call chaining.</returns>
public static ILoggingBuilder AddOpenTelemetry(
this ILoggingBuilder builder)
=> AddOpenTelemetryInternal(builder, configureBuilder: null, configureOptions: null);
/// <summary>
/// Adds an OpenTelemetry logger named 'OpenTelemetry' to the <see cref="ILoggerFactory"/>.
/// </summary>
/// <remarks><inheritdoc cref="AddOpenTelemetry(ILoggingBuilder)" path="/remarks"/></remarks>
/// <param name="builder">The <see cref="ILoggingBuilder"/> to use.</param>
/// <param name="configure">Optional configuration action.</param>
/// <returns>The supplied <see cref="ILoggingBuilder"/> for call chaining.</returns>
public static ILoggingBuilder AddOpenTelemetry(
this ILoggingBuilder builder,
Action<OpenTelemetryLoggerOptions>? configure)
=> AddOpenTelemetryInternal(builder, configureBuilder: null, configureOptions: configure);
private static ILoggingBuilder AddOpenTelemetryInternal(
ILoggingBuilder builder,
Action<LoggerProviderBuilder>? configureBuilder,
Action<OpenTelemetryLoggerOptions>? configureOptions)
{
Guard.ThrowIfNull(builder);
builder.AddConfiguration();
// Note: This will bind logger options element (eg "Logging:OpenTelemetry") to OpenTelemetryLoggerOptions
RegisterLoggerProviderOptions(builder.Services);
var services = builder.Services;
new LoggerProviderBuilderBase(builder.Services).ConfigureBuilder(
if (configureOptions != null)
{
// TODO: Move this below the RegisterLoggerProviderOptions call so
// that user-supplied delegate fires AFTER the options are bound to
// Logging:OpenTelemetry configuration.
services.Configure(configureOptions);
}
// Note: This will bind logger options element (eg "Logging:OpenTelemetry") to OpenTelemetryLoggerOptions
RegisterLoggerProviderOptions(services);
var loggingBuilder = new LoggerProviderBuilderBase(services).ConfigureBuilder(
(sp, logging) =>
{
var options = sp.GetRequiredService<IOptionsMonitor<OpenTelemetryLoggerOptions>>().CurrentValue;
@ -78,7 +106,9 @@ public static class OpenTelemetryLoggingExtensions
options.Processors.Clear();
});
builder.Services.TryAddEnumerable(
configureBuilder?.Invoke(loggingBuilder);
services.TryAddEnumerable(
ServiceDescriptor.Singleton<ILoggerProvider, OpenTelemetryLoggerProvider>(
sp => new OpenTelemetryLoggerProvider(
sp.GetRequiredService<LoggerProvider>(),
@ -107,23 +137,4 @@ public static class OpenTelemetryLoggingExtensions
LoggerProviderOptions.RegisterProviderOptions<OpenTelemetryLoggerOptions, OpenTelemetryLoggerProvider>(services);
}
}
/// <summary>
/// Adds an OpenTelemetry logger named 'OpenTelemetry' to the <see cref="ILoggerFactory"/>.
/// </summary>
/// <remarks><inheritdoc cref="AddOpenTelemetry(ILoggingBuilder)" path="/remarks"/></remarks>
/// <param name="builder">The <see cref="ILoggingBuilder"/> to use.</param>
/// <param name="configure">Optional configuration action.</param>
/// <returns>The supplied <see cref="ILoggingBuilder"/> for call chaining.</returns>
public static ILoggingBuilder AddOpenTelemetry(
this ILoggingBuilder builder,
Action<OpenTelemetryLoggerOptions>? configure)
{
if (configure != null)
{
builder.Services.Configure(configure);
}
return AddOpenTelemetry(builder);
}
}

View File

@ -277,6 +277,8 @@ public static class MeterProviderBuilderExtensions
/// <returns>The supplied <see cref="MeterProviderBuilder"/> for chaining.</returns>
public static MeterProviderBuilder SetResourceBuilder(this MeterProviderBuilder meterProviderBuilder, ResourceBuilder resourceBuilder)
{
Guard.ThrowIfNull(resourceBuilder);
meterProviderBuilder.ConfigureBuilder((sp, builder) =>
{
if (builder is MeterProviderBuilderSdk meterProviderBuilderSdk)
@ -297,6 +299,8 @@ public static class MeterProviderBuilderExtensions
/// <returns>The supplied <see cref="MeterProviderBuilder"/> for chaining.</returns>
public static MeterProviderBuilder ConfigureResource(this MeterProviderBuilder meterProviderBuilder, Action<ResourceBuilder> configure)
{
Guard.ThrowIfNull(configure);
meterProviderBuilder.ConfigureBuilder((sp, builder) =>
{
if (builder is MeterProviderBuilderSdk meterProviderBuilderSdk)

View File

@ -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<Instrument, bool> shouldListenTo = instrument => false;
internal MeterProviderSdk(
IServiceProvider serviceProvider,
@ -149,16 +151,15 @@ internal sealed class MeterProviderSdk : MeterProvider
}
// Setup Listener
Func<Instrument, bool> 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<string>(state.MeterSources, StringComparer.OrdinalIgnoreCase);
shouldListenTo = instrument => meterSourcesToSubscribe.Contains(instrument.Meter.Name);
this.shouldListenTo = instrument => meterSourcesToSubscribe.Contains(instrument.Meter.Name);
}
OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent($"Listening to following meters = \"{string.Join(";", state.MeterSources)}\".");
@ -168,116 +169,19 @@ internal sealed class MeterProviderSdk : MeterProvider
OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent($"Number of views configured = {viewConfigCount}.");
this.listener.InstrumentPublished = (instrument, listener) =>
{
object? state = this.InstrumentPublished(instrument, listeningIsManagedExternally: false);
if (state != null)
{
listener.EnableMeasurementEvents(instrument, state);
}
};
// We expect that all the readers to be added are provided before MeterProviderSdk is built.
// If there are no readers added, we do not enable measurements for the instruments.
if (viewConfigCount > 0)
{
this.listener.InstrumentPublished = (instrument, listener) =>
{
bool enabledMeasurements = false;
if (!shouldListenTo(instrument))
{
OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored(instrument.Name, instrument.Meter.Name, "Instrument belongs to a Meter not subscribed by the provider.", "Use AddMeter to add the Meter to the provider.");
return;
}
try
{
OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent($"Started publishing Instrument = \"{instrument.Name}\" of Meter = \"{instrument.Meter.Name}\".");
// Creating list with initial capacity as the maximum
// possible size, to avoid any array resize/copy internally.
// There may be excess space wasted, but it'll eligible for
// GC right after this method.
var metricStreamConfigs = new List<MetricStreamConfiguration?>(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<double>(this.MeasurementRecordedDouble);
this.listener.SetMeasurementEventCallback<float>((instrument, value, tags, state) => this.MeasurementRecordedDouble(instrument, value, tags, state));
@ -292,68 +196,6 @@ internal sealed class MeterProviderSdk : MeterProvider
}
else
{
this.listener.InstrumentPublished = (instrument, listener) =>
{
bool enabledMeasurements = false;
if (!shouldListenTo(instrument))
{
OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored(instrument.Name, instrument.Meter.Name, "Instrument belongs to a Meter not subscribed by the provider.", "Use AddMeter to add the Meter to the provider.");
return;
}
try
{
OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent($"Started publishing Instrument = \"{instrument.Name}\" of Meter = \"{instrument.Meter.Name}\".");
if (!MeterProviderBuilderSdk.IsValidInstrumentName(instrument.Name))
{
OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored(
instrument.Name,
instrument.Meter.Name,
"Instrument name is invalid.",
"The name must comply with the OpenTelemetry specification");
return;
}
if (this.reader != null)
{
if (this.compositeMetricReader == null)
{
var metric = this.reader.AddMetricWithNoViews(instrument);
if (metric != null)
{
listener.EnableMeasurementEvents(instrument, metric);
enabledMeasurements = true;
}
}
else
{
var metrics = this.compositeMetricReader.AddMetricsWithNoViews(instrument);
if (metrics.Any(metric => metric != null))
{
listener.EnableMeasurementEvents(instrument, metrics);
enabledMeasurements = true;
}
}
}
if (enabledMeasurements)
{
OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent($"Measurements for Instrument = \"{instrument.Name}\" of Meter = \"{instrument.Meter.Name}\" will be processed and aggregated by the SDK.");
}
else
{
OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent($"Measurements for Instrument = \"{instrument.Name}\" of Meter = \"{instrument.Meter.Name}\" will be dropped by the SDK.");
}
}
catch (Exception)
{
OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored(instrument.Name, instrument.Meter.Name, "SDK internal error occurred.", "Contact SDK owners.");
}
};
// Everything double
this.listener.SetMeasurementEventCallback<double>(this.MeasurementRecordedDoubleSingleStream);
this.listener.SetMeasurementEventCallback<float>((instrument, value, tags, state) => this.MeasurementRecordedDoubleSingleStream(instrument, value, tags, state));
@ -378,6 +220,162 @@ internal sealed class MeterProviderSdk : MeterProvider
internal MetricReader? Reader => this.reader;
internal int ViewCount => this.viewConfigs.Count;
internal object? InstrumentPublished(Instrument instrument, bool listeningIsManagedExternally)
{
var listenToInstrumentUsingSdkConfiguration = this.shouldListenTo(instrument);
if (listeningIsManagedExternally && listenToInstrumentUsingSdkConfiguration)
{
OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored(
instrument.Name,
instrument.Meter.Name,
"Instrument belongs to a Meter which has been enabled both externally and via a subscription on the provider. External subscription will be ignored in favor of the provider subscription.",
"Programmatic calls adding meters to the SDK (either by calling AddMeter directly or indirectly through helpers such as 'AddInstrumentation' extensions) are always favored over external registrations. When also using external management (typically IMetricsBuilder or IMetricsListener) remove programmatic calls to the SDK to allow registrations to be added and removed dynamically.");
return null;
}
else if (!listenToInstrumentUsingSdkConfiguration && !listeningIsManagedExternally)
{
OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored(
instrument.Name,
instrument.Meter.Name,
"Instrument belongs to a Meter not subscribed by the provider.",
"Use AddMeter to add the Meter to the provider.");
return null;
}
object? state = null;
var viewConfigCount = this.viewConfigs.Count;
try
{
OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent($"Started publishing Instrument = \"{instrument.Name}\" of Meter = \"{instrument.Meter.Name}\".");
if (viewConfigCount <= 0)
{
if (!MeterProviderBuilderSdk.IsValidInstrumentName(instrument.Name))
{
OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored(
instrument.Name,
instrument.Meter.Name,
"Instrument name is invalid.",
"The name must comply with the OpenTelemetry specification");
return null;
}
if (this.reader != null)
{
if (this.compositeMetricReader == null)
{
state = this.reader.AddMetricWithNoViews(instrument);
}
else
{
var metrics = this.compositeMetricReader.AddMetricsWithNoViews(instrument);
if (metrics.Any(metric => metric != null))
{
state = metrics;
}
}
}
}
else
{
// Creating list with initial capacity as the maximum
// possible size, to avoid any array resize/copy internally.
// There may be excess space wasted, but it'll eligible for
// GC right after this method.
var metricStreamConfigs = new List<MetricStreamConfiguration?>(viewConfigCount);
for (var i = 0; i < viewConfigCount; ++i)
{
var viewConfig = this.viewConfigs[i];
MetricStreamConfiguration? metricStreamConfig = null;
try
{
metricStreamConfig = viewConfig(instrument);
// The SDK provides some static MetricStreamConfigurations.
// For example, the Drop configuration. The static ViewId
// should not be changed for these configurations.
if (metricStreamConfig != null && !metricStreamConfig.ViewId.HasValue)
{
metricStreamConfig.ViewId = i;
}
if (metricStreamConfig is HistogramConfiguration
&& instrument.GetType().GetGenericTypeDefinition() != typeof(Histogram<>))
{
metricStreamConfig = null;
OpenTelemetrySdkEventSource.Log.MetricViewIgnored(
instrument.Name,
instrument.Meter.Name,
"The current SDK does not allow aggregating non-Histogram instruments as Histograms.",
"Fix the view configuration.");
}
}
catch (Exception ex)
{
OpenTelemetrySdkEventSource.Log.MetricViewIgnored(instrument.Name, instrument.Meter.Name, ex.Message, "Fix the view configuration.");
}
if (metricStreamConfig != null)
{
metricStreamConfigs.Add(metricStreamConfig);
}
}
if (metricStreamConfigs.Count == 0)
{
// No views matched. Add null
// which will apply defaults.
// Users can turn off this default
// by adding a view like below as the last view.
// .AddView(instrumentName: "*", MetricStreamConfiguration.Drop)
metricStreamConfigs.Add(null);
}
if (this.reader != null)
{
if (this.compositeMetricReader == null)
{
var metrics = this.reader.AddMetricsListWithViews(instrument, metricStreamConfigs);
if (metrics.Count > 0)
{
state = metrics;
}
}
else
{
var metricsSuperList = this.compositeMetricReader.AddMetricsSuperListWithViews(instrument, metricStreamConfigs);
if (metricsSuperList.Any(metrics => metrics.Count > 0))
{
state = metricsSuperList;
}
}
}
}
if (state != null)
{
OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent($"Measurements for Instrument = \"{instrument.Name}\" of Meter = \"{instrument.Meter.Name}\" will be processed and aggregated by the SDK.");
return state;
}
else
{
OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent($"Measurements for Instrument = \"{instrument.Name}\" of Meter = \"{instrument.Meter.Name}\" will be dropped by the SDK.");
return null;
}
}
catch (Exception)
{
OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored(instrument.Name, instrument.Meter.Name, "SDK internal error occurred.", "Contact SDK owners.");
return null;
}
}
internal void MeasurementsCompletedSingleStream(Instrument instrument, object? state)
{
Debug.Assert(instrument != null, "instrument must be non-null.");
@ -542,6 +540,8 @@ internal sealed class MeterProviderSdk : MeterProvider
try
{
this.listener.RecordObservableInstruments();
this.OnCollectObservableInstruments?.Invoke();
}
catch (Exception exception)
{

View File

@ -170,7 +170,6 @@ public sealed class Metric
this.aggStore = new AggregatorStore(instrumentIdentity, aggType, temporality, maxMetricPointsPerMetricStream, emitOverflowAttribute, shouldReclaimUnusedMetricPoints, exemplarFilter);
this.Temporality = temporality;
this.InstrumentDisposed = false;
}
/// <summary>
@ -213,7 +212,7 @@ public sealed class Metric
/// </summary>
internal MetricStreamIdentity InstrumentIdentity { get; private set; }
internal bool InstrumentDisposed { get; set; }
internal bool Active { get; set; } = true;
/// <summary>
/// Get the metric points for the metric stream.

View File

@ -16,6 +16,7 @@
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Metrics;
using OpenTelemetry.Internal;
@ -50,20 +51,11 @@ public abstract partial class MetricReader
var metricStreamIdentity = new MetricStreamIdentity(instrument, metricStreamConfiguration: null);
lock (this.instrumentCreationLock)
{
if (this.instrumentIdentityToMetric.TryGetValue(metricStreamIdentity, out var existingMetric))
if (this.TryGetExistingMetric(in metricStreamIdentity, out var existingMetric))
{
return existingMetric;
}
if (this.metricStreamNames.Contains(metricStreamIdentity.MetricStreamName))
{
OpenTelemetrySdkEventSource.Log.DuplicateMetricInstrument(
metricStreamIdentity.InstrumentName,
metricStreamIdentity.MeterName,
"Metric instrument has the same name as an existing one but differs by description, unit, or instrument type. Measurements from this instrument will still be exported but may result in conflicts.",
"Either change the name of the instrument or use MeterProviderBuilder.AddView to resolve the conflict.");
}
var index = ++this.metricIndex;
if (index >= this.maxMetricStreams)
{
@ -90,7 +82,9 @@ public abstract partial class MetricReader
this.instrumentIdentityToMetric[metricStreamIdentity] = metric;
this.metrics![index] = metric;
this.metricStreamNames.Add(metricStreamIdentity.MetricStreamName);
this.CreateOrUpdateMetricStreamRegistration(in metricStreamIdentity);
return metric;
}
}
@ -135,21 +129,12 @@ public abstract partial class MetricReader
continue;
}
if (this.instrumentIdentityToMetric.TryGetValue(metricStreamIdentity, out var existingMetric))
if (this.TryGetExistingMetric(in metricStreamIdentity, out var existingMetric))
{
metrics.Add(existingMetric);
continue;
}
if (this.metricStreamNames.Contains(metricStreamIdentity.MetricStreamName))
{
OpenTelemetrySdkEventSource.Log.DuplicateMetricInstrument(
metricStreamIdentity.InstrumentName,
metricStreamIdentity.MeterName,
"Metric instrument has the same name as an existing one but differs by description, unit, instrument type, or aggregation configuration (like histogram bounds, tag keys etc. ). Measurements from this instrument will still be exported but may result in conflicts.",
"Either change the name of the instrument or use MeterProviderBuilder.AddView to resolve the conflict.");
}
if (metricStreamConfig == MetricStreamConfiguration.Drop)
{
OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored(metricStreamIdentity.InstrumentName, metricStreamIdentity.MeterName, "View configuration asks to drop this instrument.", "Modify view configuration to allow this instrument, if desired.");
@ -169,7 +154,8 @@ public abstract partial class MetricReader
this.instrumentIdentityToMetric[metricStreamIdentity] = metric;
this.metrics![index] = metric;
metrics.Add(metric);
this.metricStreamNames.Add(metricStreamIdentity.MetricStreamName);
this.CreateOrUpdateMetricStreamRegistration(in metricStreamIdentity);
}
}
@ -215,14 +201,14 @@ public abstract partial class MetricReader
internal void CompleteSingleStreamMeasurement(Metric metric)
{
metric.InstrumentDisposed = true;
DeactivateMetric(metric);
}
internal void CompleteMeasurement(List<Metric> metrics)
{
foreach (var metric in metrics)
{
metric.InstrumentDisposed = true;
DeactivateMetric(metric);
}
}
@ -252,6 +238,41 @@ public abstract partial class MetricReader
}
}
private static void DeactivateMetric(Metric metric)
{
if (metric.Active)
{
// TODO: This will cause the metric to be removed from the storage
// array during the next collect/export. If this happens often we
// will run out of storage. Would it be better instead to set the
// end time on the metric and keep it around so it can be
// reactivated?
metric.Active = false;
OpenTelemetrySdkEventSource.Log.MetricInstrumentDeactivated(
metric.Name,
metric.MeterName);
}
}
private bool TryGetExistingMetric(in MetricStreamIdentity metricStreamIdentity, [NotNullWhen(true)] out Metric? existingMetric)
=> this.instrumentIdentityToMetric.TryGetValue(metricStreamIdentity, out existingMetric)
&& existingMetric.Active;
private void CreateOrUpdateMetricStreamRegistration(in MetricStreamIdentity metricStreamIdentity)
{
if (!this.metricStreamNames.Add(metricStreamIdentity.MetricStreamName))
{
// TODO: If a metric is deactivated and then reactivated we log the
// same warning as if it was a duplicate.
OpenTelemetrySdkEventSource.Log.DuplicateMetricInstrument(
metricStreamIdentity.InstrumentName,
metricStreamIdentity.MeterName,
"Metric instrument has the same name as an existing one but differs by description, unit, or instrument type. Measurements from this instrument will still be exported but may result in conflicts.",
"Either change the name of the instrument or use MeterProviderBuilder.AddView to resolve the conflict.");
}
}
private Batch<Metric> GetMetricsBatch()
{
Debug.Assert(this.metrics != null, "this.metrics was null");
@ -264,25 +285,20 @@ public abstract partial class MetricReader
int metricCountCurrentBatch = 0;
for (int i = 0; i < target; i++)
{
var metric = this.metrics![i];
int metricPointSize = 0;
ref var metric = ref this.metrics![i];
if (metric != null)
{
if (metric.InstrumentDisposed)
{
metricPointSize = metric.Snapshot();
this.instrumentIdentityToMetric.TryRemove(metric.InstrumentIdentity, out var _);
this.metrics[i] = null;
}
else
{
metricPointSize = metric.Snapshot();
}
int metricPointSize = metric.Snapshot();
if (metricPointSize > 0)
{
this.metricsCurrentBatch![metricCountCurrentBatch++] = metric;
}
if (!metric.Active)
{
this.RemoveMetric(ref metric);
}
}
}
@ -294,4 +310,25 @@ public abstract partial class MetricReader
return default;
}
}
private void RemoveMetric(ref Metric? metric)
{
Debug.Assert(metric != null, "metric was null");
// TODO: This logic removes the metric. If the same
// metric is published again we will create a new metric
// for it. If this happens often we will run out of
// storage. Instead, should we keep the metric around
// and set a new start time + reset its data if it comes
// back?
OpenTelemetrySdkEventSource.Log.MetricInstrumentRemoved(metric!.Name, metric.MeterName);
var result = this.instrumentIdentityToMetric.TryRemove(metric.InstrumentIdentity, out var _);
Debug.Assert(result, "result was false");
// Note: metric is a reference to the array storage so
// this clears the metric out of the array.
metric = null;
}
}

View File

@ -4,6 +4,7 @@
<TargetFrameworks>$(TargetFrameworksForTests)</TargetFrameworks>
<!-- this is temporary. will remove in future PR. -->
<Nullable>disable</Nullable>
<DefineConstants>$(DefineConstants);BUILDING_HOSTING_TESTS</DefineConstants>
</PropertyGroup>
<ItemGroup>
@ -17,6 +18,20 @@
<Compile Include="$(RepoRoot)\test\OpenTelemetry.Tests\Shared\Utils.cs" Link="Includes\Utils.cs" />
</ItemGroup>
<ItemGroup>
<!-- Note: These are SDK tests which we link and run here using the
IMetricsBuilder/IMetricsListener API added at the host level in .NET 8
instead of the direct lower-level MeterListener API added in .NET 6. -->
<Compile Include="$(RepoRoot)\test\OpenTelemetry.Tests\Metrics\AggregatorTestsBase.cs" Link="Includes\Metrics\AggregatorTestsBase.cs" />
<Compile Include="$(RepoRoot)\test\OpenTelemetry.Tests\Metrics\KnownHistogramBuckets.cs" Link="Includes\Metrics\KnownHistogramBuckets.cs" />
<Compile Include="$(RepoRoot)\test\OpenTelemetry.Tests\Metrics\MetricApiTestsBase.cs" Link="Includes\Metrics\MetricApiTestsBase.cs" />
<Compile Include="$(RepoRoot)\test\OpenTelemetry.Tests\Metrics\MetricExemplarTests.cs" Link="Includes\Metrics\MetricExemplarTests.cs" />
<Compile Include="$(RepoRoot)\test\OpenTelemetry.Tests\Metrics\MetricTestData.cs" Link="Includes\Metrics\MetricTestData.cs" />
<Compile Include="$(RepoRoot)\test\OpenTelemetry.Tests\Metrics\MetricTestsBase.cs" Link="Includes\Metrics\MetricTestsBase.cs" />
<Compile Include="$(RepoRoot)\test\OpenTelemetry.Tests\Metrics\MetricViewTests.cs" Link="Includes\Metrics\MetricViewTests.cs" />
<Compile Include="$(RepoRoot)\test\OpenTelemetry.Tests\Shared\InMemoryEventListener.cs" Link="Includes\InMemoryEventListener.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.TestHost" Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp'" />
<PackageReference Include="Microsoft.Extensions.Hosting" />

View File

@ -75,4 +75,41 @@ public class OpenTelemetryBuilderTests
loggerProvider.Resource.Attributes,
kvp => kvp.Key == "l_key1" && (string)kvp.Value == "l_value1");
}
[Fact]
public void ConfigureResourceServiceProviderTest()
{
var services = new ServiceCollection();
services.AddSingleton<TestResourceDetector>();
services.AddOpenTelemetry()
.ConfigureResource(r => r.AddDetector(sp => sp.GetRequiredService<TestResourceDetector>()))
.WithLogging()
.WithMetrics()
.WithTracing();
using var sp = services.BuildServiceProvider();
var tracerProvider = sp.GetRequiredService<TracerProvider>() as TracerProviderSdk;
var meterProvider = sp.GetRequiredService<MeterProvider>() as MeterProviderSdk;
var loggerProvider = sp.GetRequiredService<LoggerProvider>() 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<string, object>
{
["key1"] = "value1",
}).Build();
}
}

View File

@ -0,0 +1,259 @@
// <copyright file="OpenTelemetryMetricsBuilderExtensionsTests.cs" company="OpenTelemetry Authors">
// 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.
// </copyright>
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<Metric> exportedItems = new();
using (var host = MetricTestsBase.BuildHost(
useWithMetricsStyle,
configureMetricsBuilder: builder => builder.EnableMetrics(meter.Name),
configureMeterProviderBuilder: builder => builder.AddInMemoryExporter(exportedItems)))
{
var counter = meter.CreateCounter<long>("TestCounter");
counter.Add(1);
}
AssertSingleMetricWithLongSum(exportedItems);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void EnableMetricsWithAddMeterTest(bool useWithMetricsStyle)
{
using var meter = new Meter(Utils.GetCurrentMethodName());
List<Metric> 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<long>("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<Metric> 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<MeterProvider>();
var options = host.Services.GetRequiredService<IOptionsMonitor<MetricsOptions>>();
var counter = meter.CreateCounter<long>("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<Metric> 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<MeterProvider>();
var options = host.Services.GetRequiredService<IOptionsMonitor<MetricsOptions>>();
var counter = meter.CreateCounter<long>("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<Metric> exportedItems, long expectedValue = 1)
{
Assert.Single(exportedItems);
AssertMetricWithLongSum(exportedItems[0], expectedValue);
}
private static void AssertMetricWithLongSum(Metric metric, long expectedValue = 1)
{
List<MetricPoint> 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());
}
}

View File

@ -14,6 +14,9 @@
// limitations under the License.
// </copyright>
using System.Diagnostics.Metrics;
using OpenTelemetry.Internal;
using OpenTelemetry.Tests;
using Xunit;
namespace OpenTelemetry.Metrics.Tests;
@ -45,4 +48,77 @@ public class MeterProviderSdkTest
Assert.NotNull(provider);
}
[Theory]
[InlineData(false, true)]
[InlineData(true, true)]
[InlineData(false, false)]
[InlineData(true, false)]
public void TransientMeterExhaustsMetricStorageTest(bool withView, bool forceFlushAfterEachTest)
{
using var inMemoryEventListener = new InMemoryEventListener(OpenTelemetrySdkEventSource.Log);
var meterName = Utils.GetCurrentMethodName();
var exportedItems = new List<Metric>();
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<int>("Counter");
counter.Add(1);
meter.Dispose();
if (forceFlushAfterEachTest)
{
meterProvider.ForceFlush();
}
}
}
}

View File

@ -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<string, string>();
if (emitOverflowAttribute)
{
configurationData[EmitOverFlowAttributeConfigKey] = "true";
}
if (shouldReclaimUnusedMetricPoints)
{
configurationData[ReclaimUnusedMetricPointsConfigKey] = "true";
}
this.configuration = new ConfigurationBuilder()
.AddInMemoryCollection(configurationData)
.Build();
}
[Fact]
@ -64,14 +47,10 @@ public abstract class MetricApiTestsBase : MetricTestsBase
{
using var meter = new Meter(Utils.GetCurrentMethodName());
var exportedItems = new List<Metric>();
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<long>("myCounter");
counter.Add(100, new KeyValuePair<string, object>("tagWithNullValue", null));
@ -101,14 +80,10 @@ public abstract class MetricApiTestsBase : MetricTestsBase
{
using var meter = new Meter(Utils.GetCurrentMethodName());
var exportedItems = new List<Metric>();
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<int>(100, new("name", "apple"), new("color", "red"));
meter.CreateObservableGauge("myGauge", () => measurement);
@ -134,14 +109,10 @@ public abstract class MetricApiTestsBase : MetricTestsBase
{
using var meter = new Meter(Utils.GetCurrentMethodName());
var exportedItems = new List<Metric>();
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<int>(100, new("name", "apple"), new("color", "red"));
meter.CreateObservableGauge("myGauge", () => measurement);
@ -172,15 +143,10 @@ public abstract class MetricApiTestsBase : MetricTestsBase
var exportedItems = new List<Metric>();
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<long>("name1", unit);
counter.Add(10);
@ -199,15 +165,10 @@ public abstract class MetricApiTestsBase : MetricTestsBase
var exportedItems = new List<Metric>();
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<long>("name1", null, description);
counter.Add(10);
@ -223,15 +184,10 @@ public abstract class MetricApiTestsBase : MetricTestsBase
var exportedItems = new List<Metric>();
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<long>("instrumentName", "instrumentUnit", "instrumentDescription");
var duplicateInstrument = meter.CreateCounter<long>("instrumentName", "instrumentUnit", "instrumentDescription");
@ -261,15 +217,10 @@ public abstract class MetricApiTestsBase : MetricTestsBase
var exportedItems = new List<Metric>();
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<long>("instrumentName", "instrumentUnit", "instrumentDescription1");
var duplicateInstrument = meter.CreateCounter<long>("instrumentName", "instrumentUnit", "instrumentDescription2");
@ -312,15 +263,10 @@ public abstract class MetricApiTestsBase : MetricTestsBase
var exportedItems = new List<Metric>();
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<long>("instrumentName", "instrumentUnit1", "instrumentDescription");
var duplicateInstrument = meter.CreateCounter<long>("instrumentName", "instrumentUnit2", "instrumentDescription");
@ -363,15 +309,10 @@ public abstract class MetricApiTestsBase : MetricTestsBase
var exportedItems = new List<Metric>();
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<long>("instrumentName", "instrumentUnit", "instrumentDescription");
var duplicateInstrument = meter.CreateCounter<double>("instrumentName", "instrumentUnit", "instrumentDescription");
@ -412,15 +353,10 @@ public abstract class MetricApiTestsBase : MetricTestsBase
var exportedItems = new List<Metric>();
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<long>("instrumentName", "instrumentUnit", "instrumentDescription");
var duplicateInstrument = meter.CreateHistogram<long>("instrumentName", "instrumentUnit", "instrumentDescription");
@ -463,16 +399,11 @@ public abstract class MetricApiTestsBase : MetricTestsBase
using var meter1 = new Meter($"{Utils.GetCurrentMethodName()}", "1.0");
using var meter2 = new Meter($"{Utils.GetCurrentMethodName()}", "2.0");
var meterProviderBuilder = Sdk.CreateMeterProviderBuilder()
.ConfigureServices(services =>
{
services.AddSingleton(this.configuration);
})
using var container = this.BuildMeterProvider(out var meterProvider, builder => builder
.AddMeter(meter1.Name)
.AddMeter(meter2.Name)
.AddInMemoryExporter(exportedItems);
using var meterProvider = meterProviderBuilder.Build();
.AddInMemoryExporter(exportedItems));
// Expecting one metric stream.
var counterLong = meter1.CreateCounter<long>("name1");
@ -500,24 +431,22 @@ public abstract class MetricApiTestsBase : MetricTestsBase
using var meter1 = new Meter($"{Utils.GetCurrentMethodName()}.1.{temporality}");
using var meter2 = new Meter($"{Utils.GetCurrentMethodName()}.2.{temporality}");
var meterProviderBuilder = Sdk.CreateMeterProviderBuilder()
.ConfigureServices(services =>
{
services.AddSingleton(this.configuration);
})
.AddMeter(meter1.Name)
.AddMeter(meter2.Name)
.AddInMemoryExporter(exportedItems, metricReaderOptions =>
{
metricReaderOptions.TemporalityPreference = temporality;
});
if (hasView)
using var container = this.BuildMeterProvider(out var meterProvider, builder =>
{
meterProviderBuilder.AddView("name1", new MetricStreamConfiguration() { Description = "description" });
}
builder
.AddMeter(meter1.Name)
.AddMeter(meter2.Name)
.AddInMemoryExporter(exportedItems, metricReaderOptions =>
{
metricReaderOptions.TemporalityPreference = temporality;
});
using var meterProvider = meterProviderBuilder.Build();
if (hasView)
{
builder.AddView("name1", new MetricStreamConfiguration() { Description = "description" });
}
});
// Expecting one metric stream.
var counterLong = meter1.CreateCounter<long>("name1");
@ -535,6 +464,7 @@ public abstract class MetricApiTestsBase : MetricTestsBase
Assert.Equal(2, exportedItems.Count);
}
#if !BUILDING_HOSTING_TESTS
[Theory]
[InlineData(true)]
[InlineData(false)]
@ -548,22 +478,20 @@ public abstract class MetricApiTestsBase : MetricTestsBase
using var meter6 = new Meter("SomeCompany.SomeProduct.SomeComponent");
var exportedItems = new List<Metric>();
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<int>(100, new("name", "apple"), new("color", "red"));
meter1.CreateObservableGauge("myGauge1", () => measurement);
@ -591,6 +519,7 @@ public abstract class MetricApiTestsBase : MetricTestsBase
Assert.Equal("myGauge4", exportedItems[3].Name);
Assert.Equal("myGauge5", exportedItems[4].Name);
}
#endif
[Theory]
[InlineData(true)]
@ -601,19 +530,18 @@ public abstract class MetricApiTestsBase : MetricTestsBase
using var meter2 = new Meter($"abcCompany.xYzProduct.componentC.{hasView}");
var exportedItems = new List<Metric>();
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<int>(100, new("name", "apple"), new("color", "red"));
meter1.CreateObservableGauge("myGauge1", () => measurement);
@ -634,17 +562,13 @@ public abstract class MetricApiTestsBase : MetricTestsBase
using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{exportDelta}");
var counterLong = meter.CreateCounter<long>("mycounter");
using var meterProvider = Sdk.CreateMeterProviderBuilder()
.ConfigureServices(services =>
{
services.AddSingleton(this.configuration);
})
using var container = this.BuildMeterProvider(out var meterProvider, builder => builder
.AddMeter(meter.Name)
.AddInMemoryExporter(exportedItems, metricReaderOptions =>
{
metricReaderOptions.TemporalityPreference = exportDelta ? MetricReaderTemporalityPreference.Delta : MetricReaderTemporalityPreference.Cumulative;
})
.Build();
}));
counterLong.Add(10);
counterLong.Add(10);
@ -740,17 +664,12 @@ public abstract class MetricApiTestsBase : MetricTestsBase
};
});
using var meterProvider = Sdk.CreateMeterProviderBuilder()
.ConfigureServices(services =>
{
services.AddSingleton(this.configuration);
})
using var container = this.BuildMeterProvider(out var meterProvider, builder => builder
.AddMeter(meter.Name)
.AddInMemoryExporter(exportedItems, metricReaderOptions =>
{
metricReaderOptions.TemporalityPreference = exportDelta ? MetricReaderTemporalityPreference.Delta : MetricReaderTemporalityPreference.Cumulative;
})
.Build();
}));
meterProvider.ForceFlush(MaxTimeToAllowForFlush);
long sumReceived = GetLongSum(exportedItems);
@ -818,17 +737,12 @@ public abstract class MetricApiTestsBase : MetricTestsBase
};
});
using var meterProvider = Sdk.CreateMeterProviderBuilder()
.ConfigureServices(services =>
{
services.AddSingleton(this.configuration);
})
using var container = this.BuildMeterProvider(out var meterProvider, builder => builder
.AddMeter(meter.Name)
.AddInMemoryExporter(exportedItems, metricReaderOptions =>
{
metricReaderOptions.TemporalityPreference = exportDelta ? MetricReaderTemporalityPreference.Delta : MetricReaderTemporalityPreference.Cumulative;
})
.Build();
}));
// Export 1
meterProvider.ForceFlush(MaxTimeToAllowForFlush);
@ -919,18 +833,13 @@ public abstract class MetricApiTestsBase : MetricTestsBase
};
});
using var meterProvider = Sdk.CreateMeterProviderBuilder()
.ConfigureServices(services =>
{
services.AddSingleton(this.configuration);
})
using var container = this.BuildMeterProvider(out var meterProvider, builder => builder
.AddMeter(meter.Name)
.AddInMemoryExporter(exportedItems, metricReaderOptions =>
{
metricReaderOptions.TemporalityPreference = exportDelta ? MetricReaderTemporalityPreference.Delta : MetricReaderTemporalityPreference.Cumulative;
})
.AddView("requestCount", new MetricStreamConfiguration() { TagKeys = Array.Empty<string>() })
.Build();
.AddView("requestCount", new MetricStreamConfiguration() { TagKeys = Array.Empty<string>() }));
meterProvider.ForceFlush(MaxTimeToAllowForFlush);
Assert.Single(exportedItems);
@ -964,17 +873,13 @@ public abstract class MetricApiTestsBase : MetricTestsBase
using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{exportDelta}");
var counterLong = meter.CreateUpDownCounter<long>("mycounter");
using var meterProvider = Sdk.CreateMeterProviderBuilder()
.ConfigureServices(services =>
{
services.AddSingleton(this.configuration);
})
using var container = this.BuildMeterProvider(out var meterProvider, builder => builder
.AddMeter(meter.Name)
.AddInMemoryExporter(exportedItems, metricReaderOptions =>
{
metricReaderOptions.TemporalityPreference = exportDelta ? MetricReaderTemporalityPreference.Delta : MetricReaderTemporalityPreference.Cumulative;
})
.Build();
}));
counterLong.Add(10);
counterLong.Add(-5);
@ -1050,17 +955,12 @@ public abstract class MetricApiTestsBase : MetricTestsBase
};
});
using var meterProvider = Sdk.CreateMeterProviderBuilder()
.ConfigureServices(services =>
{
services.AddSingleton(this.configuration);
})
using var container = this.BuildMeterProvider(out var meterProvider, builder => builder
.AddMeter(meter.Name)
.AddInMemoryExporter(exportedItems, metricReaderOptions =>
{
metricReaderOptions.TemporalityPreference = exportDelta ? MetricReaderTemporalityPreference.Delta : MetricReaderTemporalityPreference.Cumulative;
})
.Build();
}));
meterProvider.ForceFlush(MaxTimeToAllowForFlush);
long sumReceived = GetLongSum(exportedItems);
@ -1118,17 +1018,12 @@ public abstract class MetricApiTestsBase : MetricTestsBase
};
});
using var meterProvider = Sdk.CreateMeterProviderBuilder()
.ConfigureServices(services =>
{
services.AddSingleton(this.configuration);
})
using var container = this.BuildMeterProvider(out var meterProvider, builder => builder
.AddMeter(meter.Name)
.AddInMemoryExporter(exportedItems, metricReaderOptions =>
{
metricReaderOptions.TemporalityPreference = exportDelta ? MetricReaderTemporalityPreference.Delta : MetricReaderTemporalityPreference.Cumulative;
})
.Build();
}));
// Export 1
meterProvider.ForceFlush(MaxTimeToAllowForFlush);
@ -1192,17 +1087,13 @@ public abstract class MetricApiTestsBase : MetricTestsBase
using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{exportDelta}");
var counterLong = meter.CreateCounter<long>("Counter");
using var meterProvider = Sdk.CreateMeterProviderBuilder()
.ConfigureServices(services =>
{
services.AddSingleton(this.configuration);
})
using var container = this.BuildMeterProvider(out var meterProvider, builder => builder
.AddMeter(meter.Name)
.AddInMemoryExporter(exportedItems, metricReaderOptions =>
{
metricReaderOptions.TemporalityPreference = exportDelta ? MetricReaderTemporalityPreference.Delta : MetricReaderTemporalityPreference.Cumulative;
})
.Build();
}));
// Emit the first metric with the sorted order of tag keys
counterLong.Add(5, new("Key1", "Value1"), new("Key2", "Value2"), new("Key3", "Value3"));
@ -1287,17 +1178,13 @@ public abstract class MetricApiTestsBase : MetricTestsBase
using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{exportDelta}");
var counterLong = meter.CreateCounter<long>("Counter");
using var meterProvider = Sdk.CreateMeterProviderBuilder()
.ConfigureServices(services =>
{
services.AddSingleton(this.configuration);
})
using var container = this.BuildMeterProvider(out var meterProvider, builder => builder
.AddMeter(meter.Name)
.AddInMemoryExporter(exportedItems, metricReaderOptions =>
{
metricReaderOptions.TemporalityPreference = exportDelta ? MetricReaderTemporalityPreference.Delta : MetricReaderTemporalityPreference.Cumulative;
})
.Build();
}));
// Emit the first metric with the unsorted order of tag keys
counterLong.Add(5, new("Key1", "Value1"), new("Key3", "Value3"), new("Key2", "Value2"));
@ -1384,18 +1271,14 @@ public abstract class MetricApiTestsBase : MetricTestsBase
var meter2 = new Meter($"{Utils.GetCurrentMethodName()}.{temporality}.2");
var counter1 = meter1.CreateCounter<long>("counterFromMeter1");
var counter2 = meter2.CreateCounter<long>("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<string, object>("key", "value"));
counter2.Add(10, new KeyValuePair<string, object>("key", "value"));
@ -1456,17 +1339,13 @@ public abstract class MetricApiTestsBase : MetricTestsBase
using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{temporality}");
var counterLong = meter.CreateCounter<long>("mycounterCapTest");
using var meterProvider = Sdk.CreateMeterProviderBuilder()
.ConfigureServices(services =>
{
services.AddSingleton(this.configuration);
})
using var container = this.BuildMeterProvider(out var meterProvider, builder => builder
.AddMeter(meter.Name)
.AddInMemoryExporter(exportedItems, metricReaderOptions =>
{
metricReaderOptions.TemporalityPreference = temporality;
})
.Build();
}));
// Make one Add with no tags.
// as currently we reserve 0th index
@ -1556,14 +1435,9 @@ public abstract class MetricApiTestsBase : MetricTestsBase
using var meter = new Meter("InstrumentWithInvalidNameIsIgnoredTest");
using var meterProvider = Sdk.CreateMeterProviderBuilder()
.ConfigureServices(services =>
{
services.AddSingleton(this.configuration);
})
using var container = this.BuildMeterProvider(out var meterProvider, builder => builder
.AddMeter(meter.Name)
.AddInMemoryExporter(exportedItems)
.Build();
.AddInMemoryExporter(exportedItems));
var counterLong = meter.CreateCounter<long>(instrumentName);
counterLong.Add(10);
@ -1582,14 +1456,9 @@ public abstract class MetricApiTestsBase : MetricTestsBase
using var meter = new Meter("InstrumentValidNameIsExportedTest");
using var meterProvider = Sdk.CreateMeterProviderBuilder()
.ConfigureServices(services =>
{
services.AddSingleton(this.configuration);
})
using var container = this.BuildMeterProvider(out var meterProvider, builder => builder
.AddMeter(meter.Name)
.AddInMemoryExporter(exportedItems)
.Build();
.AddInMemoryExporter(exportedItems));
var counterLong = meter.CreateCounter<long>(name);
counterLong.Add(10);
@ -1608,19 +1477,17 @@ public abstract class MetricApiTestsBase : MetricTestsBase
{
// This test ensures that MeterProviderSdk can be set up without any reader
using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{hasViews}");
var meterProviderBuilder = Sdk.CreateMeterProviderBuilder()
.ConfigureServices(services =>
{
services.AddSingleton(this.configuration);
})
.AddMeter(meter.Name);
if (hasViews)
using var container = this.BuildMeterProvider(out var meterProvider, builder =>
{
meterProviderBuilder.AddView("counter", "renamedCounter");
}
builder
.AddMeter(meter.Name);
using var meterProvider = meterProviderBuilder.Build();
if (hasViews)
{
builder.AddView("counter", "renamedCounter");
}
});
var counter = meter.CreateCounter<long>("counter");
@ -1632,14 +1499,10 @@ public abstract class MetricApiTestsBase : MetricTestsBase
{
using var meter = new Meter($"{Utils.GetCurrentMethodName()}");
var exportedItems = new List<Metric>();
using var meterProvider = Sdk.CreateMeterProviderBuilder()
.ConfigureServices(services =>
{
services.AddSingleton(this.configuration);
})
using var container = this.BuildMeterProvider(out var meterProvider, builder => builder
.AddMeter(meter.Name)
.AddInMemoryExporter(exportedItems)
.Build();
.AddInMemoryExporter(exportedItems));
using (var inMemoryEventListener = new InMemoryEventListener(OpenTelemetrySdkEventSource.Log))
{
@ -1648,13 +1511,41 @@ public abstract class MetricApiTestsBase : MetricTestsBase
// This validates that we log InstrumentIgnored event
// and not something else.
Assert.Single(inMemoryEventListener.Events.Where((e) => e.EventId == 33));
var instrumentIgnoredEvents = inMemoryEventListener.Events.Where((e) => e.EventId == 33);
#if BUILDING_HOSTING_TESTS
// Note: When using IMetricsListener this event is fired twice. Once
// for the SDK listener ignoring it because it isn't listening to
// the meter and then once for IMetricsListener ignoring it because
// decimal is not supported.
Assert.Equal(2, instrumentIgnoredEvents.Count());
#else
Assert.Single(instrumentIgnoredEvents);
#endif
}
meterProvider.ForceFlush(MaxTimeToAllowForFlush);
Assert.Empty(exportedItems);
}
internal static IConfiguration BuildConfiguration(bool emitOverflowAttribute, bool shouldReclaimUnusedMetricPoints)
{
var configurationData = new Dictionary<string, string>();
if (emitOverflowAttribute)
{
configurationData[EmitOverFlowAttributeConfigKey] = "true";
}
if (shouldReclaimUnusedMetricPoints)
{
configurationData[ReclaimUnusedMetricPointsConfigKey] = "true";
}
return new ConfigurationBuilder()
.AddInMemoryCollection(configurationData)
.Build();
}
private static void CounterUpdateThread<T>(object obj)
where T : struct, IComparable
{
@ -1716,10 +1607,10 @@ public abstract class MetricApiTestsBase : MetricTestsBase
var metricItems = new List<Metric>();
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<T>
{
@ -1772,10 +1663,10 @@ public abstract class MetricApiTestsBase : MetricTestsBase
var metricReader = new BaseExportingMetricReader(new InMemoryExporter<Metric>(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<T>
{

View File

@ -40,14 +40,14 @@ public class MetricExemplarTests : MetricTestsBase
using var meter = new Meter($"{Utils.GetCurrentMethodName()}");
var counter = meter.CreateCounter<double>("testCounter");
using var meterProvider = Sdk.CreateMeterProviderBuilder()
using var container = this.BuildMeterProvider(out var meterProvider, builder => builder
.AddMeter(meter.Name)
.SetExemplarFilter(new AlwaysOnExemplarFilter())
.AddInMemoryExporter(exportedItems, metricReaderOptions =>
{
metricReaderOptions.TemporalityPreference = MetricReaderTemporalityPreference.Delta;
})
.Build();
}));
var measurementValues = GenerateRandomValues(10);
foreach (var value in measurementValues)
@ -94,14 +94,14 @@ public class MetricExemplarTests : MetricTestsBase
using var meter = new Meter($"{Utils.GetCurrentMethodName()}");
var histogram = meter.CreateHistogram<double>("testHistogram");
using var meterProvider = Sdk.CreateMeterProviderBuilder()
using var container = this.BuildMeterProvider(out var meterProvider, builder => builder
.AddMeter(meter.Name)
.SetExemplarFilter(new AlwaysOnExemplarFilter())
.AddInMemoryExporter(exportedItems, metricReaderOptions =>
{
metricReaderOptions.TemporalityPreference = MetricReaderTemporalityPreference.Delta;
})
.Build();
}));
var measurementValues = GenerateRandomValues(10);
foreach (var value in measurementValues)
@ -147,15 +147,15 @@ public class MetricExemplarTests : MetricTestsBase
using var meter = new Meter($"{Utils.GetCurrentMethodName()}");
var histogram = meter.CreateHistogram<double>("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)

View File

@ -31,21 +31,9 @@ public abstract class MetricSnapshotTestsBase
protected MetricSnapshotTestsBase(bool emitOverflowAttribute, bool shouldReclaimUnusedMetricPoints)
{
var configurationData = new Dictionary<string, string>();
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]

View File

@ -14,6 +14,15 @@
// limitations under the License.
// </copyright>
#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<HostBuilderContext, IConfigurationBuilder> configureAppConfiguration = null,
Action<IServiceCollection> configureServices = null,
Action<IMetricsBuilder> configureMetricsBuilder = null,
Action<HostingMeterProviderBuilder> 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<MetricsSubscriptionManagerCleanupHostedService>();
});
var host = hostBuilder.Build();
host.Start();
return host;
static void ConfigureBuilder(MeterProviderBuilder builder, Action<HostingMeterProviderBuilder> 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<KeyValuePair<string, object>> expectedTags, ReadOnlyTagCollection actualTags)
@ -130,8 +208,106 @@ public class MetricTestsBase
}
}
public IDisposable BuildMeterProvider(
out MeterProvider meterProvider,
Action<MeterProviderBuilder> 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<MeterProvider>();
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
}

View File

@ -30,11 +30,11 @@ public class MetricViewTests : MetricTestsBase
{
using var meter = new Meter(Utils.GetCurrentMethodName());
var exportedItems = new List<Metric>();
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<long>("name1");
@ -53,19 +53,17 @@ public class MetricViewTests : MetricTestsBase
using var meter1 = new Meter("AddViewWithInvalidNameThrowsArgumentException");
var ex = Assert.Throws<ArgumentException>(() => Sdk.CreateMeterProviderBuilder()
var ex = Assert.Throws<ArgumentException>(() => 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<ArgumentException>(() => Sdk.CreateMeterProviderBuilder()
ex = Assert.Throws<ArgumentException>(() => this.BuildMeterProvider(out var meterProvider, builder => builder
.AddMeter(meter1.Name)
.AddView("name1", new MetricStreamConfiguration() { Name = viewNewName })
.AddInMemoryExporter(exportedItems)
.Build());
.AddInMemoryExporter(exportedItems)));
Assert.Contains($"Custom view name {viewNewName} is invalid.", ex.Message);
}
@ -77,11 +75,10 @@ public class MetricViewTests : MetricTestsBase
using var meter1 = new Meter("AddViewWithInvalidNameThrowsArgumentException");
Assert.Throws<ArgumentNullException>(() => Sdk.CreateMeterProviderBuilder()
Assert.Throws<ArgumentNullException>(() => this.BuildMeterProvider(out var meterProvider, builder => builder
.AddMeter(meter1.Name)
.AddView("name1", (MetricStreamConfiguration)null)
.AddInMemoryExporter(exportedItems)
.Build());
.AddInMemoryExporter(exportedItems)));
}
[Fact]
@ -91,11 +88,10 @@ public class MetricViewTests : MetricTestsBase
using var meter1 = new Meter("AddViewWithGuaranteedConflictThrowsInvalidArgumentException");
Assert.Throws<ArgumentException>(() => Sdk.CreateMeterProviderBuilder()
Assert.Throws<ArgumentException>(() => this.BuildMeterProvider(out var meterProvider, builder => builder
.AddMeter(meter1.Name)
.AddView("instrumenta.*", name: "newname")
.AddInMemoryExporter(exportedItems)
.Build());
.AddInMemoryExporter(exportedItems)));
}
[Fact]
@ -105,11 +101,10 @@ public class MetricViewTests : MetricTestsBase
using var meter1 = new Meter("AddViewWithGuaranteedConflictThrowsInvalidArgumentException");
Assert.Throws<ArgumentException>(() => Sdk.CreateMeterProviderBuilder()
Assert.Throws<ArgumentException>(() => this.BuildMeterProvider(out var meterProvider, builder => builder
.AddMeter(meter1.Name)
.AddView("instrumenta.*", new MetricStreamConfiguration() { Name = "newname" })
.AddInMemoryExporter(exportedItems)
.Build());
.AddInMemoryExporter(exportedItems)));
}
[Fact]
@ -118,17 +113,18 @@ public class MetricViewTests : MetricTestsBase
var exportedItems = new List<Metric>();
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<long>("counter1");
counter1.Add(1);
Assert.Single(inMemoryEventListener.Events.Where((e) => e.EventId == 41));
var metricViewIgnoredEvents = inMemoryEventListener.Events.Where((e) => e.EventId == 41);
Assert.Single(metricViewIgnoredEvents);
}
meterProvider.ForceFlush(MaxTimeToAllowForFlush);
@ -144,12 +140,11 @@ public class MetricViewTests : MetricTestsBase
var exportedItems = new List<Metric>();
using var meter1 = new Meter("AddViewWithExceptionInUserCallback");
using var meterProvider = Sdk.CreateMeterProviderBuilder()
using var container = this.BuildMeterProvider(out var meterProvider, builder => builder
.AddMeter(meter1.Name)
.AddView((instrument) => { throw new Exception("bad"); })
.AddView("*", MetricStreamConfiguration.Drop)
.AddInMemoryExporter(exportedItems)
.Build();
.AddInMemoryExporter(exportedItems));
using (var inMemoryEventListener = new InMemoryEventListener(OpenTelemetrySdkEventSource.Log))
{
@ -172,18 +167,19 @@ public class MetricViewTests : MetricTestsBase
var exportedItems = new List<Metric>();
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<long>("counter1");
counter1.Add(1);
Assert.Single(inMemoryEventListener.Events.Where((e) => e.EventId == 41));
var metricViewIgnoredEvents = inMemoryEventListener.Events.Where((e) => e.EventId == 41);
Assert.Single(metricViewIgnoredEvents);
}
meterProvider.ForceFlush(MaxTimeToAllowForFlush);
@ -198,8 +194,8 @@ public class MetricViewTests : MetricTestsBase
[MemberData(nameof(MetricTestData.InvalidHistogramBoundaries), MemberType = typeof(MetricTestData))]
public void AddViewWithInvalidHistogramBoundsThrowsArgumentException(double[] boundaries)
{
var ex = Assert.Throws<ArgumentException>(() => Sdk.CreateMeterProviderBuilder()
.AddView("name1", new ExplicitBucketHistogramConfiguration { Boundaries = boundaries }));
var ex = Assert.Throws<ArgumentException>(() => this.BuildMeterProvider(out var meterProvider, builder => builder
.AddView("name1", new ExplicitBucketHistogramConfiguration { Boundaries = boundaries })));
Assert.Contains("Histogram boundaries must be in ascending order with distinct values", ex.Message);
}
@ -210,8 +206,8 @@ public class MetricViewTests : MetricTestsBase
[InlineData(1)]
public void AddViewWithInvalidExponentialHistogramMaxSizeConfigThrowsArgumentException(int maxSize)
{
var ex = Assert.Throws<ArgumentException>(() => Sdk.CreateMeterProviderBuilder()
.AddView("name1", new Base2ExponentialBucketHistogramConfiguration { MaxSize = maxSize }));
var ex = Assert.Throws<ArgumentException>(() => this.BuildMeterProvider(out var meterProvider, builder => builder
.AddView("name1", new Base2ExponentialBucketHistogramConfiguration { MaxSize = maxSize })));
Assert.Contains("Histogram max size is invalid", ex.Message);
}
@ -221,8 +217,8 @@ public class MetricViewTests : MetricTestsBase
[InlineData(21)]
public void AddViewWithInvalidExponentialHistogramMaxScaleConfigThrowsArgumentException(int maxScale)
{
var ex = Assert.Throws<ArgumentException>(() => Sdk.CreateMeterProviderBuilder()
.AddView("name1", new Base2ExponentialBucketHistogramConfiguration { MaxScale = maxScale }));
var ex = Assert.Throws<ArgumentException>(() => this.BuildMeterProvider(out var meterProvider, builder => builder
.AddView("name1", new Base2ExponentialBucketHistogramConfiguration { MaxScale = maxScale })));
Assert.Contains("Histogram max scale is invalid", ex.Message);
}
@ -237,7 +233,7 @@ public class MetricViewTests : MetricTestsBase
var counter1 = meter1.CreateCounter<long>("counter1");
using (var provider = Sdk.CreateMeterProviderBuilder()
using (var container = this.BuildMeterProvider(out var meterProvider, builder => builder
.AddMeter(meter1.Name)
.AddView((instrument) =>
{
@ -245,8 +241,7 @@ public class MetricViewTests : MetricTestsBase
? new ExplicitBucketHistogramConfiguration() { Boundaries = boundaries }
: null;
})
.AddInMemoryExporter(exportedItems)
.Build())
.AddInMemoryExporter(exportedItems)))
{
counter1.Add(1);
}
@ -263,11 +258,10 @@ public class MetricViewTests : MetricTestsBase
var exportedItems = new List<Metric>();
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<long>("name1");
counterLong.Add(10);
@ -287,7 +281,7 @@ public class MetricViewTests : MetricTestsBase
var exportedItems = new List<Metric>();
using var meterProvider = Sdk.CreateMeterProviderBuilder()
using var container = this.BuildMeterProvider(out var meterProvider, builder => builder
.AddMeter(meter1.Name)
.AddMeter(meter2.Name)
.AddView((instrument) =>
@ -302,8 +296,7 @@ public class MetricViewTests : MetricTestsBase
return null;
}
})
.AddInMemoryExporter(exportedItems)
.Build();
.AddInMemoryExporter(exportedItems));
// Without views only 1 stream would be
// exported (the 2nd one gets dropped due to
@ -327,7 +320,7 @@ public class MetricViewTests : MetricTestsBase
{
using var meter1 = new Meter("ViewToRenameMetricConditionallyTest");
var exportedItems = new List<Metric>();
using var meterProvider = Sdk.CreateMeterProviderBuilder()
using var container = this.BuildMeterProvider(out var meterProvider, builder => builder
.AddMeter(meter1.Name)
// since here it's a func, we can't validate the name right away
@ -345,8 +338,7 @@ public class MetricViewTests : MetricTestsBase
return null;
}
})
.AddInMemoryExporter(exportedItems)
.Build();
.AddInMemoryExporter(exportedItems));
// Because the MetricStreamName passed is invalid, the view is ignored,
// and default aggregation is used.
@ -364,7 +356,7 @@ public class MetricViewTests : MetricTestsBase
{
using var meter1 = new Meter("ViewToRenameMetricConditionallyTest");
var exportedItems = new List<Metric>();
using var meterProvider = Sdk.CreateMeterProviderBuilder()
using var container = this.BuildMeterProvider(out var meterProvider, builder => builder
.AddMeter(meter1.Name)
.AddView((instrument) =>
{
@ -379,8 +371,7 @@ public class MetricViewTests : MetricTestsBase
return null;
}
})
.AddInMemoryExporter(exportedItems)
.Build();
.AddInMemoryExporter(exportedItems));
// Expecting one metric stream.
var counter1 = meter1.CreateCounter<long>("name1", "unit", "original_description");
@ -401,7 +392,7 @@ public class MetricViewTests : MetricTestsBase
using var meter = new Meter("ViewToRenameMetricConditionallyTest");
using var meterProvider = Sdk.CreateMeterProviderBuilder()
using var container = this.BuildMeterProvider(out var meterProvider, builder => builder
.AddMeter(meter.Name)
.AddView((instrument) =>
{
@ -415,8 +406,7 @@ public class MetricViewTests : MetricTestsBase
return null;
}
})
.AddInMemoryExporter(exportedItems)
.Build();
.AddInMemoryExporter(exportedItems));
// Expecting one metric stream.
// Since the View name was null, the instrument name was used instead
@ -436,12 +426,12 @@ public class MetricViewTests : MetricTestsBase
{
using var meter = new Meter(Utils.GetCurrentMethodName());
var exportedItems = new List<Metric>();
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<long>("name1");
@ -457,13 +447,13 @@ public class MetricViewTests : MetricTestsBase
{
using var meter = new Meter(Utils.GetCurrentMethodName());
var exportedItems = new List<Metric>();
using var meterProvider = Sdk.CreateMeterProviderBuilder()
using var container = this.BuildMeterProvider(out var meterProvider, builder => builder
.AddMeter(meter.Name)
.AddView("name1", "renamedStream1")
.AddView("name1", "renamedStream2")
.AddView("name1", "renamedStream2")
.AddInMemoryExporter(exportedItems)
.Build();
.AddInMemoryExporter(exportedItems));
// Expecting three metric stream.
// the second .AddView("name1", "renamedStream2")
@ -482,12 +472,12 @@ public class MetricViewTests : MetricTestsBase
{
using var meter = new Meter(Utils.GetCurrentMethodName());
var exportedItems = new List<Metric>();
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<long>("NotAHistogram");
counter.Add(10);
@ -515,12 +505,12 @@ public class MetricViewTests : MetricTestsBase
using var meter = new Meter(Utils.GetCurrentMethodName());
var exportedItems = new List<Metric>();
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<long>("MyHistogram");
histogram.Record(-10);
@ -600,11 +590,11 @@ public class MetricViewTests : MetricTestsBase
using var meter = new Meter(Utils.GetCurrentMethodName());
var exportedItems = new List<Metric>();
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<long>("MyHistogram");
var expectedHistogram = new Base2ExponentialBucketHistogram();
@ -648,11 +638,11 @@ public class MetricViewTests : MetricTestsBase
using var meter = new Meter(Utils.GetCurrentMethodName());
var histogram = meter.CreateHistogram<double>("MyHistogram");
var exportedItems = new List<Metric>();
using var meterProvider = Sdk.CreateMeterProviderBuilder()
using var container = this.BuildMeterProvider(out var meterProvider, builder => builder
.AddMeter(meter.Name)
.AddView(histogram.Name, histogramConfiguration)
.AddInMemoryExporter(exportedItems)
.Build();
.AddInMemoryExporter(exportedItems));
for (var i = 0; i < values.Length; i++)
{
@ -686,11 +676,11 @@ public class MetricViewTests : MetricTestsBase
using var meter = new Meter(Utils.GetCurrentMethodName());
var histogram = meter.CreateHistogram<double>("MyHistogram");
var exportedItems = new List<Metric>();
using var meterProvider = Sdk.CreateMeterProviderBuilder()
using var container = this.BuildMeterProvider(out var meterProvider, builder => builder
.AddMeter(meter.Name)
.AddView(histogram.Name, histogramConfiguration)
.AddInMemoryExporter(exportedItems)
.Build();
.AddInMemoryExporter(exportedItems));
for (var i = 0; i < values.Length; i++)
{
@ -714,7 +704,8 @@ public class MetricViewTests : MetricTestsBase
{
using var meter = new Meter(Utils.GetCurrentMethodName());
var exportedItems = new List<Metric>();
using var meterProvider = Sdk.CreateMeterProviderBuilder()
using var container = this.BuildMeterProvider(out var meterProvider, builder => builder
.AddMeter(meter.Name)
.AddView("FruitCounter", new MetricStreamConfiguration()
{
@ -731,8 +722,7 @@ public class MetricViewTests : MetricTestsBase
TagKeys = Array.Empty<string>(),
Name = "NoTags",
})
.AddInMemoryExporter(exportedItems)
.Build();
.AddInMemoryExporter(exportedItems));
var counter = meter.CreateCounter<long>("FruitCounter");
counter.Add(10, new("name", "apple"), new("color", "red"), new("size", "small"));
@ -785,11 +775,11 @@ public class MetricViewTests : MetricTestsBase
{
using var meter = new Meter(Utils.GetCurrentMethodName());
var exportedItems = new List<Metric>();
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<long>("counterInteresting");
@ -808,11 +798,11 @@ public class MetricViewTests : MetricTestsBase
{
using var meter = new Meter(Utils.GetCurrentMethodName());
var exportedItems = new List<Metric>();
using var meterProvider = Sdk.CreateMeterProviderBuilder()
using var container = this.BuildMeterProvider(out var meterProvider, builder => builder
.AddMeter(meter.Name)
.AddView("observableCounterNotInteresting", MetricStreamConfiguration.Drop)
.AddInMemoryExporter(exportedItems)
.Build();
.AddInMemoryExporter(exportedItems));
// Expecting one metric stream.
meter.CreateObservableCounter("observableCounterNotInteresting", () => { return 10; }, "ms");
@ -829,11 +819,11 @@ public class MetricViewTests : MetricTestsBase
{
using var meter = new Meter(Utils.GetCurrentMethodName());
var exportedItems = new List<Metric>();
using var meterProvider = Sdk.CreateMeterProviderBuilder()
using var container = this.BuildMeterProvider(out var meterProvider, builder => builder
.AddMeter(meter.Name)
.AddView("observableGaugeNotInteresting", MetricStreamConfiguration.Drop)
.AddInMemoryExporter(exportedItems)
.Build();
.AddInMemoryExporter(exportedItems));
// Expecting one metric stream.
meter.CreateObservableGauge("observableGaugeNotInteresting", () => { return 10; }, "ms");
@ -850,11 +840,11 @@ public class MetricViewTests : MetricTestsBase
{
using var meter = new Meter(Utils.GetCurrentMethodName());
var exportedItems = new List<Metric>();
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<long>("server.requests");
@ -877,12 +867,12 @@ public class MetricViewTests : MetricTestsBase
{
using var meter = new Meter(Utils.GetCurrentMethodName());
var exportedItems = new List<Metric>();
using var meterProvider = Sdk.CreateMeterProviderBuilder()
using var container = this.BuildMeterProvider(out var meterProvider, builder => builder
.AddMeter(meter.Name)
.AddView("server.requests", MetricStreamConfiguration.Drop)
.AddView("server.requests", "server.request_renamed")
.AddInMemoryExporter(exportedItems)
.Build();
.AddInMemoryExporter(exportedItems));
// Expecting one metric stream even though a View is asking
// to drop the instrument, because another View is matching
@ -902,13 +892,12 @@ public class MetricViewTests : MetricTestsBase
var exportedItems = new List<Metric>();
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<long>("instrumentName", "instrumentUnit", "instrumentDescription");
@ -949,7 +938,8 @@ public class MetricViewTests : MetricTestsBase
var exportedItems = new List<Metric>();
using var meter = new Meter($"{Utils.GetCurrentMethodName()}");
var meterProviderBuilder = Sdk.CreateMeterProviderBuilder()
using var container = this.BuildMeterProvider(out var meterProvider, builder => builder
.AddMeter(meter.Name)
.AddView((instrument) =>
{
@ -961,9 +951,7 @@ public class MetricViewTests : MetricTestsBase
? new MetricStreamConfiguration() { Name = "MetricStreamB" }
: new MetricStreamConfiguration() { Name = "MetricStreamC" };
})
.AddInMemoryExporter(exportedItems);
using var meterProvider = meterProviderBuilder.Build();
.AddInMemoryExporter(exportedItems));
var instrument1 = meter.CreateCounter<long>("name", "unit", "description1");
var instrument2 = meter.CreateCounter<long>("name", "unit", "description2");
@ -1006,7 +994,8 @@ public class MetricViewTests : MetricTestsBase
var exportedItems = new List<Metric>();
using var meter = new Meter($"{Utils.GetCurrentMethodName()}");
var meterProviderBuilder = Sdk.CreateMeterProviderBuilder()
using var container = this.BuildMeterProvider(out var meterProvider, builder => builder
.AddMeter(meter.Name)
.AddView((instrument) =>
{
@ -1016,9 +1005,7 @@ public class MetricViewTests : MetricTestsBase
{
return new MetricStreamConfiguration { TagKeys = new[] { "key2" } };
})
.AddInMemoryExporter(exportedItems);
using var meterProvider = meterProviderBuilder.Build();
.AddInMemoryExporter(exportedItems));
var instrument1 = meter.CreateCounter<long>("name");
var instrument2 = meter.CreateCounter<long>("name");
@ -1054,7 +1041,8 @@ public class MetricViewTests : MetricTestsBase
var exportedItems = new List<Metric>();
using var meter = new Meter($"{Utils.GetCurrentMethodName()}");
var meterProviderBuilder = Sdk.CreateMeterProviderBuilder()
using var container = this.BuildMeterProvider(out var meterProvider, builder => builder
.AddMeter(meter.Name)
.AddView((instrument) =>
{
@ -1064,9 +1052,7 @@ public class MetricViewTests : MetricTestsBase
{
return new MetricStreamConfiguration { TagKeys = new[] { "key1" } };
})
.AddInMemoryExporter(exportedItems);
using var meterProvider = meterProviderBuilder.Build();
.AddInMemoryExporter(exportedItems));
var instrument1 = meter.CreateCounter<long>("name");
var instrument2 = meter.CreateCounter<long>("name");
@ -1103,7 +1089,8 @@ public class MetricViewTests : MetricTestsBase
var exportedItems = new List<Metric>();
using var meter = new Meter($"{Utils.GetCurrentMethodName()}");
var meterProviderBuilder = Sdk.CreateMeterProviderBuilder()
using var container = this.BuildMeterProvider(out var meterProvider, builder => builder
.AddMeter(meter.Name)
.AddView((instrument) =>
{
@ -1113,9 +1100,7 @@ public class MetricViewTests : MetricTestsBase
{
return new ExplicitBucketHistogramConfiguration { Boundaries = new[] { 10.0, 20.0 } };
})
.AddInMemoryExporter(exportedItems);
using var meterProvider = meterProviderBuilder.Build();
.AddInMemoryExporter(exportedItems));
var instrument1 = meter.CreateHistogram<long>("name");
var instrument2 = meter.CreateHistogram<long>("name");
@ -1181,7 +1166,8 @@ public class MetricViewTests : MetricTestsBase
var exportedItems = new List<Metric>();
using var meter = new Meter($"{Utils.GetCurrentMethodName()}");
var meterProviderBuilder = Sdk.CreateMeterProviderBuilder()
using var container = this.BuildMeterProvider(out var meterProvider, builder => builder
.AddMeter(meter.Name)
.AddView((instrument) =>
{
@ -1194,9 +1180,7 @@ public class MetricViewTests : MetricTestsBase
return null;
}
})
.AddInMemoryExporter(exportedItems);
using var meterProvider = meterProviderBuilder.Build();
.AddInMemoryExporter(exportedItems));
var instrument1 = meter.CreateCounter<long>("name");
var instrument2 = meter.CreateCounter<long>("othername");
@ -1235,7 +1219,8 @@ public class MetricViewTests : MetricTestsBase
var exportedItems = new List<Metric>();
using var meter = new Meter($"{Utils.GetCurrentMethodName()}");
var meterProviderBuilder = Sdk.CreateMeterProviderBuilder()
using var container = this.BuildMeterProvider(out var meterProvider, builder => builder
.AddMeter(meter.Name)
.AddView((instrument) =>
{
@ -1248,9 +1233,7 @@ public class MetricViewTests : MetricTestsBase
return MetricStreamConfiguration.Drop;
}
})
.AddInMemoryExporter(exportedItems);
using var meterProvider = meterProviderBuilder.Build();
.AddInMemoryExporter(exportedItems));
var instrument1 = meter.CreateCounter<long>("name");
var instrument2 = meter.CreateCounter<long>("othername");