Skip to content

Commit da4d874

Browse files
mariamgergesR9 FundamentalsIEvangelist
authored
Add source generated metrics doc (#45784)
* adding source generated metrics docs * updating links in docs/core/diagnostics/source-generated-metrics.md * renamed as suggested * update in doc * fixing docs * moved code to snippets * adding missing double quotes * fixes * fix * fix * fix * Apply suggestions from code review * Apply suggestions from code review * Apply suggestions from code review * fixes * fixes --------- Co-authored-by: R9 Fundamentals <[email protected]> Co-authored-by: David Pine <[email protected]>
1 parent 28ce5b9 commit da4d874

File tree

10 files changed

+377
-117
lines changed

10 files changed

+377
-117
lines changed
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
---
2+
title: Compile-time metric source generation
3+
description: Learn how to use a source generator to create metrics in .NET
4+
ms.date: 04/11/2025
5+
---
6+
7+
# Compile-time metric source generation
8+
9+
.NET's metering infrastructure is designed to deliver a highly usable and high-performance metering solution for modern .NET applications.
10+
11+
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.
12+
13+
The source 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.
14+
15+
## Get started
16+
17+
To get started, install the [📦 Microsoft.Extensions.Telemetry.Abstractions](https://www.nuget.org/packages/Microsoft.Extensions.Telemetry.Abstractions) NuGet package:
18+
19+
### [.NET CLI](#tab/dotnet-cli)
20+
21+
```dotnetcli
22+
dotnet add package Microsoft.Extensions.Telemetry.Abstractions
23+
```
24+
25+
### [PackageReference](#tab/package-reference)
26+
27+
```xml
28+
<ItemGroup>
29+
<PackageReference Include="Microsoft.Extensions.Telemetry.Abstractions"
30+
Version="*" />
31+
</ItemGroup>
32+
```
33+
34+
---
35+
36+
For more information, see [dotnet add package](../tools/dotnet-package-add.md) or [Manage package dependencies in .NET applications](../tools/dependencies.md).
37+
38+
## Generic attributes
39+
40+
Generic attributes require C# 11 or later. For C# 10 or earlier, use nongeneric attributes instead.
41+
42+
The following example shows a class that declares three metrics. The methods are marked with an attribute and are declared as `static` and `partial`.
43+
The code generator runs at build time and provides an implementation of these methods, along with accompanying
44+
types.
45+
46+
:::code language="csharp" source="snippets/MetricsGen/MetricConstants.cs" id="constants":::
47+
48+
The following code demonstrates how to use the generator with primitive types:
49+
50+
:::code language="csharp" source="snippets/MetricsGen/Metrics.cs" id="metrics" :::
51+
52+
The previous declaration automatically returns the following:
53+
54+
- `Latency` class with a `Record` method
55+
- `TotalCount` class with an `Add` method
56+
- `TotalFailures` class with a `Add` method.
57+
58+
The attributes indicate the set of dimensions that each metric uses. The signature for the generated types looks like this:
59+
60+
```csharp
61+
internal class TotalCount
62+
{
63+
public void Add(int value, object? env, object? region, object? requestName, object? requestStatus)
64+
}
65+
66+
internal TotalFailures
67+
{
68+
public void Add(int value)
69+
}
70+
71+
internal class Latency
72+
{
73+
public void Record(long value, object? requestName, object? duration);
74+
}
75+
```
76+
77+
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:
78+
79+
:::code language="csharp" source="snippets/MetricsGen/MyClass.cs" id ="creation":::
80+
81+
## Metric methods requirements
82+
83+
Metric methods are constrained to the following:
84+
85+
- They must be `public static partial`.
86+
- The return type must be unique.
87+
- Their names must not start with an underscore.
88+
- Their parameter names must not start with an underscore.
89+
- Their first parameter must be <xref:System.Diagnostics.Metrics.Meter> type.
90+
Metric methods are constrained to the following:
91+
92+
## See also
93+
94+
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.

docs/core/diagnostics/metrics-strongly-typed.md

Lines changed: 30 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -11,95 +11,56 @@ Modern .NET applications can capture metrics using the <xref:System.Diagnostics.
1111
> [!NOTE]
1212
> 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.
1313
14-
## Tag name defaults and customization
15-
16-
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.
14+
## Get started
1715

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

20-
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:
18+
### [.NET CLI](#tab/dotnet-cli)
2119

22-
```csharp
23-
public struct RequestTags
24-
{
25-
public string Region { get; set; }
26-
}
27-
28-
public static partial class MyMetrics
29-
{
30-
[Counter<int>(typeof(RequestTags))]
31-
public static partial RequestCount CreateRequestCount(Meter meter);
32-
}
20+
```dotnetcli
21+
dotnet add package Microsoft.Extensions.Telemetry.Abstractions
3322
```
3423

35-
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.
24+
### [PackageReference](#tab/package-reference)
3625

37-
To use the generated metric, create a <xref:System.Diagnostics.Metrics.Meter> and record measurements as shown below:
26+
```xml
27+
<ItemGroup>
28+
<PackageReference Include="Microsoft.Extensions.Telemetry.Abstractions"
29+
Version="*" />
30+
</ItemGroup>
31+
```
3832

39-
```csharp
40-
Meter meter = new Meter("MyCompany.MyApp", "1.0");
41-
RequestCount requestCountMetric = MyMetrics.CreateRequestCount(meter);
33+
---
4234

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

46-
// Record a metric value with the associated tag
47-
requestCountMetric.Add(1, tags);
48-
```
49-
50-
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.
37+
## Tag name defaults and customization
5138

52-
## Example 2: Metric with nested tag objects
39+
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.
5340

54-
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:
41+
## Example 1: Basic metric with a single tag
5542

56-
```csharp
57-
public class MetricTags : MetricParentTags
58-
{
59-
[TagName("Dim1DimensionName")]
60-
public string? Dim1; // custom tag name via attribute
43+
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:
6144

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

64-
public MetricChildTags? ChildTagsObject { get; set; }
65-
}
47+
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.
6648

67-
public enum Operations
68-
{
69-
Unknown = 0,
70-
Operation1 = 1,
71-
}
49+
To use the generated metric, create a <xref:System.Diagnostics.Metrics.Meter> and record measurements as shown below:
7250

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

78-
public MetricTagsStruct ChildTagsStruct { get; set; }
79-
}
53+
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.
8054

81-
public class MetricChildTags
82-
{
83-
public string? Dim2 { get; set; } // tag name defaults to "Dim2"
84-
}
55+
## Example 2: Metric with nested tag objects
8556

86-
public struct MetricTagsStruct
87-
{
88-
public string Dim3 { get; set; } // tag name defaults to "Dim3"
89-
}
57+
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:
9058

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

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

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

10465
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:
10566

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

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

116-
```csharp
117-
internal class MyClass
118-
{
119-
private readonly Latency _latencyMetric;
120-
private readonly TotalCount _totalCountMetric;
121-
private readonly TotalFailures _totalFailuresMetric;
122-
123-
public MyClass(Meter meter)
124-
{
125-
// Create metric instances using the source-generated factory methods
126-
_latencyMetric = Metric.CreateLatency(meter);
127-
_totalCountMetric = Metric.CreateTotalCount(meter);
128-
_totalFailuresMetric = Metric.CreateTotalFailures(meter);
129-
}
130-
131-
public void DoWork()
132-
{
133-
var stopwatch = new Stopwatch();
134-
stopwatch.Start();
135-
bool requestSuccessful = true;
136-
// ... perform some operation ...
137-
stopwatch.Stop();
138-
139-
// Create a tag object with values for all tags
140-
var tags = new MetricTags
141-
{
142-
Dim1 = "Dim1Value",
143-
Operation = Operations.Operation1,
144-
ParentOperationName = "ParentOpValue",
145-
ChildTagsObject = new MetricChildTags
146-
{
147-
Dim2 = "Dim2Value",
148-
},
149-
ChildTagsStruct = new MetricTagsStruct
150-
{
151-
Dim3 = "Dim3Value"
152-
}
153-
};
154-
155-
// Record the metric values with the associated tags
156-
_latencyMetric.Record(stopwatch.ElapsedMilliseconds, tags);
157-
_totalCountMetric.Add(1, tags);
158-
if (!requestSuccessful)
159-
{
160-
_totalFailuresMetric.Add(1, tags);
161-
}
162-
}
163-
}
164-
```
77+
:::code language="csharp" source="snippets/MetricsGen/MyClass.cs" id ="creationwithtags":::
16578

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

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

18598
## See also
18699

100+
- [Source generated metrics in .NET](metrics-generator.md)
187101
- [Creating metrics in .NET (Instrumentation tutorial)](metrics-instrumentation.md)
188102
- [Collecting metrics in .NET (Using MeterListener and exporters)](metrics-collection.md)
189103
- [Logging source generation in .NET](../extensions/logger-message-generator.md) (for a similar source-generation approach applied to logging)

docs/core/diagnostics/metrics.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ There are two parts to using metrics in a .NET app:
2828

2929
- [Instrumentation tutorial](metrics-instrumentation.md) - How to create new metrics in code
3030
- [Collection tutorial](metrics-collection.md) - How to store and view metric data for your app
31+
- [Source generated metrics](metrics-generator.md) - How to use source generator to create metrics
3132
- [Source-generated metrics with strongly-typed tags](metrics-strongly-typed.md) - How to use source-generated metrics with strongly-typed tags
3233
- [Built-in metrics](built-in-metrics.md) - Discover metrics that are ready for use in .NET runtime libraries
3334
- [Compare metric APIs](compare-metric-apis.md)
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace MetricsGen;
2+
3+
// <constants>
4+
internal class MetricConstants
5+
{
6+
public const string EnvironmentName = "env";
7+
public const string Region = "region";
8+
public const string RequestName = "requestName";
9+
public const string RequestStatus = "requestStatus";
10+
}
11+
// </constants>
12+
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
using Microsoft.Extensions.Diagnostics.Metrics;
2+
3+
namespace MetricsGen;
4+
5+
public class MetricTags : MetricParentTags
6+
{
7+
[TagName("Dim1DimensionName")]
8+
public string? Dim1; // custom tag name via attribute
9+
public Operations Operation { get; set; } // tag name defaults to "Operation"
10+
public MetricChildTags? ChildTagsObject { get; set; }
11+
}
12+
13+
public enum Operations
14+
{
15+
Unknown = 0,
16+
Operation1 = 1,
17+
}
18+
19+
public class MetricParentTags
20+
{
21+
[TagName("DimensionNameOfParentOperation")]
22+
public string? ParentOperationName { get; set; } // custom tag name via attribute
23+
public MetricTagsStruct ChildTagsStruct { get; set; }
24+
}
25+
26+
public class MetricChildTags
27+
{
28+
public string? Dim2 { get; set; } // tag name defaults to "Dim2"
29+
}
30+
31+
public struct MetricTagsStruct
32+
{
33+
public string Dim3 { get; set; } // tag name defaults to "Dim3"
34+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#define FIRST // FIRST SECOND
2+
3+
#if FIRST
4+
// <metrics>
5+
using System.Diagnostics.Metrics;
6+
using Microsoft.Extensions.Diagnostics.Metrics;
7+
8+
namespace MetricsGen;
9+
10+
internal static partial class Metric
11+
{
12+
// an explicit metric name is given
13+
[Histogram<long>("requestName", "duration", Name = "MyCustomMetricName")]
14+
public static partial Latency CreateLatency(Meter meter);
15+
16+
// no explicit metric name given, it is auto-generated from the method name
17+
[Counter<int>(
18+
MetricConstants.EnvironmentName,
19+
MetricConstants.Region,
20+
MetricConstants.RequestName,
21+
MetricConstants.RequestStatus)]
22+
public static partial TotalCount CreateTotalCount(Meter meter);
23+
24+
[Counter<int>]
25+
public static partial TotalFailures CreateTotalFailures(this Meter meter);
26+
}
27+
// </metrics>
28+
#elif SECOND
29+
30+
using MetricsGen;
31+
// <tags>
32+
using System.Diagnostics.Metrics;
33+
using Microsoft.Extensions.Diagnostics.Metrics;
34+
35+
public static partial class Metric
36+
{
37+
[Histogram<long>(typeof(MetricTags))]
38+
public static partial Latency CreateLatency(Meter meter);
39+
40+
[Counter<long>(typeof(MetricTags))]
41+
public static partial TotalCount CreateTotalCount(Meter meter);
42+
43+
[Counter<int>(typeof(MetricTags))]
44+
public static partial TotalFailures CreateTotalFailures(Meter meter);
45+
}
46+
// </tags>
47+
#endif

0 commit comments

Comments
 (0)