Server pipeline
On an ASP.NET Core host Belemetry's .NET pipeline is off by default: the server
serves the browser script and nothing more, because such hosts usually have an OpenTelemetry setup of
their own. Server.CollectDotnetTelemetry turns it on, and the server's logs, request spans
and metrics go to the same exporters as everything else - joined to the browser by trace context.
Turning it on
One switch, in the server project's registration. It applies to every ASP.NET Core host Belemetry
detects: Blazor Server, static SSR, and the server half of the Auto and WebAssembly render modes.
WebAssembly and Hybrid do not need it - there, the .NET pipeline always runs in the browser or in the
app process. A host without Blazor at all - an API, a worker - works the same way: it has no JS runtime,
so its IBelemetry calls go straight to this pipeline.
Server.IgnoredRequestPaths keeps the request spans of health probes and Blazor's own traffic
out, by path prefix (url.path on .NET 10, only the route on .NET 8 and 9, so a static file
cannot be matched there), before the span is even converted. The span is dropped when it ends, so a child
it had - the database check behind /health - is still exported, with a parent that never arrives.
// Server/Program.cs
builder.Services.AddBitBelemetryServices(options =>
{
options.AddOtlpExporter("http://localhost:4318");
// Off by default: the server only serves the browser script.
options.Server.CollectDotnetTelemetry = true;
// Operator telemetry: what the server collects for everyone, independent of any one user's consent.
options.Server.Features = BelemetryFeatures.Errors | BelemetryFeatures.Logs
| BelemetryFeatures.Traces | BelemetryFeatures.Metrics; // the default
// No request spans for health probes and Blazor's own traffic.
options.Server.IgnoredRequestPaths.AddRange(["/health", "/alive", "/_framework", "/_content", "/_blazor"]);
});Operator features, not consent
Everything else in Belemetry is governed by one user's consent. The server pipeline cannot be: it is
process-wide, and serves every user at once - there is no single "user" whose choice a request span for
a shared endpoint, a Kestrel metric or a startup log belongs to. So it is governed by
Server.Features, a decision the operator makes, in the same way a server's access logs are.
| Collected by | Governed by |
|---|---|
| The server pipeline (this page) | Server.Features - default Errors | Logs | Traces | Metrics |
| The browser script, on every page | The user's consent (IBelemetryConsent) |
IBelemetry calls in a Blazor Server circuit | The user's consent - they are routed through that user's browser |
Both are still intersected with options.Features, the configuration-wide switch. Keep the
server's records free of personal data the user has not agreed to: they reach your backend whatever
the banner says, which is why Server.Features leaves out Sessions,
UserTracking and the analytics features by default.
What it records
| Signal | What | From |
|---|---|---|
| Traces | A server span per HTTP request, named {method} {route} and carrying http.request.method, http.route, url.scheme and http.response.status_code (plus url.path and server.address on .NET 10) - no middleware needed - plus every other source in Traces.Sources (outgoing HttpClient calls, your own ActivitySources) |
Microsoft.AspNetCore is added to the sources on the server |
| Logs | Every ILogger entry at or above Logs.MinimumLevel, with the active trace context |
The Belemetry ILoggerProvider (per-category levels under Logging:Belemetry) |
| Metrics | Request duration, active requests, connections, routing and exception counters, plus the runtime | Microsoft.AspNetCore.Hosting, .Server.Kestrel, .Routing and .Diagnostics are added to the meters |
| Errors | Logs at Error or Critical, or carrying an exception - unhandled request exceptions among them | The same logger provider |
One trace from click to server
When the browser script records a fetch or XHR, it adds a W3C traceparent
header for targets matched by Browser.PropagateTraceContextTo (same-origin by default).
ASP.NET Core reads that header and starts its request span as a child of the browser's
client span, so the two share a trace id and the backend draws them as one trace: how long the user
waited, and how much of that the server spent.
This site runs the server pipeline. Press the button: the page calls /api/demo/echo with a
plain fetch, the endpoint answers with the traceparent it received and the trace
id of its own request span, and the feed below shows the server's records that joined your trace.
Nothing yet - call the endpoint. The server's request span and its log arrive once your fetch span has been exported.
The traceparent is only added while Dependencies is granted - without it the
script does not instrument fetch at all. The server records the request either way: it is
governed by Server.Features, but without the header it starts a trace of its own.
Where it runs
A server describes where it runs, the way the OpenTelemetry resource detectors do, from environment
variables and /proc alone - no metadata-service call, nothing at request time:
host.name, process.pid, process.executable.name,
process.owner, container.id (Linux), and on Azure App Service, Functions or
Container Apps cloud.provider, cloud.platform, cloud.region,
cloud.resource_id, host.id and azure.*. Its
service.instance.id is the platform's instance name (WEBSITE_INSTANCE_ID,
CONTAINER_APP_REPLICA_NAME) or the machine name, so Application Insights keeps counting the
same instance across restarts. A device or a browser tab keeps a random id, which identifies nobody.
Living next to OpenTelemetry
If the server already exports with the OpenTelemetry SDK, leave CollectDotnetTelemetry off:
Belemetry keeps serving the browser script, the browser still sends traceparent, and your
existing ASP.NET Core instrumentation picks it up - the browser spans and your server spans still join,
in whichever backend both export to.
API
BelemetryServerOptions
Class
The server side (Blazor Server, static SSR, the server half of Auto). By default Belemetry only serves the browser script there - an ASP.NET Core host usually has its own OpenTelemetry setup - and its .NET pipeline stays off.
| Name | Type and default | Description |
|---|---|---|
CollectDotnetTelemetry |
bool= false |
Whether the .NET pipeline (ILogger, Activity, Meter collection and export) also runs on the server. Defaults to false. |
Features |
BelemetryFeatures= BelemetryFeatures.Errors | BelemetryFeatures.Logs | BelemetryFeatures.Traces | BelemetryFeatures.Metrics |
The features the server pipeline collects when BelemetryServerOptions.CollectDotnetTelemetry is on. Defaults to errors, logs, traces and metrics. |
IgnoredRequestPaths |
List<string>= [] |
Path prefixes (case-insensitive) of requests whose Microsoft.AspNetCore server span is dropped, e.g. /health, /alive, /_framework, /_content and /_blazor for health probes and Blazor's own traffic. Matched against url.path (.NET 10) or, failing that, http.route, which on .NET 8 and 9 is all a request span carries: there, a request that matched no endpoint (a static file) cannot be matched either. The span is dropped when it ends, so a child it had (a database check behind /health) is still exported, with a parent that never arrives. Empty by default. |