Trace Source Generation
The Cratis.Metrics.Roslyn package can generate trace span implementations from partial methods decorated with [Span]. This keeps raw ActivitySource usage out of your business code and applies a consistent convention for span creation and tagging.
Table of Contents
Section titled “Table of Contents”- Installation
- How It Works
- Getting Started
- Method Signature Requirements
- Generated Code
- Analyzer
- Diagnostics
- Best Practices
Installation
Section titled “Installation”Add the Cratis.Metrics.Roslyn package to your project:
<PackageReference Include="Cratis.Metrics.Roslyn" Version="[version]" />Or use the .NET CLI:
dotnet add package Cratis.Metrics.RoslynHow It Works
Section titled “How It Works”At compile time, the generator looks for:
static partialmethods in astatic partialclass- methods decorated with
[Span] - methods whose first parameter is
IActivitySource<T> - methods that return
IActivityScope<T>
For each matching method, the generator:
- calls
source.ActualSource.StartActivity() - applies additional parameters as tags
- converts tag names to
snake_case - returns
ActivityScope<T>
Getting Started
Section titled “Getting Started”Declare your spans in a partial class. Prefer extension methods so call sites stay close to the injected activity source:
using Cratis.Traces;using System.Diagnostics;
namespace MyApplication.Orders;
public static partial class OrderTraces{ [Span("order.process", ActivityKind.Server)] internal static partial IActivityScope<OrderService> ProcessOrder( this IActivitySource<OrderService> source, string orderId, string customerId);
[Span("order.validate")] internal static partial IActivityScope<OrderService> ValidateOrder( this IActivitySource<OrderService> source, string orderId);}Use the generated methods like this:
using var processScope = _activitySource.ProcessOrder(order.Id, order.CustomerId);using var validationScope = _activitySource.ValidateOrder(order.Id);Method Signature Requirements
Section titled “Method Signature Requirements”Every generated trace method must follow this pattern:
[Span("name", ActivityKind.Internal | ActivityKind.Server | ActivityKind.Client | ActivityKind.Producer | ActivityKind.Consumer)]static partial IActivityScope<TService> MethodName( this IActivitySource<TService> source, [tag parameters...]);Rules:
- The first parameter must be
IActivitySource<T> - The return type must be
IActivityScope<T> - Every parameter after the first becomes a tag
- Methods must be
static partial - The containing class must be
static partial - Prefer marking the first parameter with
thisso the generated method becomes an extension method
Tag naming
Section titled “Tag naming”The generator converts parameter names to snake_case tag names:
orderIdbecomesorder_idcustomerIdbecomescustomer_idorderIDbecomesorder_id
Generated Code
Section titled “Generated Code”Given this declaration:
[Span("order.process", ActivityKind.Server)]internal static partial IActivityScope<OrderService> ProcessOrder( this IActivitySource<OrderService> source, string orderId, string customerId);The generator emits code equivalent to:
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Cratis.Metrics.Roslyn", "1.0.0")]internal static partial IActivityScope<OrderService> ProcessOrder( this IActivitySource<OrderService> source, string orderId, string customerId){ var activity = source.ActualSource.StartActivity("order.process", ActivityKind.Server); activity?.SetTag("order_id", orderId); activity?.SetTag("customer_id", customerId); return new global::Cratis.Traces.ActivityScope<OrderService>(activity);}Analyzer
Section titled “Analyzer”The package also includes analyzer CRT0001.
CRT0001
Section titled “CRT0001”IActivityScope<T> must be assigned with using so the span is stopped correctly.
This produces a diagnostic:
var scope = _activitySource.ProcessOrder(order.Id, order.CustomerId);These patterns are valid:
using var scope = _activitySource.ProcessOrder(order.Id, order.CustomerId);using (_activitySource.ProcessOrder(order.Id, order.CustomerId)){ await Process(order);}Diagnostics
Section titled “Diagnostics”The trace generator reports these compile-time diagnostics:
TRACES001
Section titled “TRACES001”The method declares no parameters, so the required IActivitySource<T> first parameter is missing.
[Span("order.process")]internal static partial IActivityScope<OrderService> ProcessOrder();TRACES002
Section titled “TRACES002”The first parameter is not IActivitySource<T>.
[Span("order.process")]internal static partial IActivityScope<OrderService> ProcessOrder(string source, string orderId);TRACES003
Section titled “TRACES003”The method does not return IActivityScope<T>.
[Span("order.process")]internal static partial void ProcessOrder(IActivitySource<OrderService> source, string orderId);Best Practices
Section titled “Best Practices”- Keep generated span declarations close to the service they support
- Use names that describe work, not implementation details
- Prefer a small number of meaningful tags
- Use
ActivityKindexplicitly for public boundaries such as server and client spans - Prefer extension methods on
IActivitySource<T>for cleaner call sites - Treat
CRT0001as required, not optional