Skip to content

Commit d1d12a9

Browse files
mobratilMartin ObrátilIEvangelist
authored
Add tutorial: Source-generated metrics with strongly-typed tags (#45180)
* Added tutorial about Source-generated metrics with strongly-typed tags * Fixed invalid links * Fixed lint error * Apply suggestions from code review Co-authored-by: David Pine <[email protected]> --------- Co-authored-by: Martin Obrátil <[email protected]> Co-authored-by: David Pine <[email protected]>
1 parent a163901 commit d1d12a9

File tree

3 files changed

+193
-0
lines changed

3 files changed

+193
-0
lines changed
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
---
2+
title: Source-generated metrics with strongly-typed tags
3+
description: Learn how to use a source generator to create strongly-typed metric tags in .NET, reducing boilerplate and ensuring consistent.
4+
ms.date: 03/07/2025
5+
---
6+
7+
# Source-generated metrics with strongly-typed tags
8+
9+
Modern .NET applications can capture metrics using the <xref:System.Diagnostics.Metrics> API. These metrics often include additional context in the form of key-value pairs called *tags* (sometimes referred to as *dimensions* in telemetry systems). This article shows how to use a compile-time source generator to define **strongly-typed metric tags** (TagNames) and metric recording types and methods. By using strongly-typed tags, you eliminate repetitive boilerplate code and ensure that related metrics share the same set of tag names with compile-time safety. The primary benefit of this approach is improved developer productivity and type safety.
10+
11+
> [!NOTE]
12+
> 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.
13+
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.
17+
18+
## Example 1: Basic metric with a single tag
19+
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:
21+
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+
}
33+
```
34+
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.
36+
37+
To use the generated metric, create a <xref:System.Diagnostics.Metrics.Meter> and record measurements as shown below:
38+
39+
```csharp
40+
Meter meter = new Meter("MyCompany.MyApp", "1.0");
41+
RequestCount requestCountMetric = MyMetrics.CreateRequestCount(meter);
42+
43+
// Create a tag object with the relevant tag value
44+
var tags = new RequestTags { Region = "NorthAmerica" };
45+
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.
51+
52+
## Example 2: Metric with nested tag objects
53+
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:
55+
56+
```csharp
57+
public class MetricTags : MetricParentTags
58+
{
59+
[TagName("Dim1DimensionName")]
60+
public string? Dim1; // custom tag name via attribute
61+
62+
public Operations Operation { get; set; } // tag name defaults to "Operation"
63+
64+
public MetricChildTags? ChildTagsObject { get; set; }
65+
}
66+
67+
public enum Operations
68+
{
69+
Unknown = 0,
70+
Operation1 = 1,
71+
}
72+
73+
public class MetricParentTags
74+
{
75+
[TagName("DimensionNameOfParentOperation")]
76+
public string? ParentOperationName { get; set; } // custom tag name via attribute
77+
78+
public MetricTagsStruct ChildTagsStruct { get; set; }
79+
}
80+
81+
public class MetricChildTags
82+
{
83+
public string? Dim2 { get; set; } // tag name defaults to "Dim2"
84+
}
85+
86+
public struct MetricTagsStruct
87+
{
88+
public string Dim3 { get; set; } // tag name defaults to "Dim3"
89+
}
90+
91+
public static partial class Metric
92+
{
93+
[Histogram<long>(typeof(MetricTags))]
94+
public static partial Latency CreateLatency(Meter meter);
95+
96+
[Counter<long>(typeof(MetricTags))]
97+
public static partial TotalCount CreateTotalCount(Meter meter);
98+
99+
[Counter<int>(typeof(MetricTags))]
100+
public static partial TotalFailures CreateTotalFailures(Meter meter);
101+
}
102+
```
103+
104+
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:
105+
106+
- The `Dim1` field in `MetricTags` has a `[TagName("Dim1DimensionName")]` attribute, so its tag name will be `"Dim1DimensionName"`.
107+
- The `Operation` property has no attribute, so its tag name defaults to `"Operation"`.
108+
- In `MetricParentTags`, the `ParentOperationName` property is overridden with a custom tag name `"DimensionNameOfParentOperation"`.
109+
- The nested `MetricChildTags` class defines a `Dim2` property (no attribute, tag name `"Dim2"`).
110+
- The `MetricTagsStruct` struct defines a `Dim3` field (tag name `"Dim3"`).
111+
112+
All three metric definitions `CreateLatency`, `CreateTotalCount`, and `CreateTotalFailures` use `MetricTags` as their tag object type. This means the generated metric types (`Latency`, `TotalCount`, and `TotalFailures`) will all expect a `MetricTags` instance when recording data. **Each of these metrics will have the same set of tag names:** `Dim1DimensionName`, `Operation`, `Dim2`, `Dim3`, and `DimensionNameOfParentOperation`.
113+
114+
The following code shows how to create and use these metrics in a class:
115+
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+
```
165+
166+
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.
167+
168+
## Performance considerations
169+
170+
Using strongly-typed tags via source generation adds no overhead compared to using metrics directly. If you need to further minimize allocations for very high-frequency metrics, consider defining your tag object as a `struct` (value type) instead of a `class`. Using a `struct` for the tag object can avoid heap allocations when recording metrics, since the tags would be passed by value.
171+
172+
## Generated metric method requirements
173+
174+
When defining metric factory methods (the partial methods decorated with `[Counter]`, `[Histogram]`, etc.), the source generator imposes a few requirements:
175+
176+
- Each method must be `public static partial` (for the source generator to provide the implementation).
177+
- The return type of each partial method must be unique (so that the generator can create a uniquely named type for the metric).
178+
- The method name should not start with an underscore (`_`), and parameter names should not start with an underscore.
179+
- The first parameter must be a <xref:System.Diagnostics.Metrics.Meter> (this is the meter instance used to create the underlying instrument).
180+
- The methods cannot be generic and cannot have generic parameters.
181+
- The tag properties in the tag class can only be of type `string` or `enum`. For other types (for example, `bool` or numeric types), convert the value to a string before assigning it to the tag object.
182+
183+
Adhering to these requirements ensures that the source generator can successfully produce the metric types and methods.
184+
185+
## See also
186+
187+
- [Creating metrics in .NET (Instrumentation tutorial)](metrics-instrumentation.md)
188+
- [Collecting metrics in .NET (Using MeterListener and exporters)](metrics-collection.md)
189+
- [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 with strongly-typed tags](metrics-strongly-typed.md) - How to use source-generated metrics with strongly-typed tags
3132
- [Built-in metrics](built-in-metrics.md) - Discover metrics that are ready for use in .NET runtime libraries
3233
- [Compare metric APIs](compare-metric-apis.md)
3334
- [EventCounters](event-counters.md) - Learn what EventCounters are, how to implement them, and how to consume them

docs/navigate/tools-diagnostics/toc.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,9 @@ items:
391391
displayName: exception summarization,exception summary,exception summarizer,ExceptionSummary
392392
- name: Collection
393393
href: ../../core/diagnostics/metrics-collection.md
394+
- name: Source-generated metrics
395+
displayName: strongly-typed tags,dimensions
396+
href: ../../core/diagnostics/metrics-strongly-typed.md
394397
- name: Built-in metrics
395398
items:
396399
- name: Overview

0 commit comments

Comments
 (0)