PANiXiDA.Core.Observability is an MSBuild SDK package that adds PANiXiDA host observability conventions on top of OpenTelemetry.
It is intended for ASP.NET Core services that need consistent logging, metrics, tracing, resource metadata, and OTLP exporter wiring without duplicating bootstrap code in every service.
The same package and assembly also support Blazor WebAssembly clients.
The package configures OpenTelemetry for ASP.NET Core hosts with:
- resource metadata based on OpenTelemetry resource defaults, application assembly version, and machine name;
- ASP.NET Core, HTTP client, runtime, and Npgsql instrumentation;
- Wolverine and Quartz 4.x metrics and traces when the application uses these libraries;
- OpenTelemetry logging with formatted messages, scopes, and parsed state values;
- OTLP exporters for logs, metrics, and traces;
- configuration-driven OTLP exporter options through standard OpenTelemetry keys.
- .NET 10 SDK
- ASP.NET Core application using
WebApplicationBuilder, or a Blazor WebAssembly client usingWebAssemblyHostBuilder - OTLP-compatible collector or backend when telemetry export is enabled
Set the SDK version in global.json:
{
"msbuild-sdks": {
"PANiXiDA.Core.Observability": "2.1.1"
}
}Add to each host .csproj, keeping its existing primary SDK and net10.0 target:
<Sdk Name="PANiXiDA.Core.Observability" />The SDK resolves through configured NuGet feeds and selects server or browser dependencies.
using PANiXiDA.Core.Observability;
var builder = WebApplication.CreateBuilder(args);
builder.AddObservability();
var app = builder.Build();
app.MapGet("/", () => Results.Ok());
app.Run();service.version is resolved from Assembly.GetEntryAssembly(), which points to the application entry assembly for a normal ASP.NET Core host.
deployment.environment.name defaults to the lowercase ASP.NET Core environment name;
OTEL_RESOURCE_ATTRIBUTES can override it.
using PANiXiDA.Core.Observability;
var builder = WebApplication.CreateBuilder(args);
builder.AddObservability();
builder.Services.AddHealthChecks();
var app = builder.Build();
app.MapHealthChecks("/health");
app.MapGet("/orders/{id:guid}", (Guid id) => Results.Ok(new { id }));
app.Run();Set the service name outside the package with OpenTelemetry resource configuration:
OTEL_SERVICE_NAME=orders-apiusing PANiXiDA.Core.Observability.WebAssembly;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.AddObservability();
var host = builder.Build();
host.UseObservability();
await host.RunAsync();Enable in the client .csproj:
<PropertyGroup>
<MetricsSupport>true</MetricsSupport>
<EventSourceSupport>true</EventSourceSupport>
<HttpActivityPropagationSupport>true</HttpActivityPropagationSupport>
</PropertyGroup>Configure public wwwroot/appsettings.json:
{
"OTEL_SERVICE_NAME": "admin-client",
"OTEL_EXPORTER_OTLP_ENDPOINT": "/telemetry",
"OpenTelemetry": {
"Otlp": { "MaxRequestSizeBytes": 1048576 }
}
}The client collects Blazor/HTTP telemetry and application logs. Export is disabled
without an endpoint; enabled export requires a nonblank service name. Options
validation checks the name, HTTP/HTTPS endpoint, and optional request limit.
A generated service.instance.id is shared by all signals for the lifetime of
each WASM runtime.
Runtime metrics are enabled by default (BrowserTelemetry:Runtime:Enabled=false
opts out): allocations, GC collection counts and memory snapshots, assemblies,
exceptions, timers, lock contention and thread-pool state. Supported measurements
use System.Runtime; Mono GC counts are exported directly per generation.
browser.runtime.gc.heap.size reports current managed memory without forcing GC;
browser.runtime.gc.last_collection.heap.size and .heap.fragmentation.size
report aggregate GC snapshots. Unsupported per-generation sizes, pause/JIT timing,
working set and completed thread-pool work counters are omitted. Runtime metrics
describe one WASM runtime, not the whole browser. Allocation totals and GC snapshots
can lag until collection; sampling never forces a GC. Existing Blazor/HTTP traces and
application logs continue through the same exporters; runtime profiling dumps are
not collected.
The SDK includes the browser initializer and Google web-vitals 6.2.3 locally.
It records browser.wasm.startup.duration by phase (runtime,
document_to_runtime, first_frame), framework resource counts, accumulated
request durations and transfer/decoded bytes by resource type. Resource durations
overlap; their sum is not the total startup time. first_frame is the first
rendering opportunity after runtime startup, not completion of page data loading.
Web Vitals are latest-value gauges browser.web_vital.lcp, .inp, .cls, .fcp
and .ttfb (seconds, except unitless CLS), with startup spans and measurement logs.
Unsupported browser measurements remain absent. Web Vitals describe document
lifecycles, not individual Blazor route changes. URLs and resource names are not
exported. Set BrowserTelemetry:Performance:Enabled to false to opt out.
The .NET 10 Blazor router supplies page.route from its matched route template,
including additional assemblies; route names and parameter values are not guessed.
Web Vitals and startup/resource metrics use the document's entry route.
browser.page.navigation.duration reports route_match, render_frame, and total
first_frame phases, with page.route and navigation type; logs and spans also
identify the previous route.
It measures a rendering opportunity, not completion of all asynchronous page work.
browser.page.http.duration groups outgoing HTTP timings by their originating
page route; background telemetry export is suppressed. These browser collectors
run only in WASM; YARP forwarding metrics belong to the server profile.
Browser duration histograms use second-scale buckets from 1 ms to 60 s.
For data-ready timing, put one <PageLoadObserver IsLoading="@IsLoading" HasError="@HasError" />
inside each routed page or its shared component (PANiXiDA.Core.Observability.WebAssembly).
It records browser.page.load.duration and a span after data loading and the next
rendering opportunity, with browser.page.load.outcome: success, error, or
cancelled when leaving an unfinished page. Its histogram count gives outcome
counts. Pages without an observer have no data-ready measurement.
Global JavaScript error and unhandledrejection events produce error logs,
spans and browser.javascript.errors, attributed to the current route template.
Message/stack sizes and URL query strings are limited/redacted. Configure
BrowserTelemetry:JavaScriptErrors:Enabled (default true) and
BrowserTelemetry:JavaScriptErrors:MaxPerMinute (default 20, range 1..1000).
Relative endpoints use the app base address. A BFF can forward the v1/logs,
v1/metrics, and v1/traces requests to a collector. Match the limit with the
BFF/collector limits and keep collector credentials on the server.
On a BFF, set BrowserTelemetry:Enabled to true, configure CollectorEndpoint
(an internal HTTP/HTTPS URL) and ServiceName, then call app.MapBrowserTelemetry()
after Build(). The existing builder.AddObservability() registers the relay.
Other servers leave it disabled. Session middleware can recognize relay requests
with request.IsBrowserTelemetryRequest() instead of checking URL prefixes.
The relay requires authentication, enforces same-origin protobuf requests, and
forwards only the body and server-owned frontend identity. AuthorizationPolicy
optionally names a host policy, including its authentication schemes. Failures return
401/403 without login redirects. Defaults: Path=/telemetry, MaxRequestBodySize=1048576,
RequestsPerMinute=120, RequestTimeout=00:00:05. Use the same path in the browser
exporter and align body limits with the collector. The host configures authentication;
relay traffic should not renew its session. YARP spans are collected automatically.
Set service.name through the standard OpenTelemetry resource key:
OTEL_SERVICE_NAME=orders-apior:
OTEL_RESOURCE_ATTRIBUTES=service.name=orders-apiIn appsettings.json, use flat OpenTelemetry keys such as OTEL_SERVICE_NAME; nested keys are not equivalent.
The package does not define a custom OpenTelemetry:ServiceName key. If no OpenTelemetry service name is configured, builder.Environment.ApplicationName is used as the fallback service name required by AddService.
The package does not hard-code OTLP endpoint, protocol, headers, or timeout values. Configure them through standard OpenTelemetry configuration keys.
The server profile uses Yarp.Telemetry.Consumption to export
yarp.proxy.active_requests and yarp.proxy.errors tagged with the ForwarderError
name as error.type. These measure forwarding failures, not HTTP error responses.
No extra application calls are needed; the WASM dependency graph excludes YARP.
AddObservability() subscribes to the Wolverine activity source and the
Wolverine* meters. Wolverine names its meters Wolverine:{ApplicationName}, so
the wildcard also collects metrics from application-specific meter names. These
signals use the same configured OTLP exporters as the rest of the application.
No Wolverine package dependency or additional registration is required in this
observability package; applications without Wolverine emit no Wolverine signals.
Wolverine logs already use the standard ILogger pipeline and are exported by
the existing OpenTelemetry logging provider. Normal application logging filters
still apply, including message-type categories used by Wolverine handlers.
Enabling metrics and traces does not change logging levels.
The built-in wolverine-dead-letter-queue metric counts messages moved to a dead
letter queue; it does not measure the number of messages currently stored there.
Exporting it does not add a DLQ backlog gauge or configure alert thresholds.
See Wolverine instrumentation and metrics.
AddObservability() subscribes to the Quartz activity source and meter used by
Quartz 4.x. Job and scheduler metrics and traces use the existing OTLP exporters;
this package does not require a Quartz dependency or a separate instrumentation
package. Quartz 3.x instrumentation is not covered by these subscriptions.
Quartz logs already use ILogger and follow the configured logging filters.
Exporting the built-in signals does not add last-success tracking, job deadlines,
or alert rules.
See Quartz observability.
No OTLP exporter key is mandatory when the collector is available at the OpenTelemetry defaults. In production, configure only the values that differ from those defaults.
Usually required for a deployed service:
OTEL_SERVICE_NAME, so the service has a stableservice.name;OTEL_EXPORTER_OTLP_ENDPOINTwhen the collector is not available at the default local endpoint.
Minimal appsettings.json example for a service exporting to a gRPC collector:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"OTEL_SERVICE_NAME": "orders-api",
"OTEL_EXPORTER_OTLP_ENDPOINT": "http://otel-collector:4317"
}OTEL_SERVICE_NAME can be provided either as a real environment variable or as a flat appsettings.json key. For containers and Kubernetes, prefer deployment environment variables so the same package binary can run with different service names.
Keep OTEL keys flat in appsettings.json. A nested structure such as OTEL:EXPORTER:OTLP:ENDPOINT produces a different IConfiguration key and is not the same as OTEL_EXPORTER_OTLP_ENDPOINT.
Defaults provided by OpenTelemetry are usually enough for processor and reader settings:
OTEL_EXPORTER_OTLP_PROTOCOLdefaults togrpc.OTEL_EXPORTER_OTLP_ENDPOINTdefaults tohttp://localhost:4317for gRPC andhttp://localhost:4318for HTTP/protobuf.OTEL_EXPORTER_OTLP_TIMEOUTdefaults to10000milliseconds.- metrics export interval defaults to
60000milliseconds. - tracing and logging exporters use batch processing by default.
Set OTEL_EXPORTER_OTLP_PROTOCOL only when using HTTP/protobuf, typically with port 4318:
{
"OTEL_EXPORTER_OTLP_ENDPOINT": "http://otel-collector:4318",
"OTEL_EXPORTER_OTLP_PROTOCOL": "http/protobuf"
}AddObservability registers an HTTP context accessor and normalizes cancellation logs before OTLP batch export. An Error record becomes Information only when the current HTTP request's RequestAborted token is canceled and the exception is either OperationCanceledException (including TaskCanceledException) or a nonempty AggregateException containing only cancellations, including nested aggregates.
The record is retained with its original message, exception, stack trace, attributes, scopes, timestamp, and trace/span IDs. Npgsql cancellations represented by OperationCanceledException with an inner PostgresException follow the same rule.
Background cancellations, cancellations without an aborted request, database timeouts, bare database exceptions, unknown wrappers, empty or mixed aggregates, and unrelated application failures retain their original level. Other levels, including Critical, are unchanged. This normalization applies only to OpenTelemetry logs; console and other logging providers are unaffected. Application logging filters still apply before this processor.
Set these only when your backend or operating requirements need them:
{
"OTEL_EXPORTER_OTLP_HEADERS": "api-key=secret",
"OTEL_EXPORTER_OTLP_TIMEOUT": "10000",
"OTEL_BSP_SCHEDULE_DELAY": "5000",
"OTEL_BSP_EXPORT_TIMEOUT": "30000",
"OTEL_BLRP_SCHEDULE_DELAY": "5000",
"OTEL_BLRP_EXPORT_TIMEOUT": "30000",
"OTEL_METRIC_EXPORT_INTERVAL": "60000",
"OTEL_METRIC_EXPORT_TIMEOUT": "30000"
}The same OTLP values can be provided as environment variables:
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
OTEL_EXPORTER_OTLP_HEADERS=api-key=secret
OTEL_EXPORTER_OTLP_TIMEOUT=10000Use signal-specific keys in appsettings.json only when logs, metrics, and traces need different endpoints. These keys are supported by OpenTelemetry .NET UseOtlpExporter; if all signals use the same collector, prefer OTEL_EXPORTER_OTLP_ENDPOINT.
{
"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT": "http://traces-collector:4317",
"OTEL_EXPORTER_OTLP_METRICS_ENDPOINT": "http://metrics-collector:4317",
"OTEL_EXPORTER_OTLP_LOGS_ENDPOINT": "http://logs-collector:4317"
}For HTTP/protobuf signal-specific endpoints, provide the full signal path and configure the protocol:
{
"OTEL_EXPORTER_OTLP_PROTOCOL": "http/protobuf",
"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT": "http://traces-collector:4318/v1/traces",
"OTEL_EXPORTER_OTLP_METRICS_ENDPOINT": "http://metrics-collector:4318/v1/metrics",
"OTEL_EXPORTER_OTLP_LOGS_ENDPOINT": "http://logs-collector:4318/v1/logs"
}OpenTelemetry .NET reads these keys through IConfiguration, so they may come from environment variables, command-line arguments, or appsettings.json.
public static WebApplicationBuilder AddObservability(
this WebApplicationBuilder builder)Behavior:
- configures OpenTelemetry resource metadata using the entry assembly version when available;
- adds ASP.NET Core, HTTP client, Npgsql, and runtime instrumentation;
- subscribes to Wolverine and Quartz 4.x metrics and traces without dependencies on these libraries, and exports YARP traces and forwarding metrics;
- adds the OTLP exporter for logging, metrics, and tracing through OpenTelemetry
UseOtlpExporter; - returns the same
WebApplicationBuilderinstance for chaining.
- Run an OTLP collector or backend outside this package.
- Keep endpoints, protocols, headers, and credentials in environment-specific configuration.
- Prefer HTTP/protobuf on port
4318or gRPC on port4317, depending on the collector setup. - Ensure application logging filters are configured intentionally, because OpenTelemetry logging follows normal
ILoggerfiltering. - Treat exporter headers as secrets when they contain tokens or API keys.
.
|-- src/
| `-- PANiXiDA.Core.Observability/
|-- tests/
| `-- PANiXiDA.Core.Observability.UnitTests/
|-- Directory.Build.props
|-- Directory.Build.targets
|-- Directory.Packages.props
|-- global.json
|-- version.json
|-- README.md
|-- LICENSE
`-- icon.png
dotnet restore
dotnet build --configuration Releasedotnet formatdotnet test --configuration Releasedotnet test --configuration Release --coverage --coverage-output coverage.xml --coverage-output-format xmldotnet pack --configuration ReleaseEvery pull request and push to main runs formatting, tests, and mandatory
SonarQube analysis. Publishing from main starts only after the SonarQube
Quality Gate succeeds.
This project is licensed under the Apache-2.0 license.
See the LICENSE file for details.