Skip to content

[API Proposal]: Introduce DI friendly IMeter<T> for modern services #77514

Closed
@dpk83

Description

This is edited by @tarekgh

Runtime Metrics APIs Proposal

At present, the metrics APIs that are exposed do not integrate smoothly with DI. This is because the current support relies on creating Meter objects as static, which cannot be utilized within DI containers. As a solution, this proposal recommends developing new APIs that will enable metrics to work seamlessly with DI.

Meter Factory

The IMeterFactory interface is being introduced, which can be registered with the DI container and utilized to create or obtain Meter objects.

IMeterFactory

namespace System.Diagnostics.Metrics
{
    public interface IMeterFactory : IDisposable
    {
        Meter Create(
                string name,
                string? version = null,
                IEnumerable<KeyValuePair<string, object?>>? tags = null);
    }
}

A default implementation will be offered, which should be adequate for most use cases. Nonetheless, users may choose to provide their own implementation for alternative purposes, such as testing.

Meter factories will be accountable for the following responsibilities:

  • Creating a new meter.
  • Attaching the factory instance to the Meter constructor for all created Meter objects. Additional details regarding this will be addressed later in the metrics APIs' additions.
  • Storing created meters in a cache and returning a cached instance if a meter with the same parameters (name, version, and tags) is requested.
  • Disposing of all cached Meter objects upon factory disposal.

DI IMeterFactory Registration

The default IMeterFactory registration can be done by the API:

namespace Microsoft.Extensions.Metrics
{
    public static class MetricsServiceExtensions
    {
        public static IServiceCollection AddMetrics(this IServiceCollection services);        
    }
}

Core Metrics APIs Addition

The proposed modifications aim to facilitate the following:

  • Inclusion of default tags to the Meter
  • Addition of default tags to the instruments
  • Incorporation of scope into the Meter involves assigning an IMeterFactory object to the Meter, which serves as a marker for all Meters associated with a particular factory.
  • The Meter will now cache non-Observable instruments internally to prevent re-creation when attempting to recreate the same instrument with identical parameters such as name, unit, tags, and generic type.
namespace System.Diagnostics.Metrics
{
    public abstract class Instrument
    {
        protected Instrument(
                    Meter meter,
                    string name,
                    string? unit,
                    string? description,
                    IEnumerable<KeyValuePair<string, object?>>? tags);

         public IEnumerable<KeyValuePair<string, object?>>? Tags { get; }
    }

    public abstract class Instrument<T> : Instrument where T : struct
    {
        protected Instrument(
                    Meter meter,
                    string name,
                    string? unit,
                    string? description, IEnumerable<KeyValuePair<string, object?>>? tags);
    }

    public abstract class ObservableInstrument<T> : Instrument where T : struct
    {
        protected ObservableInstrument(
                    Meter meter,
                    string name,
                    string? unit,
                    string? description,
                    IEnumerable<KeyValuePair<string, object?>> tags);
    }

    public class Meter : IDisposable
    {
        public Meter(
                string name,
                string? version,
                IEnumerable<KeyValuePair<string, object?>>? tags,
                IMeterFactory? factory = null);

        public IEnumerable<KeyValuePair<string, object?>>? Tags { get ; }}

        public IMeterFactory? Factory { get; }

        public Counter<T> CreateCounter<T>(
                            string name,
                            string? unit,
                            string? description,
                            IEnumerable<KeyValuePair<string, object?>> tags) where T : struct;

        public UpDownCounter<T> CreateUpDownCounter<T>(
                            string name,
                            string? unit,
                            string? description,
                            IEnumerable<KeyValuePair<string, object?>> tags) where T : struct;

        public Histogram<T> CreateHistogram<T>(
                            string name,
                            string? unit,
                            string? description,
                            IEnumerable<KeyValuePair<string, object?>> tags) where T : struct;

        public ObservableCounter<T> CreateObservableCounter<T>(
                            string name,
                            Func<T> observeValue,
                            string? unit,
                            string? description,
                            IEnumerable<KeyValuePair<string, object?>> tags) where T : struct;

        public ObservableCounter<T> CreateObservableCounter<T>(
                            string name,
                            Func<Measurement<T>> observeValue,
                            string? unit,
                            string? description,
                            IEnumerable<KeyValuePair<string, object?>> tags) where T : struct;

        public ObservableCounter<T> CreateObservableCounter<T>(
                            string name,
                            Func<IEnumerable<Measurement<T>>> observeValues,
                            string? unit,
                            string? description,
                            IEnumerable<KeyValuePair<string, object?>> tags) where T : struct;

        public ObservableUpDownCounter<T> CreateObservableUpDownCounter<T>(
                            string name,
                            Func<T> observeValue,
                            string? unit,
                            string? description,
                            IEnumerable<KeyValuePair<string, object?>> tags) where T : struct;

        public ObservableUpDownCounter<T> CreateObservableUpDownCounter<T>(
                            string name,
                            Func<Measurement<T>> observeValue,
                            string? unit,
                            string? description,
                            IEnumerable<KeyValuePair<string, object?>> tags) where T : struct;

        public ObservableUpDownCounter<T> CreateObservableUpDownCounter<T>(
                            string name,
                            Func<IEnumerable<Measurement<T>>> observeValues,
                            string? unit,
                            string? description,
                            IEnumerable<KeyValuePair<string, object?>> tags) where T : struct;

