Skip to content

[API Proposal]: Optional tags for metric source generation #7265

@AndreReise

Description

@AndreReise

Background and motivation

I'm trying to define OpenTelemetry Weaver semantic conventions for my telemetry and generate csharp code from them.

My conventions looks like:

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) to transform that specification into a csharp class utilizes metric source generation feature. Essentially, the template generated code looks like:

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<double>(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<string, object?>("warehouse_conveyor_id", o.WarehouseConveyorId!),
         new global::System.Collections.Generic.KeyValuePair<string, object?>("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

public sealed class TagNameAttribute : Attribute
{
    /// <summary>
    /// Initializes a new instance of the <see cref="TagNameAttribute"/> class.
    /// </summary>
    /// <param name="name">Tag name.</param>
-  public TagNameAttribute(string name, bool optional = true)
+ public TagNameAttribute(string name, bool optional = true)
    {
        Name = name;
+      Optional = optional;
    }

    /// <summary>
    /// Gets the name of the tag.
    /// </summary>
    public string Name { get; }

+   /// <summary>
+   /// Tag is optional and will be populated only if value differs from default.
+   /// </summary>
+    public bool Optional { get;  }
}

A new generated code may look like the following:

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<string, object?>("warehouse_conveyor_id", o.WarehouseConveyorId!),
    };

    // Conditionally populate optional tags
    if (o.WarehouseConveyorError != default)
    {
        tagList.Add(new global::System.Collections.Generic.KeyValuePair<string, object?>("warehouse_conveyor_error", o.WarehouseConveyorError!));
    }

    _counter.Add(value, tagList);
}

API Usage

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions