Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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
92 changes: 92 additions & 0 deletions docs/core/diagnostics/metrics-generator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
---
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="snippet_metricConstants":::

:::code language="csharp" source="snippets/MetricsGen/Metrics.cs" id="snippet_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 ="snippet_metricCreation":::

## 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.
145 changes: 28 additions & 117 deletions docs/core/diagnostics/metrics-strongly-typed.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,95 +11,53 @@ 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
## Get started

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.
To get started, install the [📦 Microsoft.Extensions.Telemetry.Abstractions](https://www.nuget.org/packages/Microsoft.Extensions.Telemetry.Abstractions) NuGet package:

## Example 1: Basic metric with a single tag
### [.NET CLI](#tab/dotnet-cli)

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:

```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:

```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" };

// Record a metric value with the associated tag
requestCountMetric.Add(1, tags);
```xml
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Telemetry.Abstractions"
Version="*" />
</ItemGroup>
```

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.
---

## Example 2: Metric with nested tag objects
For more information, see [dotnet add package](../tools/dotnet-package-add.md) or [Manage package dependencies in .NET applications](../tools/dependencies.md).

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:
## Tag name defaults and customization

```csharp
public class MetricTags : MetricParentTags
{
[TagName("Dim1DimensionName")]
public string? Dim1; // custom tag name via attribute
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.

public Operations Operation { get; set; } // tag name defaults to "Operation"
## Example 1: Basic metric with a single tag

public MetricChildTags? ChildTagsObject { get; set; }
}
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 enum Operations
{
Unknown = 0,
Operation1 = 1,
}
:::code language="csharp" source="snippets/MetricsGen/MyMetrics.cs" id= "snippet_SimpleMetricTag":::

public class MetricParentTags
{
[TagName("DimensionNameOfParentOperation")]
public string? ParentOperationName { get; set; } // custom tag name via attribute
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.

public MetricTagsStruct ChildTagsStruct { get; set; }
}
To use the generated metric, create a <xref:System.Diagnostics.Metrics.Meter> and record measurements as shown below:

public class MetricChildTags
{
public string? Dim2 { get; set; } // tag name defaults to "Dim2"
}
:::code language="csharp" source="snippets/MetricsGen/MyClass.cs" id ="snippet_SimpleMetricTagUsage":::

public struct MetricTagsStruct
{
public string Dim3 { get; set; } // tag name defaults to "Dim3"
}
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 static partial class Metric
{
[Histogram<long>(typeof(MetricTags))]
public static partial Latency CreateLatency(Meter meter);
## Example 2: Metric with nested tag objects

[Counter<long>(typeof(MetricTags))]
public static partial TotalCount CreateTotalCount(Meter meter);
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:

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

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 +71,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 ="snippet_strongMetricCreation":::

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 +94,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;

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

33 changes: 33 additions & 0 deletions docs/core/diagnostics/snippets/MetricsGen/MetricTags.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
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
//<snippet_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);
}
//</snippet_Metrics>
#elif SECOND

using MetricsGen;
//<snippet_MetricTags>
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);
}
//</snippet_MetricTags>
#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