|
| 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) |
0 commit comments