Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions docs/core/diagnostics/metrics-generator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
---
title: Compile-time metric source generation
description: Learn how to use a source generator to create metrics in .NET
ms.date: 04/11/2025
---

# Compile-time metric source generation

.NET's metering infrastructure is designed to deliver a highly usable and high-performance metering solution for modern .NET applications.

To use source-generated metering, create a class that defines the names and dimensions of the metrics your code can produce. Then, create the class with `partial` method signatures.

The code generator automatically generates the code, which exposes strongly typed metering types and methods that you can invoke to record metric values. The generated methods are implemented in a highly efficient form, which reduces computation overhead as compared to traditional metering solutions.

## Get started

To get started, install the [📦 Microsoft.Extensions.Telemetry.Abstractions](https://www.nuget.org/packages/Microsoft.Extensions.Telemetry.Abstractions) NuGet package:

### [.NET CLI](#tab/dotnet-cli)

```dotnetcli
dotnet add package Microsoft.Extensions.Telemetry.Abstractions
```

### [PackageReference](#tab/package-reference)

```xml
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Telemetry.Abstractions"
Version="*" />
</ItemGroup>
```

---

For more information, see [dotnet add package](../tools/dotnet-package-add.md) or [Manage package dependencies in .NET applications](../tools/dependencies.md).

## Generic attributes

Generic attributes require C# 11 or later. For C# 10 or earlier, use nongeneric attributes instead.

The following example shows a class that declares two metrics. The methods are marked with an attribute and are declared as `static` and `partial`.
The code generator runs at build time and provides an implementation of these methods, along with accompanying
types.

:::code language="csharp" source="snippets/MetricsGen/MetricConstants.cs" id="constants":::

The following code demonstrates how to use the generator with primitive types:

:::code language="csharp" source="snippets/MetricsGen/Metrics.cs" id="metrics" :::

The previous declaration automatically returns the following:

- `Latency` class with a `Record` method
- `TotalCount` class with an `Add` method
- `TotalFailures` class with a `Add` method.

The attributes indicate the set of dimensions that each metric uses. The signature for the generated types looks like this:

```csharp
internal class TotalCount
{
public void Add(int value, object? env, object? region, object? requestName, object? requestStatus)
}

internal TotalFailures
{
public void Add(int value)
}

internal class Latency
{
public void Record(long value, object? requestName, object? duration);
}
```

The dimensions specified in the attributes have been turned into arguments to the `Add` and `Record` methods. You then use the generated methods to create instances of these types. With the instances created, you can call `Add` and `Record` to register metric values, as shown in the following example:

:::code language="csharp" source="snippets/MetricsGen/MyClass.cs" id ="creation":::

## Metric methods requirements

Metric methods are constrained to the following:

- They must be `public static partial`.
- The return type must be unique.
- Their names must not start with an underscore.
- Their parameter names must not start with an underscore.
- Their first parameter must be <xref:System.Diagnostics.Metrics.Meter> type.
Metric methods are constrained to the following:

## See also

For more information on the supported metrics, see [Types of instruments](metrics-instrumentation.md#types-of-instruments) to learn how to choose which instrument to use in different situations.
146 changes: 30 additions & 116 deletions docs/core/diagnostics/metrics-strongly-typed.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,95 +11,56 @@ Modern .NET applications can capture metrics using the <xref:System.Diagnostics.
> [!NOTE]
> In the context of metrics, a tag is sometimes also called a "dimension." This article uses "tag" for clarity and consistency with .NET metrics terminology.

## Tag name defaults and customization

By default, the source generator derives metric tag names from the field and property names of your tag class. In other words, each public field or property in the strongly-typed tag object becomes a tag name by default. You can override this by using the <xref:Microsoft.Extensions.Diagnostics.Metrics.TagNameAttribute> on a field or property to specify a custom tag name. In the examples below, you’ll see both approaches in action.
## Get started

## Example 1: Basic metric with a single tag
To get started, install the [📦 Microsoft.Extensions.Telemetry.Abstractions](https://www.nuget.org/packages/Microsoft.Extensions.Telemetry.Abstractions) NuGet package:

The following example demonstrates a simple counter metric with one tag. In this scenario, we want to count the number of processed requests and categorize them by a `Region` tag:
### [.NET CLI](#tab/dotnet-cli)

```csharp
public struct RequestTags
{
public string Region { get; set; }
}

public static partial class MyMetrics
{
[Counter<int>(typeof(RequestTags))]
public static partial RequestCount CreateRequestCount(Meter meter);
}
```dotnetcli
dotnet add package Microsoft.Extensions.Telemetry.Abstractions
```

In the code above, `RequestTags` is a strongly-typed tag struct with a single property `Region`. The `CreateRequestCount` method is marked with <xref:Microsoft.Extensions.Diagnostics.Metrics.CounterAttribute`1> where `T` is an `int`, indicating it generates a **Counter** instrument that tracks `int` values. The attribute references `typeof(RequestTags)`, meaning the counter will use the tags defined in `RequestTags` when recording metrics. The source generator will produce a strongly-typed instrument class (named `RequestCount`) with an `Add` method that accepts integer value and `RequestTags` object.
### [PackageReference](#tab/package-reference)

To use the generated metric, create a <xref:System.Diagnostics.Metrics.Meter> and record measurements as shown below:
```xml
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Telemetry.Abstractions"
Version="*" />
</ItemGroup>
```

```csharp
Meter meter = new Meter("MyCompany.MyApp", "1.0");
RequestCount requestCountMetric = MyMetrics.CreateRequestCount(meter);
---

// Create a tag object with the relevant tag value
var tags = new RequestTags { Region = "NorthAmerica" };
For more information, see [dotnet add package](../tools/dotnet-package-add.md) or [Manage package dependencies in .NET applications](../tools/dependencies.md).

// Record a metric value with the associated tag
requestCountMetric.Add(1, tags);
```

In this usage example, calling `MyMetrics.CreateRequestCount(meter)` creates a counter instrument (via the `Meter`) and returns a `RequestCount` metric object. When you call `requestCountMetric.Add(1, tags)`, the metric system records a count of 1 associated with the tag `Region="NorthAmerica"`. You can reuse the `RequestTags` object or create new ones to record counts for different regions, and the tag name `Region` will consistently be applied to every measurement.
## Tag name defaults and customization

## Example 2: Metric with nested tag objects
By default, the source generator derives metric tag names from the field and property names of your tag class. In other words, each public field or property in the strongly-typed tag object becomes a tag name by default. You can override this by using the <xref:Microsoft.Extensions.Diagnostics.Metrics.TagNameAttribute> on a field or property to specify a custom tag name. In the examples below, you’ll see both approaches in action.

For more complex scenarios, you can define tag classes that include multiple tags, nested objects, or even inherited properties. This allows a group of related metrics to share a common set of tags easily. In the next example, we define a set of tag classes and use them for three different metrics:
## Example 1: Basic metric with a single tag

```csharp
public class MetricTags : MetricParentTags
{
[TagName("Dim1DimensionName")]
public string? Dim1; // custom tag name via attribute
The following example demonstrates a simple counter metric with one tag. In this scenario, we want to count the number of processed requests and categorize them by a `Region` tag:

public Operations Operation { get; set; } // tag name defaults to "Operation"
:::code language="csharp" source="snippets/MetricsGen/MyMetrics.cs" id= "tag":::

public MetricChildTags? ChildTagsObject { get; set; }
}
In the preceding code, `RequestTags` is a strongly-typed tag struct with a single property `Region`. The `CreateRequestCount` method is marked with <xref:Microsoft.Extensions.Diagnostics.Metrics.CounterAttribute`1> where `T` is an `int`, indicating it generates a `Counter` instrument that tracks `int` values. The attribute references `typeof(RequestTags)`, meaning the counter uses the tags defined in `RequestTags` when recording metrics. The source generator produces a strongly-typed instrument class (named `RequestCount`) with an `Add` method that accepts integer value and `RequestTags` object.

public enum Operations
{
Unknown = 0,
Operation1 = 1,
}
To use the generated metric, create a <xref:System.Diagnostics.Metrics.Meter> and record measurements as shown below:

public class MetricParentTags
{
[TagName("DimensionNameOfParentOperation")]
public string? ParentOperationName { get; set; } // custom tag name via attribute
:::code language="csharp" source="snippets/MetricsGen/MyClass.cs" id ="tag":::

public MetricTagsStruct ChildTagsStruct { get; set; }
}
In this usage example, calling `MyMetrics.CreateRequestCount(meter)` creates a counter instrument (via the `Meter`) and returns a `RequestCount` metric object. When you call `requestCountMetric.Add(1, tags)`, the metric system records a count of 1 associated with the tag `Region="NorthAmerica"`. You can reuse the `RequestTags` object or create new ones to record counts for different regions, and the tag name `Region` will consistently be applied to every measurement.

public class MetricChildTags
{
public string? Dim2 { get; set; } // tag name defaults to "Dim2"
}
## Example 2: Metric with nested tag objects

public struct MetricTagsStruct
{
public string Dim3 { get; set; } // tag name defaults to "Dim3"
}
For more complex scenarios, you can define tag classes that include multiple tags, nested objects, or even inherited properties. This allows a group of related metrics to effectively share a common set of tags. In the next example, you define a set of tag classes and use them for three different metrics:

public static partial class Metric
{
[Histogram<long>(typeof(MetricTags))]
public static partial Latency CreateLatency(Meter meter);
:::code language="csharp" source="snippets/MetricsGen/MetricTags.cs" :::

[Counter<long>(typeof(MetricTags))]
public static partial TotalCount CreateTotalCount(Meter meter);
The preceding code defines the metric inheritance and object shapes. The following code demonstrates how to use these shapes with the generator, as shown in the `Metric` class:

[Counter<int>(typeof(MetricTags))]
public static partial TotalFailures CreateTotalFailures(Meter meter);
}
```
:::code language="csharp" source="snippets/MetricsGen/Metrics.cs" id="tags" :::

In this example, `MetricTags` is a tag class that inherits from `MetricParentTags` and also contains a nested tag object (`MetricChildTags`) and a nested struct (`MetricTagsStruct`). The tag properties demonstrate both default and customized tag names:

Expand All @@ -113,55 +74,7 @@ All three metric definitions `CreateLatency`, `CreateTotalCount`, and `CreateTot

The following code shows how to create and use these metrics in a class:

```csharp
internal class MyClass
{
private readonly Latency _latencyMetric;
private readonly TotalCount _totalCountMetric;
private readonly TotalFailures _totalFailuresMetric;

public MyClass(Meter meter)
{
// Create metric instances using the source-generated factory methods
_latencyMetric = Metric.CreateLatency(meter);
_totalCountMetric = Metric.CreateTotalCount(meter);
_totalFailuresMetric = Metric.CreateTotalFailures(meter);
}

public void DoWork()
{
var stopwatch = new Stopwatch();
stopwatch.Start();
bool requestSuccessful = true;
// ... perform some operation ...
stopwatch.Stop();

// Create a tag object with values for all tags
var tags = new MetricTags
{
Dim1 = "Dim1Value",
Operation = Operations.Operation1,
ParentOperationName = "ParentOpValue",
ChildTagsObject = new MetricChildTags
{
Dim2 = "Dim2Value",
},
ChildTagsStruct = new MetricTagsStruct
{
Dim3 = "Dim3Value"
}
};

// Record the metric values with the associated tags
_latencyMetric.Record(stopwatch.ElapsedMilliseconds, tags);
_totalCountMetric.Add(1, tags);
if (!requestSuccessful)
{
_totalFailuresMetric.Add(1, tags);
}
}
}
```
:::code language="csharp" source="snippets/MetricsGen/MyClass.cs" id ="creation":::

In the preceding `MyClass.DoWork` method, a `MetricTags` object is populated with values for each tag. This single `tags` object is then passed to all three instruments when recording data. The `Latency` metric (a histogram) records the elapsed time, and both counters (`TotalCount` and `TotalFailures`) record occurrence counts. Because all metrics share the same tag object type, the tags (`Dim1DimensionName`, `Operation`, `Dim2`, `Dim3`, `DimensionNameOfParentOperation`) are present on every measurement.

Expand All @@ -184,6 +97,7 @@ Adhering to these requirements ensures that the source generator can successfull

## See also

- [Source generated metrics in .NET](metrics-generator.md)
- [Creating metrics in .NET (Instrumentation tutorial)](metrics-instrumentation.md)
- [Collecting metrics in .NET (Using MeterListener and exporters)](metrics-collection.md)
- [Logging source generation in .NET](../extensions/logger-message-generator.md) (for a similar source-generation approach applied to logging)
1 change: 1 addition & 0 deletions docs/core/diagnostics/metrics.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ There are two parts to using metrics in a .NET app:

- [Instrumentation tutorial](metrics-instrumentation.md) - How to create new metrics in code
- [Collection tutorial](metrics-collection.md) - How to store and view metric data for your app
- [Source generated metrics](metrics-generator.md) - How to use source generator to create metrics
- [Source-generated metrics with strongly-typed tags](metrics-strongly-typed.md) - How to use source-generated metrics with strongly-typed tags
- [Built-in metrics](built-in-metrics.md) - Discover metrics that are ready for use in .NET runtime libraries
- [Compare metric APIs](compare-metric-apis.md)
Expand Down
12 changes: 12 additions & 0 deletions docs/core/diagnostics/snippets/MetricsGen/MetricConstants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace MetricsGen;

// <constants>
internal class MetricConstants
{
public const string EnvironmentName = "env";
public const string Region = "region";
public const string RequestName = "requestName";
public const string RequestStatus = "requestStatus";
}
// </constants>

34 changes: 34 additions & 0 deletions docs/core/diagnostics/snippets/MetricsGen/MetricTags.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using Microsoft.Extensions.Diagnostics.Metrics;

namespace MetricsGen;

public class MetricTags : MetricParentTags
{
[TagName("Dim1DimensionName")]
public string? Dim1; // custom tag name via attribute
public Operations Operation { get; set; } // tag name defaults to "Operation"
public MetricChildTags? ChildTagsObject { get; set; }
}

public enum Operations
{
Unknown = 0,
Operation1 = 1,
}

public class MetricParentTags
{
[TagName("DimensionNameOfParentOperation")]
public string? ParentOperationName { get; set; } // custom tag name via attribute
public MetricTagsStruct ChildTagsStruct { get; set; }
}

public class MetricChildTags
{
public string? Dim2 { get; set; } // tag name defaults to "Dim2"
}

public struct MetricTagsStruct
{
public string Dim3 { get; set; } // tag name defaults to "Dim3"
}
43 changes: 43 additions & 0 deletions docs/core/diagnostics/snippets/MetricsGen/Metrics.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#define FIRST // FIRST SECOND

#if FIRST
// <metrics>
using System.Diagnostics.Metrics;
using Microsoft.Extensions.Diagnostics.Metrics;

namespace MetricsGen;

internal static partial class Metric
{
// an explicit metric name is given
[Histogram<long>("requestName", "duration", Name = "MyCustomMetricName")]
public static partial Latency CreateLatency(Meter meter);

// no explicit metric name given, it is auto-generated from the method name
[Counter<int>(MetricConstants.EnvironmentName, MetricConstants.Region, MetricConstants.RequestName, MetricConstants.RequestStatus)]
public static partial TotalCount CreateTotalCount(Meter meter);

[Counter<int>]
public static partial TotalFailures CreateTotalFailures(this Meter meter);
}
// </metrics>
#elif SECOND

using MetricsGen;
// <tags>
using System.Diagnostics.Metrics;
using Microsoft.Extensions.Diagnostics.Metrics;

public static partial class Metric
{
[Histogram<long>(typeof(MetricTags))]
public static partial Latency CreateLatency(Meter meter);

[Counter<long>(typeof(MetricTags))]
public static partial TotalCount CreateTotalCount(Meter meter);

[Counter<int>(typeof(MetricTags))]
public static partial TotalFailures CreateTotalFailures(Meter meter);
}
// </tags>
#endif
14 changes: 14 additions & 0 deletions docs/core/diagnostics/snippets/MetricsGen/MetricsGen.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Telemetry.Abstractions" Version="9.4.0" />
</ItemGroup>

</Project>
Loading