        public ObservableGauge<T> CreateObservableGauge<T>(
                            string name,
                            Func<T> observeValue,
                            string? unit,
                            string? description,
                            IEnumerable<KeyValuePair<string, object?>> tags) where T : struct;

        public ObservableGauge<T> CreateObservableGauge<T>(
                            string name,
                            Func<Measurement<T>> observeValue,
                            string? unit,
                            string? description,
                            IEnumerable<KeyValuePair<string, object?>> tags) where T : struct;

        public ObservableGauge<T> CreateObservableGauge<T>(
                            string name,
                            Func<IEnumerable<Measurement<T>>> observeValues,
                            string? unit,
                            string? description,
                            IEnumerable<KeyValuePair<string, object?>> tags) where T : struct;
}

Testing Helper

The InstrumentRecorder class has been introduced to facilitate metric testing with DI. With this class, it is straightforward to monitor a particular instrument and its meter readings, and then generate reports on the values published by the instrument.

namespace Microsoft.Extensions.Metrics
{
    public sealed class InstrumentRecorder<T> : IDisposable where T : struct
    {
        public InstrumentRecorder(Instrument instrument);
        public InstrumentRecorder(
                        IMeterFactory? meterFactory,
                        string meterName,
                        string instrumentName);
        public InstrumentRecorder(
                        Meter meter,
                        string instrumentName);
    
        public Instrument Instrument { get; }

        public IEnumerable<Measurement<T>> GetMeasurements();
        public void Dispose();
    }
}

Code Example

    //
    // Register Metrics
    //

    services.AddMetrics();

    //
    // Access the MeterFactory
    //

    IMeterFactory meterFactory = serviceProvider.GetRequiredService<IMeterFactory>();

    //
    // Create Meter
    //

    Meter meter = meterFactory.Create(
                            "MeterName",
                            "version",
                            new TagList()
                            {
                                { "Key1", "Value1" },
                                { "Key2", "Value2" }
                            });

    //
    // Create Instrument
    //

    Counter<int> counter = meter.CreateCounter<int>(
                            "CounterName",
                            "cm",
                            "Test counter 1",
                            new TagList() { { "K1", "V1" } });

    //
    // Test instrument published values
    //

    var recorder = new InstrumentRecorder<int>(meterFactory, "MeterName", "CounterName");
    // can do the following too:
    // var recorder = new InstrumentRecorder<int>(counter);
    counter. Add(1);
    Assert.Equal(1, recorder.GetMeasurements().ElementAt(0));

end of @tarekgh edit and start of old description

Background and motivation

.NET exposes Meter API to instrument code for collecting metrics. Currently the Meter is a class which doesn't work well with DI (Dependency Injection) based model. In order to create a meter object a developer currently need to create an object of the Meter class and pass the meter name, something like this

var meter = new Meter("myMeter");

While this works find on a smaller scale, for large scale complex services this starts becoming difficult to manage and having a DI based API surface could make things much simpler. Apart from the fact that DI would improve the testability of the code, it will also allow for better management of creation of Meter object in a consistent fashion. With DI based approach a meter can be consistently created with the type name of the class where the meter is injected, which opens up a lot of possibilities for further extensions that could simplify the programming model.

We have a metrics solution that our teams uses. When we implemented the solution .NET metrics API was not in existence so we had created our own API surface for metrics. We introduced DI friendly IMeter and IMeter which works similar to ILogger and ILogger. Over the time we found the DI friendly surface extremely useful and extensible, it turned out to be super easy for services and has allowed us to have a consistent approach of creating meter object everywhere. Service owners and library owners don't need to worry about cooking up names for the meter object and the meter object automatically gets created with consistent naming. This allows us to add various capabilities by providing filtering based on the meter names via config. Few examples of the capabilities

  • Services can configure the state of individual meters via config (i.e. enable/disable a meter by configuring the metering state in their appsettings.json using the type name of namespace, similar to how one can configure logging levels with .NET ILogger)
  • Services can configure different destinations for metrics generated by different meter objects. This works even when using third party libraries seamlessly as long as the libraries are using DI to inject meter. etc.

Modern .NET services use DI heavily due to it's benefits and the trend will only increase going forward. Based on our experience with IMeter API and the feedback of simplicity from our customers, we think it would be highly useful for the rest of the community too.

API Proposal

namespace System.Diagnostics.Metrics;

public interface IMeter
{
}

public interface IMeter<out TCategoryName> : IMeter
{
}

OR

namespace System.Diagnostics.Metrics;

public class Meter<out TCategoryName>
{
}

API Usage

public class MyClass
{
    // Inject Meter<T> object using DI
    public MyClass(IMeter<MyClass> meter)
    {
        var counter = meter.CreateCounter<long>("counter1");
    }
}

public class AnotherClass
{
    // Inject a meter with the full name of the provided T gets injected using DI.
    public AnotherClass(IMeter<AnotherClass> meter)
    {
        var counter = meter.CreateCounter<long>("counter2");
    }
}

Alternative Designs

The alternative is to write a custom DI wrapper for Meter and introduce Meter in a wrapper which then defeats the purpose of exposing the Meter API exposed from .NET directly.

Risks

It's a new addition to existing APIs so shouldn't cause breaking changes.

Metadata

Assignees

Labels

api-approvedAPI was approved in API review, it can be implementedarea-System.Diagnostics.MetricenhancementProduct 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