Skip to content

Repository files navigation

WitnessSharp

NuGet version Build status Mutation testing License

Lean .NET observability on OpenTelemetry. IWitness<T> gives each call site one place for logs, metrics, and traces.

WitnessSharp keeps the underlying .NET types visible. You still work with ILogger<T>, Meter, ActivitySource, configuration binding, and OpenTelemetry exporters. The package just gives them a clean shape and a small bootstrap API.

Supports net8.0 and net10.0.

30-second quickstart

// Program.cs
builder.Services.AddWitness(builder.Configuration.GetSection("Witness"))
    .WithStandardInstrumentations()
    .WithOtlpExporter();

// In your service
public sealed class OrderService(IWitness<OrderService> witness)
{
    public void PlaceOrder(int orderId)
    {
        using var action = witness.StartAction("PlaceOrder");
        action.SetTag("order.id", orderId);
        // business logic
    }
}

AddWitness() binds WitnessOptions from the "Witness" section. Registration without any .With*() calls is valid if you only want the core primitives.

Concepts

IWitness<T>

IWitness<T> is the main injectable — it bundles ILogger<T>, Meter, and ActivitySource with no new abstractions over them. Most classes only need IWitness<T>; for witnesses created at runtime, inject IWitnessFactory and call Create<T>().

WitnessedAction

WitnessedAction wraps an Activity. Start one with witness.StartAction("Name"), attach tags or events, and dispose when the operation ends. Outcomes default to success; call Failed(Exception), Failed(string), or Cancelled() as needed. Finish() stops recording early without disposing the wrapper.

When started from a typed IWitness<T>, the action also implements IWitness<T>, so extension methods resolve directly:

public async Task<DashboardSummary> RetrieveSummaryAsync()
{
    using var action = witness.StartAction(nameof(RetrieveSummaryAsync));
    try
    {
        var summary = await _controller.RetrieveSummaryAsync();
        action.LogDashboardSummaryRetrieved(); // same extension you'd call on the witness
        return summary;
    }
    catch (Exception exception)
    {
        action.Failed(exception);
        throw;
    }
}

Use var (not an explicit WitnessedAction type) so the action keeps its IWitness<T> facet and the typed extension methods resolve.

Logging via extension methods

WitnessSharp leans toward extension methods on IWitness<T> for recurring log messages. That keeps message templates in one place and keeps call sites small.

public static class OrderServiceWitnessExtensions
{
    public static void LogOrderPlaced(this IWitness<OrderService> witness, int orderId) =>
        witness.Logger.LogInformation("Order {OrderId} placed", orderId);
}

The optional analyzer package spots these patterns and nudges you toward LoggerMessage where it pays off.

Installation

dotnet add package WitnessSharp
dotnet add package WitnessSharp.AzureMonitor  # optional
dotnet add package WitnessSharp.Analyzers     # optional
dotnet add package WitnessSharp.Testing       # test projects

Configuration reference

You can configure WitnessSharp with either overload:

builder.Services.AddWitness(builder.Configuration.GetSection("Witness"));

// or
builder.Services.AddWitness(options =>
{
    options.ServiceName = "orders-api";
});

appsettings.json

{
  "Witness": {
    "ServiceName": "orders-api",
    "ServiceNamespace": "Contoso.Commerce",
    "ServiceVersion": "1.3.0",
    "ServiceInstanceId": "orders-api-01",
    "DeploymentEnvironment": "Production",
    "AdditionalResourceAttributes": {
      "service.owner": "checkout",
      "cloud.region": "westeurope",
      "deployment.ring": "blue"
    }
  }
}

WitnessOptions

Property Description Default
ServiceName Sets service.name. This is the main identity of your service. Empty string. Set this in real apps.
ServiceNamespace Sets service.namespace. Useful when several services share the same base name. null
ServiceVersion Sets service.version. null
ServiceInstanceId Sets service.instance.id. Environment.MachineName
DeploymentEnvironment Sets deployment.environment. DOTNET_ENVIRONMENT, then ASPNETCORE_ENVIRONMENT
AdditionalResourceAttributes Adds any extra resource attributes you want on logs, metrics, and traces. Empty dictionary

Fluent builder methods

Registration by itself is valid. Add builder methods when you want instrumentations or exporters.

Method What it does Notes
WithStandardInstrumentations() Adds ASP.NET Core and HttpClient tracing instrumentation. Good default for web apps.
WithAspNetCoreInstrumentation(...) Adds ASP.NET Core tracing instrumentation. Use the overload when you need request filtering or enrichment.
WithHttpClientInstrumentation(...) Adds HttpClient tracing instrumentation. Useful for outbound calls from services or APIs.
WithOtlpExporter(...) Adds OTLP exporters for traces, metrics, and logs. Good fit for OpenTelemetry Collector, Jaeger, Tempo, and similar backends.
WithConsoleExporter() Adds console exporters for traces, metrics, and logs. Handy for local debugging.
WithAzureMonitor(...) Adds Azure Monitor exporters for traces, metrics, and logs. Comes from WitnessSharp.AzureMonitor.
ClearLoggingProviders() Clears existing Microsoft.Extensions.Logging providers before OpenTelemetry logging is added. Opt in only if you want OTel to be the only logging provider.

Escape hatches

Use the escape hatches when the built-in convenience methods are not enough:

Method Use it for
ConfigureTracing(Action<TracerProviderBuilder>) Custom sources, filters, processors, samplers, or exporter pipelines
ConfigureMetrics(Action<MeterProviderBuilder>) Custom meters, views, readers, or exporters
ConfigureLogging(Action<OpenTelemetryLoggerOptions>) OpenTelemetry logging options and exporters

If you configure an instrumentation manually through ConfigureTracing, skip the matching convenience method to avoid registering the same instrumentation twice.

Recipes

WitnessSharp does not ship hard-coded health-check or SQL filters. Those choices depend on your app. Use the escape hatches and keep the policy in your service code.

Filter out health-check spans

Use ConfigureTracing() when you need to own the ASP.NET Core instrumentation options.

builder.Services.AddWitness(builder.Configuration.GetSection("Witness"))
    .ConfigureTracing(tracing =>
    {
        tracing.AddAspNetCoreInstrumentation(options =>
        {
            options.Filter = httpContext =>
                !httpContext.Request.Path.StartsWithSegments("/health") &&
                !httpContext.Request.Path.StartsWithSegments("/ready");
        });

        tracing.AddHttpClientInstrumentation();
    })
    .WithOtlpExporter();

This pattern is a good fit when WithStandardInstrumentations() is almost right, but you need a request filter.

Filter fast SQL spans with a custom processor

Duration-based SQL filtering is app-specific, so WitnessSharp leaves it to your tracing pipeline. This example keeps SQL spans that run for at least 100 ms and exports everything else as usual.

This recipe assumes you have also installed the SQL client instrumentation package from the OpenTelemetry ecosystem.

using System.Diagnostics;
using System.Linq;
using OpenTelemetry;
using OpenTelemetry.Exporter;
using OpenTelemetry.Trace;

public sealed class MinimumDurationSqlProcessor : BaseProcessor<Activity>
{
    private readonly BatchActivityExportProcessor _inner;
    private readonly TimeSpan _minimumDuration;

    public MinimumDurationSqlProcessor(BaseExporter<Activity> exporter, TimeSpan minimumDuration)
    {
        _inner = new BatchActivityExportProcessor(exporter);
        _minimumDuration = minimumDuration;
    }

    public override void OnEnd(Activity data)
    {
        var isSqlSpan = data.Kind == ActivityKind.Client &&
            data.Tags.Any(tag => tag.Key == "db.system");

        if (!isSqlSpan || data.Duration >= _minimumDuration)
        {
            _inner.OnEnd(data);
        }
    }

    protected override bool OnForceFlush(int timeoutMilliseconds) =>
        _inner.ForceFlush(timeoutMilliseconds);

