Skip to content

[API Proposal]: Introduce source generation based support for .NET metrics API #77516

@dpk83

Description

@dpk83

Background and motivation

Our team creates Telemetry framework for internal teams and we have created metrics solution. It was created before .NET Meter API came into existence so we created our own IMeter API surface. In our internal project, everything is designed to be highly efficient (consumes less CPU and reducing allocation as much as possible) while providing an easy to use developer friendly API surface. This helps reduce the developer burden, reduces chances of mistakes and at the same time helps gain the performance benefits.

With that in mind we utilized the compile time source generation of roslyn and introduced attributes for various instruments. Here is an example of how the attributes look like in our internal project using our IMeter surface

[Counter<int>("clientRing", "clientType", "region", Name = "TestInstrument")]
public static partial TestInstrument CreateTestInstrument(this IMeter meter); 

Developer writes this signature using Counter attribute and we would generate the source code for this method underneath which generates a highly efficient source using the most efficient mechanism available to achieve the required operations.

Once the above signature is defined, developers can then use it to create the typed counter like this:

// Create the instrument
var testInstrument = meter.CreateTestInstrument();

// The generated source will method generated specifically for this instrument i.e. it will have a method which will have named 
// parameters with names defined in the Counter attribute definition. This makes it easier for a developer to know what dimensions are expected to be passed and can provide it appropriately
// Use it to record metrics wherever needed. 
testInstrument.Add(1, clientRing, clientType, region);

Similarly, these attributes also support strong typed objects e.g.

[Counter<long>(typeof(MyObject), Name="MyStrongTypedInstrument")
public static partial MyCounterInstrument CreateMyCounterInstrument(this IMeter meter);

You can then use it as

var counter = meter.MyCounterInstrument();

counter.Add(10, myObject);

The underlying code generated at the compile time would expand this object's properties into individual dimension, thus simplifying the work for developers.

We are extending this source generation to support .NET Meter API and would like to have this become part of .NET so it can be useful for the broader community.

With the code generation, we have possibilities to provide the highest performance. E.g. we could automatically use the TagList if the total tags are less than or equal to 8 and switch to expanded tag list when there are more. We can skip all the processing upfront if there are no listeners or the instrument is not enabled etc.

We have found this extremely useful and so did our customers. We can build additional functionality on top where we could add a source generator to generate reports of the metrics emitted by a service and all it's dependent libraries (so developers know upfront what metrics can be emitted by libraries that they depend on, which ones are enabled vs which ones are not etc.)

API Proposal

[AttributeUsage(AttributeTargets.Method)]
[Conditional("CODE_GENERATION_ATTRIBUTES")]
public sealed class CounterAttribute<T> : Attribute
{
    public CounterAttribute(params string[] dimensions)
    {
        Dimensions = dimensions;
    }

    public CounterAttribute(Type type)
    {
        Type = type;
    }

    public string? Name { get; set; }
    public string[]? Dimensions { get; }
    public Type? Type { get; }
}

Similarly for HistogramAttribute

API Usage

// Create the signature at some place
[Counter<int>("clientRing", "clientType", "region", Name = "TestInstrument")]
public static partial TestInstrument CreateTestInstrument(this IMeter meter); 


// Create the instrument, where needed 
var testInstrument = meter.CreateTestInstrument();

// Use it to record metrics when needed. 
testInstrument.Add(1, clientRing, clientType, region);

Similarly, these attributes also support strong typed objects e.g.

[Counter<long>(typeof(MyObject), Name="MyStrongTypedInstrument")
public static partial MyCounterInstrument CreateMyCounterInstrument(this IMeter meter);

// Create the instrument using the declared method 
var counter = meter.MyCounterInstrument();

// Record the metric and pass the object when needed
counter.Add(10, myObject);

Alternative Designs

No response

Risks

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    api-suggestionEarly API idea and discussion, it is NOT ready for implementationarea-System.Diagnostics.MetricenhancementProduct code improvement that does NOT require public API changes/additionssource-generatorIndicates an issue with a source generator feature

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions