diff --git a/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/Microsoft.Extensions.Diagnostics.Abstractions.sln b/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/Microsoft.Extensions.Diagnostics.Abstractions.sln new file mode 100644 index 00000000000000..4e51f48f75fbaa --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/Microsoft.Extensions.Diagnostics.Abstractions.sln @@ -0,0 +1,83 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.7.33711.374 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestUtilities", "..\Common\tests\TestUtilities\TestUtilities.csproj", "{79CE8C7E-A4AF-413C-A54D-86F17073559C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bcl.AsyncInterfaces", "..\Microsoft.Bcl.AsyncInterfaces\ref\Microsoft.Bcl.AsyncInterfaces.csproj", "{9052AC86-4B89-4311-BEF2-7C49FB72DC0F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bcl.AsyncInterfaces", "..\Microsoft.Bcl.AsyncInterfaces\src\Microsoft.Bcl.AsyncInterfaces.csproj", "{FA353FC1-2D03-426A-8973-0CDA8DF5E5DD}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.DependencyInjection.Abstractions", "..\Microsoft.Extensions.DependencyInjection.Abstractions\ref\Microsoft.Extensions.DependencyInjection.Abstractions.csproj", "{D6778DF4-DA03-43E7-BD9D-2E2C35DCCE7F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.DependencyInjection.Abstractions", "..\Microsoft.Extensions.DependencyInjection.Abstractions\src\Microsoft.Extensions.DependencyInjection.Abstractions.csproj", "{527CCF66-AC37-487C-871E-A4F6B94E1731}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{4DE63935-DCA9-4D63-9C1F-AAE79C89CA8B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ref", "ref", "{7631380A-FB73-4241-9987-0891A21E9769}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{548DF5F7-790C-4A1C-89EB-BD904CA1BA86}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Diagnostics.Abstractions", "src\Microsoft.Extensions.Diagnostics.Abstractions.csproj", "{0588387D-FB65-4BA9-A8B2-DA6790027CC2}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Diagnostics.Abstractions", "ref\Microsoft.Extensions.Diagnostics.Abstractions.csproj", "{EF2C8F2A-6088-4A89-9AE2-9C2FDFB0241C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Diagnostics.Abstractions.Tests", "tests\Microsoft.Extensions.Diagnostics.Abstractions.Tests.csproj", "{CB373FE5-F976-4CB2-A04E-6188D67A5816}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {79CE8C7E-A4AF-413C-A54D-86F17073559C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {79CE8C7E-A4AF-413C-A54D-86F17073559C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {79CE8C7E-A4AF-413C-A54D-86F17073559C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {79CE8C7E-A4AF-413C-A54D-86F17073559C}.Release|Any CPU.Build.0 = Release|Any CPU + {9052AC86-4B89-4311-BEF2-7C49FB72DC0F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9052AC86-4B89-4311-BEF2-7C49FB72DC0F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9052AC86-4B89-4311-BEF2-7C49FB72DC0F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9052AC86-4B89-4311-BEF2-7C49FB72DC0F}.Release|Any CPU.Build.0 = Release|Any CPU + {FA353FC1-2D03-426A-8973-0CDA8DF5E5DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FA353FC1-2D03-426A-8973-0CDA8DF5E5DD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FA353FC1-2D03-426A-8973-0CDA8DF5E5DD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FA353FC1-2D03-426A-8973-0CDA8DF5E5DD}.Release|Any CPU.Build.0 = Release|Any CPU + {D6778DF4-DA03-43E7-BD9D-2E2C35DCCE7F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D6778DF4-DA03-43E7-BD9D-2E2C35DCCE7F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D6778DF4-DA03-43E7-BD9D-2E2C35DCCE7F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D6778DF4-DA03-43E7-BD9D-2E2C35DCCE7F}.Release|Any CPU.Build.0 = Release|Any CPU + {527CCF66-AC37-487C-871E-A4F6B94E1731}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {527CCF66-AC37-487C-871E-A4F6B94E1731}.Debug|Any CPU.Build.0 = Debug|Any CPU + {527CCF66-AC37-487C-871E-A4F6B94E1731}.Release|Any CPU.ActiveCfg = Release|Any CPU + {527CCF66-AC37-487C-871E-A4F6B94E1731}.Release|Any CPU.Build.0 = Release|Any CPU + {0588387D-FB65-4BA9-A8B2-DA6790027CC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0588387D-FB65-4BA9-A8B2-DA6790027CC2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0588387D-FB65-4BA9-A8B2-DA6790027CC2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0588387D-FB65-4BA9-A8B2-DA6790027CC2}.Release|Any CPU.Build.0 = Release|Any CPU + {EF2C8F2A-6088-4A89-9AE2-9C2FDFB0241C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EF2C8F2A-6088-4A89-9AE2-9C2FDFB0241C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EF2C8F2A-6088-4A89-9AE2-9C2FDFB0241C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EF2C8F2A-6088-4A89-9AE2-9C2FDFB0241C}.Release|Any CPU.Build.0 = Release|Any CPU + {CB373FE5-F976-4CB2-A04E-6188D67A5816}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CB373FE5-F976-4CB2-A04E-6188D67A5816}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CB373FE5-F976-4CB2-A04E-6188D67A5816}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CB373FE5-F976-4CB2-A04E-6188D67A5816}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {79CE8C7E-A4AF-413C-A54D-86F17073559C} = {4DE63935-DCA9-4D63-9C1F-AAE79C89CA8B} + {9052AC86-4B89-4311-BEF2-7C49FB72DC0F} = {7631380A-FB73-4241-9987-0891A21E9769} + {FA353FC1-2D03-426A-8973-0CDA8DF5E5DD} = {548DF5F7-790C-4A1C-89EB-BD904CA1BA86} + {D6778DF4-DA03-43E7-BD9D-2E2C35DCCE7F} = {7631380A-FB73-4241-9987-0891A21E9769} + {527CCF66-AC37-487C-871E-A4F6B94E1731} = {548DF5F7-790C-4A1C-89EB-BD904CA1BA86} + {0588387D-FB65-4BA9-A8B2-DA6790027CC2} = {548DF5F7-790C-4A1C-89EB-BD904CA1BA86} + {EF2C8F2A-6088-4A89-9AE2-9C2FDFB0241C} = {7631380A-FB73-4241-9987-0891A21E9769} + {CB373FE5-F976-4CB2-A04E-6188D67A5816} = {4DE63935-DCA9-4D63-9C1F-AAE79C89CA8B} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {450DA749-CBDC-4BDC-950F-8A491CF59D49} + EndGlobalSection +EndGlobal diff --git a/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/README.md b/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/README.md new file mode 100644 index 00000000000000..200587369cc7b9 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/README.md @@ -0,0 +1,21 @@ +# Microsoft.Extensions.Diagnostics.Abstractions + +`Microsoft.Extensions.Diagnostics.Abstractions` provides abstractions of diagnostics. Interfaces defined in this package are implemented by classes in [Microsoft.Extensions.Diagnostics](https://www.nuget.org/packages/Microsoft.Extensions.Diagnostics/) and other diagnostics packages. + +Commonly Used Types: +- `Microsoft.Extensions.Diagnostics.Metrics.IMetricsBuilder` +- `Microsoft.Extensions.Diagnostics.Metrics.IMetricsListener` +- `Microsoft.Extensions.Diagnostics.Metrics.InstrumentRule` +- `Microsoft.Extensions.Diagnostics.Metrics.MeterScope` +- `Microsoft.Extensions.Diagnostics.Metrics.MetricsBuilderExtensions` +- `Microsoft.Extensions.Diagnostics.Metrics.MetricsOptions` + +Documentation can be found at https://learn.microsoft.com/en-us/dotnet/core/extensions/diagnostics. + +## Contribution Bar +- [x] [We consider new features, new APIs, bug fixes, and performance changes](../../libraries/README.md#primary-bar) + +The APIs and functionality are new in .NET 8 and will continue to be developed. + +## Deployment +[Microsoft.Extensions.Diagnostics.Abstractions](https://www.nuget.org/packages/Microsoft.Extensions.Diagnostics.Abstractions) is included in the ASP.NET Core shared framework. The package is deployed as out-of-band (OOB) too and can be referenced into projects directly. diff --git a/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/ref/Microsoft.Extensions.Diagnostics.Abstractions.cs b/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/ref/Microsoft.Extensions.Diagnostics.Abstractions.cs new file mode 100644 index 00000000000000..3a2dc49933deeb --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/ref/Microsoft.Extensions.Diagnostics.Abstractions.cs @@ -0,0 +1,75 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// ------------------------------------------------------------------------------ +// Changes to this file must follow the https://aka.ms/api-review process. +// ------------------------------------------------------------------------------ + +using System; +using System.Collections.Generic; + +namespace Microsoft.Extensions.Diagnostics.Metrics +{ + public interface IMetricsBuilder + { + Microsoft.Extensions.DependencyInjection.IServiceCollection Services { get; } + } + public interface IMetricsListener + { + public string Name { get; } + public void Initialize(IObservableInstrumentsSource source); + public bool InstrumentPublished(System.Diagnostics.Metrics.Instrument instrument, out object? userState); + public void MeasurementsCompleted(System.Diagnostics.Metrics.Instrument instrument, object? userState); + public MeasurementHandlers GetMeasurementHandlers(); + } + public class MeasurementHandlers + { + public System.Diagnostics.Metrics.MeasurementCallback? ByteHandler { get; set; } + public System.Diagnostics.Metrics.MeasurementCallback? ShortHandler { get; set; } + public System.Diagnostics.Metrics.MeasurementCallback? IntHandler { get; set; } + public System.Diagnostics.Metrics.MeasurementCallback? LongHandler { get; set; } + public System.Diagnostics.Metrics.MeasurementCallback? FloatHandler { get; set; } + public System.Diagnostics.Metrics.MeasurementCallback? DoubleHandler { get; set; } + public System.Diagnostics.Metrics.MeasurementCallback? DecimalHandler { get; set; } + } + public interface IObservableInstrumentsSource + { + public void RecordObservableInstruments(); + } + public class InstrumentRule + { + public InstrumentRule(string? meterName, string? instrumentName, string? listenerName, MeterScope scopes, bool enable) { } + public string? MeterName { get; } + public string? InstrumentName { get; } + public string? ListenerName { get; } + public MeterScope Scopes { get; } + public bool Enable { get; } + } + [Flags] + public enum MeterScope + { + None = 0, + Global, + Local + } + public static class MetricsBuilderExtensions + { + public static IMetricsBuilder AddListener<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] T> + (this IMetricsBuilder builder) where T : class, IMetricsListener { throw null!; } + public static IMetricsBuilder AddListener(this IMetricsBuilder builder, IMetricsListener listener) { throw null!; } + public static IMetricsBuilder ClearListeners(this IMetricsBuilder builder) { throw null!; } + + public static IMetricsBuilder EnableMetrics(this IMetricsBuilder builder, string? meterName) => throw null!; + public static IMetricsBuilder EnableMetrics(this IMetricsBuilder builder, string? meterName, string? instrumentName = null, string? listenerName = null, MeterScope scopes = MeterScope.Global | MeterScope.Local) => throw null!; + public static MetricsOptions EnableMetrics(this MetricsOptions options, string? meterName) => throw null!; + public static MetricsOptions EnableMetrics(this MetricsOptions options, string? meterName, string? instrumentName = null, string? listenerName = null, MeterScope scopes = MeterScope.Global | MeterScope.Local) => throw null!; + + public static IMetricsBuilder DisableMetrics(this IMetricsBuilder builder, string? meterName) => throw null!; + public static IMetricsBuilder DisableMetrics(this IMetricsBuilder builder, string? meterName, string? instrumentName = null, string? listenerName = null, MeterScope scopes = MeterScope.Global | MeterScope.Local) => throw null!; + public static MetricsOptions DisableMetrics(this MetricsOptions options, string? meterName) => throw null!; + public static MetricsOptions DisableMetrics(this MetricsOptions options, string? meterName, string? instrumentName = null, string? listenerName = null, MeterScope scopes = MeterScope.Global | MeterScope.Local) => throw null!; + } + public class MetricsOptions + { + public IList Rules { get; } = null!; + } +} diff --git a/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/ref/Microsoft.Extensions.Diagnostics.Abstractions.csproj b/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/ref/Microsoft.Extensions.Diagnostics.Abstractions.csproj new file mode 100644 index 00000000000000..f7b93cc292f7d7 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/ref/Microsoft.Extensions.Diagnostics.Abstractions.csproj @@ -0,0 +1,19 @@ + + + $(NetCoreAppCurrent);$(NetCoreAppPrevious);$(NetCoreAppMinimum);netstandard2.0;$(NetFrameworkMinimum) + + + + + + + + + + + + + + + + diff --git a/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/src/Metrics/IMetricsBuilder.cs b/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/src/Metrics/IMetricsBuilder.cs new file mode 100644 index 00000000000000..76e0d8dd451b21 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/src/Metrics/IMetricsBuilder.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.Extensions.Diagnostics.Metrics +{ + /// + /// Represents a type used to configure the metrics system by registering IMetricsListeners and using rules + /// to determine which metrics are enabled. + /// + public interface IMetricsBuilder + { + /// + /// The application . This is used by extension methods to register services. + /// + IServiceCollection Services { get; } + } +} diff --git a/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/src/Metrics/IMetricsListener.cs b/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/src/Metrics/IMetricsListener.cs new file mode 100644 index 00000000000000..e4c7fa173576ed --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/src/Metrics/IMetricsListener.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.Metrics; + +namespace Microsoft.Extensions.Diagnostics.Metrics +{ + /// + /// Represents a type used to listen to metrics emitted from the system. + /// + public interface IMetricsListener + { + /// + /// The name of the listener. This is used to identify the listener in the rules configuration. + /// + public string Name { get; } + + /// + /// Called once by the runtime to provide a used to pull for fresh metrics data. + /// + /// A that can be called to request current metrics. + public void Initialize(IObservableInstrumentsSource source); + + /// + /// Called when a new instrument is created and enabled by a matching rule. + /// + /// The new . + /// Listener state associated with this instrument. This will be returned to + /// and . + /// Returns true if the listener wants to subscribe to this instrument, otherwise false. + public bool InstrumentPublished(Instrument instrument, out object? userState); + + /// + /// Called when a instrument is disabled by the producer or a rules change. + /// + /// The being disabled. + /// The original listener state returned by . + public void MeasurementsCompleted(Instrument instrument, object? userState); + + /// + /// Called once to get the that will be used to process measurements. + /// + /// The . + public MeasurementHandlers GetMeasurementHandlers(); + } +} diff --git a/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/src/Metrics/IObservableInstrumentsSource.cs b/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/src/Metrics/IObservableInstrumentsSource.cs new file mode 100644 index 00000000000000..29657c5b069337 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/src/Metrics/IObservableInstrumentsSource.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.Metrics; + +namespace Microsoft.Extensions.Diagnostics.Metrics +{ + /// + /// An interface registered with each IMetricsListener using . The listener + /// can call to receive the current set of measurements for enabled observable instruments. + /// + public interface IObservableInstrumentsSource + { + /// + /// Requests that the current set of metrics for enabled instruments be sent to the listener's 's. + /// + public void RecordObservableInstruments(); + } +} diff --git a/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/src/Metrics/InstrumentRule.cs b/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/src/Metrics/InstrumentRule.cs new file mode 100644 index 00000000000000..d8f95b3fe4942e --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/src/Metrics/InstrumentRule.cs @@ -0,0 +1,58 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.Metrics; + +namespace Microsoft.Extensions.Diagnostics.Metrics +{ + /// + /// A set of parameters used to determine which instruments are enabled for which listeners. Unspecified + /// parameters match anything. + /// + /// + /// The most specific rule that matches a given instrument will be used. The priority of parameters is as follows: + /// - MeterName, either an exact match, or the longest prefix match. See . + /// - InstrumentName, an exact match. . + /// - ListenerName, an exact match. . + /// - Scopes + /// + /// The or prefix. + /// The . + /// The . + /// The 's to consider. + /// Enables or disabled the matched instrument for this listener. + public class InstrumentRule(string? meterName, string? instrumentName, string? listenerName, MeterScope scopes, bool enable) + { + /// + /// The , either an exact match or the longest prefix match. Only full segment matches are considered. + /// All meters are matched if this is null. + /// + public string? MeterName { get; } = meterName; + + /// + /// The , an exact match. + /// All instruments for the given meter are matched if this is null. + /// + public string? InstrumentName { get; } = instrumentName; + + /// + /// The , an exact match. + /// All listeners are matched if this is null. + /// + public string? ListenerName { get; } = listenerName; + + /// + /// The . This is used to distinguish between meters created via constructors () + /// and those created via Dependency Injection with ()."/>. + /// + public MeterScope Scopes { get; } = scopes == MeterScope.None + ? throw new ArgumentOutOfRangeException(nameof(scopes), scopes, "The MeterScope must be Global, Local, or both.") + : scopes; + + /// + /// Indicates if the instrument should be enabled for the listener. + /// + public bool Enable { get; } = enable; + } +} diff --git a/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/src/Metrics/MeasurementHandlers.cs b/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/src/Metrics/MeasurementHandlers.cs new file mode 100644 index 00000000000000..027c8e1e639dea --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/src/Metrics/MeasurementHandlers.cs @@ -0,0 +1,48 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.Metrics; + +namespace Microsoft.Extensions.Diagnostics.Metrics +{ + /// + /// A set of supported measurement types. If a listener does not support a given type, the measurement will be skipped. + /// + public class MeasurementHandlers + { + /// + /// A for . If null, byte measurements will be skipped. + /// + public MeasurementCallback? ByteHandler { get; set; } + + /// + /// A for . If null, short measurements will be skipped. + /// + public MeasurementCallback? ShortHandler { get; set; } + + /// + /// A for . If null, int measurements will be skipped. + /// + public MeasurementCallback? IntHandler { get; set; } + + /// + /// A for . If null, long measurements will be skipped. + /// + public MeasurementCallback? LongHandler { get; set; } + + /// + /// A for . If null, float measurements will be skipped. + /// + public MeasurementCallback? FloatHandler { get; set; } + + /// + /// A for . If null, double measurements will be skipped. + /// + public MeasurementCallback? DoubleHandler { get; set; } + + /// + /// A for . If null, decimal measurements will be skipped. + /// + public MeasurementCallback? DecimalHandler { get; set; } + } +} diff --git a/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/src/Metrics/MeterScope.cs b/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/src/Metrics/MeterScope.cs new file mode 100644 index 00000000000000..a117c7b4371f9f --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/src/Metrics/MeterScope.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.Metrics; + +namespace Microsoft.Extensions.Diagnostics.Metrics +{ + /// + /// This is used by to distinguish between meters created via constructors () + /// and those created via Dependency Injection with ()."/>. + /// + [Flags] + public enum MeterScope + { + /// + /// No scope is specified. This should not be used. + /// + None = 0, + + /// + /// Indicates instances created via constructors. + /// + Global, + + /// + /// Indicates instances created via Dependency Injection with . + /// + Local + } +} diff --git a/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/src/Metrics/MetricsBuilderExtensions.Listeners.cs b/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/src/Metrics/MetricsBuilderExtensions.Listeners.cs new file mode 100644 index 00000000000000..f405af4adf83b2 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/src/Metrics/MetricsBuilderExtensions.Listeners.cs @@ -0,0 +1,54 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.Extensions.Diagnostics.Metrics +{ + /// + /// Extension methods for to add or clear registrations, and to enable or disable metrics. + /// + public static partial class MetricsBuilderExtensions + { + /// + /// Registers a new of type . + /// + /// The implementation type of the listener. + /// The . + /// Returns the original for chaining. + public static IMetricsBuilder AddListener<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] T>(this IMetricsBuilder builder) where T : class, IMetricsListener + { + ThrowHelper.ThrowIfNull(builder); + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + return builder; + } + + /// + /// Registers a new instance. + /// + /// The implementation type of the listener. + /// The . + /// Returns the original for chaining. + public static IMetricsBuilder AddListener(this IMetricsBuilder builder, IMetricsListener listener) + { + ThrowHelper.ThrowIfNull(builder); + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton(listener)); + return builder; + } + + /// + /// Removes all registrations from the dependency injection container. + /// + /// The . + /// Returns the original for chaining. + public static IMetricsBuilder ClearListeners(this IMetricsBuilder builder) + { + ThrowHelper.ThrowIfNull(builder); + builder.Services.RemoveAll(); + return builder; + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/src/Metrics/MetricsBuilderExtensions.Rules.cs b/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/src/Metrics/MetricsBuilderExtensions.Rules.cs new file mode 100644 index 00000000000000..6b896aeffd75a9 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/src/Metrics/MetricsBuilderExtensions.Rules.cs @@ -0,0 +1,118 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.Metrics; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.Extensions.Diagnostics.Metrics +{ + /// + /// Extension methods for to add or clear registrations, and to enable or disable metrics. + /// + public static partial class MetricsBuilderExtensions + { + /// + /// Enables all 's for the given meter, for all registered 's. + /// + /// The . + /// The or prefix. A null value matches all meters. + /// The original for chaining. + public static IMetricsBuilder EnableMetrics(this IMetricsBuilder builder, string? meterName) + => builder.ConfigureRule(options => options.EnableMetrics(meterName)); + + /// + /// Enables a specified for the given and . + /// + /// The . + /// The or prefix. A null value matches all meters. + /// The . A null value matches all instruments. + /// The .Name. A null value matches all listeners. + /// Indicates which 's to consider. Default to all scopes. + /// The original for chaining. + public static IMetricsBuilder EnableMetrics(this IMetricsBuilder builder, string? meterName, string? instrumentName = null, string? listenerName = null, + MeterScope scopes = MeterScope.Global | MeterScope.Local) + => builder.ConfigureRule(options => options.EnableMetrics(meterName, instrumentName, listenerName, scopes)); + + /// + /// Enables all 's for the given meter, for all registered 's. + /// + /// The . + /// The or prefix. A null value matches all meters. + /// The original for chaining. + public static MetricsOptions EnableMetrics(this MetricsOptions options, string? meterName) + => options.EnableMetrics(meterName: meterName, instrumentName: null); + + /// + /// Enables a specified for the given and . + /// + /// The . + /// The or prefix. A null value matches all meters. + /// The . A null value matches all instruments. + /// The .Name. A null value matches all listeners. + /// Indicates which 's to consider. Default to all scopes. + /// The original for chaining. + public static MetricsOptions EnableMetrics(this MetricsOptions options, string? meterName, string? instrumentName = null, string? listenerName = null, + MeterScope scopes = MeterScope.Global | MeterScope.Local) + => options.AddRule(meterName, instrumentName, listenerName, scopes, enable: true); + + /// + /// Disables all 's for the given meter, for all registered 's. + /// + /// The . + /// The or prefix. A null value matches all meters. + /// The original for chaining. + public static IMetricsBuilder DisableMetrics(this IMetricsBuilder builder, string? meterName) + => builder.ConfigureRule(options => options.DisableMetrics(meterName)); + + /// + /// Disables a specified for the given and . + /// + /// The . + /// The or prefix. A null value matches all meters. + /// The . A null value matches all instruments. + /// The .Name. A null value matches all listeners. + /// Indicates which 's to consider. Default to all scopes. + /// The original for chaining. + public static IMetricsBuilder DisableMetrics(this IMetricsBuilder builder, string? meterName, string? instrumentName = null, string? listenerName = null, + MeterScope scopes = MeterScope.Global | MeterScope.Local) + => builder.ConfigureRule(options => options.DisableMetrics(meterName, instrumentName, listenerName, scopes)); + + /// + /// Disables all 's for the given meter, for all registered 's. + /// + /// The . + /// The or prefix. A null value matches all meters. + /// The original for chaining. + public static MetricsOptions DisableMetrics(this MetricsOptions options, string? meterName) + => options.DisableMetrics(meterName: meterName, instrumentName: null); + + /// + /// Disables a specified for the given and . + /// + /// The . + /// The or prefix. A null value matches all meters. + /// The . A null value matches all instruments. + /// The .Name. A null value matches all listeners. + /// Indicates which 's to consider. Default to all scopes. + /// The original for chaining. + public static MetricsOptions DisableMetrics(this MetricsOptions options, string? meterName, string? instrumentName = null, string? listenerName = null, + MeterScope scopes = MeterScope.Global | MeterScope.Local) + => options.AddRule(meterName, instrumentName, listenerName, scopes, enable: false); + + private static IMetricsBuilder ConfigureRule(this IMetricsBuilder builder, Action configureOptions) + { + ThrowHelper.ThrowIfNull(builder); + builder.Services.Configure(configureOptions); + return builder; + } + + private static MetricsOptions AddRule(this MetricsOptions options, string? meterName, string? instrumentName, string? listenerName, + MeterScope scopes, bool enable) + { + ThrowHelper.ThrowIfNull(options); + options.Rules.Add(new InstrumentRule(meterName, instrumentName, listenerName, scopes, enable)); + return options; + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/src/Metrics/MetricsOptions.cs b/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/src/Metrics/MetricsOptions.cs new file mode 100644 index 00000000000000..1dfbe1f57f148e --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/src/Metrics/MetricsOptions.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; + +namespace Microsoft.Extensions.Diagnostics.Metrics +{ + /// + /// Options for configuring the metrics system. + /// + public class MetricsOptions + { + /// + /// A list of 's that identify which metrics, instruments, and listeners are enabled. + /// + public IList Rules { get; } = new List(); + } +} diff --git a/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/src/Microsoft.Extensions.Diagnostics.Abstractions.csproj b/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/src/Microsoft.Extensions.Diagnostics.Abstractions.csproj new file mode 100644 index 00000000000000..a56170373401ee --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/src/Microsoft.Extensions.Diagnostics.Abstractions.csproj @@ -0,0 +1,40 @@ + + + + $(NetCoreAppCurrent);$(NetCoreAppPrevious);$(NetCoreAppMinimum);netstandard2.0;$(NetFrameworkMinimum) + true + true + + true + Diagnostic abstractions for Microsoft.Extensions.Diagnostics. + +Commonly Used Types: +Microsoft.Extensions.Diagnostics.Metrics.IMetricsBuilder +Microsoft.Extensions.Diagnostics.Metrics.IMetricsListener +Microsoft.Extensions.Diagnostics.Metrics.InstrumentRule +Microsoft.Extensions.Diagnostics.Metrics.MeterScope +Microsoft.Extensions.Diagnostics.Metrics.MetricsBuilderExtensions +Microsoft.Extensions.Diagnostics.Metrics.MetricsOptions + + + + + + + + + + + + + + + + + + + + + diff --git a/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/src/Resources/Strings.resx b/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/src/Resources/Strings.resx new file mode 100644 index 00000000000000..1af7de150c99c1 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/src/Resources/Strings.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/tests/InstrumentRuleTests.cs b/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/tests/InstrumentRuleTests.cs new file mode 100644 index 00000000000000..f7fd045f361594 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/tests/InstrumentRuleTests.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.Metrics.Tests +{ + public class InstrumentRuleTests + { + [Fact] + public void ScopeRequired() + { + var ex = Assert.Throws(() => new InstrumentRule(null, null, null, MeterScope.None, true)); + Assert.Equal("scopes", ex.ParamName); + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/tests/MetricsBuilderExtensionsListenersTests.cs b/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/tests/MetricsBuilderExtensionsListenersTests.cs new file mode 100644 index 00000000000000..5108f9d6b63904 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/tests/MetricsBuilderExtensionsListenersTests.cs @@ -0,0 +1,91 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.Metrics; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.Metrics; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.Tests +{ + public class MetricsBuilderExtensionsListenersTests + { + [Fact] + public void CanAddListenersByType() + { + var services = new ServiceCollection(); + var builder = new FakeBuilder(services); + + builder.AddListener(); + var container = services.BuildServiceProvider(); + Assert.IsType(Assert.Single(container.GetServices())); + + builder.AddListener(); + container = services.BuildServiceProvider(); + Assert.Equal(2, container.GetServices().Count()); + } + + [Fact] + public void CanAddListenersByInstance() + { + var services = new ServiceCollection(); + var builder = new FakeBuilder(services); + + var instanceA = new FakeListenerA(); + builder.AddListener(instanceA); + var container = services.BuildServiceProvider(); + Assert.Same(instanceA, Assert.Single(container.GetServices())); + + var instanceB = new FakeListenerB(); + builder.AddListener(instanceB); + container = services.BuildServiceProvider(); + var listeners = container.GetServices().ToList(); + Assert.Equal(2, listeners.Count); + Assert.Same(instanceA, listeners[0]); + Assert.Same(instanceB, listeners[1]); + } + + [Fact] + public void CanClearListeners() + { + var services = new ServiceCollection(); + var builder = new FakeBuilder(services); + + builder.AddListener(); + builder.AddListener(new FakeListenerB()); + var container = services.BuildServiceProvider(); + Assert.Equal(2, container.GetServices().Count()); + + builder.ClearListeners(); + container = services.BuildServiceProvider(); + Assert.Empty(container.GetServices()); + } + + private class FakeBuilder(IServiceCollection services) : IMetricsBuilder + { + public IServiceCollection Services { get; } = services; + } + + private class FakeListenerA : IMetricsListener + { + public string Name => "Fake"; + + public MeasurementHandlers GetMeasurementHandlers() => throw new NotImplementedException(); + public void Initialize(IObservableInstrumentsSource source) => throw new NotImplementedException(); + public bool InstrumentPublished(Instrument instrument, out object? userState) => throw new NotImplementedException(); + public void MeasurementsCompleted(Instrument instrument, object? userState) => throw new NotImplementedException(); + } + + private class FakeListenerB : IMetricsListener + { + public string Name => "Fake"; + + public MeasurementHandlers GetMeasurementHandlers() => throw new NotImplementedException(); + public void Initialize(IObservableInstrumentsSource source) => throw new NotImplementedException(); + public bool InstrumentPublished(Instrument instrument, out object? userState) => throw new NotImplementedException(); + public void MeasurementsCompleted(Instrument instrument, object? userState) => throw new NotImplementedException(); + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/tests/MetricsBuilderExtensionsRulesTests.cs b/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/tests/MetricsBuilderExtensionsRulesTests.cs new file mode 100644 index 00000000000000..18568a8480392a --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/tests/MetricsBuilderExtensionsRulesTests.cs @@ -0,0 +1,188 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.Metrics; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.Tests +{ + public class MetricsBuilderExtensionsRulesTests + { + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("*")] + [InlineData("foo")] + public void BuilderEnableMetricsAddsRule(string meterName) + { + var services = new ServiceCollection(); + services.AddOptions(); + var builder = new FakeBuilder(services); + + builder.EnableMetrics(meterName); + + var container = services.BuildServiceProvider(); + var options = container.GetRequiredService>(); + var instance = options.Value; + var rule = Assert.Single(instance.Rules); + Assert.Equal(meterName, rule.MeterName); + Assert.Null(rule.InstrumentName); + Assert.Null(rule.ListenerName); + Assert.Equal(MeterScope.Local | MeterScope.Global, rule.Scopes); + Assert.True(rule.Enable); + } + + [Fact] + public void BuilderEnableMetricsWithAllParamsAddsRule() + { + var services = new ServiceCollection(); + services.AddOptions(); + var builder = new FakeBuilder(services); + + builder.EnableMetrics("meter", "instance", "listener", MeterScope.Local); + + var container = services.BuildServiceProvider(); + var options = container.GetRequiredService>(); + var instance = options.Value; + var rule = Assert.Single(instance.Rules); + Assert.Equal("meter", rule.MeterName); + Assert.Equal("instance", rule.InstrumentName); + Assert.Equal("listener", rule.ListenerName); + Assert.Equal(MeterScope.Local, rule.Scopes); + Assert.True(rule.Enable); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("*")] + [InlineData("foo")] + public void OptionsEnableMetricsAddsRule(string meterName) + { + var services = new ServiceCollection(); + services.AddOptions(); + services.Configure(options => options.EnableMetrics(meterName)); + + var container = services.BuildServiceProvider(); + var options = container.GetRequiredService>(); + var instance = options.Value; + var rule = Assert.Single(instance.Rules); + Assert.Equal(meterName, rule.MeterName); + Assert.Null(rule.InstrumentName); + Assert.Null(rule.ListenerName); + Assert.Equal(MeterScope.Local | MeterScope.Global, rule.Scopes); + Assert.True(rule.Enable); + } + + [Fact] + public void OptionsEnableMetricsAllParamsAddsRule() + { + var services = new ServiceCollection(); + services.AddOptions(); + services.Configure(options + => options.EnableMetrics("meter", "instrument", "listener", MeterScope.Global)); + + var container = services.BuildServiceProvider(); + var options = container.GetRequiredService>(); + var instance = options.Value; + var rule = Assert.Single(instance.Rules); + Assert.Equal("meter", rule.MeterName); + Assert.Equal("instrument", rule.InstrumentName); + Assert.Equal("listener", rule.ListenerName); + Assert.Equal(MeterScope.Global, rule.Scopes); + Assert.True(rule.Enable); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("*")] + [InlineData("foo")] + public void BuilderDisableMetricsAddsRule(string meterName) + { + var services = new ServiceCollection(); + services.AddOptions(); + var builder = new FakeBuilder(services); + + builder.DisableMetrics(meterName); + + var container = services.BuildServiceProvider(); + var options = container.GetRequiredService>(); + var instance = options.Value; + var rule = Assert.Single(instance.Rules); + Assert.Equal(meterName, rule.MeterName); + Assert.Null(rule.InstrumentName); + Assert.Null(rule.ListenerName); + Assert.Equal(MeterScope.Local | MeterScope.Global, rule.Scopes); + Assert.False(rule.Enable); + } + + [Fact] + public void BuilderDisableMetricsWithAllParamsAddsRule() + { + var services = new ServiceCollection(); + services.AddOptions(); + var builder = new FakeBuilder(services); + + builder.DisableMetrics("meter", "instance", "listener", MeterScope.Local); + + var container = services.BuildServiceProvider(); + var options = container.GetRequiredService>(); + var instance = options.Value; + var rule = Assert.Single(instance.Rules); + Assert.Equal("meter", rule.MeterName); + Assert.Equal("instance", rule.InstrumentName); + Assert.Equal("listener", rule.ListenerName); + Assert.Equal(MeterScope.Local, rule.Scopes); + Assert.False(rule.Enable); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("*")] + [InlineData("foo")] + public void OptionsDisableMetricsAddsRule(string meterName) + { + var services = new ServiceCollection(); + services.AddOptions(); + services.Configure(options => options.DisableMetrics(meterName)); + + var container = services.BuildServiceProvider(); + var options = container.GetRequiredService>(); + var instance = options.Value; + var rule = Assert.Single(instance.Rules); + Assert.Equal(meterName, rule.MeterName); + Assert.Null(rule.InstrumentName); + Assert.Null(rule.ListenerName); + Assert.Equal(MeterScope.Local | MeterScope.Global, rule.Scopes); + Assert.False(rule.Enable); + } + + [Fact] + public void OptionsDisableMetricsAllParamsAddsRule() + { + var services = new ServiceCollection(); + services.AddOptions(); + services.Configure(options + => options.DisableMetrics("meter", "instrument", "listener", MeterScope.Global)); + + var container = services.BuildServiceProvider(); + var options = container.GetRequiredService>(); + var instance = options.Value; + var rule = Assert.Single(instance.Rules); + Assert.Equal("meter", rule.MeterName); + Assert.Equal("instrument", rule.InstrumentName); + Assert.Equal("listener", rule.ListenerName); + Assert.Equal(MeterScope.Global, rule.Scopes); + Assert.False(rule.Enable); + } + + private class FakeBuilder(IServiceCollection services) : IMetricsBuilder + { + public IServiceCollection Services { get; } = services; + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/tests/Microsoft.Extensions.Diagnostics.Abstractions.Tests.csproj b/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/tests/Microsoft.Extensions.Diagnostics.Abstractions.Tests.csproj new file mode 100644 index 00000000000000..b3043a4481a1db --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/tests/Microsoft.Extensions.Diagnostics.Abstractions.Tests.csproj @@ -0,0 +1,13 @@ + + + + $(NetCoreAppCurrent);$(NetFrameworkMinimum) + true + + + + + + + + diff --git a/src/libraries/Microsoft.Extensions.Diagnostics/Microsoft.Extensions.Diagnostics.sln b/src/libraries/Microsoft.Extensions.Diagnostics/Microsoft.Extensions.Diagnostics.sln index d0ceba54c79984..5e4931ef73fb1d 100644 --- a/src/libraries/Microsoft.Extensions.Diagnostics/Microsoft.Extensions.Diagnostics.sln +++ b/src/libraries/Microsoft.Extensions.Diagnostics/Microsoft.Extensions.Diagnostics.sln @@ -43,6 +43,22 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{A447D0CB-601 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gen", "gen", "{66953A8A-9E31-486F-AF8E-7310F6707E4F}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Diagnostics.Abstractions", "..\Microsoft.Extensions.Diagnostics.Abstractions\ref\Microsoft.Extensions.Diagnostics.Abstractions.csproj", "{AA790584-200C-4301-8545-8B2854B2F6CC}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Diagnostics.Abstractions", "..\Microsoft.Extensions.Diagnostics.Abstractions\src\Microsoft.Extensions.Diagnostics.Abstractions.csproj", "{40525D17-4553-405E-8B21-4603B07D126A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Configuration.Abstractions", "..\Microsoft.Extensions.Configuration.Abstractions\ref\Microsoft.Extensions.Configuration.Abstractions.csproj", "{2D529E61-474E-45C6-8A7E-35AAD70B9801}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Configuration.Abstractions", "..\Microsoft.Extensions.Configuration.Abstractions\src\Microsoft.Extensions.Configuration.Abstractions.csproj", "{30E93D8B-4B53-4DC1-A5E3-FCAC1E37303D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Diagnostics.Abstractions.Tests", "..\Microsoft.Extensions.Diagnostics.Abstractions\tests\Microsoft.Extensions.Diagnostics.Abstractions.Tests.csproj", "{2DC4F9C2-7C2B-4B8C-9AB3-74F3F06D359B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Options.ConfigurationExtensions", "..\Microsoft.Extensions.Options.ConfigurationExtensions\src\Microsoft.Extensions.Options.ConfigurationExtensions.csproj", "{A2853038-B04A-4BAA-B0B4-0481457003B8}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Configuration", "..\Microsoft.Extensions.Configuration\src\Microsoft.Extensions.Configuration.csproj", "{A77E804D-4576-4962-A248-92E538ED997C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Options", "..\Microsoft.Extensions.Options\ref\Microsoft.Extensions.Options.csproj", "{DBAB1C82-A3A0-4ADC-95BC-B87557C61C42}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -113,6 +129,38 @@ Global {3D040E9F-C39B-49C6-8C87-68D427AECA8F}.Debug|Any CPU.Build.0 = Debug|Any CPU {3D040E9F-C39B-49C6-8C87-68D427AECA8F}.Release|Any CPU.ActiveCfg = Release|Any CPU {3D040E9F-C39B-49C6-8C87-68D427AECA8F}.Release|Any CPU.Build.0 = Release|Any CPU + {AA790584-200C-4301-8545-8B2854B2F6CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AA790584-200C-4301-8545-8B2854B2F6CC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AA790584-200C-4301-8545-8B2854B2F6CC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AA790584-200C-4301-8545-8B2854B2F6CC}.Release|Any CPU.Build.0 = Release|Any CPU + {40525D17-4553-405E-8B21-4603B07D126A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {40525D17-4553-405E-8B21-4603B07D126A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {40525D17-4553-405E-8B21-4603B07D126A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {40525D17-4553-405E-8B21-4603B07D126A}.Release|Any CPU.Build.0 = Release|Any CPU + {2D529E61-474E-45C6-8A7E-35AAD70B9801}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2D529E61-474E-45C6-8A7E-35AAD70B9801}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2D529E61-474E-45C6-8A7E-35AAD70B9801}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2D529E61-474E-45C6-8A7E-35AAD70B9801}.Release|Any CPU.Build.0 = Release|Any CPU + {30E93D8B-4B53-4DC1-A5E3-FCAC1E37303D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {30E93D8B-4B53-4DC1-A5E3-FCAC1E37303D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {30E93D8B-4B53-4DC1-A5E3-FCAC1E37303D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {30E93D8B-4B53-4DC1-A5E3-FCAC1E37303D}.Release|Any CPU.Build.0 = Release|Any CPU + {2DC4F9C2-7C2B-4B8C-9AB3-74F3F06D359B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2DC4F9C2-7C2B-4B8C-9AB3-74F3F06D359B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2DC4F9C2-7C2B-4B8C-9AB3-74F3F06D359B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2DC4F9C2-7C2B-4B8C-9AB3-74F3F06D359B}.Release|Any CPU.Build.0 = Release|Any CPU + {A2853038-B04A-4BAA-B0B4-0481457003B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A2853038-B04A-4BAA-B0B4-0481457003B8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A2853038-B04A-4BAA-B0B4-0481457003B8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A2853038-B04A-4BAA-B0B4-0481457003B8}.Release|Any CPU.Build.0 = Release|Any CPU + {A77E804D-4576-4962-A248-92E538ED997C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A77E804D-4576-4962-A248-92E538ED997C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A77E804D-4576-4962-A248-92E538ED997C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A77E804D-4576-4962-A248-92E538ED997C}.Release|Any CPU.Build.0 = Release|Any CPU + {DBAB1C82-A3A0-4ADC-95BC-B87557C61C42}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DBAB1C82-A3A0-4ADC-95BC-B87557C61C42}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DBAB1C82-A3A0-4ADC-95BC-B87557C61C42}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DBAB1C82-A3A0-4ADC-95BC-B87557C61C42}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -134,6 +182,14 @@ Global {0A0D7CB1-3864-478F-98FC-5AA53C6A72C2} = {66953A8A-9E31-486F-AF8E-7310F6707E4F} {3FEA305D-0B5F-46A6-8E18-587387FCBFBF} = {66953A8A-9E31-486F-AF8E-7310F6707E4F} {3D040E9F-C39B-49C6-8C87-68D427AECA8F} = {9BF048D0-411D-4C2A-8C32-3A3255501D27} + {AA790584-200C-4301-8545-8B2854B2F6CC} = {9BF048D0-411D-4C2A-8C32-3A3255501D27} + {40525D17-4553-405E-8B21-4603B07D126A} = {A447D0CB-601B-479E-A2B2-76E48F5D4D61} + {2D529E61-474E-45C6-8A7E-35AAD70B9801} = {9BF048D0-411D-4C2A-8C32-3A3255501D27} + {30E93D8B-4B53-4DC1-A5E3-FCAC1E37303D} = {A447D0CB-601B-479E-A2B2-76E48F5D4D61} + {2DC4F9C2-7C2B-4B8C-9AB3-74F3F06D359B} = {76DC9C4C-EE53-47E6-B6BF-7B135EA8CAF3} + {A2853038-B04A-4BAA-B0B4-0481457003B8} = {A447D0CB-601B-479E-A2B2-76E48F5D4D61} + {A77E804D-4576-4962-A248-92E538ED997C} = {A447D0CB-601B-479E-A2B2-76E48F5D4D61} + {DBAB1C82-A3A0-4ADC-95BC-B87557C61C42} = {9BF048D0-411D-4C2A-8C32-3A3255501D27} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7D279EE5-E38F-4125-AE82-6ADE52D72F26} diff --git a/src/libraries/Microsoft.Extensions.Diagnostics/README.md b/src/libraries/Microsoft.Extensions.Diagnostics/README.md index 776b53340b7833..b8e0a36534193c 100644 --- a/src/libraries/Microsoft.Extensions.Diagnostics/README.md +++ b/src/libraries/Microsoft.Extensions.Diagnostics/README.md @@ -5,6 +5,7 @@ Commonly Used APIS: - MetricsServiceExtensions.AddMetrics(this IServiceCollection services) - MeterFactoryExtensions.Create(this IMeterFactory, string name, string? version = null, IEnumerable> tags = null, object? scope = null) +- MetricsBuilderConfigurationExtensions.AddConfiguration(this IMetricsBuilder builder, IConfiguration configuration) ## Contribution Bar - [x] [We consider new features, new APIs, bug fixes, and performance changes](https://github.com/dotnet/runtime/tree/main/src/libraries#contribution-bar) diff --git a/src/libraries/Microsoft.Extensions.Diagnostics/ref/Microsoft.Extensions.Diagnostics.cs b/src/libraries/Microsoft.Extensions.Diagnostics/ref/Microsoft.Extensions.Diagnostics.cs index 9c1afafdd2fb53..47fe8a3cf524fa 100644 --- a/src/libraries/Microsoft.Extensions.Diagnostics/ref/Microsoft.Extensions.Diagnostics.cs +++ b/src/libraries/Microsoft.Extensions.Diagnostics/ref/Microsoft.Extensions.Diagnostics.cs @@ -6,5 +6,28 @@ namespace Microsoft.Extensions.DependencyInjection public static class MetricsServiceExtensions { public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddMetrics(this Microsoft.Extensions.DependencyInjection.IServiceCollection services) { throw null; } + public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddMetrics(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, System.Action configure) { throw null; } } -} \ No newline at end of file +} +namespace Microsoft.Extensions.Diagnostics.Metrics +{ + public static class ConsoleMetrics + { + public static string DebugListenerName => throw null!; + } + public static class MetricsBuilderConsoleExtensions + { + public static IMetricsBuilder AddDebugConsole(this IMetricsBuilder builder) => throw null!; + } + public static class MetricsBuilderConfigurationExtensions + { + public static IMetricsBuilder AddConfiguration(this IMetricsBuilder builder, Microsoft.Extensions.Configuration.IConfiguration configuration) => throw null!; + } +} +namespace Microsoft.Extensions.Diagnostics.Metrics.Configuration +{ + public interface IMetricListenerConfigurationFactory + { + Microsoft.Extensions.Configuration.IConfiguration GetConfiguration(string listenerName); + } +} diff --git a/src/libraries/Microsoft.Extensions.Diagnostics/ref/Microsoft.Extensions.Diagnostics.csproj b/src/libraries/Microsoft.Extensions.Diagnostics/ref/Microsoft.Extensions.Diagnostics.csproj index 2dbcc3c5ac7b62..1299aac25941d9 100644 --- a/src/libraries/Microsoft.Extensions.Diagnostics/ref/Microsoft.Extensions.Diagnostics.csproj +++ b/src/libraries/Microsoft.Extensions.Diagnostics/ref/Microsoft.Extensions.Diagnostics.csproj @@ -9,8 +9,9 @@ - - + + + diff --git a/src/libraries/Microsoft.Extensions.Diagnostics/src/Metrics/Configuration/IMetricListenerConfigurationFactory.cs b/src/libraries/Microsoft.Extensions.Diagnostics/src/Metrics/Configuration/IMetricListenerConfigurationFactory.cs new file mode 100644 index 00000000000000..a8ac79fd1897dd --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Diagnostics/src/Metrics/Configuration/IMetricListenerConfigurationFactory.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Configuration; + +namespace Microsoft.Extensions.Diagnostics.Metrics.Configuration +{ + /// + /// Used to retrieve the metrics configuration for any listener name. + /// + public interface IMetricListenerConfigurationFactory + { + /// + /// Gets the configuration for the given listener. + /// + /// The name of listener. + /// The configuration for this listener type. + IConfiguration GetConfiguration(string listenerName); + } +} diff --git a/src/libraries/Microsoft.Extensions.Diagnostics/src/Metrics/Configuration/MetricListenerConfigurationFactory.cs b/src/libraries/Microsoft.Extensions.Diagnostics/src/Metrics/Configuration/MetricListenerConfigurationFactory.cs new file mode 100644 index 00000000000000..4c5ee6cefff6fe --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Diagnostics/src/Metrics/Configuration/MetricListenerConfigurationFactory.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Configuration; + +namespace Microsoft.Extensions.Diagnostics.Metrics.Configuration +{ + internal sealed class MetricListenerConfigurationFactory : IMetricListenerConfigurationFactory + { + private readonly IEnumerable _configurations; + + public MetricListenerConfigurationFactory(IEnumerable configurations) + { + _configurations = configurations ?? throw new ArgumentNullException(nameof(configurations)); + } + + public IConfiguration GetConfiguration(string listenerName) + { + ThrowHelper.ThrowIfNull(listenerName); + + var configurationBuilder = new ConfigurationBuilder(); + foreach (MetricsConfiguration configuration in _configurations) + { + var section = configuration.Configuration.GetSection(listenerName); + configurationBuilder.AddConfiguration(section); + } + return configurationBuilder.Build(); + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Diagnostics/src/Metrics/Configuration/MetricsBuilderConfigurationExtensions.cs b/src/libraries/Microsoft.Extensions.Diagnostics/src/Metrics/Configuration/MetricsBuilderConfigurationExtensions.cs new file mode 100644 index 00000000000000..9061c2461fb819 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Diagnostics/src/Metrics/Configuration/MetricsBuilderConfigurationExtensions.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.Metrics; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.Metrics.Configuration; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.Diagnostics.Metrics +{ + /// + /// Extensions for for enabling metrics based on . + /// + public static class MetricsBuilderConfigurationExtensions + { + /// + /// Reads metrics configuration from the provided section and configures + /// which 's, 's, and 's are enabled. + /// + /// The . + /// The section to load. + /// The original for chaining. + public static IMetricsBuilder AddConfiguration(this IMetricsBuilder builder, IConfiguration configuration) + { + if (builder is null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (configuration is null) + { + throw new ArgumentNullException(nameof(configuration)); + } + + builder.Services.AddSingleton>(new MetricsConfigureOptions(configuration)); + builder.Services.AddSingleton>(new ConfigurationChangeTokenSource(configuration)); + builder.Services.AddSingleton(new MetricsConfiguration(configuration)); + return builder; + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Diagnostics/src/Metrics/Configuration/MetricsConfiguration.cs b/src/libraries/Microsoft.Extensions.Diagnostics/src/Metrics/Configuration/MetricsConfiguration.cs new file mode 100644 index 00000000000000..70f9d3348ffe87 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Diagnostics/src/Metrics/Configuration/MetricsConfiguration.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Configuration; + +namespace Microsoft.Extensions.Diagnostics.Metrics.Configuration +{ + internal sealed class MetricsConfiguration + { + public MetricsConfiguration(IConfiguration configuration) + { + Configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); + } + + public IConfiguration Configuration { get; } + } +} diff --git a/src/libraries/Microsoft.Extensions.Diagnostics/src/Metrics/Configuration/MetricsConfigureOptions.cs b/src/libraries/Microsoft.Extensions.Diagnostics/src/Metrics/Configuration/MetricsConfigureOptions.cs new file mode 100644 index 00000000000000..9af86bcdde6458 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Diagnostics/src/Metrics/Configuration/MetricsConfigureOptions.cs @@ -0,0 +1,113 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Linq; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.Diagnostics.Metrics.Configuration +{ + internal sealed class MetricsConfigureOptions : IConfigureOptions + { + private const string EnabledMetricsKey = "EnabledMetrics"; + private const string EnabledGlobalMetricsKey = "EnabledGlobalMetrics"; + private const string EnabledLocalMetricsKey = "EnabledLocalMetrics"; + private const string DefaultKey = "Default"; + private readonly IConfiguration _configuration; + + public MetricsConfigureOptions(IConfiguration configuration) + { + _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); + } + + public void Configure(MetricsOptions options) => LoadConfig(options); + + private void LoadConfig(MetricsOptions options) + { + foreach (var configurationSection in _configuration.GetChildren()) + { + if (configurationSection.Key.Equals(EnabledMetricsKey, StringComparison.OrdinalIgnoreCase)) + { + // Load listener defaults + LoadMeterRules(options, configurationSection, MeterScope.Global | MeterScope.Local, null); + } + else if (configurationSection.Key.Equals(EnabledGlobalMetricsKey, StringComparison.OrdinalIgnoreCase)) + { + // Load global listener defaults + LoadMeterRules(options, configurationSection, MeterScope.Global, null); + } + else if (configurationSection.Key.Equals(EnabledLocalMetricsKey, StringComparison.OrdinalIgnoreCase)) + { + // Load local listener defaults + LoadMeterRules(options, configurationSection, MeterScope.Local, null); + } + else + { + // Load listener specific rules + var listenerName = configurationSection.Key; + var enabledMetricsSection = configurationSection.GetSection(EnabledMetricsKey); + if (enabledMetricsSection != null) + { + LoadMeterRules(options, enabledMetricsSection, MeterScope.Global | MeterScope.Local, listenerName); + } + var enabledGlobalMetricsSection = configurationSection.GetSection(EnabledGlobalMetricsKey); + if (enabledGlobalMetricsSection != null) + { + LoadMeterRules(options, enabledGlobalMetricsSection, MeterScope.Global, listenerName); + } + var enabledLocalMetricsSection = configurationSection.GetSection(EnabledLocalMetricsKey); + if (enabledLocalMetricsSection != null) + { + LoadMeterRules(options, enabledLocalMetricsSection, MeterScope.Local, listenerName); + } + } + } + } + + // Internal for testing + internal static void LoadMeterRules(MetricsOptions options, IConfigurationSection configurationSection, MeterScope scopes, string? listenerName) + { + foreach (var meterSection in configurationSection.GetChildren()) + { + // Is the meter a simple on/off bool or is it an object listing individual instruments? + if (meterSection.GetChildren().Any()) + { + // It's an object, load individual instruments + LoadInstrumentRules(options, meterSection, scopes, listenerName); + } + // Otherwise, it's a simple bool + else if (bool.TryParse(meterSection.Value, out var meterEnabled)) + { + var meterName = meterSection.Key; + if (string.Equals(DefaultKey, meterName, StringComparison.OrdinalIgnoreCase)) + { + // "Default" is a special key that applies to all meters + meterName = null; + } + // Simple bool, enable/disable all instruments for this meter + options.Rules.Add(new InstrumentRule(meterName, instrumentName: null, listenerName, scopes, meterEnabled)); + } + } + } + + // Internal for testing + internal static void LoadInstrumentRules(MetricsOptions options, IConfigurationSection meterSection, MeterScope scopes, string? listenerName) + { + foreach (var instrumentPair in meterSection.AsEnumerable(makePathsRelative: true)) + { + if (bool.TryParse(instrumentPair.Value, out var instrumentEnabled)) + { + var instrumentName = instrumentPair.Key; + if (string.Equals(DefaultKey, instrumentName, StringComparison.OrdinalIgnoreCase)) + { + // "Default" is a special key that applies to all instruments + instrumentName = null; + } + // Simple bool, enable/disable all instruments for this meter + options.Rules.Add(new InstrumentRule(meterSection.Key, instrumentName, listenerName, scopes, instrumentEnabled)); + } + } + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Diagnostics/src/Metrics/ConsoleMetrics.cs b/src/libraries/Microsoft.Extensions.Diagnostics/src/Metrics/ConsoleMetrics.cs new file mode 100644 index 00000000000000..4a2136677b21b6 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Diagnostics/src/Metrics/ConsoleMetrics.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Diagnostics.Metrics +{ + /// + /// Constants for the Console metrics listener. + /// + public static class ConsoleMetrics + { + /// + /// The name of the listener used in configuration and enabling instruments. + /// + public static string DebugListenerName => "DebugConsole"; + } +} diff --git a/src/libraries/Microsoft.Extensions.Diagnostics/src/Metrics/DebugConsoleMetricListener.cs b/src/libraries/Microsoft.Extensions.Diagnostics/src/Metrics/DebugConsoleMetricListener.cs new file mode 100644 index 00000000000000..293023e13e7eb2 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Diagnostics/src/Metrics/DebugConsoleMetricListener.cs @@ -0,0 +1,83 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.Metrics; +using System.IO; +using System.Threading; + +namespace Microsoft.Extensions.Diagnostics.Metrics +{ + internal sealed class DebugConsoleMetricListener : IMetricsListener, IDisposable + { + private readonly Timer _timer; + private int _timerStarted; + private IObservableInstrumentsSource? _source; +#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value + internal TextWriter? _textWriter; // For testing +#pragma warning restore CS0649 // Field is never assigned to, and will always have its default value + + public DebugConsoleMetricListener() + { + _timer = new Timer(OnTimer, null, Timeout.Infinite, Timeout.Infinite); + } + + public string Name => ConsoleMetrics.DebugListenerName; + + public bool InstrumentPublished(Instrument instrument, out object? userState) + { + // Start the timer if this is the first observable instrument. + if (instrument.IsObservable && Interlocked.Exchange(ref _timerStarted, 1) == 0) + { + _timer.Change(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1)); + } + + WriteLine($"{instrument.Meter.Name}-{instrument.Name} Started; Description: {instrument.Description}."); + userState = this; + return true; + } + + public void MeasurementsCompleted(Instrument instrument, object? userState) + { + Debug.Assert(userState == this); + WriteLine($"{instrument.Meter.Name}-{instrument.Name} Stopped."); + } + + public void Initialize(IObservableInstrumentsSource source) => _source = source; + + public MeasurementHandlers GetMeasurementHandlers() => new MeasurementHandlers + { + ByteHandler = MeasurementHandler, + ShortHandler = MeasurementHandler, + IntHandler = MeasurementHandler, + LongHandler = MeasurementHandler, + FloatHandler = MeasurementHandler, + DoubleHandler = MeasurementHandler, + DecimalHandler = MeasurementHandler, + }; + + private void MeasurementHandler(Instrument instrument, T measurement, ReadOnlySpan> tags, object? state) where T : struct + { + Debug.Assert(state == this); + WriteLine($"{instrument.Meter.Name}-{instrument.Name} {measurement} {instrument.Unit}"); + } + + private void WriteLine(string output) + { + var writer = _textWriter ?? Console.Out; + lock (writer) + { + writer.WriteLine(output); + } + } + + private void OnTimer(object? _) + { + _source?.RecordObservableInstruments(); + } + + public void Dispose() => _timer.Dispose(); + } +} diff --git a/src/libraries/Microsoft.Extensions.Diagnostics/src/Metrics/ListenerSubscription.cs b/src/libraries/Microsoft.Extensions.Diagnostics/src/Metrics/ListenerSubscription.cs new file mode 100644 index 00000000000000..24c6d0b7b7f297 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Diagnostics/src/Metrics/ListenerSubscription.cs @@ -0,0 +1,289 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.Metrics; + +namespace Microsoft.Extensions.Diagnostics.Metrics +{ + internal sealed class ListenerSubscription : IObservableInstrumentsSource, IDisposable + { + private readonly MeterListener _meterListener; + private readonly IMetricsListener _metricsListener; + private readonly IMeterFactory _meterFactory; + private readonly Dictionary _instruments = new(); + private IList _rules = Array.Empty(); + private bool _disposed; + + internal ListenerSubscription(IMetricsListener metricsListener, IMeterFactory meterFactory) + { + _metricsListener = metricsListener; + _meterFactory = meterFactory; + _meterListener = new MeterListener(); + } + + public void Initialize() + { + _meterListener.InstrumentPublished = InstrumentPublished; + _meterListener.MeasurementsCompleted = MeasurementsCompleted; + var handlers = _metricsListener.GetMeasurementHandlers(); + _meterListener.SetMeasurementEventCallback(handlers.ByteHandler); + _meterListener.SetMeasurementEventCallback(handlers.ShortHandler); + _meterListener.SetMeasurementEventCallback(handlers.IntHandler); + _meterListener.SetMeasurementEventCallback(handlers.LongHandler); + _meterListener.SetMeasurementEventCallback(handlers.FloatHandler); + _meterListener.SetMeasurementEventCallback(handlers.DoubleHandler); + _meterListener.SetMeasurementEventCallback(handlers.DecimalHandler); + _metricsListener.Initialize(this); + _meterListener.Start(); + } + + private void InstrumentPublished(Instrument instrument, MeterListener _) + { + lock (_instruments) + { + if (_disposed) + { + return; + } + + if (_instruments.ContainsKey(instrument)) + { + Debug.Assert(false, "InstrumentPublished called for an instrument we're already listening to."); + return; + } + + RefreshInstrument(instrument); + } + } + + // Called when we call DisableMeasurementEvents, like when a rule is disabled, + // or when the meter/factory is disposed. + private void MeasurementsCompleted(Instrument instrument, object? state) + { + lock (_instruments) + { + if (_disposed) + { + return; + } + + if (_instruments.TryGetValue(instrument, out var listenerState)) + { + _instruments.Remove(instrument); + _metricsListener.MeasurementsCompleted(instrument, listenerState); + _meterListener.DisableMeasurementEvents(instrument); + } + } + } + + internal void UpdateRules(IList rules) + { + lock (_instruments) + { + if (_disposed) + { + return; + } + + _rules = rules; + + // Get a fresh list of instruments to compare against the new rules. + using var tempListener = new MeterListener(); + tempListener.InstrumentPublished = (instrument, _) => RefreshInstrument(instrument); + tempListener.Start(); + } + } + + // Called under _instrument lock + private void RefreshInstrument(Instrument instrument) + { + var alreadyEnabled = _instruments.TryGetValue(instrument, out var state); + var enable = false; + var rule = GetMostSpecificRule(instrument); + if (rule != null) + { + enable = rule.Enable; + } + + if (!enable && alreadyEnabled) + { + _instruments.Remove(instrument); + _metricsListener.MeasurementsCompleted(instrument, state); + _meterListener.DisableMeasurementEvents(instrument); + } + else if (enable && !alreadyEnabled) + { + // The listener gets a chance to decline the instrument. + if (_metricsListener.InstrumentPublished(instrument, out state)) + { + _instruments.Add(instrument, state); + _meterListener.EnableMeasurementEvents(instrument, state); + } + } + } + + private InstrumentRule? GetMostSpecificRule(Instrument instrument) + { + InstrumentRule? best = null; + foreach (var rule in _rules) + { + if (RuleMatches(rule, instrument, _metricsListener.Name, _meterFactory) + && IsMoreSpecific(rule, best, isLocalScope: instrument.Meter.Scope == _meterFactory)) + { + best = rule; + } + } + + return best; + } + + // internal for testing + internal static bool RuleMatches(InstrumentRule rule, Instrument instrument, string listenerName, IMeterFactory meterFactory) + { + // Exact match or empty + if (!string.IsNullOrEmpty(rule.ListenerName) + && !string.Equals(rule.ListenerName, listenerName, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + // Exact match or empty + if (!string.IsNullOrEmpty(rule.InstrumentName) + && !string.Equals(rule.InstrumentName, instrument.Name, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + if (!(rule.Scopes.HasFlag(MeterScope.Global) && instrument.Meter.Scope == null) + && !(rule.Scopes.HasFlag(MeterScope.Local) && instrument.Meter.Scope == meterFactory)) + { + return false; + } + + // Meter + + var ruleMeterName = rule.MeterName.AsSpan(); + // Don't allow "*" anywhere except at the end. + var starIndex = ruleMeterName.IndexOf('*'); + if (starIndex != -1 && starIndex != ruleMeterName.Length - 1) + { + return false; + } + // Rule "System.Net.*" matches meter "System.Net" and "System.Net.Http" + if (ruleMeterName.EndsWith(".*".AsSpan(), StringComparison.Ordinal)) + { + ruleMeterName = ruleMeterName.Slice(0, ruleMeterName.Length - 2); + } + // System.Net* matches System.Net and System.Net.Http + else if (starIndex != -1) + { + ruleMeterName = ruleMeterName.Slice(0, ruleMeterName.Length - 1); + } + + // Rule "" matches everything + if (ruleMeterName.IsEmpty) + { + return true; + } + + // "System.Net" matches "System.Net" and "System.Net.Http" + return instrument.Meter.Name.AsSpan().StartsWith(ruleMeterName, StringComparison.OrdinalIgnoreCase) + // Exact match +/- ".*" + && (ruleMeterName.Length == instrument.Meter.Name.Length + // Only allow StartsWith on segment boundaries + || instrument.Meter.Name[ruleMeterName.Length] == '.'); + } + + // Everything must already match the Instrument and listener, or be blank. + // internal for testing + internal static bool IsMoreSpecific(InstrumentRule rule, InstrumentRule? best, bool isLocalScope) + { + if (best == null) + { + return true; + } + + // Listener name + if (!string.IsNullOrEmpty(rule.ListenerName) && string.IsNullOrEmpty(best.ListenerName)) + { + return true; + } + else if (string.IsNullOrEmpty(rule.ListenerName) && !string.IsNullOrEmpty(best.ListenerName)) + { + return false; + } + + // Meter name + if (!string.IsNullOrEmpty(rule.MeterName)) + { + if (string.IsNullOrEmpty(best.MeterName)) + { + return true; + } + + // Longer is more specific. + if (rule.MeterName.Length != best.MeterName.Length) + { + return rule.MeterName.Length > best.MeterName.Length; + } + } + else if (!string.IsNullOrEmpty(best.MeterName)) + { + return false; + } + + // Instrument name + if (!string.IsNullOrEmpty(rule.InstrumentName) && string.IsNullOrEmpty(best.InstrumentName)) + { + return true; + } + else if (string.IsNullOrEmpty(rule.InstrumentName) && !string.IsNullOrEmpty(best.InstrumentName)) + { + return false; + } + + // Scope + + // Already matched as local + if (isLocalScope) + { + // Local is more specific than Local+Global + if (!rule.Scopes.HasFlag(MeterScope.Global) && best.Scopes.HasFlag(MeterScope.Global)) + { + return true; + } + else if (rule.Scopes.HasFlag(MeterScope.Global) && !best.Scopes.HasFlag(MeterScope.Global)) + { + return false; + } + } + // Already matched as global + else + { + // Global is more specific than Local+Global + if (!rule.Scopes.HasFlag(MeterScope.Local) && best.Scopes.HasFlag(MeterScope.Local)) + { + return true; + } + else if (rule.Scopes.HasFlag(MeterScope.Local) && !best.Scopes.HasFlag(MeterScope.Local)) + { + return false; + } + } + + // All things being equal, take the last one. + return true; + } + + public void RecordObservableInstruments() => _meterListener.RecordObservableInstruments(); + + public void Dispose() + { + _disposed = true; + _meterListener.Dispose(); + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Diagnostics/src/Metrics/MetricsBuilderConsoleExtensions.cs b/src/libraries/Microsoft.Extensions.Diagnostics/src/Metrics/MetricsBuilderConsoleExtensions.cs new file mode 100644 index 00000000000000..b5613cc46e1a73 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Diagnostics/src/Metrics/MetricsBuilderConsoleExtensions.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Diagnostics.Metrics +{ + /// + /// IMetricsBuilder extension methods for console output. + /// + public static class MetricsBuilderConsoleExtensions + { + /// + /// Enables console output for metrics for debugging purposes. This is not recommended for production use. + /// + /// The metrics builder. + /// The original metrics builder for chaining. + public static IMetricsBuilder AddDebugConsole(this IMetricsBuilder builder) => builder.AddListener(); + } +} diff --git a/src/libraries/Microsoft.Extensions.Diagnostics/src/Metrics/MetricsServiceExtensions.cs b/src/libraries/Microsoft.Extensions.Diagnostics/src/Metrics/MetricsServiceExtensions.cs index 6bf2228e72345a..79a08c78b82e7c 100644 --- a/src/libraries/Microsoft.Extensions.Diagnostics/src/Metrics/MetricsServiceExtensions.cs +++ b/src/libraries/Microsoft.Extensions.Diagnostics/src/Metrics/MetricsServiceExtensions.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Diagnostics.Metrics; +using Microsoft.Extensions.Diagnostics.Metrics.Configuration; using System; using System.Diagnostics.Metrics; @@ -25,9 +26,45 @@ public static IServiceCollection AddMetrics(this IServiceCollection services) throw new ArgumentNullException(nameof(services)); } + services.AddOptions(); + services.TryAddSingleton(); + services.TryAddSingleton(); + // Make sure the subscription manager is started when the host starts. + // The host will trigger options validation. + services.AddOptions().Configure((_, manager) => manager.Initialize()).ValidateOnStart(); + + services.TryAddSingleton(); + + return services; + } + + /// + /// Adds metrics services to the specified . + /// + /// The to add services to. + /// A callback to configure the . + /// The so that additional calls can be chained. + public static IServiceCollection AddMetrics(this IServiceCollection services, Action configure) + { + if (configure is null) + { + throw new ArgumentNullException(nameof(configure)); + } + + services.AddMetrics(); + + var builder = new MetricsBuilder(services); + configure(builder); return services; } + + private sealed class MetricsBuilder(IServiceCollection services) : IMetricsBuilder + { + public IServiceCollection Services { get; } = services; + } + + private sealed class NoOpOptions { } } } diff --git a/src/libraries/Microsoft.Extensions.Diagnostics/src/Metrics/MetricsSubscriptionManager.cs b/src/libraries/Microsoft.Extensions.Diagnostics/src/Metrics/MetricsSubscriptionManager.cs new file mode 100644 index 00000000000000..b8d3aa31d89c03 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Diagnostics/src/Metrics/MetricsSubscriptionManager.cs @@ -0,0 +1,67 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics.Metrics; +using System.Linq; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.Diagnostics.Metrics +{ + internal sealed class MetricsSubscriptionManager + { + private readonly ListenerSubscription[] _listeners; + private readonly IDisposable? _changeTokenRegistration; + private bool _disposed; + + public MetricsSubscriptionManager(IEnumerable listeners, IOptionsMonitor options, IMeterFactory meterFactory) + { + var list = listeners.ToList(); + _listeners = new ListenerSubscription[list.Count]; + for (int i = 0; i < _listeners.Length; i++) + { + _listeners[i] = new ListenerSubscription(list[i], meterFactory); + } + _changeTokenRegistration = options.OnChange(UpdateRules); + UpdateRules(options.CurrentValue); + } + + public void Initialize() + { + foreach (var listener in _listeners) + { + listener.Initialize(); + } + } + + private void UpdateRules(MetricsOptions options) + { + if (_disposed) + { + return; + } + var rules = options.Rules; + + foreach (var listener in _listeners) + { + listener.UpdateRules(rules); + } + } + + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + _changeTokenRegistration?.Dispose(); + foreach (var listener in _listeners) + { + listener.Dispose(); + } + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Diagnostics/src/Microsoft.Extensions.Diagnostics.csproj b/src/libraries/Microsoft.Extensions.Diagnostics/src/Microsoft.Extensions.Diagnostics.csproj index 7a879044946db4..443137251e6412 100644 --- a/src/libraries/Microsoft.Extensions.Diagnostics/src/Microsoft.Extensions.Diagnostics.csproj +++ b/src/libraries/Microsoft.Extensions.Diagnostics/src/Microsoft.Extensions.Diagnostics.csproj @@ -13,11 +13,13 @@ + - - + + + diff --git a/src/libraries/Microsoft.Extensions.Diagnostics/tests/DebugConsoleMetricListenerTests.cs b/src/libraries/Microsoft.Extensions.Diagnostics/tests/DebugConsoleMetricListenerTests.cs new file mode 100644 index 00000000000000..3758b7cef276da --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Diagnostics/tests/DebugConsoleMetricListenerTests.cs @@ -0,0 +1,96 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.Metrics; +using System.IO; +using Microsoft.DotNet.RemoteExecutor; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.Metrics.Tests +{ + public class DebugConsoleMetricListenerTests + { + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public void ListenerCanResolveLocalMetrics() + { + RemoteExecutor.Invoke(() => + { + var services = new ServiceCollection(); + services.AddMetrics(builder => + { + builder.AddListener(); + builder.EnableMetrics("TestMeter", scopes: MeterScope.Local, listenerName: ConsoleMetrics.DebugListenerName); + }); + using var sp = services.BuildServiceProvider(); + // Make sure the subscription manager is started. + sp.GetRequiredService().Validate(); + + var listener = sp.GetRequiredService(); + var consoleListener = Assert.IsType(listener); + var output = new StringWriter(); + consoleListener._textWriter = output; + + var factory = sp.GetRequiredService(); + var meter = factory.Create("TestMeter"); + var counter = meter.CreateCounter("counter", "blip", "I count blips"); + counter.Add(4); + counter.Add(1); + + // The rule doesn't match, we shouldn't get this output. + var negativeMeter = factory.Create("NegativeMeter"); + counter = negativeMeter.CreateCounter("counter", "blop", "I count blops"); + counter.Add(1); + + // Meters from the factory can't be disposed, you have to dispose the whole factory. + sp.Dispose(); + + Assert.Equal("TestMeter-counter Started; Description: I count blips." + Environment.NewLine + + "TestMeter-counter 4 blip" + Environment.NewLine + + "TestMeter-counter 1 blip" + Environment.NewLine + + "TestMeter-counter Stopped." + Environment.NewLine, output.ToString()); + }).Dispose(); + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public void ListenerCanResolveGlobalMetrics() + { + RemoteExecutor.Invoke(() => + { + ServiceCollection services = new ServiceCollection(); + services.AddMetrics(builder => + { + builder.AddListener(); + builder.EnableMetrics("TestMeter", scopes: MeterScope.Global, listenerName: ConsoleMetrics.DebugListenerName); + }); + using var sp = services.BuildServiceProvider(); + // Make sure the subscription manager is started. + sp.GetRequiredService().Validate(); + + var listener = sp.GetRequiredService(); + var consoleListener = Assert.IsType(listener); + var output = new StringWriter(); + consoleListener._textWriter = output; + + var meter = new Meter("TestMeter"); + var counter = meter.CreateCounter("counter", "blip", "I count blips"); + counter.Add(4); + counter.Add(1); + + // The rule doesn't match, we shouldn't get this output. + var negativeMeter = new Meter("NegativeMeter"); + counter = negativeMeter.CreateCounter("counter", "blop", "I count blops"); + counter.Add(1); + + meter.Dispose(); + + Assert.Equal("TestMeter-counter Started; Description: I count blips." + Environment.NewLine + + "TestMeter-counter 4 blip" + Environment.NewLine + + "TestMeter-counter 1 blip" + Environment.NewLine + + "TestMeter-counter Stopped." + Environment.NewLine, output.ToString()); + }).Dispose(); + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Diagnostics/tests/MetricsTests.cs b/src/libraries/Microsoft.Extensions.Diagnostics/tests/DefaultMetricsFactoryTests.cs similarity index 97% rename from src/libraries/Microsoft.Extensions.Diagnostics/tests/MetricsTests.cs rename to src/libraries/Microsoft.Extensions.Diagnostics/tests/DefaultMetricsFactoryTests.cs index 8594e1c7692ca1..f73b197136bd7e 100644 --- a/src/libraries/Microsoft.Extensions.Diagnostics/tests/MetricsTests.cs +++ b/src/libraries/Microsoft.Extensions.Diagnostics/tests/DefaultMetricsFactoryTests.cs @@ -1,22 +1,18 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.DotNet.RemoteExecutor; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Diagnostics.Metrics; using System; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.Metrics; using System.Linq; -using System.Threading; -using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Xunit; namespace Microsoft.Extensions.Diagnostics.Metrics.Tests { - public class MetricsTests + public class DefaultMetricsFactoryTests { [Fact] public void FactoryDITest() diff --git a/src/libraries/Microsoft.Extensions.Diagnostics/tests/ListenerSubscriptionTests.cs b/src/libraries/Microsoft.Extensions.Diagnostics/tests/ListenerSubscriptionTests.cs new file mode 100644 index 00000000000000..da59cffc28d6a6 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Diagnostics/tests/ListenerSubscriptionTests.cs @@ -0,0 +1,390 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics.Metrics; +using System.Threading.Tasks; +using Microsoft.DotNet.RemoteExecutor; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.Metrics; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.Tests +{ + public class ListenerSubscriptionTests + { + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public void SubscriptionReceivesNewGlobalMetersAndInstruments() + { + RemoteExecutor.Invoke(() => + { + var publishedTcs = new TaskCompletionSource(); + var completedTcs = new TaskCompletionSource(); + var measurementTcs = new TaskCompletionSource(); + + var fakeListener = new FakeMetricListener(); + fakeListener.OnPublished = (instrument) => + { + Assert.Null(instrument.Meter.Scope); // Global + publishedTcs.TrySetResult(instrument); + return (true, null); + }; + fakeListener.OnCompleted = (instrument, state) => + { + completedTcs.TrySetResult(instrument); + }; + fakeListener.OnMeasurement = (instrument, measurement, tags, state) => + { + measurementTcs.TrySetResult((int)measurement); + }; + + var subscription = new ListenerSubscription(fakeListener, new FakeMeterFactory()); + Assert.Null(fakeListener.Source); + + subscription.Initialize(); + Assert.Same(subscription, fakeListener.Source); + + // No rules yet, so we shouldn't get any notifications. + var meter = new Meter("TestMeter"); + var counter = meter.CreateCounter("counter", "blip", "I count blips"); + counter.Add(1); + + Assert.False(publishedTcs.Task.IsCompleted); + Assert.False(measurementTcs.Task.IsCompleted); + + // Add a rule that matches the counter. + subscription.UpdateRules(new[] { new InstrumentRule("TestMeter", "counter", null, MeterScope.Global, enable: true) }); + + Assert.True(publishedTcs.Task.IsCompleted); + + counter.Add(2); + Assert.True(measurementTcs.Task.IsCompleted); + Assert.Equal(2, measurementTcs.Task.Result); + + Assert.False(completedTcs.Task.IsCompleted); + + // Disable + subscription.UpdateRules(new[] { new InstrumentRule("TestMeter", "counter", null, MeterScope.Global, enable: false) }); + + Assert.True(completedTcs.Task.IsCompleted); + + }).Dispose(); + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public void SubscriptionReceivesNewLocalMetersAndInstruments() + { + RemoteExecutor.Invoke(() => + { + var publishedTcs = new TaskCompletionSource(); + var completedTcs = new TaskCompletionSource(); + var measurementTcs = new TaskCompletionSource(); + + using var services = new ServiceCollection().AddMetrics().BuildServiceProvider(); + var factory = services.GetRequiredService(); + + var fakeListener = new FakeMetricListener(); + fakeListener.OnPublished = (instrument) => + { + Assert.Equal(factory, instrument.Meter.Scope); // Local + publishedTcs.TrySetResult(instrument); + return (true, null); + }; + fakeListener.OnCompleted = (instrument, state) => + { + completedTcs.TrySetResult(instrument); + }; + fakeListener.OnMeasurement = (instrument, measurement, tags, state) => + { + measurementTcs.TrySetResult((int)measurement); + }; + + var subscription = new ListenerSubscription(fakeListener, factory); + Assert.Null(fakeListener.Source); + + subscription.Initialize(); + Assert.Same(subscription, fakeListener.Source); + + // No rules yet, so we shouldn't get any notifications. + var meter = factory.Create("TestMeter"); + var counter = meter.CreateCounter("counter", "blip", "I count blips"); + counter.Add(1); + + Assert.False(publishedTcs.Task.IsCompleted); + Assert.False(measurementTcs.Task.IsCompleted); + + // Add a rule that matches the counter. + subscription.UpdateRules(new[] { new InstrumentRule("TestMeter", "counter", null, MeterScope.Local, enable: true) }); + + Assert.True(publishedTcs.Task.IsCompleted); + + counter.Add(2); + Assert.True(measurementTcs.Task.IsCompleted); + Assert.Equal(2, measurementTcs.Task.Result); + + Assert.False(completedTcs.Task.IsCompleted); + + // Disable + subscription.UpdateRules(new[] { new InstrumentRule("TestMeter", "counter", null, MeterScope.Local, enable: false) }); + + Assert.True(completedTcs.Task.IsCompleted); + + }).Dispose(); + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public void RuleCanBeTurnedOffAndOnAgain() + { + RemoteExecutor.Invoke(() => + { + var publishCalled = 0; + var onCompletedCalled = 0; + var measurements = new List(); + + using var services = new ServiceCollection().AddMetrics().BuildServiceProvider(); + var factory = services.GetRequiredService(); + + var fakeListener = new FakeMetricListener(); + fakeListener.OnPublished = (instrument) => + { + publishCalled++; + Assert.Equal(factory, instrument.Meter.Scope); // Local + return (true, fakeListener); + }; + fakeListener.OnCompleted = (instrument, state) => + { + onCompletedCalled++; + Assert.Equal(fakeListener, state); + }; + fakeListener.OnMeasurement = (instrument, measurement, tags, state) => + { + measurements.Add((int)measurement); + Assert.Equal(fakeListener, state); + }; + + var subscription = new ListenerSubscription(fakeListener, factory); + Assert.Null(fakeListener.Source); + + subscription.Initialize(); + Assert.Same(subscription, fakeListener.Source); + + // No rules yet, so we shouldn't get any notifications. + var meter = factory.Create("TestMeter"); + var counter = meter.CreateCounter("counter", "blip", "I count blips"); + counter.Add(1); + + Assert.Equal(0, measurements.Count); + Assert.Equal(0, publishCalled); + Assert.Equal(0, onCompletedCalled); + + // Add a rule that matches the counter. + subscription.UpdateRules(new[] { new InstrumentRule("TestMeter", "counter", null, MeterScope.Local, enable: true) }); + + Assert.Equal(1, publishCalled); + + counter.Add(2); + Assert.Equal(1, measurements.Count); + Assert.Equal(2, measurements[0]); + Assert.Equal(0, onCompletedCalled); + + // Disable + subscription.UpdateRules(new[] { new InstrumentRule("TestMeter", "counter", null, MeterScope.Local, enable: false) }); + + Assert.Equal(1, onCompletedCalled); + counter.Add(3); + // Not received + Assert.Equal(1, measurements.Count); + + // Re-enable + subscription.UpdateRules(new[] { new InstrumentRule("TestMeter", "counter", null, MeterScope.Local, enable: true) }); + Assert.Equal(2, publishCalled); + + counter.Add(4); + Assert.Equal(2, measurements.Count); + Assert.Equal(4, measurements[1]); + + services.Dispose(); + Assert.Equal(2, onCompletedCalled); + }).Dispose(); + } + + [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + // [InlineData(null, null, null)] // RemoteExecutor can't handle nulls + [InlineData("", "", "")] + [InlineData("*", "", "")] + [InlineData("lonG", "", "")] + [InlineData("lonG*", "", "")] + [InlineData("lonG.*", "", "")] + [InlineData("lonG.sillY.meteR", "", "")] + [InlineData("lonG.sillY.meteR*", "", "")] + [InlineData("lonG.sillY.meteR.*", "", "")] + [InlineData("lonG.sillY.meteR.namE", "", "")] + [InlineData("lonG.sillY.meteR.namE.*", "", "")] + [InlineData("", "instrumenTnamE", "")] + [InlineData("lonG.sillY.meteR.namE", "instrumenTnamE", "")] + [InlineData("", "", "listeneRnamE")] + [InlineData("lonG.sillY.meteR.namE", "", "listeneRnamE")] + [InlineData("lonG.sillY.meteR.namE", "instrumenTnamE", "listeneRnamE")] + public void RuleMatchesTest(string meterName, string instrumentName, string listenerName) + { + RemoteExecutor.Invoke((string m, string i, string l) => { + var rule = new InstrumentRule(m, i, l, MeterScope.Global, enable: true); + var meter = new Meter("Long.Silly.Meter.Name"); + var instrument = meter.CreateCounter("InstrumentName"); + Assert.True(ListenerSubscription.RuleMatches(rule, instrument, "ListenerName", new FakeMeterFactory())); + }, meterName, instrumentName, listenerName).Dispose(); + } + + [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + [InlineData("*.*", "", "")] + [InlineData("", "*", "")] + [InlineData("", "", "*")] + [InlineData("lonG.", "", "")] + [InlineData("lonG.sil", "", "")] + [InlineData("lonG.sil*", "", "")] + [InlineData("sillY.meteR.namE", "", "")] + [InlineData("namE", "", "")] + [InlineData("*.namE", "", "")] + [InlineData("wrongMeter", "", "")] + [InlineData("wrongMeter", "InstrumentName", "")] + [InlineData("wrongMeter", "", "ListenerName")] + [InlineData("", "wrongInstrument", "")] + [InlineData("", "", "wrongListener")] + public void RuleMatchesNegativeTest(string meterName, string instrumentName, string listenerName) + { + RemoteExecutor.Invoke((string m, string i, string l) => { + var rule = new InstrumentRule(m, i, l, MeterScope.Global, enable: true); + var meter = new Meter("Long.Silly.Meter.Name"); + var instrument = meter.CreateCounter("InstrumentName"); + Assert.False(ListenerSubscription.RuleMatches(rule, instrument, "ListenerName", new FakeMeterFactory())); + }, meterName, instrumentName, listenerName).Dispose(); + } + + [Theory] + [MemberData(nameof(IsMoreSpecificTestData))] + public void IsMoreSpecificTest(InstrumentRule rule, InstrumentRule? best, bool isLocalScope) + { + Assert.True(ListenerSubscription.IsMoreSpecific(rule, best, isLocalScope)); + + if (best != null) + { + Assert.False(ListenerSubscription.IsMoreSpecific(best, rule, isLocalScope)); + } + } + + public static IEnumerable IsMoreSpecificTestData() => new object[][] + { + // Anything is better than null + new object[] { new InstrumentRule(null, null, null, MeterScope.Global, true), null, false }, + + // Any field is better than empty + new object[] { new InstrumentRule("meterName", null, null, MeterScope.Global, true), + new InstrumentRule(null, null, null, MeterScope.Global, true), false }, + new object[] { new InstrumentRule(null, "instrumentName", null, MeterScope.Global, true), + new InstrumentRule(null, null, null, MeterScope.Global, true), false }, + new object[] { new InstrumentRule(null, null, "listenerName", MeterScope.Global, true), + new InstrumentRule(null, null, null, MeterScope.Global, true), false }, + + // Listener > Meter > Instrument + new object[] { new InstrumentRule(null, null, "listenerName", MeterScope.Global, true), + new InstrumentRule("meterName", null, null, MeterScope.Global, true), false }, + new object[] { new InstrumentRule(null, "instrumentName", "listenerName", MeterScope.Global, true), + new InstrumentRule("meterName", null, null, MeterScope.Global, true), false }, + new object[] { new InstrumentRule(null, null, "listenerName", MeterScope.Global, true), + new InstrumentRule(null, "instrumentName", null, MeterScope.Global, true), false }, + new object[] { new InstrumentRule("meterName", null, null, MeterScope.Global, true), + new InstrumentRule(null, "instrumentName", null, MeterScope.Global, true), false }, + + // Multiple fields are better than one. + new object[] { new InstrumentRule("meterName", "instrumentName", null, MeterScope.Global, true), + new InstrumentRule("meterName", null, null, MeterScope.Global, true), false }, + new object[] { new InstrumentRule("meterName", null, "listenerName", MeterScope.Global, true), + new InstrumentRule("meterName", null, null, MeterScope.Global, true), false }, + new object[] { new InstrumentRule("meterName", "instrumentName", "listenerName", MeterScope.Global, true), + new InstrumentRule("meterName", null, null, MeterScope.Global, true), false }, + + new object[] { new InstrumentRule("meterName", "instrumentName", null, MeterScope.Global, true), + new InstrumentRule(null, "instrumentName", null, MeterScope.Global, true), false }, + new object[] { new InstrumentRule("meterName", null, "listenerName", MeterScope.Global, true), + new InstrumentRule(null, "instrumentName", null, MeterScope.Global, true), false }, + new object[] { new InstrumentRule("meterName", "instrumentName", "listenerName", MeterScope.Global, true), + new InstrumentRule(null, "instrumentName", null, MeterScope.Global, true), false }, + + // Except Listener wins regardless + new object[] { new InstrumentRule(null, null, "listenerName", MeterScope.Global, true), + new InstrumentRule("meterName", "instrumentName", null, MeterScope.Global, true), false }, + new object[] { new InstrumentRule("meterName", null, "listenerName", MeterScope.Global, true), + new InstrumentRule(null, null, "listenerName", MeterScope.Global, true), false }, + new object[] { new InstrumentRule("meterName", "instrumentName", "listenerName", MeterScope.Global, true), + new InstrumentRule(null, null, "listenerName", MeterScope.Global, true), false }, + + // Longer Meter Name is better + new object[] { new InstrumentRule("meterName", null, null, MeterScope.Global, true), + new InstrumentRule("*", null, null, MeterScope.Global, true), false }, + new object[] { new InstrumentRule("meterName.*", null, null, MeterScope.Global, true), + new InstrumentRule("meterName", null, null, MeterScope.Global, true), false }, + new object[] { new InstrumentRule("meter.Name", null, null, MeterScope.Global, true), + new InstrumentRule("meter", null, null, MeterScope.Global, true), false }, + new object[] { new InstrumentRule("meter.Name", null, null, MeterScope.Global, true), + new InstrumentRule("meter.*", null, null, MeterScope.Global, true), false }, + + // Scopes: Local > Global+Local, Global > Global+Local + new object[] { new InstrumentRule(null, null, null, MeterScope.Local, true), + new InstrumentRule(null, null, null, MeterScope.Global | MeterScope.Local, true), true }, + new object[] { new InstrumentRule(null, null, null, MeterScope.Global, true), + new InstrumentRule(null, null, null, MeterScope.Global | MeterScope.Local, true), false }, + }; + + [Fact] + public void EqualMatchRulesTakeLastTest() + { + var emptyTrue = new InstrumentRule(null, null, null, MeterScope.Global, true); + var emptyFalse = new InstrumentRule(null, null, null, MeterScope.Global, false); + Assert.True(ListenerSubscription.IsMoreSpecific(emptyFalse, emptyTrue, isLocalScope: false)); + Assert.True(ListenerSubscription.IsMoreSpecific(emptyTrue, emptyFalse, isLocalScope: false)); + } + + public delegate void FakeMeasurementCallback(Instrument instrument, object measurement, ReadOnlySpan> tags, object? state); + private class FakeMetricListener : IMetricsListener + { + public string Name => "FakeListener"; + + public Func OnPublished { get; set; } = (_) => (true, null); + public Action OnCompleted { get; set; } = (_, _) => { }; + public IObservableInstrumentsSource? Source { get; set; } + public FakeMeasurementCallback OnMeasurement { get; set; } + + public MeasurementHandlers GetMeasurementHandlers() => new MeasurementHandlers + { + ByteHandler = CallOnMeasurement, + ShortHandler = CallOnMeasurement, + IntHandler = CallOnMeasurement, + LongHandler = CallOnMeasurement, + FloatHandler = CallOnMeasurement, + DoubleHandler = CallOnMeasurement, + DecimalHandler = CallOnMeasurement, + }; + + private void CallOnMeasurement(Instrument instrument, T measurement, ReadOnlySpan> tags, object? state) + { + OnMeasurement(instrument, measurement, tags, state); + } + + public bool InstrumentPublished(Instrument instrument, out object? userState) + { + (var published, userState) = OnPublished(instrument); + return published; + } + + public void MeasurementsCompleted(Instrument instrument, object? userState) => OnCompleted(instrument, userState); + public void Initialize(IObservableInstrumentsSource source) => Source = source; + } + + private class FakeMeterFactory : IMeterFactory + { + public Meter Create(MeterOptions options) => throw new NotImplementedException(); + public void Dispose() => throw new NotImplementedException(); + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Diagnostics/tests/MetricsConfigureOptionsTests.cs b/src/libraries/Microsoft.Extensions.Diagnostics/tests/MetricsConfigureOptionsTests.cs new file mode 100644 index 00000000000000..f4ac522987df82 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Diagnostics/tests/MetricsConfigureOptionsTests.cs @@ -0,0 +1,132 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Linq; +using Microsoft.Extensions.Configuration; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.Metrics.Configuration +{ + public class MetricsConfigureOptionsTests + { + [Fact] + public void LoadInstrumentRulesTest() + { + var options = new MetricsOptions(); + var configuration = new ConfigurationBuilder().AddInMemoryCollection().Build(); + configuration["MeterName1:InstrumentName1"] = "true"; + configuration["MeterName1:InstrumentName2"] = "false"; + configuration["MeterName1:Default"] = "true"; + + MetricsConfigureOptions.LoadInstrumentRules(options, configuration.GetSection("MeterName1"), MeterScope.Local, "ListenerName"); + + Assert.Equal(3, options.Rules.Count); + + var rule1 = options.Rules.Single(rule => rule.InstrumentName == "InstrumentName1"); + AssertRule(rule1, "MeterName1", "InstrumentName1", "ListenerName", MeterScope.Local, true); + var rule2 = options.Rules.Single(rule => rule.InstrumentName == "InstrumentName2"); + AssertRule(rule2, "MeterName1", "InstrumentName2", "ListenerName", MeterScope.Local, false); + var rule3 = options.Rules.Single(rule => rule.InstrumentName == null); + AssertRule(rule3, "MeterName1", null, "ListenerName", MeterScope.Local, true); + } + + [Fact] + public void LoadMeterRulesBoolTest() + { + var options = new MetricsOptions(); + var configuration = new ConfigurationBuilder().AddInMemoryCollection().Build(); + configuration["Section:MeterName1"] = "true"; + configuration["Section:MeterName2"] = "false"; + configuration["Section:Default"] = "true"; + MetricsConfigureOptions.LoadMeterRules(options, configuration.GetSection("Section"), MeterScope.Local, listenerName: "ListenerName"); + + Assert.Equal(3, options.Rules.Count); + + var rule1 = options.Rules.Single(rule => rule.MeterName == "MeterName1"); + AssertRule(rule1, "MeterName1", null, "ListenerName", MeterScope.Local, true); + var rule2 = options.Rules.Single(rule => rule.MeterName == "MeterName2"); + AssertRule(rule2, "MeterName2", null, "ListenerName", MeterScope.Local, false); + var rule3 = options.Rules.Single(rule => rule.MeterName == null); + AssertRule(rule3, null, null, "ListenerName", MeterScope.Local, true); + } + + [Fact] + public void LoadMeterRulesWithInstrumentsTest() + { + var options = new MetricsOptions(); + var configuration = new ConfigurationBuilder().AddInMemoryCollection().Build(); + configuration["Section:MeterName1:InstrumentName1"] = "true"; + configuration["Section:MeterName1:InstrumentName2"] = "false"; + configuration["Section:MeterName1:Default"] = "true"; + configuration["Section:MeterName2:InstrumentName1"] = "true"; + configuration["Section:MeterName2:InstrumentName2"] = "false"; + configuration["Section:MeterName2:Default"] = "true"; + configuration["Section:Default"] = "true"; + MetricsConfigureOptions.LoadMeterRules(options, configuration.GetSection("Section"), MeterScope.Local, listenerName: "ListenerName"); + + Assert.Equal(7, options.Rules.Count); + + var rule1 = options.Rules.Single(rule => rule.MeterName == "MeterName1" && rule.InstrumentName == "InstrumentName1"); + AssertRule(rule1, "MeterName1", "InstrumentName1", "ListenerName", MeterScope.Local, true); + var rule2 = options.Rules.Single(rule => rule.MeterName == "MeterName1" && rule.InstrumentName == "InstrumentName2"); + AssertRule(rule2, "MeterName1", "InstrumentName2", "ListenerName", MeterScope.Local, false); + var rule3 = options.Rules.Single(rule => rule.MeterName == "MeterName1" && rule.InstrumentName == null); + AssertRule(rule3, "MeterName1", null, "ListenerName", MeterScope.Local, true); + + var rule4 = options.Rules.Single(rule => rule.MeterName == "MeterName2" && rule.InstrumentName == "InstrumentName1"); + AssertRule(rule4, "MeterName2", "InstrumentName1", "ListenerName", MeterScope.Local, true); + var rule5 = options.Rules.Single(rule => rule.MeterName == "MeterName2" && rule.InstrumentName == "InstrumentName2"); + AssertRule(rule5, "MeterName2", "InstrumentName2", "ListenerName", MeterScope.Local, false); + var rule6 = options.Rules.Single(rule => rule.MeterName == "MeterName2" && rule.InstrumentName == null); + AssertRule(rule6, "MeterName2", null, "ListenerName", MeterScope.Local, true); + + var rule7 = options.Rules.Single(rule => rule.MeterName == null); + AssertRule(rule7, null, null, "ListenerName", MeterScope.Local, true); + } + + [Theory] + [InlineData("EnabledMetrics:Default", "true", null, null, null, MeterScope.Global | MeterScope.Local, true)] + [InlineData("EnabledMetrics:MeterName", "false", "MeterName", null, null, MeterScope.Global | MeterScope.Local, false)] + [InlineData("EnabledMetrics:MeterName:Default", "true", "MeterName", null, null, MeterScope.Global | MeterScope.Local, true)] + [InlineData("EnabledMetrics:MeterName:InstrumentName", "false", "MeterName", "InstrumentName", null, MeterScope.Global | MeterScope.Local, false)] + [InlineData("EnabledGlobalMetrics:Default", "true", null, null, null, MeterScope.Global, true)] + [InlineData("EnabledGlobalMetrics:MeterName", "false", "MeterName", null, null, MeterScope.Global, false)] + [InlineData("EnabledGlobalMetrics:MeterName:Default", "true", "MeterName", null, null, MeterScope.Global, true)] + [InlineData("EnabledGlobalMetrics:MeterName:InstrumentName", "false", "MeterName", "InstrumentName", null, MeterScope.Global, false)] + [InlineData("EnabledLocalMetrics:Default", "true", null, null, null, MeterScope.Local, true)] + [InlineData("EnabledLocalMetrics:MeterName", "false", "MeterName", null, null, MeterScope.Local, false)] + [InlineData("EnabledLocalMetrics:MeterName:Default", "true", "MeterName", null, null, MeterScope.Local, true)] + [InlineData("EnabledLocalMetrics:MeterName:InstrumentName", "false", "MeterName", "InstrumentName", null, MeterScope.Local, false)] + [InlineData("Listener:EnabledMetrics:Default", "true", null, null, "Listener", MeterScope.Global | MeterScope.Local, true)] + [InlineData("Listener:EnabledMetrics:MeterName", "false", "MeterName", null, "Listener", MeterScope.Global | MeterScope.Local, false)] + [InlineData("Listener:EnabledMetrics:MeterName:Default", "true", "MeterName", null, "Listener", MeterScope.Global | MeterScope.Local, true)] + [InlineData("Listener:EnabledMetrics:MeterName:InstrumentName", "false", "MeterName", "InstrumentName", "Listener", MeterScope.Global | MeterScope.Local, false)] + [InlineData("Listener:EnabledGlobalMetrics:Default", "true", null, null, "Listener", MeterScope.Global, true)] + [InlineData("Listener:EnabledGlobalMetrics:MeterName", "false", "MeterName", null, "Listener", MeterScope.Global, false)] + [InlineData("Listener:EnabledGlobalMetrics:MeterName:Default", "true", "MeterName", null, "Listener", MeterScope.Global, true)] + [InlineData("Listener:EnabledGlobalMetrics:MeterName:InstrumentName", "false", "MeterName", "InstrumentName", "Listener", MeterScope.Global, false)] + [InlineData("Listener:EnabledLocalMetrics:Default", "true", null, null, "Listener", MeterScope.Local, true)] + [InlineData("Listener:EnabledLocalMetrics:MeterName", "false", "MeterName", null, "Listener", MeterScope.Local, false)] + [InlineData("Listener:EnabledLocalMetrics:MeterName:Default", "true", "MeterName", null, "Listener", MeterScope.Local, true)] + [InlineData("Listener:EnabledLocalMetrics:MeterName:InstrumentName", "false", "MeterName", "InstrumentName", "Listener", MeterScope.Local, false)] + public void LoadTopLevelRulesTest(string key, string value, string? meterName, string? instrumentName, string? listenerName, MeterScope scopes, bool enabled) + { + var options = new MetricsOptions(); + var configuration = new ConfigurationBuilder().AddInMemoryCollection().Build(); + configuration[key] = value; + + new MetricsConfigureOptions(configuration).Configure(options); + var rule = Assert.Single(options.Rules); + AssertRule(rule, meterName, instrumentName, listenerName, scopes, enabled); + } + + private static void AssertRule(InstrumentRule rule, string? meterName, string? instrumentName, string? listenerName, MeterScope scopes, bool enable) + { + Assert.Equal(meterName, rule.MeterName); + Assert.Equal(instrumentName, rule.InstrumentName); + Assert.Equal(listenerName, rule.ListenerName); + Assert.Equal(scopes, rule.Scopes); + Assert.Equal(enable, rule.Enable); + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Diagnostics/tests/Microsoft.Extensions.Diagnostics.Tests.csproj b/src/libraries/Microsoft.Extensions.Diagnostics/tests/Microsoft.Extensions.Diagnostics.Tests.csproj index 31b45c09024daa..5a7d75e403232a 100644 --- a/src/libraries/Microsoft.Extensions.Diagnostics/tests/Microsoft.Extensions.Diagnostics.Tests.csproj +++ b/src/libraries/Microsoft.Extensions.Diagnostics/tests/Microsoft.Extensions.Diagnostics.Tests.csproj @@ -6,11 +6,15 @@ true + + + + + + - - diff --git a/src/libraries/Microsoft.Extensions.Hosting.Abstractions/ref/Microsoft.Extensions.Hosting.Abstractions.cs b/src/libraries/Microsoft.Extensions.Hosting.Abstractions/ref/Microsoft.Extensions.Hosting.Abstractions.cs index cc663649830ad8..dc256ff6466f30 100644 --- a/src/libraries/Microsoft.Extensions.Hosting.Abstractions/ref/Microsoft.Extensions.Hosting.Abstractions.cs +++ b/src/libraries/Microsoft.Extensions.Hosting.Abstractions/ref/Microsoft.Extensions.Hosting.Abstractions.cs @@ -102,6 +102,7 @@ public partial interface IHostApplicationBuilder Microsoft.Extensions.Configuration.IConfigurationManager Configuration { get; } Microsoft.Extensions.Hosting.IHostEnvironment Environment { get; } Microsoft.Extensions.Logging.ILoggingBuilder Logging { get; } + Microsoft.Extensions.Diagnostics.Metrics.IMetricsBuilder Metrics { get; } System.Collections.Generic.IDictionary Properties { get; } Microsoft.Extensions.DependencyInjection.IServiceCollection Services { get; } void ConfigureContainer(Microsoft.Extensions.DependencyInjection.IServiceProviderFactory factory, System.Action? configure = null) where TContainerBuilder : notnull; diff --git a/src/libraries/Microsoft.Extensions.Hosting.Abstractions/ref/Microsoft.Extensions.Hosting.Abstractions.csproj b/src/libraries/Microsoft.Extensions.Hosting.Abstractions/ref/Microsoft.Extensions.Hosting.Abstractions.csproj index e69771980113fc..96046ae4010396 100644 --- a/src/libraries/Microsoft.Extensions.Hosting.Abstractions/ref/Microsoft.Extensions.Hosting.Abstractions.csproj +++ b/src/libraries/Microsoft.Extensions.Hosting.Abstractions/ref/Microsoft.Extensions.Hosting.Abstractions.csproj @@ -16,6 +16,7 @@ + diff --git a/src/libraries/Microsoft.Extensions.Hosting.Abstractions/src/IHostApplicationBuilder.cs b/src/libraries/Microsoft.Extensions.Hosting.Abstractions/src/IHostApplicationBuilder.cs index 5ae02d03a7b59d..ddad9f58d2538f 100644 --- a/src/libraries/Microsoft.Extensions.Hosting.Abstractions/src/IHostApplicationBuilder.cs +++ b/src/libraries/Microsoft.Extensions.Hosting.Abstractions/src/IHostApplicationBuilder.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.Metrics; using Microsoft.Extensions.Logging; namespace Microsoft.Extensions.Hosting; @@ -37,6 +38,11 @@ public interface IHostApplicationBuilder /// ILoggingBuilder Logging { get; } + /// + /// Allows enabling metrics and directing their output. + /// + IMetricsBuilder Metrics { get; } + /// /// Gets a collection of services for the application to compose. This is useful for adding user provided or framework provided services. /// diff --git a/src/libraries/Microsoft.Extensions.Hosting.Abstractions/src/Microsoft.Extensions.Hosting.Abstractions.csproj b/src/libraries/Microsoft.Extensions.Hosting.Abstractions/src/Microsoft.Extensions.Hosting.Abstractions.csproj index f7881ef99062f4..c2a3841ec4902f 100644 --- a/src/libraries/Microsoft.Extensions.Hosting.Abstractions/src/Microsoft.Extensions.Hosting.Abstractions.csproj +++ b/src/libraries/Microsoft.Extensions.Hosting.Abstractions/src/Microsoft.Extensions.Hosting.Abstractions.csproj @@ -21,6 +21,7 @@ + diff --git a/src/libraries/Microsoft.Extensions.Hosting/Microsoft.Extensions.Hosting.sln b/src/libraries/Microsoft.Extensions.Hosting/Microsoft.Extensions.Hosting.sln index f07ea3b0139d2e..3b0d013e478b87 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/Microsoft.Extensions.Hosting.sln +++ b/src/libraries/Microsoft.Extensions.Hosting/Microsoft.Extensions.Hosting.sln @@ -1,4 +1,8 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.7.33711.374 +MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestUtilities", "..\Common\tests\TestUtilities\TestUtilities.csproj", "{BCAE2699-A994-48FE-B9B0-5580D267BD2E}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bcl.AsyncInterfaces", "..\Microsoft.Bcl.AsyncInterfaces\ref\Microsoft.Bcl.AsyncInterfaces.csproj", "{47ACDB6F-34CB-478D-9E43-F3662EE5838D}" @@ -165,6 +169,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{59A29BF0-B76 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gen", "gen", "{A9A8D649-4C09-4FD1-9837-EE7B9D902253}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Diagnostics", "..\Microsoft.Extensions.Diagnostics\src\Microsoft.Extensions.Diagnostics.csproj", "{5B630ECF-5C4D-4F66-9AB9-59B014520A5C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Diagnostics.Abstractions", "..\Microsoft.Extensions.Diagnostics.Abstractions\src\Microsoft.Extensions.Diagnostics.Abstractions.csproj", "{FB173557-75BD-4943-B813-0AD13E0337A3}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Diagnostics.Abstractions", "..\Microsoft.Extensions.Diagnostics.Abstractions\ref\Microsoft.Extensions.Diagnostics.Abstractions.csproj", "{982C5C02-DD1E-452C-B5BC-13207D2CB14F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Diagnostics", "..\Microsoft.Extensions.Diagnostics\ref\Microsoft.Extensions.Diagnostics.csproj", "{A9181F84-BD47-4B46-AD71-908E289A695E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -487,90 +499,110 @@ Global {0813853E-8C78-429A-B01A-3FB2EF1898F8}.Debug|Any CPU.Build.0 = Debug|Any CPU {0813853E-8C78-429A-B01A-3FB2EF1898F8}.Release|Any CPU.ActiveCfg = Release|Any CPU {0813853E-8C78-429A-B01A-3FB2EF1898F8}.Release|Any CPU.Build.0 = Release|Any CPU + {5B630ECF-5C4D-4F66-9AB9-59B014520A5C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5B630ECF-5C4D-4F66-9AB9-59B014520A5C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5B630ECF-5C4D-4F66-9AB9-59B014520A5C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5B630ECF-5C4D-4F66-9AB9-59B014520A5C}.Release|Any CPU.Build.0 = Release|Any CPU + {FB173557-75BD-4943-B813-0AD13E0337A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FB173557-75BD-4943-B813-0AD13E0337A3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FB173557-75BD-4943-B813-0AD13E0337A3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FB173557-75BD-4943-B813-0AD13E0337A3}.Release|Any CPU.Build.0 = Release|Any CPU + {982C5C02-DD1E-452C-B5BC-13207D2CB14F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {982C5C02-DD1E-452C-B5BC-13207D2CB14F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {982C5C02-DD1E-452C-B5BC-13207D2CB14F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {982C5C02-DD1E-452C-B5BC-13207D2CB14F}.Release|Any CPU.Build.0 = Release|Any CPU + {A9181F84-BD47-4B46-AD71-908E289A695E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A9181F84-BD47-4B46-AD71-908E289A695E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A9181F84-BD47-4B46-AD71-908E289A695E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A9181F84-BD47-4B46-AD71-908E289A695E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {BCAE2699-A994-48FE-B9B0-5580D267BD2E} = {06E9ED4F-D3CF-4216-A8C7-E8A0AB16E33D} - {2A882DCC-96C1-4EDF-A7F0-B526EC81533F} = {06E9ED4F-D3CF-4216-A8C7-E8A0AB16E33D} - {495208B7-31BB-4802-A769-CEE4917BDF75} = {06E9ED4F-D3CF-4216-A8C7-E8A0AB16E33D} - {33C3D8F0-297F-4471-92B0-F4E8717F10E3} = {06E9ED4F-D3CF-4216-A8C7-E8A0AB16E33D} {47ACDB6F-34CB-478D-9E43-F3662EE5838D} = {E041754F-1A93-443A-9294-87DC1C30B471} + {C24E4188-27CB-4E00-A5F0-62AE23D536EE} = {59A29BF0-B76B-41F8-A733-E2A0847AB992} {EDAC3418-1D4E-4216-9371-0A36EA1E13FE} = {E041754F-1A93-443A-9294-87DC1C30B471} + {020874FC-11A2-4FC7-8929-527462F8819A} = {59A29BF0-B76B-41F8-A733-E2A0847AB992} + {53A78CBA-7270-4F79-84EF-09F669729079} = {A9A8D649-4C09-4FD1-9837-EE7B9D902253} {670AB88D-85C9-4674-A652-C27488ED73F9} = {E041754F-1A93-443A-9294-87DC1C30B471} + {3411D565-223A-44B5-864C-E30F826001B4} = {59A29BF0-B76B-41F8-A733-E2A0847AB992} {4AB3E652-6709-4011-AC2F-C379A0415BAC} = {E041754F-1A93-443A-9294-87DC1C30B471} + {ECA6E734-3908-45B4-9DFA-FDDA49AD620D} = {59A29BF0-B76B-41F8-A733-E2A0847AB992} {3D657A5A-C7DF-4817-864F-944755DCE6DF} = {E041754F-1A93-443A-9294-87DC1C30B471} + {0C041643-1217-466B-AF2E-1E44C7B117B1} = {59A29BF0-B76B-41F8-A733-E2A0847AB992} {BD938E1D-6FC8-4D46-B103-B4D35761CEA2} = {E041754F-1A93-443A-9294-87DC1C30B471} + {6DB8DE55-5419-48EA-B4CD-2880E00409FB} = {59A29BF0-B76B-41F8-A733-E2A0847AB992} {1C07ECD0-F69E-4E35-9C68-E4063B5D97EC} = {E041754F-1A93-443A-9294-87DC1C30B471} + {0A738527-821F-4089-B64E-3C0F4714DC78} = {59A29BF0-B76B-41F8-A733-E2A0847AB992} {37A36947-2652-4AFD-BCF8-AAFD4D4D2827} = {E041754F-1A93-443A-9294-87DC1C30B471} + {EA9E6747-867B-4312-94B3-624EEB1C7A7A} = {59A29BF0-B76B-41F8-A733-E2A0847AB992} {13BC7AAA-7831-4500-9D28-A93FA4CA3C74} = {E041754F-1A93-443A-9294-87DC1C30B471} + {EB889E78-AE59-4D41-AC29-8BC4D58BCE16} = {59A29BF0-B76B-41F8-A733-E2A0847AB992} {135F551E-ACB8-4073-ABB5-A1FA558455DE} = {E041754F-1A93-443A-9294-87DC1C30B471} + {0EEA7382-25A8-4FB0-AE9A-4ECDF2FF6FB7} = {59A29BF0-B76B-41F8-A733-E2A0847AB992} {5532E155-E423-4FFD-B009-80B4281D36BA} = {E041754F-1A93-443A-9294-87DC1C30B471} + {6C6DDBF6-AAF3-4A2A-8CB1-C7A630B7C49F} = {59A29BF0-B76B-41F8-A733-E2A0847AB992} {24220AD7-03ED-427A-BFC8-114C475EAD0F} = {E041754F-1A93-443A-9294-87DC1C30B471} + {0A021166-613C-430C-8460-50F1E0DF7AD8} = {59A29BF0-B76B-41F8-A733-E2A0847AB992} {376BB9D1-6C3E-4BB1-B13A-F0750D2BE01F} = {E041754F-1A93-443A-9294-87DC1C30B471} + {1E3D564C-A79E-4E28-8E13-626EE7780831} = {59A29BF0-B76B-41F8-A733-E2A0847AB992} {E4157F2E-F11D-48C6-A146-B4D12D9211F7} = {E041754F-1A93-443A-9294-87DC1C30B471} + {9C06E60B-2D45-4284-B7C8-7AE6D8194CE0} = {59A29BF0-B76B-41F8-A733-E2A0847AB992} {2F25C0DB-E010-4802-8030-C88E2D09D3B0} = {E041754F-1A93-443A-9294-87DC1C30B471} + {973CE6DA-B55D-4E55-88D5-53BE69D44410} = {59A29BF0-B76B-41F8-A733-E2A0847AB992} {0D7771CB-B3D1-4FF4-A523-40BFD3B12A84} = {E041754F-1A93-443A-9294-87DC1C30B471} + {F5CF1FC4-8F56-49BD-BFC2-5AD42AE6302D} = {59A29BF0-B76B-41F8-A733-E2A0847AB992} + {2A882DCC-96C1-4EDF-A7F0-B526EC81533F} = {06E9ED4F-D3CF-4216-A8C7-E8A0AB16E33D} + {495208B7-31BB-4802-A769-CEE4917BDF75} = {06E9ED4F-D3CF-4216-A8C7-E8A0AB16E33D} + {33C3D8F0-297F-4471-92B0-F4E8717F10E3} = {06E9ED4F-D3CF-4216-A8C7-E8A0AB16E33D} + {1B235247-6666-4B62-95A4-AC043626FDEA} = {A9A8D649-4C09-4FD1-9837-EE7B9D902253} + {5F6EF6F2-A742-445B-9418-682188F61130} = {A9A8D649-4C09-4FD1-9837-EE7B9D902253} + {3F732A69-1E48-4EA7-A40E-9C6C9332388E} = {A9A8D649-4C09-4FD1-9837-EE7B9D902253} {42E1BF94-6FE0-4017-9702-55913BD95EDE} = {E041754F-1A93-443A-9294-87DC1C30B471} + {8845E6FF-94B2-4994-A8F4-DF30844A2168} = {59A29BF0-B76B-41F8-A733-E2A0847AB992} {15AD68F8-6C58-41A3-99B3-558C42A6E7A5} = {E041754F-1A93-443A-9294-87DC1C30B471} + {99C8FB58-8718-4E76-AEFA-3C42F2F729B1} = {59A29BF0-B76B-41F8-A733-E2A0847AB992} {28C9D427-83BA-46A6-BEF5-6994A07F2258} = {E041754F-1A93-443A-9294-87DC1C30B471} + {F3230087-28E2-4ADF-A7D1-D48C5D9CFFE9} = {59A29BF0-B76B-41F8-A733-E2A0847AB992} {098CEEE6-91D3-4A7C-A5D5-6BB329BB2B7B} = {E041754F-1A93-443A-9294-87DC1C30B471} + {B729474D-0E96-4296-B317-450EE24F6B48} = {59A29BF0-B76B-41F8-A733-E2A0847AB992} {9ADBD2EE-D390-490C-BBEA-F844FE6F371E} = {E041754F-1A93-443A-9294-87DC1C30B471} + {2D4DBF5A-3BF4-4846-89F2-6FCDB80BF295} = {59A29BF0-B76B-41F8-A733-E2A0847AB992} {465AE1C4-84DD-4864-916A-74D89DC3BBBC} = {E041754F-1A93-443A-9294-87DC1C30B471} + {D466E363-F930-4D26-AE55-0256182961DD} = {59A29BF0-B76B-41F8-A733-E2A0847AB992} {D69326FC-CD05-4690-91C0-1537A80ACBFF} = {E041754F-1A93-443A-9294-87DC1C30B471} + {518D4AE0-FBFF-493A-A2DF-8ACBA842AE19} = {59A29BF0-B76B-41F8-A733-E2A0847AB992} {014EE6B4-BE08-4E50-9EBD-0D7A0CB7A76E} = {E041754F-1A93-443A-9294-87DC1C30B471} + {0AEAD15B-CD38-4462-A36C-655ED8D0CBD1} = {59A29BF0-B76B-41F8-A733-E2A0847AB992} {DD121F9F-3548-4247-8E10-FB584FC0827C} = {E041754F-1A93-443A-9294-87DC1C30B471} + {25C5119A-AAFB-4C74-9E12-F747CA4D80E7} = {59A29BF0-B76B-41F8-A733-E2A0847AB992} {45235656-E284-4682-BE70-9A284FD73243} = {E041754F-1A93-443A-9294-87DC1C30B471} + {D975AE29-0CA2-43FC-90A0-B266DF7D4C2A} = {59A29BF0-B76B-41F8-A733-E2A0847AB992} {FA490103-0140-473C-A572-72D529249234} = {E041754F-1A93-443A-9294-87DC1C30B471} {F90D2ED9-A38D-4E0B-AB9C-651FD24F23E2} = {E041754F-1A93-443A-9294-87DC1C30B471} {28726AEF-2732-4832-8930-074ACD9DC732} = {E041754F-1A93-443A-9294-87DC1C30B471} - {34ECA788-A105-411B-AE8B-17B7CC1D703D} = {E041754F-1A93-443A-9294-87DC1C30B471} - {9DB83E87-F09F-4DE0-8127-8C9509F0649B} = {E041754F-1A93-443A-9294-87DC1C30B471} - {000F87DC-0D56-4B37-8A0E-F7BCB3C3B61A} = {E041754F-1A93-443A-9294-87DC1C30B471} - {C595A27A-FA05-4BC8-9048-402338D7AF76} = {E041754F-1A93-443A-9294-87DC1C30B471} - {1E3D79D4-51D6-46C6-BF0F-DF51A47C5952} = {E041754F-1A93-443A-9294-87DC1C30B471} - {C24E4188-27CB-4E00-A5F0-62AE23D536EE} = {59A29BF0-B76B-41F8-A733-E2A0847AB992} - {020874FC-11A2-4FC7-8929-527462F8819A} = {59A29BF0-B76B-41F8-A733-E2A0847AB992} - {3411D565-223A-44B5-864C-E30F826001B4} = {59A29BF0-B76B-41F8-A733-E2A0847AB992} - {ECA6E734-3908-45B4-9DFA-FDDA49AD620D} = {59A29BF0-B76B-41F8-A733-E2A0847AB992} - {0C041643-1217-466B-AF2E-1E44C7B117B1} = {59A29BF0-B76B-41F8-A733-E2A0847AB992} - {6DB8DE55-5419-48EA-B4CD-2880E00409FB} = {59A29BF0-B76B-41F8-A733-E2A0847AB992} - {0A738527-821F-4089-B64E-3C0F4714DC78} = {59A29BF0-B76B-41F8-A733-E2A0847AB992} - {EA9E6747-867B-4312-94B3-624EEB1C7A7A} = {59A29BF0-B76B-41F8-A733-E2A0847AB992} - {EB889E78-AE59-4D41-AC29-8BC4D58BCE16} = {59A29BF0-B76B-41F8-A733-E2A0847AB992} - {0EEA7382-25A8-4FB0-AE9A-4ECDF2FF6FB7} = {59A29BF0-B76B-41F8-A733-E2A0847AB992} - {6C6DDBF6-AAF3-4A2A-8CB1-C7A630B7C49F} = {59A29BF0-B76B-41F8-A733-E2A0847AB992} - {0A021166-613C-430C-8460-50F1E0DF7AD8} = {59A29BF0-B76B-41F8-A733-E2A0847AB992} - {1E3D564C-A79E-4E28-8E13-626EE7780831} = {59A29BF0-B76B-41F8-A733-E2A0847AB992} - {9C06E60B-2D45-4284-B7C8-7AE6D8194CE0} = {59A29BF0-B76B-41F8-A733-E2A0847AB992} - {973CE6DA-B55D-4E55-88D5-53BE69D44410} = {59A29BF0-B76B-41F8-A733-E2A0847AB992} - {F5CF1FC4-8F56-49BD-BFC2-5AD42AE6302D} = {59A29BF0-B76B-41F8-A733-E2A0847AB992} - {8845E6FF-94B2-4994-A8F4-DF30844A2168} = {59A29BF0-B76B-41F8-A733-E2A0847AB992} - {99C8FB58-8718-4E76-AEFA-3C42F2F729B1} = {59A29BF0-B76B-41F8-A733-E2A0847AB992} - {F3230087-28E2-4ADF-A7D1-D48C5D9CFFE9} = {59A29BF0-B76B-41F8-A733-E2A0847AB992} - {B729474D-0E96-4296-B317-450EE24F6B48} = {59A29BF0-B76B-41F8-A733-E2A0847AB992} - {2D4DBF5A-3BF4-4846-89F2-6FCDB80BF295} = {59A29BF0-B76B-41F8-A733-E2A0847AB992} - {D466E363-F930-4D26-AE55-0256182961DD} = {59A29BF0-B76B-41F8-A733-E2A0847AB992} - {518D4AE0-FBFF-493A-A2DF-8ACBA842AE19} = {59A29BF0-B76B-41F8-A733-E2A0847AB992} - {0AEAD15B-CD38-4462-A36C-655ED8D0CBD1} = {59A29BF0-B76B-41F8-A733-E2A0847AB992} - {25C5119A-AAFB-4C74-9E12-F747CA4D80E7} = {59A29BF0-B76B-41F8-A733-E2A0847AB992} - {D975AE29-0CA2-43FC-90A0-B266DF7D4C2A} = {59A29BF0-B76B-41F8-A733-E2A0847AB992} {0BC9E4F4-5C34-4B90-80AB-2933992D99A6} = {59A29BF0-B76B-41F8-A733-E2A0847AB992} + {34ECA788-A105-411B-AE8B-17B7CC1D703D} = {E041754F-1A93-443A-9294-87DC1C30B471} {A5DD36AF-F0AD-4616-AB91-BC63E89B2744} = {59A29BF0-B76B-41F8-A733-E2A0847AB992} {4ED9C0A9-C1EF-47ED-99F8-74B7411C971B} = {59A29BF0-B76B-41F8-A733-E2A0847AB992} - {B41AA17B-5129-41CC-8EA4-250B80BABF87} = {59A29BF0-B76B-41F8-A733-E2A0847AB992} - {0813853E-8C78-429A-B01A-3FB2EF1898F8} = {59A29BF0-B76B-41F8-A733-E2A0847AB992} - {53A78CBA-7270-4F79-84EF-09F669729079} = {A9A8D649-4C09-4FD1-9837-EE7B9D902253} - {1B235247-6666-4B62-95A4-AC043626FDEA} = {A9A8D649-4C09-4FD1-9837-EE7B9D902253} - {5F6EF6F2-A742-445B-9418-682188F61130} = {A9A8D649-4C09-4FD1-9837-EE7B9D902253} - {3F732A69-1E48-4EA7-A40E-9C6C9332388E} = {A9A8D649-4C09-4FD1-9837-EE7B9D902253} + {9DB83E87-F09F-4DE0-8127-8C9509F0649B} = {E041754F-1A93-443A-9294-87DC1C30B471} {CCF8862C-E2D7-45B0-96EF-54134178DA5A} = {A9A8D649-4C09-4FD1-9837-EE7B9D902253} {C00722C2-E56B-424F-9216-FA6A91788986} = {A9A8D649-4C09-4FD1-9837-EE7B9D902253} {90DD4D77-E3DC-456E-A27F-F13DA6194481} = {A9A8D649-4C09-4FD1-9837-EE7B9D902253} + {000F87DC-0D56-4B37-8A0E-F7BCB3C3B61A} = {E041754F-1A93-443A-9294-87DC1C30B471} + {C595A27A-FA05-4BC8-9048-402338D7AF76} = {E041754F-1A93-443A-9294-87DC1C30B471} + {B41AA17B-5129-41CC-8EA4-250B80BABF87} = {59A29BF0-B76B-41F8-A733-E2A0847AB992} {0A495D7B-44DE-4E59-8497-6CBF7C55CFA5} = {A9A8D649-4C09-4FD1-9837-EE7B9D902253} {B69B7D59-6E7D-43FC-B83F-AA481B677B06} = {A9A8D649-4C09-4FD1-9837-EE7B9D902253} {4F7612BF-0AFB-4AC5-952F-7D12D99FA357} = {A9A8D649-4C09-4FD1-9837-EE7B9D902253} + {1E3D79D4-51D6-46C6-BF0F-DF51A47C5952} = {E041754F-1A93-443A-9294-87DC1C30B471} + {0813853E-8C78-429A-B01A-3FB2EF1898F8} = {59A29BF0-B76B-41F8-A733-E2A0847AB992} + {5B630ECF-5C4D-4F66-9AB9-59B014520A5C} = {59A29BF0-B76B-41F8-A733-E2A0847AB992} + {FB173557-75BD-4943-B813-0AD13E0337A3} = {59A29BF0-B76B-41F8-A733-E2A0847AB992} + {982C5C02-DD1E-452C-B5BC-13207D2CB14F} = {E041754F-1A93-443A-9294-87DC1C30B471} + {A9181F84-BD47-4B46-AD71-908E289A695E} = {E041754F-1A93-443A-9294-87DC1C30B471} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {971198CC-ACB9-4718-9024-42A87E02E503} diff --git a/src/libraries/Microsoft.Extensions.Hosting/ref/Microsoft.Extensions.Hosting.cs b/src/libraries/Microsoft.Extensions.Hosting/ref/Microsoft.Extensions.Hosting.cs index c6415b41429ad5..b9c2a6021282de 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/ref/Microsoft.Extensions.Hosting.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/ref/Microsoft.Extensions.Hosting.cs @@ -35,6 +35,7 @@ public HostApplicationBuilder(string[]? args) { } public Microsoft.Extensions.Configuration.ConfigurationManager Configuration { get { throw null; } } public Microsoft.Extensions.Hosting.IHostEnvironment Environment { get { throw null; } } public Microsoft.Extensions.Logging.ILoggingBuilder Logging { get { throw null; } } + public Microsoft.Extensions.Diagnostics.Metrics.IMetricsBuilder Metrics { get { throw null; } } Microsoft.Extensions.Configuration.IConfigurationManager Microsoft.Extensions.Hosting.IHostApplicationBuilder.Configuration { get { throw null; } } System.Collections.Generic.IDictionary Microsoft.Extensions.Hosting.IHostApplicationBuilder.Properties { get { throw null; } } public Microsoft.Extensions.DependencyInjection.IServiceCollection Services { get { throw null; } } @@ -72,6 +73,8 @@ public static partial class HostingHostBuilderExtensions public static Microsoft.Extensions.Hosting.IHostBuilder ConfigureHostOptions(this Microsoft.Extensions.Hosting.IHostBuilder hostBuilder, System.Action configureOptions) { throw null; } public static Microsoft.Extensions.Hosting.IHostBuilder ConfigureLogging(this Microsoft.Extensions.Hosting.IHostBuilder hostBuilder, System.Action configureLogging) { throw null; } public static Microsoft.Extensions.Hosting.IHostBuilder ConfigureLogging(this Microsoft.Extensions.Hosting.IHostBuilder hostBuilder, System.Action configureLogging) { throw null; } + public static Microsoft.Extensions.Hosting.IHostBuilder ConfigureMetrics(this Microsoft.Extensions.Hosting.IHostBuilder hostBuilder, System.Action configureMetrics) => throw null!; + public static Microsoft.Extensions.Hosting.IHostBuilder ConfigureMetrics(this Microsoft.Extensions.Hosting.IHostBuilder hostBuilder, System.Action configureMetrics) => throw null!; public static Microsoft.Extensions.Hosting.IHostBuilder ConfigureServices(this Microsoft.Extensions.Hosting.IHostBuilder hostBuilder, System.Action configureDelegate) { throw null; } [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("android")] [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")] diff --git a/src/libraries/Microsoft.Extensions.Hosting/src/HostApplicationBuilder.cs b/src/libraries/Microsoft.Extensions.Hosting/src/HostApplicationBuilder.cs index 2de31175450a4a..95947c2d4b08dc 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/src/HostApplicationBuilder.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/src/HostApplicationBuilder.cs @@ -7,6 +7,7 @@ using System.IO; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.Metrics; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Hosting.Internal; using Microsoft.Extensions.Logging; @@ -22,6 +23,7 @@ public sealed class HostApplicationBuilder : IHostApplicationBuilder private readonly ServiceCollection _serviceCollection = new(); private readonly IHostEnvironment _environment; private readonly LoggingBuilder _logging; + private readonly MetricsBuilder _metrics; private Func _createServiceProvider; private Action _configureContainer = _ => { }; @@ -94,7 +96,7 @@ public HostApplicationBuilder(HostApplicationBuilderSettings? settings) Configuration.AddEnvironmentVariables(prefix: "DOTNET_"); } - Initialize(settings, out _hostBuilderContext, out _environment, out _logging); + Initialize(settings, out _hostBuilderContext, out _environment, out _logging, out _metrics); ServiceProviderOptions? serviceProviderOptions = null; if (!settings.DisableDefaults) @@ -120,7 +122,7 @@ internal HostApplicationBuilder(HostApplicationBuilderSettings? settings, bool e settings ??= new HostApplicationBuilderSettings(); Configuration = settings.Configuration ?? new ConfigurationManager(); - Initialize(settings, out _hostBuilderContext, out _environment, out _logging); + Initialize(settings, out _hostBuilderContext, out _environment, out _logging, out _metrics); _createServiceProvider = () => { @@ -131,7 +133,7 @@ internal HostApplicationBuilder(HostApplicationBuilderSettings? settings, bool e }; } - private void Initialize(HostApplicationBuilderSettings settings, out HostBuilderContext hostBuilderContext, out IHostEnvironment environment, out LoggingBuilder logging) + private void Initialize(HostApplicationBuilderSettings settings, out HostBuilderContext hostBuilderContext, out IHostEnvironment environment, out LoggingBuilder logging, out MetricsBuilder metrics) { // Command line args are added even when settings.DisableDefaults == true. If the caller didn't want settings.Args applied, // they wouldn't have set them on the settings. @@ -180,6 +182,7 @@ private void Initialize(HostApplicationBuilderSettings settings, out HostBuilder () => _appServices!); logging = new LoggingBuilder(Services); + metrics = new MetricsBuilder(Services); } IDictionary IHostApplicationBuilder.Properties => _hostBuilderContext.Properties; @@ -203,6 +206,9 @@ private void Initialize(HostApplicationBuilderSettings settings, out HostBuilder /// public ILoggingBuilder Logging => _logging; + /// + public IMetricsBuilder Metrics => _metrics; + /// public void ConfigureContainer(IServiceProviderFactory factory, Action? configure = null) where TContainerBuilder : notnull { @@ -394,5 +400,10 @@ public LoggingBuilder(IServiceCollection services) public IServiceCollection Services { get; } } + + private sealed class MetricsBuilder(IServiceCollection services) : IMetricsBuilder + { + public IServiceCollection Services { get; } = services; + } } } diff --git a/src/libraries/Microsoft.Extensions.Hosting/src/HostBuilder.cs b/src/libraries/Microsoft.Extensions.Hosting/src/HostBuilder.cs index 596c9af102f593..7a965e0efc85f5 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/src/HostBuilder.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/src/HostBuilder.cs @@ -328,6 +328,7 @@ internal static void PopulateServiceCollection( }); services.AddOptions().Configure(options => { options.Initialize(hostBuilderContext.Configuration); }); services.AddLogging(); + services.AddMetrics(); } [MemberNotNull(nameof(_appServices))] diff --git a/src/libraries/Microsoft.Extensions.Hosting/src/HostingHostBuilderExtensions.cs b/src/libraries/Microsoft.Extensions.Hosting/src/HostingHostBuilderExtensions.cs index 5b2f56abe6a665..80447a8ccae0ae 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/src/HostingHostBuilderExtensions.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/src/HostingHostBuilderExtensions.cs @@ -12,6 +12,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.Metrics; using Microsoft.Extensions.Hosting.Internal; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.EventLog; @@ -314,6 +315,11 @@ internal static void AddDefaultServices(HostBuilderContext hostingContext, IServ ActivityTrackingOptions.ParentId; }); }); + + services.AddMetrics(metrics => + { + metrics.AddConfiguration(hostingContext.Configuration.GetSection("Metrics")); + }); } internal static ServiceProviderOptions CreateDefaultServiceProviderOptions(HostBuilderContext context) @@ -391,5 +397,27 @@ public static Task RunConsoleAsync(this IHostBuilder hostBuilder, Action + /// Adds a delegate for configuring the provided . This may be called multiple times. + /// + /// The to configure. + /// The delegate that configures the . + /// The same instance of the for chaining. + public static IHostBuilder ConfigureMetrics(this IHostBuilder hostBuilder, Action configureMetrics) + { + return hostBuilder.ConfigureServices((context, collection) => collection.AddMetrics(builder => configureMetrics(builder))); + } + + /// + /// Adds a delegate for configuring the provided . This may be called multiple times. + /// + /// The to configure. + /// The delegate that configures the . + /// The same instance of the for chaining. + public static IHostBuilder ConfigureMetrics(this IHostBuilder hostBuilder, Action configureMetrics) + { + return hostBuilder.ConfigureServices((context, collection) => collection.AddMetrics(builder => configureMetrics(context, builder))); + } } } diff --git a/src/libraries/Microsoft.Extensions.Hosting/src/Microsoft.Extensions.Hosting.csproj b/src/libraries/Microsoft.Extensions.Hosting/src/Microsoft.Extensions.Hosting.csproj index 1ea45457d06272..aac48bc7861490 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/src/Microsoft.Extensions.Hosting.csproj +++ b/src/libraries/Microsoft.Extensions.Hosting/src/Microsoft.Extensions.Hosting.csproj @@ -35,6 +35,7 @@ + diff --git a/src/libraries/NetCoreAppLibrary.props b/src/libraries/NetCoreAppLibrary.props index fe63bb48a4dbdc..49fc1048b2b1cf 100644 --- a/src/libraries/NetCoreAppLibrary.props +++ b/src/libraries/NetCoreAppLibrary.props @@ -199,6 +199,7 @@ Microsoft.Extensions.DependencyInjection; Microsoft.Extensions.DependencyInjection.Abstractions; Microsoft.Extensions.Diagnostics; + Microsoft.Extensions.Diagnostics.Abstractions; Microsoft.Extensions.FileProviders.Abstractions; Microsoft.Extensions.FileProviders.Composite; Microsoft.Extensions.FileProviders.Physical;