    protected override bool OnShutdown(int timeoutMilliseconds) =>
        _inner.Shutdown(timeoutMilliseconds);

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            _inner.Dispose();
        }

        base.Dispose(disposing);
    }
}
builder.Services.AddWitness(builder.Configuration.GetSection("Witness"))
    .ConfigureTracing(tracing =>
    {
        tracing.AddSqlClientInstrumentation();
        tracing.AddProcessor(new MinimumDurationSqlProcessor(
            new OtlpTraceExporter(new OtlpExporterOptions
            {
                Endpoint = new Uri("http://localhost:4317")
            }),
            TimeSpan.FromMilliseconds(100)));
    })
    .ConfigureMetrics(metrics => metrics.AddOtlpExporter())
    .ConfigureLogging(logging => logging.AddOtlpExporter());

Do not combine this trace setup with .WithOtlpExporter(), or you will export traces twice.

Send all three signals to Azure Monitor

Install WitnessSharp.AzureMonitor, then add the Azure Monitor exporters with one call.

builder.Services.AddWitness(builder.Configuration.GetSection("Witness"))
    .WithStandardInstrumentations()
    .WithAzureMonitor(options =>
    {
        options.ConnectionString = builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"];
    });

If your environment already sets APPLICATIONINSIGHTS_CONNECTION_STRING, the parameterless .WithAzureMonitor() overload also works.

See the Azure Monitor OpenTelemetry exporter docs for Azure-specific options and guidance.

Add custom resource attributes

AdditionalResourceAttributes adds shared metadata to logs, metrics, and traces. To set it in code:

builder.Services.AddWitness(options =>
{
    options.ServiceName = "orders-api";
    options.AdditionalResourceAttributes["service.owner"] = "checkout";
    options.AdditionalResourceAttributes["cloud.region"] = "westeurope";
    options.AdditionalResourceAttributes["deployment.ring"] = "blue";
});

Testing

WitnessSharp.Testing gives you TestWitness<T>, an in-memory test double that records logged messages, metrics, and activities. It includes AssertLogged(...), AssertMetricRecorded(...), and AssertActivityStarted(...) assertion helpers.

Example:

using Microsoft.Extensions.Logging;
using WitnessSharp.Testing;

public class OrderServiceTests
{
    [Fact]
    public void PlaceOrder_emits_expected_telemetry()
    {
        using var witness = new TestWitness<OrderService>();
        var counter = witness.Meter.CreateCounter<int>("orders");

        witness.Logger.LogInformation("Placed order 42");
        counter.Add(1);

        using (witness.StartAction("PlaceOrder"))
        {
        }

        witness.AssertLogged(LogLevel.Information, "Placed order");
        witness.AssertMetricRecorded("orders");
        witness.AssertActivityStarted("PlaceOrder");
    }
}

Analyzer (WS0001)

WitnessSharp.Analyzers is an optional Roslyn analyzer package. WS0001 flags witness.Logger.Log*(...) calls inside IWitness<T> extension methods and provides a code fix that rewrites them to [LoggerMessage] for allocation-free structured logging. See the WS0001 rule documentation for the full fix pattern.

Configure severity in .editorconfig

dotnet_diagnostic.WS0001.severity = warning

For background on source-generated logging, see the official LoggerMessage docs.

AOT support

WitnessSharp is designed to stay friendly to trimming and native AOT. The core package uses standard .NET and OpenTelemetry APIs. Your final AOT story depends on the instrumentations and exporters you enable — when publishing with PublishAot=true, watch for warnings from upstream packages.

Package family

Package Purpose
WitnessSharp Core primitives, DI registration, IWitness<T>, WitnessedAction, options, and fluent builder extensions
WitnessSharp.AzureMonitor Azure Monitor exporter wiring via .WithAzureMonitor()
WitnessSharp.Analyzers Roslyn analyzer package with WS0001
WitnessSharp.Testing TestWitness<T> and assertion helpers for test projects

Contributing

Contributions are welcome. Build with dotnet build WitnessSharp.slnx, test with dotnet test WitnessSharp.slnx, then open a pull request. If a CONTRIBUTING.md appears, follow that file first.

License

MIT. See LICENSE.

Further reading

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages