The case for an application-level tracing API in .NET
If you want to record a log event from your application in .NET, you can do that today without a lot of noise or ceremony using Microsoft.Extensions.Logging:
_log.LogInformation("Completing order {OrderId} for {Customer}", order.Id, order.Customer);
Admittedly it’s a long line, but from this statement, packed in alongside the timestamp, level, and whatever correlation identifiers are set behind the scenes, you get:
- A human-friendly message —
"Completing order 15 for nblumhardt"
- A low-cardinality event type —
"Completing order {OrderId} for {Customer}"
- A fully-structured payload —
{"OrderId": 15, "Customer": "nblumhardt"}
Packing all of this into a single statement is important: it’s easy for diagnostics to start obscuring the real application logic, otherwise.
If you want to record a trace span (timed operation) from your application code in .NET today, you’ll need to use System.Diagnostics:
using var complete = MyActivitySource.StartActivity("Complete order");
complete?.AddTag("OrderId", order.Id);
complete?.AddTag("Customer", order.Custome);
Three lines in this case, but also, while we’ve maintained the structured payload and low-cardinality/machine-readable event type ("Complete order"
), there’s no pleasant human-readable description of the span that includes the vital details of this span itself. If you want that you’ll need to add at least one more line.
There’s also the distracting ?
language machinery needed to handle null
activities, or danger of forgetting about them in projects that don’t enable strict null checks.
But what do we actually get in return, that the logging API doesn’t provide? Just a duration and a parent span id, if there is one. The activity ends up producing output that isn’t all that different from a log event.
So far, the major consumers of System.Diagnostics.ActivitySource
are building frameworks and libraries, and using the current API it’s possible to generate spans precisely and efficiently. Frameworks like ASP.NET Core have different needs from regular applications, and seeing a bit more plumbing exposed in those codebases isn’t a big problem.
Noise and complexity is a bigger concern in application code, though.
Couldn’t we combine the ideas from these APIs and have a tracing experience that’s the best of both worlds?
using var complete = MyActivitySource.StartActivity(
"Complete order {OrderId} for {Customer}", order.Id, order.Customer);
This is the API that SerilogTracing provides:
using var complete = _log.StartActivity(
"Complete order {OrderId} for {Customer}", order.Id, order.Customer);
In framework code it’s just sugar; but in application code — this is how I want to record traces! As SerilogTracing picks up steam ahead of its 1.0, it will be interesting to see whether someone can bring this UX back to ILogger<T>
and/or ActivitySource
, too.