Skip to main content

Source Generator

ZeroAlloc.Telemetry bundles a Roslyn incremental source generator. For every interface annotated with [Instrument] it emits a sealed proxy class that wraps an inner implementation and records instrumentation per method.

Trigger

The generator uses ForAttributeWithMetadataName to watch for ZeroAlloc.Telemetry.InstrumentAttribute on interface declarations. Only interface targets are supported — class is ignored.

Naming

The generated class name is the interface name with the leading I stripped (if present) plus Instrumented:

InterfaceGenerated class
IOrderServiceOrderServiceInstrumented
IPaymentGatewayPaymentGatewayInstrumented
OrderRepository (no I)OrderRepositoryInstrumented

The generated class is internal sealed and placed in the same namespace as the interface.

Generated Class Layout

Given:

[Instrument("MyApp.Orders")]
public interface IOrderService
{
[Trace("order.create")]
[Count("orders.created")]
ValueTask<OrderId> CreateOrderAsync(CreateOrderCommand cmd, CancellationToken ct);

[Trace("order.get")]
[Histogram("order.get_ms")]
ValueTask<Order> GetOrderAsync(OrderId id, CancellationToken ct);
}

The generator emits:

// <auto-generated />
#nullable enable

using System;
using System.Diagnostics;
using System.Diagnostics.Metrics;

namespace MyApp.Orders;

internal sealed class OrderServiceInstrumented : IOrderService
{
// One ActivitySource per [Instrument] annotation — static, process-lifetime
private static readonly ActivitySource _activitySource = new("MyApp.Orders");
// One Meter per [Instrument] annotation — same name as ActivitySource
private static readonly Meter _meter = new("MyApp.Orders");
// One Counter<long> per unique [Count] metric name across all methods
private static readonly Counter<long> _orders_created =
_meter.CreateCounter<long>("orders.created");
// One Histogram<double> per unique [Histogram] metric name across all methods
private static readonly Histogram<double> _order_get_ms =
_meter.CreateHistogram<double>("order.get_ms");

private readonly IOrderService _inner;
public OrderServiceInstrumented(IOrderService inner) => _inner = inner;

// ... one method per interface method
}

Static metric fields are deduplicated by metric name — if two methods share [Count("orders.created")], only one Counter<long> field is emitted.

Per-Method Output

[Trace] + [Count]

Input:

[Trace("order.create")]
[Count("orders.created")]
ValueTask<OrderId> CreateOrderAsync(CreateOrderCommand cmd, CancellationToken ct);

Output:

public async ValueTask<OrderId> CreateOrderAsync(CreateOrderCommand cmd, CancellationToken ct)
{
using var _activity = _activitySource.StartActivity("order.create");
try
{
var _result = await _inner.CreateOrderAsync(cmd, ct);
_orders_created.Add(1);
return _result;
}
catch (Exception _ex)
{
_activity?.SetStatus(ActivityStatusCode.Error, _ex.Message);
throw;
}
}

[Trace] + [Histogram]

Input:

[Trace("order.get")]
[Histogram("order.get_ms")]
ValueTask<Order> GetOrderAsync(OrderId id, CancellationToken ct);

Output:

public async ValueTask<Order> GetOrderAsync(OrderId id, CancellationToken ct)
{
using var _activity = _activitySource.StartActivity("order.get");
var _sw = Stopwatch.GetTimestamp();
try
{
var _result = await _inner.GetOrderAsync(id, ct);
_order_get_ms.Record(Stopwatch.GetElapsedTime(_sw).TotalMilliseconds);
return _result;
}
catch (Exception _ex)
{
_activity?.SetStatus(ActivityStatusCode.Error, _ex.Message);
_order_get_ms.Record(Stopwatch.GetElapsedTime(_sw).TotalMilliseconds);
throw;
}
}

No attributes (pass-through)

Input:

ValueTask DeleteOrderAsync(OrderId id, CancellationToken ct);

Output:

public async ValueTask DeleteOrderAsync(OrderId id, CancellationToken ct)
{
await _inner.DeleteOrderAsync(id, ct);
}

No try/catch, no timing, no span.

Field Name Derivation

Metric names are converted to valid C# identifiers for field names by replacing . and - with _:

Metric nameField name
orders.created_orders_created
order.get_ms_order_get_ms
payment.charge-duration_payment_charge_duration

v1 Limitations

  • interface targets only — class is not supported
  • ref and out parameters are not supported
  • Generic interface methods are not supported
  • Sync methods are supported (no async/await wrapper needed)