[API Proposal]: Meter Configuration and Pipeline #85684

Description

@noahfalk

Background and motivation

In the current runtime APIs for metrics we have the ability to create and use Meters, and in-progress we have work to make it easier to consume those APIs from DI-centered workloads. However if an app developer wants to configure a set of Meters to enable and where to export that data that still requires 3rd party APIs such as OpenTelemetry, AppMetrics, or Prometheus.NET. This feature is to support a built-in option for configuring which Meters are enabled and one or more sinks to send that data to. The configuration should support being done both programmatically and via external configuration sources. For sinks I'd expect a neutral interface that any 3rd party component could use but we should look most closely at supporting the already existing 3rd party libraries.

Design is still very uncertain at this point but the initial thought is to follow the lead of the Logging APIs which have LoggingBuilder, LoggingBuilder.AddFilter(), and LoggingBuilder.AddProvider().

API Proposal

Microsoft.Extensions.Diagnostics assembly

namespaceMicrosoft.Extensions.DependencyInjection;publicstaticclassMetricsServiceCollectionExtensions{publicstaticIServiceCollectionAddMetrics(thisIServiceCollection);publicstaticIServiceCollectionAddMetrics(thisIServiceCollection,Action<IMetricsBuilder>configure);}

Microsoft.Extensions.Diagnostics.Abstractions assembly

namespaceMicrosoft.Extensions.Diagnostics.Metrics;publicinterfaceIMetricsBuilder{IServiceCollectionServices{get;}}publicstaticclassMetricsBuilderExtensions{publicstaticIMetricsBuilderAddListener(thisIMetricsBuilderbuilder,IMetricsListenerlistener);publicstaticIMetricsBuilderAddListener<T>(thisIMetricsBuilderbuilder)whereT:class,IMetricsListenerpublicstaticIMetricsBuilderClearListeners(thisIMetricsBuilderbuilder);}publicinterfaceIMetricsSource{publicvoidRecordObservableInstruments();}publicinterfaceIMetricsListener{publicstringName{get;}publicvoidSetSource(IMetricsSourcesource);publicboolInstrumentPublished(Instrumentinstrument,outobject?userState);publicvoidMeasurementsCompleted(Instrumentinstrument,object?userState);publicMeasurementCallback<T>GetMeasurementHandler<T>();}publicclassMetricsEnableOptions{publicIList<InstrumentEnableRule>Rules{get;}}publicclassInstrumentEnableRule{publicInstrumentEnableRule(string?listenerName,string?meterName,MeterScopescopes,string?instrumentName,boolenable);publicstring?ListenerName{get;}publicstring?MeterName{get;}publicMeterScopeScopes{get;}publicstring?InstrumentName{get;}publicboolEnable{get;}}[Flags]publicenumMeterScope{Global,Local}publicinterfaceIMetricsSubscriptionManager{publicvoidStart();}publicstaticMetricsBuilderEnableExtensions{publicstaticIMetricsBuilderEnableMetrics(thisIMetricsBuilderbuilder)=>thrownull!;publicstaticIMetricsBuilderEnableMetrics(thisIMetricsBuilderbuilder,string?meterName)=>thrownull!;publicstaticIMetricsBuilderEnableMetrics(thisIMetricsBuilderbuilder,string?meterName,string?instrumentName)=>thrownull!;publicstaticIMetricsBuilderEnableMetrics(thisIMetricsBuilderbuilder,string?meterName,string?instrumentName,string?listenerName)=>thrownull!;publicstaticIMetricsBuilderEnableMetrics(thisIMetricsBuilderbuilder,string?meterName,string?instrumentName,string?listenerName,MeterScopescopes)=>thrownull!;publicstaticMetricsEnableOptionsEnableMetrics(thisMetricsEnableOptionsoptions)=>thrownull!;publicstaticMetricsEnableOptionsEnableMetrics(thisMetricsEnableOptionsoptions,string?meterName)=>thrownull!;publicstaticMetricsEnableOptionsEnableMetrics(thisMetricsEnableOptionsoptions,string?meterName,string?instrumentName)=>thrownull!;publicstaticMetricsEnableOptionsEnableMetrics(thisMetricsEnableOptionsoptions,string?meterName,string?instrumentName,string?listenerName)=>thrownull!;publicstaticMetricsEnableOptionsEnableMetrics(thisMetricsEnableOptionsoptions,string?meterName,string?instrumentName,string?listenerName,MeterScopescopes)=>thrownull!;// Which overloads of this do we want?publicstaticMetricsEnableOptionsDisableMetrics(thisMetricsEnableOptionsoptions,string?meterName,string?instrumentName,string?listenerName,MeterScopescopes)=>thrownull!;}

Integration with configuration

We want users to be able to configure which metrics to capture for different listeners using appsettings.json or other
IConfiguration sources

Data format

Metrics is the top level section under which "EnabledMetrics" configures which metrics are enabled for all listeners and
a section named with a listener name adds additional rules specific to that listener. The key under "EnabledMetrics" is a
Meter name prefix and the value is true if the Meter should be enabled or false if disabled.

{
"Metrics": {
"EnabledMetrics": {
"System.Runtime": true
},
"OpenTelemetry": {
"EnabledMetrics": {
"Microsoft.AspNetCore": true
}
}
}
}

Similar to logging, "Default" acts as a zero-length meter prefix creating a default rule applying to all Meters if no
longer prefix rule matches. Logically this allows creating opt-in or opt-out policies. If no rule matches a Meter it will
not be enabled, so omitting "Default" is the same behavior as including Default=false.

{
"Metrics": {
"EnabledMetrics": {
"Default": true"NoisyLibrary": false
},
}
}

To enable individual instruments within a Meter replace the boolean with object syntax where the keys are instrument names or prefixes
and the value is true iff enabled. "Default" is also honored at this scope as the zero-length instrument name prefix. Not specifying a
"Default" is the same as "Default": false.

{
"Metrics": {
"EnabledMetrics": {
"System.Runtime": {
"gc": true,
"jit": true
}
"Microsoft.AspNet.Core": {
"Default": true,
"connection-duration": false
}
}
}
}

To make rules apply only to Meters at a specific Global or Local scope instead of "EnabledMetrics" you can use "EnabledGlobalMetrics" or
"EnabledLocalMetrics". Rules in a "EnabledMetrics" section apply to Meters in both scopes. The expectation is that using this local/global config is rare and maybe we should just remove it?

{
"Metrics": {
"EnabledGlobalMetrics": {
"System.Runtime": true
}
"EnabledLocalMetrics": {
"System.Net.Http": true
}
"EnabledMetrics": {
"Meter1": true,
"Meter2": true
}
}
}

API

Microsoft.Extensions.Diagnostics.Configuration.dll:

namespaceMicrosoft.Extensions.Diagnostics.Metrics{publicstaticclassMetricsBuilderExtensions{publicstaticIMetricsBuilderAddConfiguration(thisIMetricsBuilderbuilder,IConfigurationconfiguration);}}namespaceMicrosoft.Extensions.Diagnostics.Metrics.Configuration{publicinterfaceIMetricListenerConfigurationFactory{IConfigurationGetConfiguration(TypelistenerType);}publicinterfaceIMetricListenerConfiguration<T>{IConfigurationConfiguration{get;}}}

Integration with Hosting APIs

We should include a Metrics property in parallel where existing hosting APIs expose Logging

publicclassHostApplicationBuilder//pre-existing{publicIMetricsBuilderMetrics{get;}}publicclassWebApplicationBuilder//pre-existing{publicIMetricsBuilderMetrics{get;}}publicstaticclassHostingHostBuilderExtensions//pre-existing{publicstaticIHostBuilderConfigureMetrics(thisIHostBuilderhostBuilder,Action<IMetricsBuilder>configureMetrics);publicstaticIHostBuilderConfigureMetrics(thisIHostBuilderhostBuilder,Action<HostBuilderContext,IMetricsBuilder>configureMetrics);}

Default sinks

We should include a simple console output of the metric measurements intended for debugging purposes.
This doesn't have any of the customization that the ILogger console does because it isn't intended to be left
enabled in production code. Recording raw measurements to text is very inefficient.

// All in Microsoft.Extensions.Diagnostics.dllnamespaceMicrosoft.Extensions.Diagnostics.MetricspublicstaticclassMetricsBuilderConsoleExtensions{publicstaticIMetricsBuilderAddDebugConsole(thisIMetricsBuilderbuilder);}publicstaticclassConsoleMetrics{publicstaticstringListenerName=>"Console";}

API Usage

App dev sends the default metrics telemetry to an OLTP endpoint using OpenTelemetry

Details NOTE: This scenario may be sending metric data that doesn't conform to OpenTelemetry naming conventions
varbuilder=WebApplication.CreateBuilder(args);builder.Metrics.AddOpenTelemetry(builder =>builder.AddOtlpExporter());

The defaults are controlled from appsettings.json which would contain this in a new web app template:

{
"Metrics": {
"EnabledMetrics": {
"System": true,
"Microsoft.AspNetCore": true
}
}
}

The specific destination for the telemetry is picked up by OTel from the environment variable OTEL_EXPORTER_OTLP_ENDPOINT. Other variations allow the configuration to be passed as a parameter ot AddOltpExporter() or using the service container to configure the
OtlpExporterOptions object.

The AddOpenTelemetry() call here is a hypothetical extension method implemented by the OTel.NET project similar to their current
ILoggingBuilder.AddOpenTelemetry() API.

App dev wants to send default metrics via Prometheus.NET

Details
varbuilder=WebApplication.CreateBuilder(args);builder.Metrics.AddPrometheus();[omittingstandard webapp code here]
app.MapMetrics();

Prometheus requires Kestrel to host its scrape endpoint which is why it integrates again in app.UseEndpoints(). The same appsettings.json as above (not shown) is controlling the default metrics being sent to Prometheus.

App dev sends the default metrics telemetry, named with OTel conventions, to an OLTP endpoint using OpenTelemetry

Details

Option #1: Use instrumentation libraries provided by OpenTelemetry

varbuilder=WebApplication.CreateBuilder(args);builder.Metrics.AddRuntimeMetrics();builder.Metrics.AddAspNetCoreMetrics();builder.Metrics.AddOpenTelemetry(builder =>builder.AddOtlpExporter());

The AddXXXMetrics() APIs are new hypothetical extension methods provided by OpenTelemetry instrumentation libraries. These calls produce
metrics under a Meter name defined by the OTel instrumentation libraries such as "OpenTelemetry.Instrumentation.AspNetCore". If the
metrics are left enabled in appsettings.json then both OpenTelmetry and built-in metrics would be transmitted. The app developer
should exlucde built-in metrics from the list if they don't want both.

Option #2: Use APIs provided by runtime libraries (Optional future development, does not ship in .NET 8)

varbuilder=WebApplication.CreateBuilder(args);builder.Metrics.AddRuntimeMetrics(RuntimeMetricNaming.OpenTelemetry1_0);builder.Metrics.AddAspNetCoreMetrics(AspNetCoreMetricNaming.OpenTelemetry1_0);builder.Metrics.AddOpenTelemetry(builder =>builder.AddOtlpExporter());

Once OpenTelemetry has stable conventions it is possible for the libraries that produce metrics to also include extension methods that
apply OpenTelemetry naming rules. The enumeration would likely start with two options, the default metric names shipped originally and
OpenTelemetry's names. Over time more options might be added to the enumeration for updated versions of OpenTelemetry's conventions or
other standards that have gained a broad interest. Unlike option #1, these metrics are produced with the default Meter name for the
library such as "Microsoft.AspNetCore" and it replaces the default metric rather than adding new metrics in parallel.

App dev wants to enable metrics from certain Meters to get sent (config-based approach)

Details
{
"Metrics": {
"EnabledMetrics": {
"System": true,
"Microsoft.AspNetCore": true"Microsoft.Extensions.Caching": true// hypothetical runtime provided Meter"StackExchange.Redis": true// hypothetical Meter in a 3P library
}
}
}

App dev wants to enable metrics from certain Meters to get sent (programmatic approach)

Details
varbuilder=WebApplication.CreateBuilder(args);builder.Metrics.EnableMetrics("Microsoft.Extensions.Caching").EnableMetrics("StackExchange.Redis").AddOpenTelemetry(builder =>builder.AddOtlpExporter());

App dev wants to enable metrics from certain Meters to get sent (custom-instrumentation approach)

Details

Meters need to be created in order for turning them on to have any effect. In some cases it may be convenient to both create them
and turn them on as a single step using a strongly typed extension method:

varbuilder=WebApplication.CreateBuilder(args);builder.Metrics.AddSqlClientInstrumentation().AddOpenTelemetry(builder =>builder.AddOtlpExporter());

This AddSqlClientInstrumentation method is a hypothetical extension method that could be offered by an OpenTelemetry instrumentation package. Note that some instrumentation packages are likely to be duplicative with built-in metrics. If the app developer doesn't want
both then they should ensure the built-in metrics aren't included in their configuration.

App dev wants to disable sending metrics for some instruments on a Meter (config-based approach)

Details
{
"Metrics": {
"EnabledMetrics": {
"System": true,
"Microsoft.AspNet.Core": {
"Default": true, // enable default metrics for all the instruments"connection-duration": false// but exclude the connection-duration metric
}
}
}
}

App dev wants to only enable specific instruments on a Meter (config-based approach)

Details
{
"Metrics": {
"EnabledMetrics": {
"System": true,
"Microsoft.AspNet.Core": {
"connection-duration": true// enable only the connection-duration metric
}
}
}
}

App dev wants to only enable specific instruments on a Meter (API-based approach)

Details
varbuilder=WebApplication.CreateBuilder(args);builder.Metrics// enable only the duration-counter metric.EnableMetrics("Microsoft.AspNetCore","duration-counter");.AddOpenTelemetry(builder =>builder.AddOtlpExporter());

App dev wants to disable sending metrics for some instruments on a Meter (API-based approach)

Details
varbuilder=WebApplication.CreateBuilder(args);builder.Metrics// enable all default metrics for all instruments on this Meter except duration-counter.EnableMetrics("Microsoft.AspNetCore", instrument =>instrument.Name!="duration-counter");.AddOpenTelemetry(builder =>builder.AddOtlpExporter());

The idea above is that the HostBuilder.Metrics property is a new MetricsBuilder type, the host defaults automatically constructed MetricsBuilder to load configuration from appsettings.json, and the config file specified which Meters to enable. The hypothetical AddOpenTelemetry() API is an extension method that would be provided by the OpenTelemetry NuGet package, very similar to the extension method they provide for LoggingBuilder.

Metadata

Metadata

Labels

api-approvedAPI was approved in API review, it can be implementedarea-System.Diagnostics.MetricblockingMarks issues that we want to fast track in order to unblock other important workenhancementProduct code improvement that does NOT require public API changes/additionspartner-impactThis issue impacts a partner who needs to be kept updated

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

    , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
     blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
    }
    } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
    })();
    (function(){
    try {
    var __m = "github.com";
    var __re = new RegExp('^' + "github\\.com" + '
    
    Skip to content

    [API Proposal]: Meter Configuration and Pipeline #85684

    Description

    @noahfalk

    Background and motivation

    In the current runtime APIs for metrics we have the ability to create and use Meters, and in-progress we have work to make it easier to consume those APIs from DI-centered workloads. However if an app developer wants to configure a set of Meters to enable and where to export that data that still requires 3rd party APIs such as OpenTelemetry, AppMetrics, or Prometheus.NET. This feature is to support a built-in option for configuring which Meters are enabled and one or more sinks to send that data to. The configuration should support being done both programmatically and via external configuration sources. For sinks I'd expect a neutral interface that any 3rd party component could use but we should look most closely at supporting the already existing 3rd party libraries.

    Design is still very uncertain at this point but the initial thought is to follow the lead of the Logging APIs which have LoggingBuilder, LoggingBuilder.AddFilter(), and LoggingBuilder.AddProvider().

    API Proposal

    Microsoft.Extensions.Diagnostics assembly

    namespaceMicrosoft.Extensions.DependencyInjection;publicstaticclassMetricsServiceCollectionExtensions{publicstaticIServiceCollectionAddMetrics(thisIServiceCollection);publicstaticIServiceCollectionAddMetrics(thisIServiceCollection,Action<IMetricsBuilder>configure);}

    Microsoft.Extensions.Diagnostics.Abstractions assembly

    namespaceMicrosoft.Extensions.Diagnostics.Metrics;publicinterfaceIMetricsBuilder{IServiceCollectionServices{get;}}publicstaticclassMetricsBuilderExtensions{publicstaticIMetricsBuilderAddListener(thisIMetricsBuilderbuilder,IMetricsListenerlistener);publicstaticIMetricsBuilderAddListener<T>(thisIMetricsBuilderbuilder)whereT:class,IMetricsListenerpublicstaticIMetricsBuilderClearListeners(thisIMetricsBuilderbuilder);}publicinterfaceIMetricsSource{publicvoidRecordObservableInstruments();}publicinterfaceIMetricsListener{publicstringName{get;}publicvoidSetSource(IMetricsSourcesource);publicboolInstrumentPublished(Instrumentinstrument,outobject?userState);publicvoidMeasurementsCompleted(Instrumentinstrument,object?userState);publicMeasurementCallback<T>GetMeasurementHandler<T>();}publicclassMetricsEnableOptions{publicIList<InstrumentEnableRule>Rules{get;}}publicclassInstrumentEnableRule{publicInstrumentEnableRule(string?listenerName,string?meterName,MeterScopescopes,string?instrumentName,boolenable);publicstring?ListenerName{get;}publicstring?MeterName{get;}publicMeterScopeScopes{get;}publicstring?InstrumentName{get;}publicboolEnable{get;}}[Flags]publicenumMeterScope{Global,Local}publicinterfaceIMetricsSubscriptionManager{publicvoidStart();}publicstaticMetricsBuilderEnableExtensions{publicstaticIMetricsBuilderEnableMetrics(thisIMetricsBuilderbuilder)=>thrownull!;publicstaticIMetricsBuilderEnableMetrics(thisIMetricsBuilderbuilder,string?meterName)=>thrownull!;publicstaticIMetricsBuilderEnableMetrics(thisIMetricsBuilderbuilder,string?meterName,string?instrumentName)=>thrownull!;publicstaticIMetricsBuilderEnableMetrics(thisIMetricsBuilderbuilder,string?meterName,string?instrumentName,string?listenerName)=>thrownull!;publicstaticIMetricsBuilderEnableMetrics(thisIMetricsBuilderbuilder,string?meterName,string?instrumentName,string?listenerName,MeterScopescopes)=>thrownull!;publicstaticMetricsEnableOptionsEnableMetrics(thisMetricsEnableOptionsoptions)=>thrownull!;publicstaticMetricsEnableOptionsEnableMetrics(thisMetricsEnableOptionsoptions,string?meterName)=>thrownull!;publicstaticMetricsEnableOptionsEnableMetrics(thisMetricsEnableOptionsoptions,string?meterName,string?instrumentName)=>thrownull!;publicstaticMetricsEnableOptionsEnableMetrics(thisMetricsEnableOptionsoptions,string?meterName,string?instrumentName,string?listenerName)=>thrownull!;publicstaticMetricsEnableOptionsEnableMetrics(thisMetricsEnableOptionsoptions,string?meterName,string?instrumentName,string?listenerName,MeterScopescopes)=>thrownull!;// Which overloads of this do we want?publicstaticMetricsEnableOptionsDisableMetrics(thisMetricsEnableOptionsoptions,string?meterName,string?instrumentName,string?listenerName,MeterScopescopes)=>thrownull!;}

    Integration with configuration

    We want users to be able to configure which metrics to capture for different listeners using appsettings.json or other
    IConfiguration sources

    Data format

    Metrics is the top level section under which "EnabledMetrics" configures which metrics are enabled for all listeners and
    a section named with a listener name adds additional rules specific to that listener. The key under "EnabledMetrics" is a
    Meter name prefix and the value is true if the Meter should be enabled or false if disabled.

    {
    "Metrics": {
    "EnabledMetrics": {
    "System.Runtime": true
    },
    "OpenTelemetry": {
    "EnabledMetrics": {
    "Microsoft.AspNetCore": true
    }
    }
    }
    }

    Similar to logging, "Default" acts as a zero-length meter prefix creating a default rule applying to all Meters if no
    longer prefix rule matches. Logically this allows creating opt-in or opt-out policies. If no rule matches a Meter it will
    not be enabled, so omitting "Default" is the same behavior as including Default=false.

    {
    "Metrics": {
    "EnabledMetrics": {
    "Default": true"NoisyLibrary": false
    },
    }
    }

    To enable individual instruments within a Meter replace the boolean with object syntax where the keys are instrument names or prefixes
    and the value is true iff enabled. "Default" is also honored at this scope as the zero-length instrument name prefix. Not specifying a
    "Default" is the same as "Default": false.

    {
    "Metrics": {
    "EnabledMetrics": {
    "System.Runtime": {
    "gc": true,
    "jit": true
    }
    "Microsoft.AspNet.Core": {
    "Default": true,
    "connection-duration": false
    }
    }
    }
    }

    To make rules apply only to Meters at a specific Global or Local scope instead of "EnabledMetrics" you can use "EnabledGlobalMetrics" or
    "EnabledLocalMetrics". Rules in a "EnabledMetrics" section apply to Meters in both scopes. The expectation is that using this local/global config is rare and maybe we should just remove it?

    {
    "Metrics": {
    "EnabledGlobalMetrics": {
    "System.Runtime": true
    }
    "EnabledLocalMetrics": {
    "System.Net.Http": true
    }
    "EnabledMetrics": {
    "Meter1": true,
    "Meter2": true
    }
    }
    }

    API

    Microsoft.Extensions.Diagnostics.Configuration.dll:

    namespaceMicrosoft.Extensions.Diagnostics.Metrics{publicstaticclassMetricsBuilderExtensions{publicstaticIMetricsBuilderAddConfiguration(thisIMetricsBuilderbuilder,IConfigurationconfiguration);}}namespaceMicrosoft.Extensions.Diagnostics.Metrics.Configuration{publicinterfaceIMetricListenerConfigurationFactory{IConfigurationGetConfiguration(TypelistenerType);}publicinterfaceIMetricListenerConfiguration<T>{IConfigurationConfiguration{get;}}}

    Integration with Hosting APIs

    We should include a Metrics property in parallel where existing hosting APIs expose Logging

    publicclassHostApplicationBuilder//pre-existing{publicIMetricsBuilderMetrics{get;}}publicclassWebApplicationBuilder//pre-existing{publicIMetricsBuilderMetrics{get;}}publicstaticclassHostingHostBuilderExtensions//pre-existing{publicstaticIHostBuilderConfigureMetrics(thisIHostBuilderhostBuilder,Action<IMetricsBuilder>configureMetrics);publicstaticIHostBuilderConfigureMetrics(thisIHostBuilderhostBuilder,Action<HostBuilderContext,IMetricsBuilder>configureMetrics);}

    Default sinks

    We should include a simple console output of the metric measurements intended for debugging purposes.
    This doesn't have any of the customization that the ILogger console does because it isn't intended to be left
    enabled in production code. Recording raw measurements to text is very inefficient.

    // All in Microsoft.Extensions.Diagnostics.dllnamespaceMicrosoft.Extensions.Diagnostics.MetricspublicstaticclassMetricsBuilderConsoleExtensions{publicstaticIMetricsBuilderAddDebugConsole(thisIMetricsBuilderbuilder);}publicstaticclassConsoleMetrics{publicstaticstringListenerName=>"Console";}

    API Usage

    App dev sends the default metrics telemetry to an OLTP endpoint using OpenTelemetry

    Details NOTE: This scenario may be sending metric data that doesn't conform to OpenTelemetry naming conventions
    varbuilder=WebApplication.CreateBuilder(args);builder.Metrics.AddOpenTelemetry(builder =>builder.AddOtlpExporter());

    The defaults are controlled from appsettings.json which would contain this in a new web app template:

    {
    "Metrics": {
    "EnabledMetrics": {
    "System": true,
    "Microsoft.AspNetCore": true
    }
    }
    }

    The specific destination for the telemetry is picked up by OTel from the environment variable OTEL_EXPORTER_OTLP_ENDPOINT. Other variations allow the configuration to be passed as a parameter ot AddOltpExporter() or using the service container to configure the
    OtlpExporterOptions object.

    The AddOpenTelemetry() call here is a hypothetical extension method implemented by the OTel.NET project similar to their current
    ILoggingBuilder.AddOpenTelemetry() API.

    App dev wants to send default metrics via Prometheus.NET

    Details
    varbuilder=WebApplication.CreateBuilder(args);builder.Metrics.AddPrometheus();[omittingstandard webapp code here]
    app.MapMetrics();

    Prometheus requires Kestrel to host its scrape endpoint which is why it integrates again in app.UseEndpoints(). The same appsettings.json as above (not shown) is controlling the default metrics being sent to Prometheus.

    App dev sends the default metrics telemetry, named with OTel conventions, to an OLTP endpoint using OpenTelemetry

    Details

    Option #1: Use instrumentation libraries provided by OpenTelemetry

    varbuilder=WebApplication.CreateBuilder(args);builder.Metrics.AddRuntimeMetrics();builder.Metrics.AddAspNetCoreMetrics();builder.Metrics.AddOpenTelemetry(builder =>builder.AddOtlpExporter());

    The AddXXXMetrics() APIs are new hypothetical extension methods provided by OpenTelemetry instrumentation libraries. These calls produce
    metrics under a Meter name defined by the OTel instrumentation libraries such as "OpenTelemetry.Instrumentation.AspNetCore". If the
    metrics are left enabled in appsettings.json then both OpenTelmetry and built-in metrics would be transmitted. The app developer
    should exlucde built-in metrics from the list if they don't want both.

    Option #2: Use APIs provided by runtime libraries (Optional future development, does not ship in .NET 8)

    varbuilder=WebApplication.CreateBuilder(args);builder.Metrics.AddRuntimeMetrics(RuntimeMetricNaming.OpenTelemetry1_0);builder.Metrics.AddAspNetCoreMetrics(AspNetCoreMetricNaming.OpenTelemetry1_0);builder.Metrics.AddOpenTelemetry(builder =>builder.AddOtlpExporter());

    Once OpenTelemetry has stable conventions it is possible for the libraries that produce metrics to also include extension methods that
    apply OpenTelemetry naming rules. The enumeration would likely start with two options, the default metric names shipped originally and
    OpenTelemetry's names. Over time more options might be added to the enumeration for updated versions of OpenTelemetry's conventions or
    other standards that have gained a broad interest. Unlike option #1, these metrics are produced with the default Meter name for the
    library such as "Microsoft.AspNetCore" and it replaces the default metric rather than adding new metrics in parallel.

    App dev wants to enable metrics from certain Meters to get sent (config-based approach)

    Details
    {
    "Metrics": {
    "EnabledMetrics": {
    "System": true,
    "Microsoft.AspNetCore": true"Microsoft.Extensions.Caching": true// hypothetical runtime provided Meter"StackExchange.Redis": true// hypothetical Meter in a 3P library
    }
    }
    }

    App dev wants to enable metrics from certain Meters to get sent (programmatic approach)

    Details
    varbuilder=WebApplication.CreateBuilder(args);builder.Metrics.EnableMetrics("Microsoft.Extensions.Caching").EnableMetrics("StackExchange.Redis").AddOpenTelemetry(builder =>builder.AddOtlpExporter());

    App dev wants to enable metrics from certain Meters to get sent (custom-instrumentation approach)

    Details

    Meters need to be created in order for turning them on to have any effect. In some cases it may be convenient to both create them
    and turn them on as a single step using a strongly typed extension method:

    varbuilder=WebApplication.CreateBuilder(args);builder.Metrics.AddSqlClientInstrumentation().AddOpenTelemetry(builder =>builder.AddOtlpExporter());

    This AddSqlClientInstrumentation method is a hypothetical extension method that could be offered by an OpenTelemetry instrumentation package. Note that some instrumentation packages are likely to be duplicative with built-in metrics. If the app developer doesn't want
    both then they should ensure the built-in metrics aren't included in their configuration.

    App dev wants to disable sending metrics for some instruments on a Meter (config-based approach)

    Details
    {
    "Metrics": {
    "EnabledMetrics": {
    "System": true,
    "Microsoft.AspNet.Core": {
    "Default": true, // enable default metrics for all the instruments"connection-duration": false// but exclude the connection-duration metric
    }
    }
    }
    }

    App dev wants to only enable specific instruments on a Meter (config-based approach)

    Details
    {
    "Metrics": {
    "EnabledMetrics": {
    "System": true,
    "Microsoft.AspNet.Core": {
    "connection-duration": true// enable only the connection-duration metric
    }
    }
    }
    }

    App dev wants to only enable specific instruments on a Meter (API-based approach)

    Details
    varbuilder=WebApplication.CreateBuilder(args);builder.Metrics// enable only the duration-counter metric.EnableMetrics("Microsoft.AspNetCore","duration-counter");.AddOpenTelemetry(builder =>builder.AddOtlpExporter());

    App dev wants to disable sending metrics for some instruments on a Meter (API-based approach)

    Details
    varbuilder=WebApplication.CreateBuilder(args);builder.Metrics// enable all default metrics for all instruments on this Meter except duration-counter.EnableMetrics("Microsoft.AspNetCore", instrument =>instrument.Name!="duration-counter");.AddOpenTelemetry(builder =>builder.AddOtlpExporter());

    The idea above is that the HostBuilder.Metrics property is a new MetricsBuilder type, the host defaults automatically constructed MetricsBuilder to load configuration from appsettings.json, and the config file specified which Meters to enable. The hypothetical AddOpenTelemetry() API is an extension method that would be provided by the OpenTelemetry NuGet package, very similar to the extension method they provide for LoggingBuilder.

    Metadata

    Metadata

    Labels

    api-approvedAPI was approved in API review, it can be implementedarea-System.Diagnostics.MetricblockingMarks issues that we want to fast track in order to unblock other important workenhancementProduct code improvement that does NOT require public API changes/additionspartner-impactThis issue impacts a partner who needs to be kept updated

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
      Skip to content

      [API Proposal]: Meter Configuration and Pipeline #85684

      Description

      @noahfalk

      Background and motivation

      In the current runtime APIs for metrics we have the ability to create and use Meters, and in-progress we have work to make it easier to consume those APIs from DI-centered workloads. However if an app developer wants to configure a set of Meters to enable and where to export that data that still requires 3rd party APIs such as OpenTelemetry, AppMetrics, or Prometheus.NET. This feature is to support a built-in option for configuring which Meters are enabled and one or more sinks to send that data to. The configuration should support being done both programmatically and via external configuration sources. For sinks I'd expect a neutral interface that any 3rd party component could use but we should look most closely at supporting the already existing 3rd party libraries.

      Design is still very uncertain at this point but the initial thought is to follow the lead of the Logging APIs which have LoggingBuilder, LoggingBuilder.AddFilter(), and LoggingBuilder.AddProvider().

      API Proposal

      Microsoft.Extensions.Diagnostics assembly

      namespaceMicrosoft.Extensions.DependencyInjection;publicstaticclassMetricsServiceCollectionExtensions{publicstaticIServiceCollectionAddMetrics(thisIServiceCollection);publicstaticIServiceCollectionAddMetrics(thisIServiceCollection,Action<IMetricsBuilder>configure);}

      Microsoft.Extensions.Diagnostics.Abstractions assembly

      namespaceMicrosoft.Extensions.Diagnostics.Metrics;publicinterfaceIMetricsBuilder{IServiceCollectionServices{get;}}publicstaticclassMetricsBuilderExtensions{publicstaticIMetricsBuilderAddListener(thisIMetricsBuilderbuilder,IMetricsListenerlistener);publicstaticIMetricsBuilderAddListener<T>(thisIMetricsBuilderbuilder)whereT:class,IMetricsListenerpublicstaticIMetricsBuilderClearListeners(thisIMetricsBuilderbuilder);}publicinterfaceIMetricsSource{publicvoidRecordObservableInstruments();}publicinterfaceIMetricsListener{publicstringName{get;}publicvoidSetSource(IMetricsSourcesource);publicboolInstrumentPublished(Instrumentinstrument,outobject?userState);publicvoidMeasurementsCompleted(Instrumentinstrument,object?userState);publicMeasurementCallback<T>GetMeasurementHandler<T>();}publicclassMetricsEnableOptions{publicIList<InstrumentEnableRule>Rules{get;}}publicclassInstrumentEnableRule{publicInstrumentEnableRule(string?listenerName,string?meterName,MeterScopescopes,string?instrumentName,boolenable);publicstring?ListenerName{get;}publicstring?MeterName{get;}publicMeterScopeScopes{get;}publicstring?InstrumentName{get;}publicboolEnable{get;}}[Flags]publicenumMeterScope{Global,Local}publicinterfaceIMetricsSubscriptionManager{publicvoidStart();}publicstaticMetricsBuilderEnableExtensions{publicstaticIMetricsBuilderEnableMetrics(thisIMetricsBuilderbuilder)=>thrownull!;publicstaticIMetricsBuilderEnableMetrics(thisIMetricsBuilderbuilder,string?meterName)=>thrownull!;publicstaticIMetricsBuilderEnableMetrics(thisIMetricsBuilderbuilder,string?meterName,string?instrumentName)=>thrownull!;publicstaticIMetricsBuilderEnableMetrics(thisIMetricsBuilderbuilder,string?meterName,string?instrumentName,string?listenerName)=>thrownull!;publicstaticIMetricsBuilderEnableMetrics(thisIMetricsBuilderbuilder,string?meterName,string?instrumentName,string?listenerName,MeterScopescopes)=>thrownull!;publicstaticMetricsEnableOptionsEnableMetrics(thisMetricsEnableOptionsoptions)=>thrownull!;publicstaticMetricsEnableOptionsEnableMetrics(thisMetricsEnableOptionsoptions,string?meterName)=>thrownull!;publicstaticMetricsEnableOptionsEnableMetrics(thisMetricsEnableOptionsoptions,string?meterName,string?instrumentName)=>thrownull!;publicstaticMetricsEnableOptionsEnableMetrics(thisMetricsEnableOptionsoptions,string?meterName,string?instrumentName,string?listenerName)=>thrownull!;publicstaticMetricsEnableOptionsEnableMetrics(thisMetricsEnableOptionsoptions,string?meterName,string?instrumentName,string?listenerName,MeterScopescopes)=>thrownull!;// Which overloads of this do we want?publicstaticMetricsEnableOptionsDisableMetrics(thisMetricsEnableOptionsoptions,string?meterName,string?instrumentName,string?listenerName,MeterScopescopes)=>thrownull!;}

      Integration with configuration

      We want users to be able to configure which metrics to capture for different listeners using appsettings.json or other
      IConfiguration sources

      Data format

      Metrics is the top level section under which "EnabledMetrics" configures which metrics are enabled for all listeners and
      a section named with a listener name adds additional rules specific to that listener. The key under "EnabledMetrics" is a
      Meter name prefix and the value is true if the Meter should be enabled or false if disabled.

      {
      "Metrics": {
      "EnabledMetrics": {
      "System.Runtime": true
      },
      "OpenTelemetry": {
      "EnabledMetrics": {
      "Microsoft.AspNetCore": true
      }
      }
      }
      }

      Similar to logging, "Default" acts as a zero-length meter prefix creating a default rule applying to all Meters if no
      longer prefix rule matches. Logically this allows creating opt-in or opt-out policies. If no rule matches a Meter it will
      not be enabled, so omitting "Default" is the same behavior as including Default=false.

      {
      "Metrics": {
      "EnabledMetrics": {
      "Default": true"NoisyLibrary": false
      },
      }
      }

      To enable individual instruments within a Meter replace the boolean with object syntax where the keys are instrument names or prefixes
      and the value is true iff enabled. "Default" is also honored at this scope as the zero-length instrument name prefix. Not specifying a
      "Default" is the same as "Default": false.

      {
      "Metrics": {
      "EnabledMetrics": {
      "System.Runtime": {
      "gc": true,
      "jit": true
      }
      "Microsoft.AspNet.Core": {
      "Default": true,
      "connection-duration": false
      }
      }
      }
      }

      To make rules apply only to Meters at a specific Global or Local scope instead of "EnabledMetrics" you can use "EnabledGlobalMetrics" or
      "EnabledLocalMetrics". Rules in a "EnabledMetrics" section apply to Meters in both scopes. The expectation is that using this local/global config is rare and maybe we should just remove it?

      {
      "Metrics": {
      "EnabledGlobalMetrics": {
      "System.Runtime": true
      }
      "EnabledLocalMetrics": {
      "System.Net.Http": true
      }
      "EnabledMetrics": {
      "Meter1": true,
      "Meter2": true
      }
      }
      }

      API

      Microsoft.Extensions.Diagnostics.Configuration.dll:

      namespaceMicrosoft.Extensions.Diagnostics.Metrics{publicstaticclassMetricsBuilderExtensions{publicstaticIMetricsBuilderAddConfiguration(thisIMetricsBuilderbuilder,IConfigurationconfiguration);}}namespaceMicrosoft.Extensions.Diagnostics.Metrics.Configuration{publicinterfaceIMetricListenerConfigurationFactory{IConfigurationGetConfiguration(TypelistenerType);}publicinterfaceIMetricListenerConfiguration<T>{IConfigurationConfiguration{get;}}}

      Integration with Hosting APIs

      We should include a Metrics property in parallel where existing hosting APIs expose Logging

      publicclassHostApplicationBuilder//pre-existing{publicIMetricsBuilderMetrics{get;}}publicclassWebApplicationBuilder//pre-existing{publicIMetricsBuilderMetrics{get;}}publicstaticclassHostingHostBuilderExtensions//pre-existing{publicstaticIHostBuilderConfigureMetrics(thisIHostBuilderhostBuilder,Action<IMetricsBuilder>configureMetrics);publicstaticIHostBuilderConfigureMetrics(thisIHostBuilderhostBuilder,Action<HostBuilderContext,IMetricsBuilder>configureMetrics);}

      Default sinks

      We should include a simple console output of the metric measurements intended for debugging purposes.
      This doesn't have any of the customization that the ILogger console does because it isn't intended to be left
      enabled in production code. Recording raw measurements to text is very inefficient.

      // All in Microsoft.Extensions.Diagnostics.dllnamespaceMicrosoft.Extensions.Diagnostics.MetricspublicstaticclassMetricsBuilderConsoleExtensions{publicstaticIMetricsBuilderAddDebugConsole(thisIMetricsBuilderbuilder);}publicstaticclassConsoleMetrics{publicstaticstringListenerName=>"Console";}

      API Usage

      App dev sends the default metrics telemetry to an OLTP endpoint using OpenTelemetry

      Details NOTE: This scenario may be sending metric data that doesn't conform to OpenTelemetry naming conventions
      varbuilder=WebApplication.CreateBuilder(args);builder.Metrics.AddOpenTelemetry(builder =>builder.AddOtlpExporter());

      The defaults are controlled from appsettings.json which would contain this in a new web app template:

      {
      "Metrics": {
      "EnabledMetrics": {
      "System": true,
      "Microsoft.AspNetCore": true
      }
      }
      }

      The specific destination for the telemetry is picked up by OTel from the environment variable OTEL_EXPORTER_OTLP_ENDPOINT. Other variations allow the configuration to be passed as a parameter ot AddOltpExporter() or using the service container to configure the
      OtlpExporterOptions object.

      The AddOpenTelemetry() call here is a hypothetical extension method implemented by the OTel.NET project similar to their current
      ILoggingBuilder.AddOpenTelemetry() API.

      App dev wants to send default metrics via Prometheus.NET

      Details
      varbuilder=WebApplication.CreateBuilder(args);builder.Metrics.AddPrometheus();[omittingstandard webapp code here]
      app.MapMetrics();

      Prometheus requires Kestrel to host its scrape endpoint which is why it integrates again in app.UseEndpoints(). The same appsettings.json as above (not shown) is controlling the default metrics being sent to Prometheus.

      App dev sends the default metrics telemetry, named with OTel conventions, to an OLTP endpoint using OpenTelemetry

      Details

      Option #1: Use instrumentation libraries provided by OpenTelemetry

      varbuilder=WebApplication.CreateBuilder(args);builder.Metrics.AddRuntimeMetrics();builder.Metrics.AddAspNetCoreMetrics();builder.Metrics.AddOpenTelemetry(builder =>builder.AddOtlpExporter());

      The AddXXXMetrics() APIs are new hypothetical extension methods provided by OpenTelemetry instrumentation libraries. These calls produce
      metrics under a Meter name defined by the OTel instrumentation libraries such as "OpenTelemetry.Instrumentation.AspNetCore". If the
      metrics are left enabled in appsettings.json then both OpenTelmetry and built-in metrics would be transmitted. The app developer
      should exlucde built-in metrics from the list if they don't want both.

      Option #2: Use APIs provided by runtime libraries (Optional future development, does not ship in .NET 8)

      varbuilder=WebApplication.CreateBuilder(args);builder.Metrics.AddRuntimeMetrics(RuntimeMetricNaming.OpenTelemetry1_0);builder.Metrics.AddAspNetCoreMetrics(AspNetCoreMetricNaming.OpenTelemetry1_0);builder.Metrics.AddOpenTelemetry(builder =>builder.AddOtlpExporter());

      Once OpenTelemetry has stable conventions it is possible for the libraries that produce metrics to also include extension methods that
      apply OpenTelemetry naming rules. The enumeration would likely start with two options, the default metric names shipped originally and
      OpenTelemetry's names. Over time more options might be added to the enumeration for updated versions of OpenTelemetry's conventions or
      other standards that have gained a broad interest. Unlike option #1, these metrics are produced with the default Meter name for the
      library such as "Microsoft.AspNetCore" and it replaces the default metric rather than adding new metrics in parallel.

      App dev wants to enable metrics from certain Meters to get sent (config-based approach)

      Details
      {
      "Metrics": {
      "EnabledMetrics": {
      "System": true,
      "Microsoft.AspNetCore": true"Microsoft.Extensions.Caching": true// hypothetical runtime provided Meter"StackExchange.Redis": true// hypothetical Meter in a 3P library
      }
      }
      }

      App dev wants to enable metrics from certain Meters to get sent (programmatic approach)

      Details
      varbuilder=WebApplication.CreateBuilder(args);builder.Metrics.EnableMetrics("Microsoft.Extensions.Caching").EnableMetrics("StackExchange.Redis").AddOpenTelemetry(builder =>builder.AddOtlpExporter());

      App dev wants to enable metrics from certain Meters to get sent (custom-instrumentation approach)

      Details

      Meters need to be created in order for turning them on to have any effect. In some cases it may be convenient to both create them
      and turn them on as a single step using a strongly typed extension method:

      varbuilder=WebApplication.CreateBuilder(args);builder.Metrics.AddSqlClientInstrumentation().AddOpenTelemetry(builder =>builder.AddOtlpExporter());

      This AddSqlClientInstrumentation method is a hypothetical extension method that could be offered by an OpenTelemetry instrumentation package. Note that some instrumentation packages are likely to be duplicative with built-in metrics. If the app developer doesn't want
      both then they should ensure the built-in metrics aren't included in their configuration.

      App dev wants to disable sending metrics for some instruments on a Meter (config-based approach)

      Details
      {
      "Metrics": {
      "EnabledMetrics": {
      "System": true,
      "Microsoft.AspNet.Core": {
      "Default": true, // enable default metrics for all the instruments"connection-duration": false// but exclude the connection-duration metric
      }
      }
      }
      }

      App dev wants to only enable specific instruments on a Meter (config-based approach)

      Details
      {
      "Metrics": {
      "EnabledMetrics": {
      "System": true,
      "Microsoft.AspNet.Core": {
      "connection-duration": true// enable only the connection-duration metric
      }
      }
      }
      }

      App dev wants to only enable specific instruments on a Meter (API-based approach)

      Details
      varbuilder=WebApplication.CreateBuilder(args);builder.Metrics// enable only the duration-counter metric.EnableMetrics("Microsoft.AspNetCore","duration-counter");.AddOpenTelemetry(builder =>builder.AddOtlpExporter());

      App dev wants to disable sending metrics for some instruments on a Meter (API-based approach)

      Details
      varbuilder=WebApplication.CreateBuilder(args);builder.Metrics// enable all default metrics for all instruments on this Meter except duration-counter.EnableMetrics("Microsoft.AspNetCore", instrument =>instrument.Name!="duration-counter");.AddOpenTelemetry(builder =>builder.AddOtlpExporter());

      The idea above is that the HostBuilder.Metrics property is a new MetricsBuilder type, the host defaults automatically constructed MetricsBuilder to load configuration from appsettings.json, and the config file specified which Meters to enable. The hypothetical AddOpenTelemetry() API is an extension method that would be provided by the OpenTelemetry NuGet package, very similar to the extension method they provide for LoggingBuilder.

      Metadata

      Metadata

      Labels

      api-approvedAPI was approved in API review, it can be implementedarea-System.Diagnostics.MetricblockingMarks issues that we want to fast track in order to unblock other important workenhancementProduct code improvement that does NOT require public API changes/additionspartner-impactThis issue impacts a partner who needs to be kept updated

      Type

      No type

      Projects

      No projects

        Milestone

        Relationships

        None yet

        Development

        No branches or pull requests

        Issue actions

        , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
        Skip to content

        [API Proposal]: Meter Configuration and Pipeline #85684

        Description

        @noahfalk

        Background and motivation

        In the current runtime APIs for metrics we have the ability to create and use Meters, and in-progress we have work to make it easier to consume those APIs from DI-centered workloads. However if an app developer wants to configure a set of Meters to enable and where to export that data that still requires 3rd party APIs such as OpenTelemetry, AppMetrics, or Prometheus.NET. This feature is to support a built-in option for configuring which Meters are enabled and one or more sinks to send that data to. The configuration should support being done both programmatically and via external configuration sources. For sinks I'd expect a neutral interface that any 3rd party component could use but we should look most closely at supporting the already existing 3rd party libraries.

        Design is still very uncertain at this point but the initial thought is to follow the lead of the Logging APIs which have LoggingBuilder, LoggingBuilder.AddFilter(), and LoggingBuilder.AddProvider().

        API Proposal

        Microsoft.Extensions.Diagnostics assembly

        namespaceMicrosoft.Extensions.DependencyInjection;publicstaticclassMetricsServiceCollectionExtensions{publicstaticIServiceCollectionAddMetrics(thisIServiceCollection);publicstaticIServiceCollectionAddMetrics(thisIServiceCollection,Action<IMetricsBuilder>configure);}

        Microsoft.Extensions.Diagnostics.Abstractions assembly

        namespaceMicrosoft.Extensions.Diagnostics.Metrics;publicinterfaceIMetricsBuilder{IServiceCollectionServices{get;}}publicstaticclassMetricsBuilderExtensions{publicstaticIMetricsBuilderAddListener(thisIMetricsBuilderbuilder,IMetricsListenerlistener);publicstaticIMetricsBuilderAddListener<T>(thisIMetricsBuilderbuilder)whereT:class,IMetricsListenerpublicstaticIMetricsBuilderClearListeners(thisIMetricsBuilderbuilder);}publicinterfaceIMetricsSource{publicvoidRecordObservableInstruments();}publicinterfaceIMetricsListener{publicstringName{get;}publicvoidSetSource(IMetricsSourcesource);publicboolInstrumentPublished(Instrumentinstrument,outobject?userState);publicvoidMeasurementsCompleted(Instrumentinstrument,object?userState);publicMeasurementCallback<T>GetMeasurementHandler<T>();}publicclassMetricsEnableOptions{publicIList<InstrumentEnableRule>Rules{get;}}publicclassInstrumentEnableRule{publicInstrumentEnableRule(string?listenerName,string?meterName,MeterScopescopes,string?instrumentName,boolenable);publicstring?ListenerName{get;}publicstring?MeterName{get;}publicMeterScopeScopes{get;}publicstring?InstrumentName{get;}publicboolEnable{get;}}[Flags]publicenumMeterScope{Global,Local}publicinterfaceIMetricsSubscriptionManager{publicvoidStart();}publicstaticMetricsBuilderEnableExtensions{publicstaticIMetricsBuilderEnableMetrics(thisIMetricsBuilderbuilder)=>thrownull!;publicstaticIMetricsBuilderEnableMetrics(thisIMetricsBuilderbuilder,string?meterName)=>thrownull!;publicstaticIMetricsBuilderEnableMetrics(thisIMetricsBuilderbuilder,string?meterName,string?instrumentName)=>thrownull!;publicstaticIMetricsBuilderEnableMetrics(thisIMetricsBuilderbuilder,string?meterName,string?instrumentName,string?listenerName)=>thrownull!;publicstaticIMetricsBuilderEnableMetrics(thisIMetricsBuilderbuilder,string?meterName,string?instrumentName,string?listenerName,MeterScopescopes)=>thrownull!;publicstaticMetricsEnableOptionsEnableMetrics(thisMetricsEnableOptionsoptions)=>thrownull!;publicstaticMetricsEnableOptionsEnableMetrics(thisMetricsEnableOptionsoptions,string?meterName)=>thrownull!;publicstaticMetricsEnableOptionsEnableMetrics(thisMetricsEnableOptionsoptions,string?meterName,string?instrumentName)=>thrownull!;publicstaticMetricsEnableOptionsEnableMetrics(thisMetricsEnableOptionsoptions,string?meterName,string?instrumentName,string?listenerName)=>thrownull!;publicstaticMetricsEnableOptionsEnableMetrics(thisMetricsEnableOptionsoptions,string?meterName,string?instrumentName,string?listenerName,MeterScopescopes)=>thrownull!;// Which overloads of this do we want?publicstaticMetricsEnableOptionsDisableMetrics(thisMetricsEnableOptionsoptions,string?meterName,string?instrumentName,string?listenerName,MeterScopescopes)=>thrownull!;}

        Integration with configuration

        We want users to be able to configure which metrics to capture for different listeners using appsettings.json or other
        IConfiguration sources

        Data format

        Metrics is the top level section under which "EnabledMetrics" configures which metrics are enabled for all listeners and
        a section named with a listener name adds additional rules specific to that listener. The key under "EnabledMetrics" is a
        Meter name prefix and the value is true if the Meter should be enabled or false if disabled.

        {
        "Metrics": {
        "EnabledMetrics": {
        "System.Runtime": true
        },
        "OpenTelemetry": {
        "EnabledMetrics": {
        "Microsoft.AspNetCore": true
        }
        }
        }
        }

        Similar to logging, "Default" acts as a zero-length meter prefix creating a default rule applying to all Meters if no
        longer prefix rule matches. Logically this allows creating opt-in or opt-out policies. If no rule matches a Meter it will
        not be enabled, so omitting "Default" is the same behavior as including Default=false.

        {
        "Metrics": {
        "EnabledMetrics": {
        "Default": true"NoisyLibrary": false
        },
        }
        }

        To enable individual instruments within a Meter replace the boolean with object syntax where the keys are instrument names or prefixes
        and the value is true iff enabled. "Default" is also honored at this scope as the zero-length instrument name prefix. Not specifying a
        "Default" is the same as "Default": false.

        {
        "Metrics": {
        "EnabledMetrics": {
        "System.Runtime": {
        "gc": true,
        "jit": true
        }
        "Microsoft.AspNet.Core": {
        "Default": true,
        "connection-duration": false
        }
        }
        }
        }

        To make rules apply only to Meters at a specific Global or Local scope instead of "EnabledMetrics" you can use "EnabledGlobalMetrics" or
        "EnabledLocalMetrics". Rules in a "EnabledMetrics" section apply to Meters in both scopes. The expectation is that using this local/global config is rare and maybe we should just remove it?

        {
        "Metrics": {
        "EnabledGlobalMetrics": {
        "System.Runtime": true
        }
        "EnabledLocalMetrics": {
        "System.Net.Http": true
        }
        "EnabledMetrics": {
        "Meter1": true,
        "Meter2": true
        }
        }
        }

        API

        Microsoft.Extensions.Diagnostics.Configuration.dll:

        namespaceMicrosoft.Extensions.Diagnostics.Metrics{publicstaticclassMetricsBuilderExtensions{publicstaticIMetricsBuilderAddConfiguration(thisIMetricsBuilderbuilder,IConfigurationconfiguration);}}namespaceMicrosoft.Extensions.Diagnostics.Metrics.Configuration{publicinterfaceIMetricListenerConfigurationFactory{IConfigurationGetConfiguration(TypelistenerType);}publicinterfaceIMetricListenerConfiguration<T>{IConfigurationConfiguration{get;}}}

        Integration with Hosting APIs

        We should include a Metrics property in parallel where existing hosting APIs expose Logging

        publicclassHostApplicationBuilder//pre-existing{publicIMetricsBuilderMetrics{get;}}publicclassWebApplicationBuilder//pre-existing{publicIMetricsBuilderMetrics{get;}}publicstaticclassHostingHostBuilderExtensions//pre-existing{publicstaticIHostBuilderConfigureMetrics(thisIHostBuilderhostBuilder,Action<IMetricsBuilder>configureMetrics);publicstaticIHostBuilderConfigureMetrics(thisIHostBuilderhostBuilder,Action<HostBuilderContext,IMetricsBuilder>configureMetrics);}

        Default sinks

        We should include a simple console output of the metric measurements intended for debugging purposes.
        This doesn't have any of the customization that the ILogger console does because it isn't intended to be left
        enabled in production code. Recording raw measurements to text is very inefficient.

        // All in Microsoft.Extensions.Diagnostics.dllnamespaceMicrosoft.Extensions.Diagnostics.MetricspublicstaticclassMetricsBuilderConsoleExtensions{publicstaticIMetricsBuilderAddDebugConsole(thisIMetricsBuilderbuilder);}publicstaticclassConsoleMetrics{publicstaticstringListenerName=>"Console";}

        API Usage

        App dev sends the default metrics telemetry to an OLTP endpoint using OpenTelemetry

        Details NOTE: This scenario may be sending metric data that doesn't conform to OpenTelemetry naming conventions
        varbuilder=WebApplication.CreateBuilder(args);builder.Metrics.AddOpenTelemetry(builder =>builder.AddOtlpExporter());

        The defaults are controlled from appsettings.json which would contain this in a new web app template:

        {
        "Metrics": {
        "EnabledMetrics": {
        "System": true,
        "Microsoft.AspNetCore": true
        }
        }
        }

        The specific destination for the telemetry is picked up by OTel from the environment variable OTEL_EXPORTER_OTLP_ENDPOINT. Other variations allow the configuration to be passed as a parameter ot AddOltpExporter() or using the service container to configure the
        OtlpExporterOptions object.

        The AddOpenTelemetry() call here is a hypothetical extension method implemented by the OTel.NET project similar to their current
        ILoggingBuilder.AddOpenTelemetry() API.

        App dev wants to send default metrics via Prometheus.NET

        Details
        varbuilder=WebApplication.CreateBuilder(args);builder.Metrics.AddPrometheus();[omittingstandard webapp code here]
        app.MapMetrics();

        Prometheus requires Kestrel to host its scrape endpoint which is why it integrates again in app.UseEndpoints(). The same appsettings.json as above (not shown) is controlling the default metrics being sent to Prometheus.

        App dev sends the default metrics telemetry, named with OTel conventions, to an OLTP endpoint using OpenTelemetry

        Details

        Option #1: Use instrumentation libraries provided by OpenTelemetry

        varbuilder=WebApplication.CreateBuilder(args);builder.Metrics.AddRuntimeMetrics();builder.Metrics.AddAspNetCoreMetrics();builder.Metrics.AddOpenTelemetry(builder =>builder.AddOtlpExporter());

        The AddXXXMetrics() APIs are new hypothetical extension methods provided by OpenTelemetry instrumentation libraries. These calls produce
        metrics under a Meter name defined by the OTel instrumentation libraries such as "OpenTelemetry.Instrumentation.AspNetCore". If the
        metrics are left enabled in appsettings.json then both OpenTelmetry and built-in metrics would be transmitted. The app developer
        should exlucde built-in metrics from the list if they don't want both.

        Option #2: Use APIs provided by runtime libraries (Optional future development, does not ship in .NET 8)

        varbuilder=WebApplication.CreateBuilder(args);builder.Metrics.AddRuntimeMetrics(RuntimeMetricNaming.OpenTelemetry1_0);builder.Metrics.AddAspNetCoreMetrics(AspNetCoreMetricNaming.OpenTelemetry1_0);builder.Metrics.AddOpenTelemetry(builder =>builder.AddOtlpExporter());

        Once OpenTelemetry has stable conventions it is possible for the libraries that produce metrics to also include extension methods that
        apply OpenTelemetry naming rules. The enumeration would likely start with two options, the default metric names shipped originally and
        OpenTelemetry's names. Over time more options might be added to the enumeration for updated versions of OpenTelemetry's conventions or
        other standards that have gained a broad interest. Unlike option #1, these metrics are produced with the default Meter name for the
        library such as "Microsoft.AspNetCore" and it replaces the default metric rather than adding new metrics in parallel.

        App dev wants to enable metrics from certain Meters to get sent (config-based approach)

        Details
        {
        "Metrics": {
        "EnabledMetrics": {
        "System": true,
        "Microsoft.AspNetCore": true"Microsoft.Extensions.Caching": true// hypothetical runtime provided Meter"StackExchange.Redis": true// hypothetical Meter in a 3P library
        }
        }
        }

        App dev wants to enable metrics from certain Meters to get sent (programmatic approach)

        Details
        varbuilder=WebApplication.CreateBuilder(args);builder.Metrics.EnableMetrics("Microsoft.Extensions.Caching").EnableMetrics("StackExchange.Redis").AddOpenTelemetry(builder =>builder.AddOtlpExporter());

        App dev wants to enable metrics from certain Meters to get sent (custom-instrumentation approach)

        Details

        Meters need to be created in order for turning them on to have any effect. In some cases it may be convenient to both create them
        and turn them on as a single step using a strongly typed extension method:

        varbuilder=WebApplication.CreateBuilder(args);builder.Metrics.AddSqlClientInstrumentation().AddOpenTelemetry(builder =>builder.AddOtlpExporter());

        This AddSqlClientInstrumentation method is a hypothetical extension method that could be offered by an OpenTelemetry instrumentation package. Note that some instrumentation packages are likely to be duplicative with built-in metrics. If the app developer doesn't want
        both then they should ensure the built-in metrics aren't included in their configuration.

        App dev wants to disable sending metrics for some instruments on a Meter (config-based approach)

        Details
        {
        "Metrics": {
        "EnabledMetrics": {
        "System": true,
        "Microsoft.AspNet.Core": {
        "Default": true, // enable default metrics for all the instruments"connection-duration": false// but exclude the connection-duration metric
        }
        }
        }
        }

        App dev wants to only enable specific instruments on a Meter (config-based approach)

        Details
        {
        "Metrics": {
        "EnabledMetrics": {
        "System": true,
        "Microsoft.AspNet.Core": {
        "connection-duration": true// enable only the connection-duration metric
        }
        }
        }
        }

        App dev wants to only enable specific instruments on a Meter (API-based approach)

        Details
        varbuilder=WebApplication.CreateBuilder(args);builder.Metrics// enable only the duration-counter metric.EnableMetrics("Microsoft.AspNetCore","duration-counter");.AddOpenTelemetry(builder =>builder.AddOtlpExporter());

        App dev wants to disable sending metrics for some instruments on a Meter (API-based approach)

        Details
        varbuilder=WebApplication.CreateBuilder(args);builder.Metrics// enable all default metrics for all instruments on this Meter except duration-counter.EnableMetrics("Microsoft.AspNetCore", instrument =>instrument.Name!="duration-counter");.AddOpenTelemetry(builder =>builder.AddOtlpExporter());

        The idea above is that the HostBuilder.Metrics property is a new MetricsBuilder type, the host defaults automatically constructed MetricsBuilder to load configuration from appsettings.json, and the config file specified which Meters to enable. The hypothetical AddOpenTelemetry() API is an extension method that would be provided by the OpenTelemetry NuGet package, very similar to the extension method they provide for LoggingBuilder.

        Metadata

        Metadata

        Labels

        api-approvedAPI was approved in API review, it can be implementedarea-System.Diagnostics.MetricblockingMarks issues that we want to fast track in order to unblock other important workenhancementProduct code improvement that does NOT require public API changes/additionspartner-impactThis issue impacts a partner who needs to be kept updated

        Type

        No type

        Projects

        No projects

          Milestone

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
          Skip to content

          [API Proposal]: Meter Configuration and Pipeline #85684

          Description

          @noahfalk

          Background and motivation

          In the current runtime APIs for metrics we have the ability to create and use Meters, and in-progress we have work to make it easier to consume those APIs from DI-centered workloads. However if an app developer wants to configure a set of Meters to enable and where to export that data that still requires 3rd party APIs such as OpenTelemetry, AppMetrics, or Prometheus.NET. This feature is to support a built-in option for configuring which Meters are enabled and one or more sinks to send that data to. The configuration should support being done both programmatically and via external configuration sources. For sinks I'd expect a neutral interface that any 3rd party component could use but we should look most closely at supporting the already existing 3rd party libraries.

          Design is still very uncertain at this point but the initial thought is to follow the lead of the Logging APIs which have LoggingBuilder, LoggingBuilder.AddFilter(), and LoggingBuilder.AddProvider().

          API Proposal

          Microsoft.Extensions.Diagnostics assembly

          namespaceMicrosoft.Extensions.DependencyInjection;publicstaticclassMetricsServiceCollectionExtensions{publicstaticIServiceCollectionAddMetrics(thisIServiceCollection);publicstaticIServiceCollectionAddMetrics(thisIServiceCollection,Action<IMetricsBuilder>configure);}

          Microsoft.Extensions.Diagnostics.Abstractions assembly

          namespaceMicrosoft.Extensions.Diagnostics.Metrics;publicinterfaceIMetricsBuilder{IServiceCollectionServices{get;}}publicstaticclassMetricsBuilderExtensions{publicstaticIMetricsBuilderAddListener(thisIMetricsBuilderbuilder,IMetricsListenerlistener);publicstaticIMetricsBuilderAddListener<T>(thisIMetricsBuilderbuilder)whereT:class,IMetricsListenerpublicstaticIMetricsBuilderClearListeners(thisIMetricsBuilderbuilder);}publicinterfaceIMetricsSource{publicvoidRecordObservableInstruments();}publicinterfaceIMetricsListener{publicstringName{get;}publicvoidSetSource(IMetricsSourcesource);publicboolInstrumentPublished(Instrumentinstrument,outobject?userState);publicvoidMeasurementsCompleted(Instrumentinstrument,object?userState);publicMeasurementCallback<T>GetMeasurementHandler<T>();}publicclassMetricsEnableOptions{publicIList<InstrumentEnableRule>Rules{get;}}publicclassInstrumentEnableRule{publicInstrumentEnableRule(string?listenerName,string?meterName,MeterScopescopes,string?instrumentName,boolenable);publicstring?ListenerName{get;}publicstring?MeterName{get;}publicMeterScopeScopes{get;}publicstring?InstrumentName{get;}publicboolEnable{get;}}[Flags]publicenumMeterScope{Global,Local}publicinterfaceIMetricsSubscriptionManager{publicvoidStart();}publicstaticMetricsBuilderEnableExtensions{publicstaticIMetricsBuilderEnableMetrics(thisIMetricsBuilderbuilder)=>thrownull!;publicstaticIMetricsBuilderEnableMetrics(thisIMetricsBuilderbuilder,string?meterName)=>thrownull!;publicstaticIMetricsBuilderEnableMetrics(thisIMetricsBuilderbuilder,string?meterName,string?instrumentName)=>thrownull!;publicstaticIMetricsBuilderEnableMetrics(thisIMetricsBuilderbuilder,string?meterName,string?instrumentName,string?listenerName)=>thrownull!;publicstaticIMetricsBuilderEnableMetrics(thisIMetricsBuilderbuilder,string?meterName,string?instrumentName,string?listenerName,MeterScopescopes)=>thrownull!;publicstaticMetricsEnableOptionsEnableMetrics(thisMetricsEnableOptionsoptions)=>thrownull!;publicstaticMetricsEnableOptionsEnableMetrics(thisMetricsEnableOptionsoptions,string?meterName)=>thrownull!;publicstaticMetricsEnableOptionsEnableMetrics(thisMetricsEnableOptionsoptions,string?meterName,string?instrumentName)=>thrownull!;publicstaticMetricsEnableOptionsEnableMetrics(thisMetricsEnableOptionsoptions,string?meterName,string?instrumentName,string?listenerName)=>thrownull!;publicstaticMetricsEnableOptionsEnableMetrics(thisMetricsEnableOptionsoptions,string?meterName,string?instrumentName,string?listenerName,MeterScopescopes)=>thrownull!;// Which overloads of this do we want?publicstaticMetricsEnableOptionsDisableMetrics(thisMetricsEnableOptionsoptions,string?meterName,string?instrumentName,string?listenerName,MeterScopescopes)=>thrownull!;}

          Integration with configuration

          We want users to be able to configure which metrics to capture for different listeners using appsettings.json or other
          IConfiguration sources

          Data format

          Metrics is the top level section under which "EnabledMetrics" configures which metrics are enabled for all listeners and
          a section named with a listener name adds additional rules specific to that listener. The key under "EnabledMetrics" is a
          Meter name prefix and the value is true if the Meter should be enabled or false if disabled.

          {
          "Metrics": {
          "EnabledMetrics": {
          "System.Runtime": true
          },
          "OpenTelemetry": {
          "EnabledMetrics": {
          "Microsoft.AspNetCore": true
          }
          }
          }
          }

          Similar to logging, "Default" acts as a zero-length meter prefix creating a default rule applying to all Meters if no
          longer prefix rule matches. Logically this allows creating opt-in or opt-out policies. If no rule matches a Meter it will
          not be enabled, so omitting "Default" is the same behavior as including Default=false.

          {
          "Metrics": {
          "EnabledMetrics": {
          "Default": true"NoisyLibrary": false
          },
          }
          }

          To enable individual instruments within a Meter replace the boolean with object syntax where the keys are instrument names or prefixes
          and the value is true iff enabled. "Default" is also honored at this scope as the zero-length instrument name prefix. Not specifying a
          "Default" is the same as "Default": false.

          {
          "Metrics": {
          "EnabledMetrics": {
          "System.Runtime": {
          "gc": true,
          "jit": true
          }
          "Microsoft.AspNet.Core": {
          "Default": true,
          "connection-duration": false
          }
          }
          }
          }

          To make rules apply only to Meters at a specific Global or Local scope instead of "EnabledMetrics" you can use "EnabledGlobalMetrics" or
          "EnabledLocalMetrics". Rules in a "EnabledMetrics" section apply to Meters in both scopes. The expectation is that using this local/global config is rare and maybe we should just remove it?

          {
          "Metrics": {
          "EnabledGlobalMetrics": {
          "System.Runtime": true
          }
          "EnabledLocalMetrics": {
          "System.Net.Http": true
          }
          "EnabledMetrics": {
          "Meter1": true,
          "Meter2": true
          }
          }
          }

          API

          Microsoft.Extensions.Diagnostics.Configuration.dll:

          namespaceMicrosoft.Extensions.Diagnostics.Metrics{publicstaticclassMetricsBuilderExtensions{publicstaticIMetricsBuilderAddConfiguration(thisIMetricsBuilderbuilder,IConfigurationconfiguration);}}namespaceMicrosoft.Extensions.Diagnostics.Metrics.Configuration{publicinterfaceIMetricListenerConfigurationFactory{IConfigurationGetConfiguration(TypelistenerType);}publicinterfaceIMetricListenerConfiguration<T>{IConfigurationConfiguration{get;}}}

          Integration with Hosting APIs

          We should include a Metrics property in parallel where existing hosting APIs expose Logging

          publicclassHostApplicationBuilder//pre-existing{publicIMetricsBuilderMetrics{get;}}publicclassWebApplicationBuilder//pre-existing{publicIMetricsBuilderMetrics{get;}}publicstaticclassHostingHostBuilderExtensions//pre-existing{publicstaticIHostBuilderConfigureMetrics(thisIHostBuilderhostBuilder,Action<IMetricsBuilder>configureMetrics);publicstaticIHostBuilderConfigureMetrics(thisIHostBuilderhostBuilder,Action<HostBuilderContext,IMetricsBuilder>configureMetrics);}

          Default sinks

          We should include a simple console output of the metric measurements intended for debugging purposes.
          This doesn't have any of the customization that the ILogger console does because it isn't intended to be left
          enabled in production code. Recording raw measurements to text is very inefficient.

          // All in Microsoft.Extensions.Diagnostics.dllnamespaceMicrosoft.Extensions.Diagnostics.MetricspublicstaticclassMetricsBuilderConsoleExtensions{publicstaticIMetricsBuilderAddDebugConsole(thisIMetricsBuilderbuilder);}publicstaticclassConsoleMetrics{publicstaticstringListenerName=>"Console";}

          API Usage

          App dev sends the default metrics telemetry to an OLTP endpoint using OpenTelemetry

          Details NOTE: This scenario may be sending metric data that doesn't conform to OpenTelemetry naming conventions
          varbuilder=WebApplication.CreateBuilder(args);builder.Metrics.AddOpenTelemetry(builder =>builder.AddOtlpExporter());

          The defaults are controlled from appsettings.json which would contain this in a new web app template:

          {
          "Metrics": {
          "EnabledMetrics": {
          "System": true,
          "Microsoft.AspNetCore": true
          }
          }
          }

          The specific destination for the telemetry is picked up by OTel from the environment variable OTEL_EXPORTER_OTLP_ENDPOINT. Other variations allow the configuration to be passed as a parameter ot AddOltpExporter() or using the service container to configure the
          OtlpExporterOptions object.

          The AddOpenTelemetry() call here is a hypothetical extension method implemented by the OTel.NET project similar to their current
          ILoggingBuilder.AddOpenTelemetry() API.

          App dev wants to send default metrics via Prometheus.NET

          Details
          varbuilder=WebApplication.CreateBuilder(args);builder.Metrics.AddPrometheus();[omittingstandard webapp code here]
          app.MapMetrics();

          Prometheus requires Kestrel to host its scrape endpoint which is why it integrates again in app.UseEndpoints(). The same appsettings.json as above (not shown) is controlling the default metrics being sent to Prometheus.

          App dev sends the default metrics telemetry, named with OTel conventions, to an OLTP endpoint using OpenTelemetry

          Details

          Option #1: Use instrumentation libraries provided by OpenTelemetry

          varbuilder=WebApplication.CreateBuilder(args);builder.Metrics.AddRuntimeMetrics();builder.Metrics.AddAspNetCoreMetrics();builder.Metrics.AddOpenTelemetry(builder =>builder.AddOtlpExporter());

          The AddXXXMetrics() APIs are new hypothetical extension methods provided by OpenTelemetry instrumentation libraries. These calls produce
          metrics under a Meter name defined by the OTel instrumentation libraries such as "OpenTelemetry.Instrumentation.AspNetCore". If the
          metrics are left enabled in appsettings.json then both OpenTelmetry and built-in metrics would be transmitted. The app developer
          should exlucde built-in metrics from the list if they don't want both.

          Option #2: Use APIs provided by runtime libraries (Optional future development, does not ship in .NET 8)

          varbuilder=WebApplication.CreateBuilder(args);builder.Metrics.AddRuntimeMetrics(RuntimeMetricNaming.OpenTelemetry1_0);builder.Metrics.AddAspNetCoreMetrics(AspNetCoreMetricNaming.OpenTelemetry1_0);builder.Metrics.AddOpenTelemetry(builder =>builder.AddOtlpExporter());

          Once OpenTelemetry has stable conventions it is possible for the libraries that produce metrics to also include extension methods that
          apply OpenTelemetry naming rules. The enumeration would likely start with two options, the default metric names shipped originally and
          OpenTelemetry's names. Over time more options might be added to the enumeration for updated versions of OpenTelemetry's conventions or
          other standards that have gained a broad interest. Unlike option #1, these metrics are produced with the default Meter name for the
          library such as "Microsoft.AspNetCore" and it replaces the default metric rather than adding new metrics in parallel.

          App dev wants to enable metrics from certain Meters to get sent (config-based approach)

          Details
          {
          "Metrics": {
          "EnabledMetrics": {
          "System": true,
          "Microsoft.AspNetCore": true"Microsoft.Extensions.Caching": true// hypothetical runtime provided Meter"StackExchange.Redis": true// hypothetical Meter in a 3P library
          }
          }
          }

          App dev wants to enable metrics from certain Meters to get sent (programmatic approach)

          Details
          varbuilder=WebApplication.CreateBuilder(args);builder.Metrics.EnableMetrics("Microsoft.Extensions.Caching").EnableMetrics("StackExchange.Redis").AddOpenTelemetry(builder =>builder.AddOtlpExporter());

          App dev wants to enable metrics from certain Meters to get sent (custom-instrumentation approach)

          Details

          Meters need to be created in order for turning them on to have any effect. In some cases it may be convenient to both create them
          and turn them on as a single step using a strongly typed extension method:

          varbuilder=WebApplication.CreateBuilder(args);builder.Metrics.AddSqlClientInstrumentation().AddOpenTelemetry(builder =>builder.AddOtlpExporter());

          This AddSqlClientInstrumentation method is a hypothetical extension method that could be offered by an OpenTelemetry instrumentation package. Note that some instrumentation packages are likely to be duplicative with built-in metrics. If the app developer doesn't want
          both then they should ensure the built-in metrics aren't included in their configuration.

          App dev wants to disable sending metrics for some instruments on a Meter (config-based approach)

          Details
          {
          "Metrics": {
          "EnabledMetrics": {
          "System": true,
          "Microsoft.AspNet.Core": {
          "Default": true, // enable default metrics for all the instruments"connection-duration": false// but exclude the connection-duration metric
          }
          }
          }
          }

          App dev wants to only enable specific instruments on a Meter (config-based approach)

          Details
          {
          "Metrics": {
          "EnabledMetrics": {
          "System": true,
          "Microsoft.AspNet.Core": {
          "connection-duration": true// enable only the connection-duration metric
          }
          }
          }
          }

          App dev wants to only enable specific instruments on a Meter (API-based approach)

          Details
          varbuilder=WebApplication.CreateBuilder(args);builder.Metrics// enable only the duration-counter metric.EnableMetrics("Microsoft.AspNetCore","duration-counter");.AddOpenTelemetry(builder =>builder.AddOtlpExporter());

          App dev wants to disable sending metrics for some instruments on a Meter (API-based approach)

          Details
          varbuilder=WebApplication.CreateBuilder(args);builder.Metrics// enable all default metrics for all instruments on this Meter except duration-counter.EnableMetrics("Microsoft.AspNetCore", instrument =>instrument.Name!="duration-counter");.AddOpenTelemetry(builder =>builder.AddOtlpExporter());

          The idea above is that the HostBuilder.Metrics property is a new MetricsBuilder type, the host defaults automatically constructed MetricsBuilder to load configuration from appsettings.json, and the config file specified which Meters to enable. The hypothetical AddOpenTelemetry() API is an extension method that would be provided by the OpenTelemetry NuGet package, very similar to the extension method they provide for LoggingBuilder.

          Metadata

          Metadata

          Labels

          api-approvedAPI was approved in API review, it can be implementedarea-System.Diagnostics.MetricblockingMarks issues that we want to fast track in order to unblock other important workenhancementProduct code improvement that does NOT require public API changes/additionspartner-impactThis issue impacts a partner who needs to be kept updated

          Type

          No type

          Projects

          No projects

            Milestone

            Relationships

            None yet

            Development

            No branches or pull requests

            Issue actions

            , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
            Skip to content

            [API Proposal]: Meter Configuration and Pipeline #85684

            Description

            @noahfalk

            Background and motivation

            In the current runtime APIs for metrics we have the ability to create and use Meters, and in-progress we have work to make it easier to consume those APIs from DI-centered workloads. However if an app developer wants to configure a set of Meters to enable and where to export that data that still requires 3rd party APIs such as OpenTelemetry, AppMetrics, or Prometheus.NET. This feature is to support a built-in option for configuring which Meters are enabled and one or more sinks to send that data to. The configuration should support being done both programmatically and via external configuration sources. For sinks I'd expect a neutral interface that any 3rd party component could use but we should look most closely at supporting the already existing 3rd party libraries.

            Design is still very uncertain at this point but the initial thought is to follow the lead of the Logging APIs which have LoggingBuilder, LoggingBuilder.AddFilter(), and LoggingBuilder.AddProvider().

            API Proposal

            Microsoft.Extensions.Diagnostics assembly

            namespaceMicrosoft.Extensions.DependencyInjection;publicstaticclassMetricsServiceCollectionExtensions{publicstaticIServiceCollectionAddMetrics(thisIServiceCollection);publicstaticIServiceCollectionAddMetrics(thisIServiceCollection,Action<IMetricsBuilder>configure);}

            Microsoft.Extensions.Diagnostics.Abstractions assembly

            namespaceMicrosoft.Extensions.Diagnostics.Metrics;publicinterfaceIMetricsBuilder{IServiceCollectionServices{get;}}publicstaticclassMetricsBuilderExtensions{publicstaticIMetricsBuilderAddListener(thisIMetricsBuilderbuilder,IMetricsListenerlistener);publicstaticIMetricsBuilderAddListener<T>(thisIMetricsBuilderbuilder)whereT:class,IMetricsListenerpublicstaticIMetricsBuilderClearListeners(thisIMetricsBuilderbuilder);}publicinterfaceIMetricsSource{publicvoidRecordObservableInstruments();}publicinterfaceIMetricsListener{publicstringName{get;}publicvoidSetSource(IMetricsSourcesource);publicboolInstrumentPublished(Instrumentinstrument,outobject?userState);publicvoidMeasurementsCompleted(Instrumentinstrument,object?userState);publicMeasurementCallback<T>GetMeasurementHandler<T>();}publicclassMetricsEnableOptions{publicIList<InstrumentEnableRule>Rules{get;}}publicclassInstrumentEnableRule{publicInstrumentEnableRule(string?listenerName,string?meterName,MeterScopescopes,string?instrumentName,boolenable);publicstring?ListenerName{get;}publicstring?MeterName{get;}publicMeterScopeScopes{get;}publicstring?InstrumentName{get;}publicboolEnable{get;}}[Flags]publicenumMeterScope{Global,Local}publicinterfaceIMetricsSubscriptionManager{publicvoidStart();}publicstaticMetricsBuilderEnableExtensions{publicstaticIMetricsBuilderEnableMetrics(thisIMetricsBuilderbuilder)=>thrownull!;publicstaticIMetricsBuilderEnableMetrics(thisIMetricsBuilderbuilder,string?meterName)=>thrownull!;publicstaticIMetricsBuilderEnableMetrics(thisIMetricsBuilderbuilder,string?meterName,string?instrumentName)=>thrownull!;publicstaticIMetricsBuilderEnableMetrics(thisIMetricsBuilderbuilder,string?meterName,string?instrumentName,string?listenerName)=>thrownull!;publicstaticIMetricsBuilderEnableMetrics(thisIMetricsBuilderbuilder,string?meterName,string?instrumentName,string?listenerName,MeterScopescopes)=>thrownull!;publicstaticMetricsEnableOptionsEnableMetrics(thisMetricsEnableOptionsoptions)=>thrownull!;publicstaticMetricsEnableOptionsEnableMetrics(thisMetricsEnableOptionsoptions,string?meterName)=>thrownull!;publicstaticMetricsEnableOptionsEnableMetrics(thisMetricsEnableOptionsoptions,string?meterName,string?instrumentName)=>thrownull!;publicstaticMetricsEnableOptionsEnableMetrics(thisMetricsEnableOptionsoptions,string?meterName,string?instrumentName,string?listenerName)=>thrownull!;publicstaticMetricsEnableOptionsEnableMetrics(thisMetricsEnableOptionsoptions,string?meterName,string?instrumentName,string?listenerName,MeterScopescopes)=>thrownull!;// Which overloads of this do we want?publicstaticMetricsEnableOptionsDisableMetrics(thisMetricsEnableOptionsoptions,string?meterName,string?instrumentName,string?listenerName,MeterScopescopes)=>thrownull!;}

            Integration with configuration

            We want users to be able to configure which metrics to capture for different listeners using appsettings.json or other
            IConfiguration sources

            Data format

            Metrics is the top level section under which "EnabledMetrics" configures which metrics are enabled for all listeners and
            a section named with a listener name adds additional rules specific to that listener. The key under "EnabledMetrics" is a
            Meter name prefix and the value is true if the Meter should be enabled or false if disabled.

            {
            "Metrics": {
            "EnabledMetrics": {
            "System.Runtime": true
            },
            "OpenTelemetry": {
            "EnabledMetrics": {
            "Microsoft.AspNetCore": true
            }
            }
            }
            }

            Similar to logging, "Default" acts as a zero-length meter prefix creating a default rule applying to all Meters if no
            longer prefix rule matches. Logically this allows creating opt-in or opt-out policies. If no rule matches a Meter it will
            not be enabled, so omitting "Default" is the same behavior as including Default=false.

            {
            "Metrics": {
            "EnabledMetrics": {
            "Default": true"NoisyLibrary": false
            },
            }
            }

            To enable individual instruments within a Meter replace the boolean with object syntax where the keys are instrument names or prefixes
            and the value is true iff enabled. "Default" is also honored at this scope as the zero-length instrument name prefix. Not specifying a
            "Default" is the same as "Default": false.

            {
            "Metrics": {
            "EnabledMetrics": {
            "System.Runtime": {
            "gc": true,
            "jit": true
            }
            "Microsoft.AspNet.Core": {
            "Default": true,
            "connection-duration": false
            }
            }
            }
            }

            To make rules apply only to Meters at a specific Global or Local scope instead of "EnabledMetrics" you can use "EnabledGlobalMetrics" or
            "EnabledLocalMetrics". Rules in a "EnabledMetrics" section apply to Meters in both scopes. The expectation is that using this local/global config is rare and maybe we should just remove it?

            {
            "Metrics": {
            "EnabledGlobalMetrics": {
            "System.Runtime": true
            }
            "EnabledLocalMetrics": {
            "System.Net.Http": true
            }
            "EnabledMetrics": {
            "Meter1": true,
            "Meter2": true
            }
            }
            }

            API

            Microsoft.Extensions.Diagnostics.Configuration.dll:

            namespaceMicrosoft.Extensions.Diagnostics.Metrics{publicstaticclassMetricsBuilderExtensions{publicstaticIMetricsBuilderAddConfiguration(thisIMetricsBuilderbuilder,IConfigurationconfiguration);}}namespaceMicrosoft.Extensions.Diagnostics.Metrics.Configuration{publicinterfaceIMetricListenerConfigurationFactory{IConfigurationGetConfiguration(TypelistenerType);}publicinterfaceIMetricListenerConfiguration<T>{IConfigurationConfiguration{get;}}}

            Integration with Hosting APIs

            We should include a Metrics property in parallel where existing hosting APIs expose Logging

            publicclassHostApplicationBuilder//pre-existing{publicIMetricsBuilderMetrics{get;}}publicclassWebApplicationBuilder//pre-existing{publicIMetricsBuilderMetrics{get;}}publicstaticclassHostingHostBuilderExtensions//pre-existing{publicstaticIHostBuilderConfigureMetrics(thisIHostBuilderhostBuilder,Action<IMetricsBuilder>configureMetrics);publicstaticIHostBuilderConfigureMetrics(thisIHostBuilderhostBuilder,Action<HostBuilderContext,IMetricsBuilder>configureMetrics);}

            Default sinks

            We should include a simple console output of the metric measurements intended for debugging purposes.
            This doesn't have any of the customization that the ILogger console does because it isn't intended to be left
            enabled in production code. Recording raw measurements to text is very inefficient.

            // All in Microsoft.Extensions.Diagnostics.dllnamespaceMicrosoft.Extensions.Diagnostics.MetricspublicstaticclassMetricsBuilderConsoleExtensions{publicstaticIMetricsBuilderAddDebugConsole(thisIMetricsBuilderbuilder);}publicstaticclassConsoleMetrics{publicstaticstringListenerName=>"Console";}

            API Usage

            App dev sends the default metrics telemetry to an OLTP endpoint using OpenTelemetry

            Details NOTE: This scenario may be sending metric data that doesn't conform to OpenTelemetry naming conventions
            varbuilder=WebApplication.CreateBuilder(args);builder.Metrics.AddOpenTelemetry(builder =>builder.AddOtlpExporter());

            The defaults are controlled from appsettings.json which would contain this in a new web app template:

            {
            "Metrics": {
            "EnabledMetrics": {
            "System": true,
            "Microsoft.AspNetCore": true
            }
            }
            }

            The specific destination for the telemetry is picked up by OTel from the environment variable OTEL_EXPORTER_OTLP_ENDPOINT. Other variations allow the configuration to be passed as a parameter ot AddOltpExporter() or using the service container to configure the
            OtlpExporterOptions object.

            The AddOpenTelemetry() call here is a hypothetical extension method implemented by the OTel.NET project similar to their current
            ILoggingBuilder.AddOpenTelemetry() API.

            App dev wants to send default metrics via Prometheus.NET

            Details
            varbuilder=WebApplication.CreateBuilder(args);builder.Metrics.AddPrometheus();[omittingstandard webapp code here]
            app.MapMetrics();

            Prometheus requires Kestrel to host its scrape endpoint which is why it integrates again in app.UseEndpoints(). The same appsettings.json as above (not shown) is controlling the default metrics being sent to Prometheus.

            App dev sends the default metrics telemetry, named with OTel conventions, to an OLTP endpoint using OpenTelemetry

            Details

            Option #1: Use instrumentation libraries provided by OpenTelemetry

            varbuilder=WebApplication.CreateBuilder(args);builder.Metrics.AddRuntimeMetrics();builder.Metrics.AddAspNetCoreMetrics();builder.Metrics.AddOpenTelemetry(builder =>builder.AddOtlpExporter());

            The AddXXXMetrics() APIs are new hypothetical extension methods provided by OpenTelemetry instrumentation libraries. These calls produce
            metrics under a Meter name defined by the OTel instrumentation libraries such as "OpenTelemetry.Instrumentation.AspNetCore". If the
            metrics are left enabled in appsettings.json then both OpenTelmetry and built-in metrics would be transmitted. The app developer
            should exlucde built-in metrics from the list if they don't want both.

            Option #2: Use APIs provided by runtime libraries (Optional future development, does not ship in .NET 8)

            varbuilder=WebApplication.CreateBuilder(args);builder.Metrics.AddRuntimeMetrics(RuntimeMetricNaming.OpenTelemetry1_0);builder.Metrics.AddAspNetCoreMetrics(AspNetCoreMetricNaming.OpenTelemetry1_0);builder.Metrics.AddOpenTelemetry(builder =>builder.AddOtlpExporter());

            Once OpenTelemetry has stable conventions it is possible for the libraries that produce metrics to also include extension methods that
            apply OpenTelemetry naming rules. The enumeration would likely start with two options, the default metric names shipped originally and
            OpenTelemetry's names. Over time more options might be added to the enumeration for updated versions of OpenTelemetry's conventions or
            other standards that have gained a broad interest. Unlike option #1, these metrics are produced with the default Meter name for the
            library such as "Microsoft.AspNetCore" and it replaces the default metric rather than adding new metrics in parallel.

            App dev wants to enable metrics from certain Meters to get sent (config-based approach)

            Details
            {
            "Metrics": {
            "EnabledMetrics": {
            "System": true,
            "Microsoft.AspNetCore": true"Microsoft.Extensions.Caching": true// hypothetical runtime provided Meter"StackExchange.Redis": true// hypothetical Meter in a 3P library
            }
            }
            }

            App dev wants to enable metrics from certain Meters to get sent (programmatic approach)

            Details
            varbuilder=WebApplication.CreateBuilder(args);builder.Metrics.EnableMetrics("Microsoft.Extensions.Caching").EnableMetrics("StackExchange.Redis").AddOpenTelemetry(builder =>builder.AddOtlpExporter());

            App dev wants to enable metrics from certain Meters to get sent (custom-instrumentation approach)

            Details

            Meters need to be created in order for turning them on to have any effect. In some cases it may be convenient to both create them
            and turn them on as a single step using a strongly typed extension method:

            varbuilder=WebApplication.CreateBuilder(args);builder.Metrics.AddSqlClientInstrumentation().AddOpenTelemetry(builder =>builder.AddOtlpExporter());

            This AddSqlClientInstrumentation method is a hypothetical extension method that could be offered by an OpenTelemetry instrumentation package. Note that some instrumentation packages are likely to be duplicative with built-in metrics. If the app developer doesn't want
            both then they should ensure the built-in metrics aren't included in their configuration.

            App dev wants to disable sending metrics for some instruments on a Meter (config-based approach)

            Details
            {
            "Metrics": {
            "EnabledMetrics": {
            "System": true,
            "Microsoft.AspNet.Core": {
            "Default": true, // enable default metrics for all the instruments"connection-duration": false// but exclude the connection-duration metric
            }
            }
            }
            }

            App dev wants to only enable specific instruments on a Meter (config-based approach)

            Details
            {
            "Metrics": {
            "EnabledMetrics": {
            "System": true,
            "Microsoft.AspNet.Core": {
            "connection-duration": true// enable only the connection-duration metric
            }
            }
            }
            }

            App dev wants to only enable specific instruments on a Meter (API-based approach)

            Details
            varbuilder=WebApplication.CreateBuilder(args);builder.Metrics// enable only the duration-counter metric.EnableMetrics("Microsoft.AspNetCore","duration-counter");.AddOpenTelemetry(builder =>builder.AddOtlpExporter());

            App dev wants to disable sending metrics for some instruments on a Meter (API-based approach)

            Details
            varbuilder=WebApplication.CreateBuilder(args);builder.Metrics// enable all default metrics for all instruments on this Meter except duration-counter.EnableMetrics("Microsoft.AspNetCore", instrument =>instrument.Name!="duration-counter");.AddOpenTelemetry(builder =>builder.AddOtlpExporter());

            The idea above is that the HostBuilder.Metrics property is a new MetricsBuilder type, the host defaults automatically constructed MetricsBuilder to load configuration from appsettings.json, and the config file specified which Meters to enable. The hypothetical AddOpenTelemetry() API is an extension method that would be provided by the OpenTelemetry NuGet package, very similar to the extension method they provide for LoggingBuilder.

            Metadata

            Metadata

            Labels

            api-approvedAPI was approved in API review, it can be implementedarea-System.Diagnostics.MetricblockingMarks issues that we want to fast track in order to unblock other important workenhancementProduct code improvement that does NOT require public API changes/additionspartner-impactThis issue impacts a partner who needs to be kept updated

            Type

            No type

            Projects

            No projects

              Milestone

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
              Skip to content

              [API Proposal]: Meter Configuration and Pipeline #85684

              Description

              @noahfalk

              Background and motivation

              In the current runtime APIs for metrics we have the ability to create and use Meters, and in-progress we have work to make it easier to consume those APIs from DI-centered workloads. However if an app developer wants to configure a set of Meters to enable and where to export that data that still requires 3rd party APIs such as OpenTelemetry, AppMetrics, or Prometheus.NET. This feature is to support a built-in option for configuring which Meters are enabled and one or more sinks to send that data to. The configuration should support being done both programmatically and via external configuration sources. For sinks I'd expect a neutral interface that any 3rd party component could use but we should look most closely at supporting the already existing 3rd party libraries.

              Design is still very uncertain at this point but the initial thought is to follow the lead of the Logging APIs which have LoggingBuilder, LoggingBuilder.AddFilter(), and LoggingBuilder.AddProvider().

              API Proposal

              Microsoft.Extensions.Diagnostics assembly

              namespaceMicrosoft.Extensions.DependencyInjection;publicstaticclassMetricsServiceCollectionExtensions{publicstaticIServiceCollectionAddMetrics(thisIServiceCollection);publicstaticIServiceCollectionAddMetrics(thisIServiceCollection,Action<IMetricsBuilder>configure);}

              Microsoft.Extensions.Diagnostics.Abstractions assembly

              namespaceMicrosoft.Extensions.Diagnostics.Metrics;publicinterfaceIMetricsBuilder{IServiceCollectionServices{get;}}publicstaticclassMetricsBuilderExtensions{publicstaticIMetricsBuilderAddListener(thisIMetricsBuilderbuilder,IMetricsListenerlistener);publicstaticIMetricsBuilderAddListener<T>(thisIMetricsBuilderbuilder)whereT:class,IMetricsListenerpublicstaticIMetricsBuilderClearListeners(thisIMetricsBuilderbuilder);}publicinterfaceIMetricsSource{publicvoidRecordObservableInstruments();}publicinterfaceIMetricsListener{publicstringName{get;}publicvoidSetSource(IMetricsSourcesource);publicboolInstrumentPublished(Instrumentinstrument,outobject?userState);publicvoidMeasurementsCompleted(Instrumentinstrument,object?userState);publicMeasurementCallback<T>GetMeasurementHandler<T>();}publicclassMetricsEnableOptions{publicIList<InstrumentEnableRule>Rules{get;}}publicclassInstrumentEnableRule{publicInstrumentEnableRule(string?listenerName,string?meterName,MeterScopescopes,string?instrumentName,boolenable);publicstring?ListenerName{get;}publicstring?MeterName{get;}publicMeterScopeScopes{get;}publicstring?InstrumentName{get;}publicboolEnable{get;}}[Flags]publicenumMeterScope{Global,Local}publicinterfaceIMetricsSubscriptionManager{publicvoidStart();}publicstaticMetricsBuilderEnableExtensions{publicstaticIMetricsBuilderEnableMetrics(thisIMetricsBuilderbuilder)=>thrownull!;publicstaticIMetricsBuilderEnableMetrics(thisIMetricsBuilderbuilder,string?meterName)=>thrownull!;publicstaticIMetricsBuilderEnableMetrics(thisIMetricsBuilderbuilder,string?meterName,string?instrumentName)=>thrownull!;publicstaticIMetricsBuilderEnableMetrics(thisIMetricsBuilderbuilder,string?meterName,string?instrumentName,string?listenerName)=>thrownull!;publicstaticIMetricsBuilderEnableMetrics(thisIMetricsBuilderbuilder,string?meterName,string?instrumentName,string?listenerName,MeterScopescopes)=>thrownull!;publicstaticMetricsEnableOptionsEnableMetrics(thisMetricsEnableOptionsoptions)=>thrownull!;publicstaticMetricsEnableOptionsEnableMetrics(thisMetricsEnableOptionsoptions,string?meterName)=>thrownull!;publicstaticMetricsEnableOptionsEnableMetrics(thisMetricsEnableOptionsoptions,string?meterName,string?instrumentName)=>thrownull!;publicstaticMetricsEnableOptionsEnableMetrics(thisMetricsEnableOptionsoptions,string?meterName,string?instrumentName,string?listenerName)=>thrownull!;publicstaticMetricsEnableOptionsEnableMetrics(thisMetricsEnableOptionsoptions,string?meterName,string?instrumentName,string?listenerName,MeterScopescopes)=>thrownull!;// Which overloads of this do we want?publicstaticMetricsEnableOptionsDisableMetrics(thisMetricsEnableOptionsoptions,string?meterName,string?instrumentName,string?listenerName,MeterScopescopes)=>thrownull!;}

              Integration with configuration

              We want users to be able to configure which metrics to capture for different listeners using appsettings.json or other
              IConfiguration sources

              Data format

              Metrics is the top level section under which "EnabledMetrics" configures which metrics are enabled for all listeners and
              a section named with a listener name adds additional rules specific to that listener. The key under "EnabledMetrics" is a
              Meter name prefix and the value is true if the Meter should be enabled or false if disabled.

              {
              "Metrics": {
              "EnabledMetrics": {
              "System.Runtime": true
              },
              "OpenTelemetry": {
              "EnabledMetrics": {
              "Microsoft.AspNetCore": true
              }
              }
              }
              }

              Similar to logging, "Default" acts as a zero-length meter prefix creating a default rule applying to all Meters if no
              longer prefix rule matches. Logically this allows creating opt-in or opt-out policies. If no rule matches a Meter it will
              not be enabled, so omitting "Default" is the same behavior as including Default=false.

              {
              "Metrics": {
              "EnabledMetrics": {
              "Default": true"NoisyLibrary": false
              },
              }
              }

              To enable individual instruments within a Meter replace the boolean with object syntax where the keys are instrument names or prefixes
              and the value is true iff enabled. "Default" is also honored at this scope as the zero-length instrument name prefix. Not specifying a
              "Default" is the same as "Default": false.

              {
              "Metrics": {
              "EnabledMetrics": {
              "System.Runtime": {
              "gc": true,
              "jit": true
              }
              "Microsoft.AspNet.Core": {
              "Default": true,
              "connection-duration": false
              }
              }
              }
              }

              To make rules apply only to Meters at a specific Global or Local scope instead of "EnabledMetrics" you can use "EnabledGlobalMetrics" or
              "EnabledLocalMetrics". Rules in a "EnabledMetrics" section apply to Meters in both scopes. The expectation is that using this local/global config is rare and maybe we should just remove it?

              {
              "Metrics": {
              "EnabledGlobalMetrics": {
              "System.Runtime": true
              }
              "EnabledLocalMetrics": {
              "System.Net.Http": true
              }
              "EnabledMetrics": {
              "Meter1": true,
              "Meter2": true
              }
              }
              }

              API

              Microsoft.Extensions.Diagnostics.Configuration.dll:

              namespaceMicrosoft.Extensions.Diagnostics.Metrics{publicstaticclassMetricsBuilderExtensions{publicstaticIMetricsBuilderAddConfiguration(thisIMetricsBuilderbuilder,IConfigurationconfiguration);}}namespaceMicrosoft.Extensions.Diagnostics.Metrics.Configuration{publicinterfaceIMetricListenerConfigurationFactory{IConfigurationGetConfiguration(TypelistenerType);}publicinterfaceIMetricListenerConfiguration<T>{IConfigurationConfiguration{get;}}}

              Integration with Hosting APIs

              We should include a Metrics property in parallel where existing hosting APIs expose Logging

              publicclassHostApplicationBuilder//pre-existing{publicIMetricsBuilderMetrics{get;}}publicclassWebApplicationBuilder//pre-existing{publicIMetricsBuilderMetrics{get;}}publicstaticclassHostingHostBuilderExtensions//pre-existing{publicstaticIHostBuilderConfigureMetrics(thisIHostBuilderhostBuilder,Action<IMetricsBuilder>configureMetrics);publicstaticIHostBuilderConfigureMetrics(thisIHostBuilderhostBuilder,Action<HostBuilderContext,IMetricsBuilder>configureMetrics);}

              Default sinks

              We should include a simple console output of the metric measurements intended for debugging purposes.
              This doesn't have any of the customization that the ILogger console does because it isn't intended to be left
              enabled in production code. Recording raw measurements to text is very inefficient.

              // All in Microsoft.Extensions.Diagnostics.dllnamespaceMicrosoft.Extensions.Diagnostics.MetricspublicstaticclassMetricsBuilderConsoleExtensions{publicstaticIMetricsBuilderAddDebugConsole(thisIMetricsBuilderbuilder);}publicstaticclassConsoleMetrics{publicstaticstringListenerName=>"Console";}

              API Usage

              App dev sends the default metrics telemetry to an OLTP endpoint using OpenTelemetry

              Details NOTE: This scenario may be sending metric data that doesn't conform to OpenTelemetry naming conventions
              varbuilder=WebApplication.CreateBuilder(args);builder.Metrics.AddOpenTelemetry(builder =>builder.AddOtlpExporter());

              The defaults are controlled from appsettings.json which would contain this in a new web app template:

              {
              "Metrics": {
              "EnabledMetrics": {
              "System": true,
              "Microsoft.AspNetCore": true
              }
              }
              }

              The specific destination for the telemetry is picked up by OTel from the environment variable OTEL_EXPORTER_OTLP_ENDPOINT. Other variations allow the configuration to be passed as a parameter ot AddOltpExporter() or using the service container to configure the
              OtlpExporterOptions object.

              The AddOpenTelemetry() call here is a hypothetical extension method implemented by the OTel.NET project similar to their current
              ILoggingBuilder.AddOpenTelemetry() API.

              App dev wants to send default metrics via Prometheus.NET

              Details
              varbuilder=WebApplication.CreateBuilder(args);builder.Metrics.AddPrometheus();[omittingstandard webapp code here]
              app.MapMetrics();

              Prometheus requires Kestrel to host its scrape endpoint which is why it integrates again in app.UseEndpoints(). The same appsettings.json as above (not shown) is controlling the default metrics being sent to Prometheus.

              App dev sends the default metrics telemetry, named with OTel conventions, to an OLTP endpoint using OpenTelemetry

              Details

              Option #1: Use instrumentation libraries provided by OpenTelemetry

              varbuilder=WebApplication.CreateBuilder(args);builder.Metrics.AddRuntimeMetrics();builder.Metrics.AddAspNetCoreMetrics();builder.Metrics.AddOpenTelemetry(builder =>builder.AddOtlpExporter());

              The AddXXXMetrics() APIs are new hypothetical extension methods provided by OpenTelemetry instrumentation libraries. These calls produce
              metrics under a Meter name defined by the OTel instrumentation libraries such as "OpenTelemetry.Instrumentation.AspNetCore". If the
              metrics are left enabled in appsettings.json then both OpenTelmetry and built-in metrics would be transmitted. The app developer
              should exlucde built-in metrics from the list if they don't want both.

              Option #2: Use APIs provided by runtime libraries (Optional future development, does not ship in .NET 8)

              varbuilder=WebApplication.CreateBuilder(args);builder.Metrics.AddRuntimeMetrics(RuntimeMetricNaming.OpenTelemetry1_0);builder.Metrics.AddAspNetCoreMetrics(AspNetCoreMetricNaming.OpenTelemetry1_0);builder.Metrics.AddOpenTelemetry(builder =>builder.AddOtlpExporter());

              Once OpenTelemetry has stable conventions it is possible for the libraries that produce metrics to also include extension methods that
              apply OpenTelemetry naming rules. The enumeration would likely start with two options, the default metric names shipped originally and
              OpenTelemetry's names. Over time more options might be added to the enumeration for updated versions of OpenTelemetry's conventions or
              other standards that have gained a broad interest. Unlike option #1, these metrics are produced with the default Meter name for the
              library such as "Microsoft.AspNetCore" and it replaces the default metric rather than adding new metrics in parallel.

              App dev wants to enable metrics from certain Meters to get sent (config-based approach)

              Details
              {
              "Metrics": {
              "EnabledMetrics": {
              "System": true,
              "Microsoft.AspNetCore": true"Microsoft.Extensions.Caching": true// hypothetical runtime provided Meter"StackExchange.Redis": true// hypothetical Meter in a 3P library
              }
              }
              }

              App dev wants to enable metrics from certain Meters to get sent (programmatic approach)

              Details
              varbuilder=WebApplication.CreateBuilder(args);builder.Metrics.EnableMetrics("Microsoft.Extensions.Caching").EnableMetrics("StackExchange.Redis").AddOpenTelemetry(builder =>builder.AddOtlpExporter());

              App dev wants to enable metrics from certain Meters to get sent (custom-instrumentation approach)

              Details

              Meters need to be created in order for turning them on to have any effect. In some cases it may be convenient to both create them
              and turn them on as a single step using a strongly typed extension method:

              varbuilder=WebApplication.CreateBuilder(args);builder.Metrics.AddSqlClientInstrumentation().AddOpenTelemetry(builder =>builder.AddOtlpExporter());

              This AddSqlClientInstrumentation method is a hypothetical extension method that could be offered by an OpenTelemetry instrumentation package. Note that some instrumentation packages are likely to be duplicative with built-in metrics. If the app developer doesn't want
              both then they should ensure the built-in metrics aren't included in their configuration.

              App dev wants to disable sending metrics for some instruments on a Meter (config-based approach)

              Details
              {
              "Metrics": {
              "EnabledMetrics": {
              "System": true,
              "Microsoft.AspNet.Core": {
              "Default": true, // enable default metrics for all the instruments"connection-duration": false// but exclude the connection-duration metric
              }
              }
              }
              }

              App dev wants to only enable specific instruments on a Meter (config-based approach)

              Details
              {
              "Metrics": {
              "EnabledMetrics": {
              "System": true,
              "Microsoft.AspNet.Core": {
              "connection-duration": true// enable only the connection-duration metric
              }
              }
              }
              }

              App dev wants to only enable specific instruments on a Meter (API-based approach)

              Details
              varbuilder=WebApplication.CreateBuilder(args);builder.Metrics// enable only the duration-counter metric.EnableMetrics("Microsoft.AspNetCore","duration-counter");.AddOpenTelemetry(builder =>builder.AddOtlpExporter());

              App dev wants to disable sending metrics for some instruments on a Meter (API-based approach)

              Details
              varbuilder=WebApplication.CreateBuilder(args);builder.Metrics// enable all default metrics for all instruments on this Meter except duration-counter.EnableMetrics("Microsoft.AspNetCore", instrument =>instrument.Name!="duration-counter");.AddOpenTelemetry(builder =>builder.AddOtlpExporter());

              The idea above is that the HostBuilder.Metrics property is a new MetricsBuilder type, the host defaults automatically constructed MetricsBuilder to load configuration from appsettings.json, and the config file specified which Meters to enable. The hypothetical AddOpenTelemetry() API is an extension method that would be provided by the OpenTelemetry NuGet package, very similar to the extension method they provide for LoggingBuilder.

              Metadata

              Metadata

              Labels

              api-approvedAPI was approved in API review, it can be implementedarea-System.Diagnostics.MetricblockingMarks issues that we want to fast track in order to unblock other important workenhancementProduct code improvement that does NOT require public API changes/additionspartner-impactThis issue impacts a partner who needs to be kept updated

              Type

              No type

              Projects

              No projects

                Milestone

                Relationships

                None yet

                Development

                No branches or pull requests

                Issue actions

                , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
                Skip to content

                [API Proposal]: Meter Configuration and Pipeline #85684

                Description

                @noahfalk

                Background and motivation

                In the current runtime APIs for metrics we have the ability to create and use Meters, and in-progress we have work to make it easier to consume those APIs from DI-centered workloads. However if an app developer wants to configure a set of Meters to enable and where to export that data that still requires 3rd party APIs such as OpenTelemetry, AppMetrics, or Prometheus.NET. This feature is to support a built-in option for configuring which Meters are enabled and one or more sinks to send that data to. The configuration should support being done both programmatically and via external configuration sources. For sinks I'd expect a neutral interface that any 3rd party component could use but we should look most closely at supporting the already existing 3rd party libraries.

                Design is still very uncertain at this point but the initial thought is to follow the lead of the Logging APIs which have LoggingBuilder, LoggingBuilder.AddFilter(), and LoggingBuilder.AddProvider().

                API Proposal

                Microsoft.Extensions.Diagnostics assembly

                namespaceMicrosoft.Extensions.DependencyInjection;publicstaticclassMetricsServiceCollectionExtensions{publicstaticIServiceCollectionAddMetrics(thisIServiceCollection);publicstaticIServiceCollectionAddMetrics(thisIServiceCollection,Action<IMetricsBuilder>configure);}

                Microsoft.Extensions.Diagnostics.Abstractions assembly

                namespaceMicrosoft.Extensions.Diagnostics.Metrics;publicinterfaceIMetricsBuilder{IServiceCollectionServices{get;}}publicstaticclassMetricsBuilderExtensions{publicstaticIMetricsBuilderAddListener(thisIMetricsBuilderbuilder,IMetricsListenerlistener);publicstaticIMetricsBuilderAddListener<T>(thisIMetricsBuilderbuilder)whereT:class,IMetricsListenerpublicstaticIMetricsBuilderClearListeners(thisIMetricsBuilderbuilder);}publicinterfaceIMetricsSource{publicvoidRecordObservableInstruments();}publicinterfaceIMetricsListener{publicstringName{get;}publicvoidSetSource(IMetricsSourcesource);publicboolInstrumentPublished(Instrumentinstrument,outobject?userState);publicvoidMeasurementsCompleted(Instrumentinstrument,object?userState);publicMeasurementCallback<T>GetMeasurementHandler<T>();}publicclassMetricsEnableOptions{publicIList<InstrumentEnableRule>Rules{get;}}publicclassInstrumentEnableRule{publicInstrumentEnableRule(string?listenerName,string?meterName,MeterScopescopes,string?instrumentName,boolenable);publicstring?ListenerName{get;}publicstring?MeterName{get;}publicMeterScopeScopes{get;}publicstring?InstrumentName{get;}publicboolEnable{get;}}[Flags]publicenumMeterScope{Global,Local}publicinterfaceIMetricsSubscriptionManager{publicvoidStart();}publicstaticMetricsBuilderEnableExtensions{publicstaticIMetricsBuilderEnableMetrics(thisIMetricsBuilderbuilder)=>thrownull!;publicstaticIMetricsBuilderEnableMetrics(thisIMetricsBuilderbuilder,string?meterName)=>thrownull!;publicstaticIMetricsBuilderEnableMetrics(thisIMetricsBuilderbuilder,string?meterName,string?instrumentName)=>thrownull!;publicstaticIMetricsBuilderEnableMetrics(thisIMetricsBuilderbuilder,string?meterName,string?instrumentName,string?listenerName)=>thrownull!;publicstaticIMetricsBuilderEnableMetrics(thisIMetricsBuilderbuilder,string?meterName,string?instrumentName,string?listenerName,MeterScopescopes)=>thrownull!;publicstaticMetricsEnableOptionsEnableMetrics(thisMetricsEnableOptionsoptions)=>thrownull!;publicstaticMetricsEnableOptionsEnableMetrics(thisMetricsEnableOptionsoptions,string?meterName)=>thrownull!;publicstaticMetricsEnableOptionsEnableMetrics(thisMetricsEnableOptionsoptions,string?meterName,string?instrumentName)=>thrownull!;publicstaticMetricsEnableOptionsEnableMetrics(thisMetricsEnableOptionsoptions,string?meterName,string?instrumentName,string?listenerName)=>thrownull!;publicstaticMetricsEnableOptionsEnableMetrics(thisMetricsEnableOptionsoptions,string?meterName,string?instrumentName,string?listenerName,MeterScopescopes)=>thrownull!;// Which overloads of this do we want?publicstaticMetricsEnableOptionsDisableMetrics(thisMetricsEnableOptionsoptions,string?meterName,string?instrumentName,string?listenerName,MeterScopescopes)=>thrownull!;}

                Integration with configuration

                We want users to be able to configure which metrics to capture for different listeners using appsettings.json or other
                IConfiguration sources

                Data format

                Metrics is the top level section under which "EnabledMetrics" configures which metrics are enabled for all listeners and
                a section named with a listener name adds additional rules specific to that listener. The key under "EnabledMetrics" is a
                Meter name prefix and the value is true if the Meter should be enabled or false if disabled.

                {
                "Metrics": {
                "EnabledMetrics": {
                "System.Runtime": true
                },
                "OpenTelemetry": {
                "EnabledMetrics": {
                "Microsoft.AspNetCore": true
                }
                }
                }
                }

                Similar to logging, "Default" acts as a zero-length meter prefix creating a default rule applying to all Meters if no
                longer prefix rule matches. Logically this allows creating opt-in or opt-out policies. If no rule matches a Meter it will
                not be enabled, so omitting "Default" is the same behavior as including Default=false.

                {
                "Metrics": {
                "EnabledMetrics": {
                "Default": true"NoisyLibrary": false
                },
                }
                }

                To enable individual instruments within a Meter replace the boolean with object syntax where the keys are instrument names or prefixes
                and the value is true iff enabled. "Default" is also honored at this scope as the zero-length instrument name prefix. Not specifying a
                "Default" is the same as "Default": false.

                {
                "Metrics": {
                "EnabledMetrics": {
                "System.Runtime": {
                "gc": true,
                "jit": true
                }
                "Microsoft.AspNet.Core": {
                "Default": true,
                "connection-duration": false
                }
                }
                }
                }

                To make rules apply only to Meters at a specific Global or Local scope instead of "EnabledMetrics" you can use "EnabledGlobalMetrics" or
                "EnabledLocalMetrics". Rules in a "EnabledMetrics" section apply to Meters in both scopes. The expectation is that using this local/global config is rare and maybe we should just remove it?

                {
                "Metrics": {
                "EnabledGlobalMetrics": {
                "System.Runtime": true
                }
                "EnabledLocalMetrics": {
                "System.Net.Http": true
                }
                "EnabledMetrics": {
                "Meter1": true,
                "Meter2": true
                }
                }
                }

                API

                Microsoft.Extensions.Diagnostics.Configuration.dll:

                namespaceMicrosoft.Extensions.Diagnostics.Metrics{publicstaticclassMetricsBuilderExtensions{publicstaticIMetricsBuilderAddConfiguration(thisIMetricsBuilderbuilder,IConfigurationconfiguration);}}namespaceMicrosoft.Extensions.Diagnostics.Metrics.Configuration{publicinterfaceIMetricListenerConfigurationFactory{IConfigurationGetConfiguration(TypelistenerType);}publicinterfaceIMetricListenerConfiguration<T>{IConfigurationConfiguration{get;}}}

                Integration with Hosting APIs

                We should include a Metrics property in parallel where existing hosting APIs expose Logging

                publicclassHostApplicationBuilder//pre-existing{publicIMetricsBuilderMetrics{get;}}publicclassWebApplicationBuilder//pre-existing{publicIMetricsBuilderMetrics{get;}}publicstaticclassHostingHostBuilderExtensions//pre-existing{publicstaticIHostBuilderConfigureMetrics(thisIHostBuilderhostBuilder,Action<IMetricsBuilder>configureMetrics);publicstaticIHostBuilderConfigureMetrics(thisIHostBuilderhostBuilder,Action<HostBuilderContext,IMetricsBuilder>configureMetrics);}

                Default sinks

                We should include a simple console output of the metric measurements intended for debugging purposes.
                This doesn't have any of the customization that the ILogger console does because it isn't intended to be left
                enabled in production code. Recording raw measurements to text is very inefficient.

                // All in Microsoft.Extensions.Diagnostics.dllnamespaceMicrosoft.Extensions.Diagnostics.MetricspublicstaticclassMetricsBuilderConsoleExtensions{publicstaticIMetricsBuilderAddDebugConsole(thisIMetricsBuilderbuilder);}publicstaticclassConsoleMetrics{publicstaticstringListenerName=>"Console";}

                API Usage

                App dev sends the default metrics telemetry to an OLTP endpoint using OpenTelemetry

                Details NOTE: This scenario may be sending metric data that doesn't conform to OpenTelemetry naming conventions
                varbuilder=WebApplication.CreateBuilder(args);builder.Metrics.AddOpenTelemetry(builder =>builder.AddOtlpExporter());

                The defaults are controlled from appsettings.json which would contain this in a new web app template:

                {
                "Metrics": {
                "EnabledMetrics": {
                "System": true,
                "Microsoft.AspNetCore": true
                }
                }
                }

                The specific destination for the telemetry is picked up by OTel from the environment variable OTEL_EXPORTER_OTLP_ENDPOINT. Other variations allow the configuration to be passed as a parameter ot AddOltpExporter() or using the service container to configure the
                OtlpExporterOptions object.

                The AddOpenTelemetry() call here is a hypothetical extension method implemented by the OTel.NET project similar to their current
                ILoggingBuilder.AddOpenTelemetry() API.

                App dev wants to send default metrics via Prometheus.NET

                Details
                varbuilder=WebApplication.CreateBuilder(args);builder.Metrics.AddPrometheus();[omittingstandard webapp code here]
                app.MapMetrics();

                Prometheus requires Kestrel to host its scrape endpoint which is why it integrates again in app.UseEndpoints(). The same appsettings.json as above (not shown) is controlling the default metrics being sent to Prometheus.

                App dev sends the default metrics telemetry, named with OTel conventions, to an OLTP endpoint using OpenTelemetry

                Details

                Option #1: Use instrumentation libraries provided by OpenTelemetry

                varbuilder=WebApplication.CreateBuilder(args);builder.Metrics.AddRuntimeMetrics();builder.Metrics.AddAspNetCoreMetrics();builder.Metrics.AddOpenTelemetry(builder =>builder.AddOtlpExporter());

                The AddXXXMetrics() APIs are new hypothetical extension methods provided by OpenTelemetry instrumentation libraries. These calls produce
                metrics under a Meter name defined by the OTel instrumentation libraries such as "OpenTelemetry.Instrumentation.AspNetCore". If the
                metrics are left enabled in appsettings.json then both OpenTelmetry and built-in metrics would be transmitted. The app developer
                should exlucde built-in metrics from the list if they don't want both.

                Option #2: Use APIs provided by runtime libraries (Optional future development, does not ship in .NET 8)

                varbuilder=WebApplication.CreateBuilder(args);builder.Metrics.AddRuntimeMetrics(RuntimeMetricNaming.OpenTelemetry1_0);builder.Metrics.AddAspNetCoreMetrics(AspNetCoreMetricNaming.OpenTelemetry1_0);builder.Metrics.AddOpenTelemetry(builder =>builder.AddOtlpExporter());

                Once OpenTelemetry has stable conventions it is possible for the libraries that produce metrics to also include extension methods that
                apply OpenTelemetry naming rules. The enumeration would likely start with two options, the default metric names shipped originally and
                OpenTelemetry's names. Over time more options might be added to the enumeration for updated versions of OpenTelemetry's conventions or
                other standards that have gained a broad interest. Unlike option #1, these metrics are produced with the default Meter name for the
                library such as "Microsoft.AspNetCore" and it replaces the default metric rather than adding new metrics in parallel.

                App dev wants to enable metrics from certain Meters to get sent (config-based approach)

                Details
                {
                "Metrics": {
                "EnabledMetrics": {
                "System": true,
                "Microsoft.AspNetCore": true"Microsoft.Extensions.Caching": true// hypothetical runtime provided Meter"StackExchange.Redis": true// hypothetical Meter in a 3P library
                }
                }
                }

                App dev wants to enable metrics from certain Meters to get sent (programmatic approach)

                Details
                varbuilder=WebApplication.CreateBuilder(args);builder.Metrics.EnableMetrics("Microsoft.Extensions.Caching").EnableMetrics("StackExchange.Redis").AddOpenTelemetry(builder =>builder.AddOtlpExporter());

                App dev wants to enable metrics from certain Meters to get sent (custom-instrumentation approach)

                Details

                Meters need to be created in order for turning them on to have any effect. In some cases it may be convenient to both create them
                and turn them on as a single step using a strongly typed extension method:

                varbuilder=WebApplication.CreateBuilder(args);builder.Metrics.AddSqlClientInstrumentation().AddOpenTelemetry(builder =>builder.AddOtlpExporter());

                This AddSqlClientInstrumentation method is a hypothetical extension method that could be offered by an OpenTelemetry instrumentation package. Note that some instrumentation packages are likely to be duplicative with built-in metrics. If the app developer doesn't want
                both then they should ensure the built-in metrics aren't included in their configuration.

                App dev wants to disable sending metrics for some instruments on a Meter (config-based approach)

                Details
                {
                "Metrics": {
                "EnabledMetrics": {
                "System": true,
                "Microsoft.AspNet.Core": {
                "Default": true, // enable default metrics for all the instruments"connection-duration": false// but exclude the connection-duration metric
                }
                }
                }
                }

                App dev wants to only enable specific instruments on a Meter (config-based approach)

                Details
                {
                "Metrics": {
                "EnabledMetrics": {
                "System": true,
                "Microsoft.AspNet.Core": {
                "connection-duration": true// enable only the connection-duration metric
                }
                }
                }
                }

                App dev wants to only enable specific instruments on a Meter (API-based approach)

                Details
                varbuilder=WebApplication.CreateBuilder(args);builder.Metrics// enable only the duration-counter metric.EnableMetrics("Microsoft.AspNetCore","duration-counter");.AddOpenTelemetry(builder =>builder.AddOtlpExporter());

                App dev wants to disable sending metrics for some instruments on a Meter (API-based approach)

                Details
                varbuilder=WebApplication.CreateBuilder(args);builder.Metrics// enable all default metrics for all instruments on this Meter except duration-counter.EnableMetrics("Microsoft.AspNetCore", instrument =>instrument.Name!="duration-counter");.AddOpenTelemetry(builder =>builder.AddOtlpExporter());

                The idea above is that the HostBuilder.Metrics property is a new MetricsBuilder type, the host defaults automatically constructed MetricsBuilder to load configuration from appsettings.json, and the config file specified which Meters to enable. The hypothetical AddOpenTelemetry() API is an extension method that would be provided by the OpenTelemetry NuGet package, very similar to the extension method they provide for LoggingBuilder.

                Metadata

                Metadata

                Labels

                api-approvedAPI was approved in API review, it can be implementedarea-System.Diagnostics.MetricblockingMarks issues that we want to fast track in order to unblock other important workenhancementProduct code improvement that does NOT require public API changes/additionspartner-impactThis issue impacts a partner who needs to be kept updated

                Type

                No type

                Projects

                No projects

                  Milestone

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions