WitnessSharp 0.0.2-alpha
dotnet add package WitnessSharp --version 0.0.2-alpha
NuGet\Install-Package WitnessSharp -Version 0.0.2-alpha
<PackageReference Include="WitnessSharp" Version="0.0.2-alpha" />
<PackageVersion Include="WitnessSharp" Version="0.0.2-alpha" />
<PackageReference Include="WitnessSharp" />
paket add WitnessSharp --version 0.0.2-alpha
#r "nuget: WitnessSharp, 0.0.2-alpha"
#:package WitnessSharp@0.0.2-alpha
#addin nuget:?package=WitnessSharp&version=0.0.2-alpha&prerelease
#tool nuget:?package=WitnessSharp&version=0.0.2-alpha&prerelease
WitnessSharp
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 thing you inject. It bundles:
ILogger<T>for logsMeterfor metricsActivitySourcefor traces
That shape keeps constructors short and keeps related observability tools together. It also avoids inventing new logging or metrics abstractions. If you already know the built-in .NET types, you already know most of WitnessSharp.
Most classes only need IWitness<T>. If you need a typed witness for a type discovered at runtime, inject IWitnessFactory and call Create<T>().
WitnessedAction
WitnessedAction is a small wrapper around an Activity. Start one with witness.StartAction("Name"), attach tags or events, and dispose it when the operation ends.
Outcomes are explicit:
- success is the default
Failed(Exception)orFailed(string)marks the action as a failureCancelled()marks it as cancelled
Dispose() sets the final activity status and closes the activity. Finish() is also available when you need to stop early without disposing the wrapper yet.
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.
Design philosophy
- Lean defaults. Nothing is enabled unless you opt in.
- Fluent setup. Start with
AddWitness(), then add instrumentations and exporters you actually want. - Native .NET first. WitnessSharp does not hide
ILogger,Meter,ActivitySource,IConfiguration, or OpenTelemetry builders. - One injectable per call site. Logs, metrics, and traces stay together.
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.
<details> <summary>Filter out health-check spans</summary>
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.
</details>
<details> <summary>Filter fast SQL spans with a custom processor</summary>
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.
</details>
<details> <summary>Send all three signals to Azure Monitor</summary>
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.
</details>
<details> <summary>Add custom resource attributes</summary>
You can add shared metadata once and have it show up on logs, metrics, and traces.
{
"Witness": {
"ServiceName": "orders-api",
"AdditionalResourceAttributes": {
"service.owner": "checkout",
"cloud.region": "westeurope",
"deployment.ring": "blue"
}
}
}
You can do the same in code if you prefer:
builder.Services.AddWitness(options =>
{
options.ServiceName = "orders-api";
options.AdditionalResourceAttributes["service.owner"] = "checkout";
options.AdditionalResourceAttributes["cloud.region"] = "westeurope";
options.AdditionalResourceAttributes["deployment.ring"] = "blue";
});
</details>
Testing
WitnessSharp.Testing gives you TestWitness<T>, an in-memory test double that records:
- logged messages
- recorded metrics
- started activities
It also ships assertion helpers:
AssertLogged(...)AssertActivityStarted(...)AssertMetricRecorded(...)
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. Its first rule, WS0001, flags witness.Logger.LogInformation(...), witness.Logger.LogWarning(...), and witness.Logger.Log(LogLevel, ...) calls inside IWitness or IWitness<T> extension methods such as:
public static void LogOrderPlaced(this IWitness<OrderService> witness, int orderId) =>
witness.Logger.LogInformation("Order {OrderId} placed", orderId);
That pattern is convenient, but hot paths often benefit from the LoggerMessage source generator. WS0001 nudges you toward moving the template into a dedicated generated method, and the package includes a code fix to help with the rewrite.
Install the analyzer
dotnet add package WitnessSharp.Analyzers
Configure severity in .editorconfig
dotnet_diagnostic.WS0001.severity = warning
The LoggerMessage pattern it promotes
public static partial class OrderLogs
{
[LoggerMessage(
EventId = 1001,
Level = LogLevel.Information,
Message = "Order {OrderId} placed")]
public static partial void OrderPlaced(this ILogger logger, int orderId);
}
public static class OrderServiceWitnessExtensions
{
public static void LogOrderPlaced(this IWitness<OrderService> witness, int orderId) =>
witness.Logger.OrderPlaced(orderId);
}
For background on source-generated logging, see the official LoggerMessage docs.
AOT support
WitnessSharp is designed to stay friendly to trimming and native AOT.
A few practical notes:
- The core package keeps to standard .NET and OpenTelemetry APIs instead of runtime-heavy abstractions.
- Your final AOT story still depends on the instrumentations and exporters you enable.
- When you publish with
PublishAot=true, watch for warnings from upstream OpenTelemetry or exporter packages and treat them seriously.
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. If you want to help:
- Build the solution with
dotnet build WitnessSharp.slnx - Run the test suite with
dotnet test WitnessSharp.slnx - Open a pull request with a clear description of the change
If a future CONTRIBUTING.md appears, follow that file first.
License
MIT. See LICENSE.
Further reading
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.Extensions.Configuration (>= 10.0.0)
- Microsoft.Extensions.Configuration.Abstractions (>= 10.0.0)
- Microsoft.Extensions.DependencyInjection (>= 10.0.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.0)
- Microsoft.Extensions.Logging (>= 10.0.0)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.0)
- Microsoft.Extensions.Options (>= 10.0.0)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 10.0.0)
- OpenTelemetry (>= 1.15.3)
- OpenTelemetry.Exporter.Console (>= 1.15.3)
- OpenTelemetry.Exporter.OpenTelemetryProtocol (>= 1.15.3)
- OpenTelemetry.Extensions.Hosting (>= 1.15.3)
- OpenTelemetry.Instrumentation.AspNetCore (>= 1.12.0)
- OpenTelemetry.Instrumentation.Http (>= 1.12.0)
-
net8.0
- Microsoft.Extensions.Configuration (>= 10.0.0)
- Microsoft.Extensions.Configuration.Abstractions (>= 10.0.0)
- Microsoft.Extensions.DependencyInjection (>= 10.0.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.0)
- Microsoft.Extensions.Logging (>= 10.0.0)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.0)
- Microsoft.Extensions.Options (>= 10.0.0)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 10.0.0)
- OpenTelemetry (>= 1.15.3)
- OpenTelemetry.Exporter.Console (>= 1.15.3)
- OpenTelemetry.Exporter.OpenTelemetryProtocol (>= 1.15.3)
- OpenTelemetry.Extensions.Hosting (>= 1.15.3)
- OpenTelemetry.Instrumentation.AspNetCore (>= 1.12.0)
- OpenTelemetry.Instrumentation.Http (>= 1.12.0)
NuGet packages (2)
Showing the top 2 NuGet packages that depend on WitnessSharp:
| Package | Downloads |
|---|---|
|
WitnessSharp.AzureMonitor
Azure Monitor exporter glue for WitnessSharp (`.WithAzureMonitor()`). |
|
|
WitnessSharp.Testing
Test doubles (TestWitness<T>) and assertion helpers for WitnessSharp. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.0.2-alpha | 45 | 5/22/2026 |
| 0.0.1-alpha | 41 | 5/22/2026 |