Skip to content

Tracing ​

Zero-cost developer tracing built on System.Diagnostics.ActivitySource. When no listener is configured, trace calls are no-ops (single null-check).

The entry point is Senf.Tracing — a static ServusTrace instance. You create named channels, trace through them, and wire up a listener at startup.

Quick start ​

csharp
using Servus.Diagnostics;

// 1. Create a channel (store in a static field for zero-allocation reuse)
private static readonly TraceChannel _trace = Senf.Tracing.For("OrderService");

// 2. Trace at any level
_trace.Info(this, "Processing order {0}", orderId);
_trace.Debug(this, "Calculated total: {0:C}", total);
_trace.Warning(this, "Retry attempt {0} of {1}", attempt, maxRetries);

Without a listener configured, every call above is a no-op.

Configuring a listener ​

Via DI — bridge to ILogger ​

The most common setup: pipe trace events into the standard logging pipeline.

csharp
using Servus.Diagnostics;

builder.Services.AddServusLoggerTracing(TraceLevel.Debug);

Filter by category:

csharp
builder.Services.AddServusLoggerTracing(
    TraceLevel.Debug,
    "OrderService", "PaymentService");

// Or with a predicate
builder.Services.AddServusLoggerTracing(
    TraceLevel.Debug,
    category => category.StartsWith("Order"));

Manual — custom listener ​

Implement IServusTraceListener for custom sinks (metrics, file, network):

csharp
public class ConsoleTraceListener : IServusTraceListener
{
    public bool IsEnabled(TraceLevel level, string category) => true;

    public void Write(in TraceEvent evt)
    {
        Console.WriteLine($"[{evt.Level}] {evt.Category}/{evt.SourceType}: {evt.FormatMessage()}");
    }
}

// Register
builder.Services.AddServusTraceListener(
    new ConsoleTraceListener(),
    TraceLevel.Trace);

TraceChannel ​

A channel is bound to a single category. It exposes level-specific methods:

csharp
private static readonly TraceChannel _trace = Senf.Tracing.For("MyCategory");

_trace.Trace(this, "Finest detail");
_trace.Debug(this, "Diagnostic info");
_trace.Info(this, "Notable event: {0}", eventName);
_trace.Warning(this, "Unexpected state: {0}", state);
_trace.Error(this, "Failed: {0}", exception.Message);

Each method takes the source object (this) for identity tracking and a format template with optional arguments. Formatting is deferred — the TraceEvent.FormatMessage() allocation only happens inside the listener.

TraceEvent ​

An immutable struct passed to listeners:

PropertyTypeDescription
TimestampTickslongStopwatch.GetTimestamp() value
LevelTraceLevelSeverity
CategorystringChannel category name
SourceTypestringShort type name of the source object
SourceHashintIdentity hash of the source object
TemplatestringFormat template

Call FormatMessage() to produce the formatted string.

TraceLevel ​

csharp
public enum TraceLevel : byte
{
    Trace   = 0,
    Debug   = 1,
    Info    = 2,
    Warning = 3,
    Error   = 4,
}

Maps directly to Microsoft.Extensions.Logging.LogLevel.

ActivitySource ​

ServusTrace also exposes an ActivitySource named "Servus" for OpenTelemetry integration:

csharp
using var activity = Senf.Tracing.Source.StartActivity("process-order");

API ​

csharp
public static class Senf
{
    public static readonly ServusTrace Tracing;
    public static readonly ServusMetrics Metrics;
}

public class ServusTrace
{
    public ActivitySource Source { get; }

    public void Configure(IServusTraceListener listener,
        TraceLevel minimumLevel = TraceLevel.Trace,
        Func<string, bool>? categoryFilter = null);

    public TraceChannel For(string categoryName);
    public void Disable();
}

public readonly struct TraceChannel
{
    public void Trace<T>(T source, string message, params object?[] args);
    public void Debug<T>(T source, string message, params object?[] args);
    public void Info<T>(T source, string message, params object?[] args);
    public void Warning<T>(T source, string message, params object?[] args);
    public void Error<T>(T source, string message, params object?[] args);
}

public interface IServusTraceListener
{
    bool IsEnabled(TraceLevel level, string category);
    void Write(in TraceEvent evt);
}

public static class ServusTraceExtensions
{
    public static IServiceCollection AddServusLoggerTracing(
        this IServiceCollection services,
        TraceLevel minimumLevel = TraceLevel.Debug,
        params string[] categories);

    public static IServiceCollection AddServusLoggerTracing(
        this IServiceCollection services,
        TraceLevel minimumLevel = TraceLevel.Debug,
        Func<string, bool>? categoryFilter = null);

    public static IServiceCollection AddServusTraceListener(
        this IServiceCollection services,
        IServusTraceListener listener,
        TraceLevel minimumLevel = TraceLevel.Debug,
        params string[] categories);

    public static IServiceCollection AddServusTraceListener(
        this IServiceCollection services,
        IServusTraceListener listener,
        TraceLevel minimumLevel = TraceLevel.Debug,
        Func<string, bool>? categoryFilter = null);
}

Servus and happy coding! 🥨🍺