Skip to content

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 byGoverned by
The server pipeline (this page)Server.Features - default Errors | Logs | Traces | Metrics
The browser script, on every pageThe user's consent (IBelemetryConsent)
IBelemetry calls in a Blazor Server circuitThe 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

SignalWhatFrom
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.

Livetraceparent
The live feed starts once the page is interactive.

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.
An unhandled error has occurred. Reload 🗙