Budget Experiment ships a layered, opt-in observability stack.
By default only structured console logging is active — no external services required.
Application Code (ILogger<T> — unchanged)
│
Serilog Pipeline
┌──────────┬──────────┬───────────┬──────────┐
│ Console │ File │ Seq │ OTLP │
│ (always) │ (opt-in) │ (opt-in) │ (opt-in) │
└──────────┴──────────┴───────────┴──────────┘
│
OpenTelemetry SDK (when OTLP enabled)
┌──────────┬──────────┬───────────┐
│ Tracing │ Metrics │ Logs │
└──────────┴──────────┴───────────┘
All settings live under the Observability section in appsettings.json or can be set via environment variables.
| Setting | Env Var | Default | Description |
|---|---|---|---|
Observability:ServiceName |
Observability__ServiceName |
BudgetExperiment |
OTLP resource service.name |
Observability:ServiceVersion |
Observability__ServiceVersion |
Assembly version | OTLP resource service.version |
Log levels are configured via the Serilog configuration section:
{
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft.AspNetCore": "Warning",
"Microsoft.EntityFrameworkCore": "Warning",
"System.Net.Http.HttpClient": "Warning"
}
}
}
}Override via environment: Serilog__MinimumLevel__Default=Debug.
| Setting | Env Var | Default | Description |
|---|---|---|---|
Observability:File:Path |
Observability__File__Path |
(empty = disabled) | Log file path. Set to enable file logging. |
Observability:File:FileSizeLimitBytes |
Observability__File__FileSizeLimitBytes |
10485760 (10 MB) |
Max size per log file before rolling. |
Observability:File:RetainedFileCountLimit |
Observability__File__RetainedFileCountLimit |
5 |
Number of rolled files to keep. |
| Setting | Env Var | Default | Description |
|---|---|---|---|
Observability:Seq:Url |
Observability__Seq__Url |
(empty = disabled) | Seq ingestion URL (e.g., http://seq:5341). |
Observability:Seq:ApiKey |
Observability__Seq__ApiKey |
(empty) | API key for authenticated Seq instances. |
| Setting | Env Var | Default | Description |
|---|---|---|---|
Observability:Otlp:Endpoint |
Observability__Otlp__Endpoint |
(empty = disabled) | OTLP collector endpoint (e.g., http://otel-collector:4317). |
Observability:Otlp:Protocol |
Observability__Otlp__Protocol |
grpc |
grpc (port 4317) or http/protobuf (port 4318). |
Observability:Otlp:Headers |
Observability__Otlp__Headers |
(empty) | Custom headers (e.g., x-api-key=abc123). |
| Environment | Format |
|---|---|
Development |
Human-readable (colored, one line per event) |
Production / others |
Compact JSON (machine-parseable, compatible with docker logs) |
Seq provides a free single-user license — no external accounts needed.
# From the project root:
docker compose -f docker-compose.pi.yml -f docker-compose.observability.yml up -d- Seq UI:
http://<your-host>:8081 - Logs are automatically forwarded to Seq via the Serilog Seq sink.
- No application code changes or restarts needed (the compose overlay sets
Observability__Seq__Url).
Set the OTLP endpoint in your .env or compose override:
Observability__Otlp__Endpoint=http://otel-collector:4317
Observability__Otlp__Protocol=grpcThis exports:
- Traces: ASP.NET Core requests, HttpClient calls, EF Core queries
- Metrics: ASP.NET Core request duration/count, HTTP client metrics
- Logs: All structured log events
Any OTLP-compatible backend works: Grafana Cloud, Datadog, New Relic, Jaeger, etc.
Serilog request logging middleware produces a single summary line per HTTP request:
HTTP GET /api/v1/budgets responded 200 in 12.3 ms
- Health check requests (
/health) are excluded from request logs by default. - 4xx responses are logged as
Warning; 5xx asError.
Every log entry is automatically enriched with:
| Property | Description |
|---|---|
MachineName |
Hostname of the container/machine |
EnvironmentName |
ASP.NET Core environment (Development, Production, etc.) |
ApplicationVersion |
Assembly informational version |
TraceId / SpanId |
W3C trace context (populated by ASP.NET Core) |
When an error occurs in the UI, users can download a sanitized debug log bundle and attach it to a GitHub issue. The bundle contains the exception details, recent log entries leading up to the error, and environment metadata — with all personally identifiable information (PII) stripped before the file is assembled.
- When an API call returns an error with a
traceId, theErrorAlertcomponent shows a "Debug Log" download button. - Clicking it calls
GET /api/v1/debug/logs/{traceId}, which returns an indented JSON file. - The user can review the file, then attach it to a GitHub issue.
| Setting | Env Var | Default | Description |
|---|---|---|---|
Observability:DebugExport:Enabled |
Observability__DebugExport__Enabled |
true |
Enable/disable the debug export feature. Only active when Serilog is configured. |
Observability:DebugExport:BufferSize |
Observability__DebugExport__BufferSize |
1000 |
Maximum number of log entries retained in the in-memory circular buffer. |
Observability:DebugExport:RetentionSeconds |
Observability__DebugExport__RetentionSeconds |
300 |
How long entries are kept (seconds) before expiry. Default: 5 minutes. |
- Exception type, sanitized message, and full stack trace
traceIdfor cross-referencing with server-side logs (if available)- Recent log entries from the same request pipeline (up to 50 entries or 30 seconds)
- App version, .NET runtime version, OS description, environment name
- HTTP method, route template, status code, elapsed time
The sanitizer uses an allowlist approach — only explicitly safe properties pass through. Everything else is stripped or replaced with [REDACTED]:
- User identity (UserId, Username, Email)
- Account names → replaced with
Account-{hash} - Transaction descriptions, financial amounts
- Location data, external references
- Authentication tokens, IP addresses
- Raw request/response bodies (never captured)
- Request path parameters (route template used instead of raw URL)
The exported file includes a _redactionSummary showing how many fields were redacted and what categories.
Set Observability:DebugExport:Enabled to false to disable the feature without code changes. The in-memory buffer will not be allocated, the Serilog sink will not register, the API endpoint returns 501, and the download button is hidden in the UI.
All opt-in features are disabled by leaving their activation setting empty or removing it:
- File logging: Remove or empty
Observability:File:Path - Seq: Remove or empty
Observability:Seq:Url - OTLP: Remove or empty
Observability:Otlp:Endpoint - Debug export: Set
Observability:DebugExport:Enabledtofalse
No code changes, no feature flags — just configuration.