Lean .NET observability on OpenTelemetry. IWitness<T> gives each call site one place for logs, metrics, and traces while keeping ILogger<T>, Meter, ActivitySource, and OpenTelemetry exporters directly accessible. Supports net8.0 and net10.0.
// 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.
IWitness<T> bundles ILogger<T>, Meter, and ActivitySource into one injectable.
WitnessedAction wraps an Activity created via witness.StartAction("Name"). Mark outcomes with Dispose(), Failed(ex), or Cancelled():
using var action = witness.StartAction("RetrieveSummary");
try { return await _controller.RetrieveSummaryAsync(); }
catch (Exception ex) { action.Failed(ex); throw; }Logging extension methods on IWitness<T> are optimized on net9.0+ via source-generator interceptor; on net8.0, use the WS0001 analyzer's code-fix:
public static void LogOrderPlaced(this IWitness<OrderService> witness, int orderId) =>
witness.Logger.LogInformation("Order {OrderId} placed", orderId);dotnet add package WitnessSharp
dotnet add package WitnessSharp.AzureMonitor # optional
dotnet add package WitnessSharp.Analyzers # optional
dotnet add package WitnessSharp.Testing # test projectsConfigure from appsettings.json:
{
"Witness": {
"ServiceName": "orders-api",
"ServiceNamespace": "Contoso.Commerce",
"ServiceVersion": "1.3.0",
"ServiceInstanceId": "orders-api-01",
"DeploymentEnvironment": "Production",
"AdditionalResourceAttributes": { "service.owner": "checkout" }
}
}Or use the fluent builder: AddWitness(options => { ... }).WithStandardInstrumentations().WithOtlpExporter() for direct OTel SDK access via .ConfigureTracing(...), .ConfigureMetrics(...), etc.
Filtering traces: Use ConfigureTracing() to customize. For example, exclude health checks:
.ConfigureTracing(tracing => tracing.AddAspNetCoreInstrumentation(options =>
options.Filter = ctx => !ctx.Request.Path.StartsWithSegments("/health")))Azure Monitor: Call .WithAzureMonitor() from WitnessSharp.AzureMonitor. The connection string is read from APPLICATIONINSIGHTS_CONNECTION_STRING.
WitnessSharp.Testing provides TestWitness<T> to capture and assert on logs, metrics, and activities:
using var witness = new TestWitness<OrderService>();
witness.Logger.LogInformation("Placed order 42");
witness.Meter.CreateCounter<int>("orders").Add(1);
witness.StartAction("PlaceOrder").Dispose();
witness.AssertLogged(LogLevel.Information, "Placed order");
witness.AssertMetricRecorded("orders");
witness.AssertActivityStarted("PlaceOrder");WitnessSharp.Analyzers suggests [LoggerMessage] for templated logging in extension methods. See WS0001 rule and LoggerMessage docs.
WitnessSharp is AOT/trim-friendly. Upstream instrumentation and exporter packages may emit warnings when publishing with PublishAot=true.
Build with dotnet build WitnessSharp.slnx, test with dotnet test WitnessSharp.slnx, then open a pull request.
MIT. See LICENSE.