Observability
BSFT separates telemetry into two layers:
- Emit (
Bsft.Instrumentation) — vendor-neutral. Modules and application code emit throughIInstrumentationSource, a named pairing of aSystem.Diagnostics.ActivitySourceand aMeter(created viaIMeterFactory). There is no OpenTelemetry dependency here. - Collect/export (
Bsft.Observability) — wraps the OpenTelemetry SDK.AddBsftObservabilitydiscovers every source advertised viaObservabilitySourceRegistrationand exports it.
Sources advertise themselves with AddBsftInstrumentationSource(name). Discovery runs at
provider-build time, so the order of AddBsftObservability relative to the modules does not
matter.
The emit layer is a package in its own right, not the contract half of Bsft.Observability: the
collector implements nothing declared in it, it only reads what the emit layer advertises. That is
why AddBsftInstrumentationSource lives there. Six deliberately dependency-light packages —
Bsft.Mediator, Bsft.Data, Bsft.Messaging, Bsft.Scheduling, Bsft.Security and
Bsft.EventAggregator.Saga — advertise their own source from their own entry point, and moving the
call into Bsft.Observability would either pull the whole OpenTelemetry SDK into each of them or
silently stop their telemetry from being collected. Until 1.3.5 the package was called
Bsft.Observability.Abstractions, which is what made its registration look like a layering defect;
the 1.3.5 migration notes have the rename.
Enabling
builder.Services
.AddBsftMediator(c => { c.RegisterServicesFromAssembly(asm); c.EnableTracing(); })
.AddBsftSaga() // saga telemetry is always on (no-op without a listener)
.AddBsftObservability(o =>
{
o.ServiceName = "orders-api";
o.EnableConsoleExporter = builder.Environment.IsDevelopment();
// OTLP is the default exporter; set o.EnableOtlpExporter = false to run without a collector.
});
Options can also be bound from configuration: AddBsftObservability(configuration), which reads
BsftObservabilityOptions.SectionName ("Bsft:Observability") from the configuration root. Earlier
versions took the section itself; the overload now takes the root, as every other entry point in the
framework does.
Standard OTEL_* environment variables (endpoint, service name, sampler) are honored; the options
only override them when set. The ConfigureTracerProvider / ConfigureMeterProvider hooks let you
add your own exporters, samplers, or instrumentation after the framework defaults.
Log export
Traces and metrics are exported by default; log export is opt-in, because it attaches a provider to
the application's own ILoggingBuilder pipeline rather than adding a side-channel collector, which is
a bigger behavioral change than adding a trace or metric provider:
builder.Services.AddBsftObservability(o =>
{
o.ServiceName = "orders-api";
o.EnableLogging = true;
// Uses the same EnableOtlpExporter / EnableConsoleExporter / OtlpEndpoint / OtlpProtocol options
// as traces and metrics.
});
or from configuration:
{
"Bsft": {
"Observability": {
"ServiceName": "orders-api",
"EnableLogging": true,
"OtlpEndpoint": "http://collector:4317"
}
}
}
ILogger categories are collected as a whole rather than discovered like activity sources and
meters; use the standard Logging configuration section or ILoggingBuilder filters to scope what is
collected. The ConfigureLoggerProvider hook runs after the framework defaults, mirroring
ConfigureTracerProvider / ConfigureMeterProvider.
Telemetry contract
These names are a stable public contract.
Activity sources / meters
| Name | Emitted by |
|---|---|
Bsft.Mediator |
Command/query/notification dispatch (enabled by EnableTracing()). |
Bsft.Saga |
Saga start, steps, completion, and durable timeouts. |
Bsft.Data |
Full-text search execution (registered by AddBsftDataQueryFiltering). |
Bsft.Scheduling |
Job enqueue, claim and execution (registered by AddBsftScheduling). |
Metrics
| Instrument | Type | Unit | Tags |
|---|---|---|---|
bsft.mediator.request.duration |
Histogram | s | request_name, outcome |
bsft.mediator.requests |
Counter | request_name, outcome |
|
bsft.mediator.failures |
Counter | request_name |
|
bsft.saga.started |
Counter | saga.type |
|
bsft.saga.completed |
Counter | saga.type |
|
bsft.saga.step.duration |
Histogram | s | saga.type, outcome |
bsft.saga.timeout.scheduled |
Counter | saga.type |
|
bsft.saga.timeout.fired |
Counter | saga.type |
|
bsft.saga.timeout.canceled |
Counter | saga.type |
|
bsft.saga.timeout.missed |
Counter | saga.type |
|
bsft.data.search.duration |
Histogram | s | search.group |
bsft.data.search.results |
Histogram | search.group |
|
bsft.job.enqueued |
Counter | job.name |
|
bsft.job.claimed |
Counter | job.name |
|
bsft.job.skipped |
Counter | job.name |
|
bsft.job.execution.duration |
Histogram | s | job.name, outcome |
outcome is one of success, expected_error, error (mediator) or executed, skipped (saga).
Expected framework exceptions (BsftException and its subtypes) are recorded as expected_error
and do not mark the span as an error or increment bsft.mediator.failures.
Span attributes
correlation_id is recorded on incoming-request and mediator spans, but only when the value is
well-formed (^[A-Za-z0-9._-]{1,128}\z), so unvalidated client input never reaches telemetry. It is
never used as a metric dimension. Exception detail follows the OpenTelemetry exception convention;
treat the trace backend as a sensitive sink.
Custom sources
builder.Services.AddBsftInstrumentationSource("MyApp.Orders");
public sealed class OrderService(IInstrumentationSourceProvider sources)
{
private readonly IInstrumentationSource _telemetry = sources.Get("MyApp.Orders");
public void Import(string orderId)
{
using var activity = _telemetry.StartActivity("ImportOrder");
activity?.SetTag("order.id", orderId);
_telemetry.GetOrCreateCounter("orders.imported").Add(1);
}
}
A runnable example is wired in samples/Bsft.Sample.WebApi (the Sample.Orders source).
Compatibility (1.1.0)
The mediator and saga contracts moved into Bsft.Mediator.Abstractions (the
Bsft.Mediator.Abstractions namespace is unchanged). Type forwarders preserve binary compatibility, so
this is an additive change. AddBsftObservability pulls OpenTelemetry 1.15.x, which raises the
Microsoft.Extensions.* floor to 9.0.x across the framework.