[API Proposal]: Optional tags for metric source generation
- Dominant language
- C#
- Stars
- 3.2k
- Forks
- 894
- Avg merge
- 1d 12h
- Merged PRs (30d)
- 23
Description
### Background and motivation
I'm trying to define [OpenTelemetry Weaver](https://github.com/open-telemetry/weaver/tree/main) [semantic conventions](https://github.com/open-telemetry/weaver/blob/main/schemas/semconv-syntax.md) for my telemetry and generate csharp code from them.
My conventions looks like:
```yml
groups:
- id: metric.warehouse.conveyor.processed
type: metric
metric_name: warehouse.conveyor.processed.total
stability: stable
brief: "Total number of items processed by the conveyor"
instrument: counter
unit: "{item}"
attributes:
- id: warehouse.conveyor.id
type: string
stability: stable
brief: "The conveyor unique identifier."
requirement_level: required
- id: warehouse.conveyor.error
type: string
stability: stable
brief: "The error message."
requirement_level:
conditionally_required: if applicable
```
I use Jinja templating ([defined here](https://github.com/AndreReise/weaver-dotnet-example/tree/main/Weaver/Templates/registry/csharp)) to transform that specification into a csharp class utilizes metric source generation feature. Essentially, the template generated code looks like:
```csharp
public static partial class Metrics
{
public struct MetricWarehouseConveyorProcessedTags
{
[global::Microsoft.Extensions.Diagnostics.Metrics.TagName("warehouse_conveyor_id")]
public required string WarehouseConveyorId { get; set; }
[global::Microsoft.Extensions.Diagnostics.Metrics.TagName("warehouse_conveyor_error")]
public string WarehouseConveyorError { get; set; }
}
[global::Microsoft.Extensions.Diagnostics.Metrics.Counter(typeof(MetricWarehouseConveyorProcessedTags), Name = "warehouse_conveyor_processed_total")]
public static partial MetricWarehouseConveyorProcessedMetric CreateMetricWarehouseConveyorProcessed(Meter meter);
}
```
I use the `required` keyword to enforce mandatory properties.
However the source generator produces the following code:
```
public void Add(double value, global::Application.Metrics.MetricWarehouseConveyorProcessedTags o)
{
var tagList = new global::System.Diagnostics.TagList
{
new global::System.Collections.Generic.KeyValuePair("warehouse_conveyor_id", o.WarehouseConveyorId!),
new global::System.Collections.Generic.KeyValuePair("warehouse_conveyor_error", o.WarehouseConveyorError!),
};
_counter.Add(value, tagList);
}
```
Even if `WarehouseConveyorError` is not set, `null` is sent to the metrics backend.
I would like to propose an API to control the behavior of sending (or not sending) default values through the metric processing pipeline.
### API Proposal
```diff
public sealed class TagNameAttribute : Attribute
{
///
/// Initializes a new instance of the class.
///
/// Tag name.
- public TagNameAttribute(string name, bool optional = true)
+ public TagNameAttribute(string name, bool optional = true)
{
Name = name;
+ Optional = optional;
}
///
/// Gets the name of the tag.
///
public string Name { get; }
+ ///
+ /// Tag is optional and will be populated only if value differs from default.
+ ///
+ public bool Optional { get; }
}
```
A new generated code may look like the following:
```csharp
public void Add(double value, global::Application.Metrics.MetricWarehouseConveyorProcessedTags o)
{
var tagList = new global::System.Diagnostics.TagList
{
// Pass only the required in obj initializer
new global::System.Collections.Generic.KeyValuePair("warehouse_conveyor_id", o.WarehouseConveyorId!),
};
// Conditionally populate optional tags
if (o.WarehouseConveyorError != default)
{
tagList.Add(new global::System.Collections.Generic.KeyValuePair("warehouse_conveyor_error", o.WarehouseConveyorError!));
}
_counter.Add(value, tagList);
}
```
### API Usage
```csharp
public struct MetricWarehouseConveyorProcessedTags
{
[TagName("warehouse_conveyor_id")]
public required string WarehouseConveyorId { get; set; }
[TagName("warehouse_conveyor_error", optional: true)]
public string WarehouseConveyorError { get; set; }
}
```
### Alternative Designs
Alternatively, the code generator could emit the mentioned `if (o.WarehouseConveyorError != default)` statement depending on the property's nullability. However, this would be a breaking change.
### Risks
Performance implications: to be determined.
Contributor guide
Assessment
This issue has not been assessed yet.