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:
| Interface | Generated class |
|---|---|
IOrderService | OrderServiceInstrumented |
IPaymentGateway | PaymentGatewayInstrumented |
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 name | Field name |
|---|---|
orders.created | _orders_created |
order.get_ms | _order_get_ms |
payment.charge-duration | _payment_charge_duration |
v1 Limitations
interfacetargets only —classis not supportedrefandoutparameters are not supported- Generic interface methods are not supported
- Sync methods are supported (no
async/awaitwrapper needed)