Ofbirds.Observability
1.1.0
dotnet add package Ofbirds.Observability --version 1.1.0
NuGet\Install-Package Ofbirds.Observability -Version 1.1.0
<PackageReference Include="Ofbirds.Observability" Version="1.1.0" />
<PackageVersion Include="Ofbirds.Observability" Version="1.1.0" />
<PackageReference Include="Ofbirds.Observability" />
paket add Ofbirds.Observability --version 1.1.0
#r "nuget: Ofbirds.Observability, 1.1.0"
#:package Ofbirds.Observability@1.1.0
#addin nuget:?package=Ofbirds.Observability&version=1.1.0
#tool nuget:?package=Ofbirds.Observability&version=1.1.0
Ofbirds.Observability
One-call logging + OpenTelemetry for ASP.NET Core services, so every service emits the same shape of telemetry. Defaults are the standard; the opt-outs exist for the rare case where a piece is impossible or genuinely unwanted.
var builder = WebApplication.CreateBuilder(args);
builder.AddOfbirdsObservability("my-app"); // FIRST — it sets the static Log.Logger
var app = builder.Build();
app.UseOfbirdsObservability(); // request logging + health endpoints
What you get
Logging (Serilog) — console, rolling compact-JSON file (logs/log-.json, 31 days), and
Seq when SEQ_URL is set. Enrichers: log context, machine name, process id, thread id,
Application, Version. Minimum level Information, with Microsoft.AspNetCore and
System.Net.Http.HttpClient at Warning. Both the static Log.Logger and the DI logger are
wired, so startup-time Log.Warning(...) calls before the host is built still reach the sinks.
Tracing (OpenTelemetry) — ASP.NET Core + HttpClient instrumentation plus your app's own
ActivitySource, exported over OTLP/HTTP-protobuf.
Metrics — ASP.NET Core, HttpClient, runtime, and your app's Meter. Off unless
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT is set, because a log/trace-only backend rejects them.
Health endpoints — /api/health (all checks) and /api/alive (checks tagged live), both
mapped AllowAnonymous so an app with a global "require authenticated user" fallback policy
does not answer its own probes with 401 and read as permanently down.
Quiet probe traffic — request logs for the health paths drop below the minimum level, so
Kubernetes probes don't fill the log store with thousands of daily non-events. Failures still
log: 5xx and unhandled exceptions stay at Error. Adjust with QuietRequestPaths.
Correlation — ObservabilityScope puts identifiers on every log event inside the scope
and, when a span is active, as tags on it:
using var _ = ObservabilityScope.Begin(("ScanId", scanId), ("AccountId", accountId));
Custom spans and metrics — take OfbirdsTelemetry from DI (or OfbirdsTelemetry.Instance
where DI is not available); its Source and Meter are the ones the pipeline listens to:
using var activity = telemetry.Source.StartActivity("scan.account");
var counter = telemetry.Meter.CreateCounter<long>("documents.routed");
Configuration
Everything is environment-driven — no endpoint is ever baked into the package.
| Variable | Effect |
|---|---|
SEQ_URL |
Enables the Seq sink; also the fallback source for the OTLP trace endpoint (<host>:5341/ingest/otlp). |
SEQ_API_KEY |
Optional Seq ingestion key. |
OTEL_EXPORTER_OTLP_ENDPOINT |
OTLP base for traces; wins over the SEQ_URL fallback. |
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT |
Signal-specific traces endpoint; wins over everything. |
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT |
Metrics endpoint. Unset = no metrics exported. |
APP_VERSION |
Release tag on every log and span. Falls back to the entry assembly's informational version, then dev. |
LOG_LEVEL |
The floor every sink sees: Verbose/Trace, Debug, Information/Info, Warning/Warn, Error, Fatal/Critical (case-insensitive). Default Information. Wins over the app's own MinimumLevel, so an app can be turned up on a running pod without a code change. An unrecognised value falls back rather than silencing the app or drowning it. |
Signal-specific variables accept either the full URL (…/v1/traces, per the OTel spec) or a
base URL — the signal path is appended only when missing.
Unset everything and the service still runs, logging to console and file.
The SEQ_URL trace fallback assumes the Seq host also exposes OTLP on 5341, since it
rewrites the port (http://seq.example.com:8080 → http://seq.example.com:5341/ingest/otlp/v1/traces). If Seq
sits behind a reverse proxy, or its 5341 is not published, set OTEL_EXPORTER_OTLP_ENDPOINT
explicitly — a wrong OTLP endpoint fails silently, it does not throw.
Shutdown: the logger is disposed with the host, which is what flushes Seq. Keep the usual
try { app.Run(); } catch (ex) { Log.Fatal(ex, "..."); } finally { Log.CloseAndFlush(); }
wrapper, but be aware that a fatal logged after host disposal only reaches the console —
the network sinks are already closed. Log.CloseAndFlush() after host disposal is safe.
Turning an app up
The floor is a LoggingLevelSwitch, not a fixed level, so it can move while the process runs:
options.LevelSwitch.MinimumLevel = LogEventLevel.Debug; // e.g. from an admin endpoint
That matters because raising the level to diagnose something must not require the restart that
destroys the state being diagnosed. Until this existed the floor was hardcoded at Information,
which meant every LogDebug and LogTrace an app wrote was discarded before reaching a sink —
so writing them was pointless, and the only way to see inside a running app was to add
LogInformation calls and redeploy.
Framework noise stays capped by the standard overrides (Microsoft.AspNetCore at Warning), so
LOG_LEVEL=Debug surfaces the app's own detail rather than ASP.NET's.
Opt-outs
builder.AddOfbirdsObservability("my-app", o =>
{
o.DisableFileSink = true; // containers without a writable scratch volume
o.DisableHealthEndpoints = true; // host already owns /api/health
o.DisableRequestLogging = true;
o.LevelOverrides["Npgsql"] = LogEventLevel.Warning;
o.ConfigureTracing = t => t.AddSource("HotChocolate"); // app-specific instrumentation
});
Two routes on the same path throw AmbiguousMatchException at request time — an app that
already maps /api/health should register its probe as an IHealthCheck rather than keeping
a parallel endpoint.
Conventions this package assumes
- Three levels in practice.
Informationfor lifecycle and business events,Warningfor handled problems including failed input validation,Errorfor failures that need someone.Criticalonly when the process cannot continue. - Named placeholders, always:
logger.LogInformation("Scan {ScanId} finished in {Elapsed}ms", id, ms). Never string interpolation — it destroys the structure Seq queries. - Exception first:
logger.LogError(ex, "template", args). - Never log whole entities. Log identifiers and the few fields that matter; entity dumps are a volume and privacy problem in a shared log store.
- Log where the failure is absorbed. A swallowed exception logs; a rethrown one does not (whoever handles it will log it) — no double reporting.
- Correlate at the boundary. Open an
ObservabilityScopewhere a unit of work starts so every downstream line carries its identifiers.
MIT licensed.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | 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
- OpenTelemetry.Exporter.OpenTelemetryProtocol (>= 1.16.0)
- OpenTelemetry.Extensions.Hosting (>= 1.16.0)
- OpenTelemetry.Instrumentation.AspNetCore (>= 1.15.2)
- OpenTelemetry.Instrumentation.Http (>= 1.15.1)
- OpenTelemetry.Instrumentation.Runtime (>= 1.15.1)
- Serilog.AspNetCore (>= 10.0.0)
- Serilog.Enrichers.Environment (>= 3.0.1)
- Serilog.Enrichers.Process (>= 3.0.0)
- Serilog.Enrichers.Thread (>= 4.0.0)
- Serilog.Formatting.Compact (>= 3.0.0)
- Serilog.Sinks.Seq (>= 9.1.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.