dotnet / dotnet/extensions

[API Proposal]: Optional tags for metric source generation

Open
#7,265 0 comments 0 reactions 0 assignees View on GitHub
api-suggestion area-telemetry untriaged
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.