Put [Instrument] on an interface and get OpenTelemetry spans and metrics for every call, generated at compile time: no hand-written ActivitySource boilerplate, no reflection, and nothing allocated when nobody is listening.
- 0 B per call when nothing listens, the common production case after sampling. Tests assert it for
Task,ValueTaskandValueTask<T>methods, tagged metrics included. - 7.5× faster than a straightforward hand-written
ActivitySource+Meterwrapper with no listener attached, measured in the same run: the proxy skips the instrumented path entirely until something subscribes. - Works under NativeAOT and trimming. Plain generated code over BCL
ActivitySourceandMeter, no reflection and noparams object[]; CI publishes a NativeAOT app. - Mistakes surface at compile time. A result member that is not numeric, or a metric attribute on an interface without
[Instrument], is a compiler diagnostic.
dotnet add package ZeroAlloc.TelemetryNo OpenTelemetry SDK dependency: add the exporters you want, OTLP, Prometheus or Console, in your application.
using System.Diagnostics;
using ZeroAlloc.Telemetry;
using var listener = new ActivityListener
{
ShouldListenTo = source => source.Name == "MyApp.Orders",
Sample = (ref ActivityCreationOptions<ActivityContext> _) => ActivitySamplingResult.AllData,
ActivityStopped = activity => Console.WriteLine($"span {activity.DisplayName}: {activity.Duration.TotalMilliseconds:F1} ms"),
};
ActivitySource.AddActivityListener(listener);
IOrderService orders = new OrderServiceInstrumented(new OrderService()); // generated proxy
await orders.PlaceOrderAsync("book", CancellationToken.None); // prints the order.place span
[Instrument("MyApp.Orders")]
public interface IOrderService
{
[Trace("order.place")]
[Count("orders.placed")]
[Histogram("order.place_ms")]
ValueTask<int> PlaceOrderAsync(string product, CancellationToken ct);
}
public sealed class OrderService : IOrderService
{
public async ValueTask<int> PlaceOrderAsync(string product, CancellationToken ct)
{
await Task.Delay(10, ct);
return 42;
}
}In an application, register the proxy in DI in place of the implementation, and subscribe with OpenTelemetry's AddSource("MyApp.Orders") and AddMeter("MyApp.Orders"). Your code keeps calling IOrderService.
- Spans per method, with error status and
error.typeon exceptions, or on a failedResultyou choose to treat as an error. - Counters and duration histograms per method, recorded on success or on every call, optionally only when a result guard such as
IsSuccessholds. - Metrics from the return value, such as tokens consumed or a cost, recorded straight from a member of the result.
- Tags on spans and metrics from arguments, from the result, or as constants, built only when a listener needs them.
- Consistent instrumentation: every method gets the same span, counter and histogram pattern, with no Activity left undisposed and no metric forgotten.
- Testable with BCL listeners, no exporter needed: Testing.
- Every attribute and option: attribute reference. Compile-time diagnostics: the full list.
No listener attached, the production case after sampling. .NET 10.0.13, BenchmarkDotNet v0.15.8. Time is relative to the uninstrumented call in the same run; allocations are exact.
| Method | Time vs direct call | Allocated |
|---|---|---|
| Direct call, no instrumentation | 1× | 0 B |
Hand-written ActivitySource + Counter + Histogram |
60.6× | 0 B |
| ZA.Telemetry generated proxy | 8.1× | 0 B |
Neither wrapper allocates. The hand-written version still runs its async state machine, takes timestamps and calls instruments nobody listens to; the proxy checks for listeners and hands back the inner task as is. Hand-written code can do the same, but every method has to remember to. Methodology: docs/performance.md.
- Documentation: telemetry.zeroalloc.net
- Getting started, what the generator emits and AOT and trimming
- Related packages: ZeroAlloc.Cache and ZeroAlloc.Resilience, proxies generated the same way; ZeroAlloc.Results for
Result-aware metrics - Contributing: issues and pull requests are welcome on GitHub
- License: MIT