Skip to content

Live telemetry

This site is instrumented with Belemetry and exports to itself. Everything you do here - page views, clicks, web vitals, errors, fetch spans, the WebAssembly app's logs, traces and metrics - is sent to the demo server as OTLP/JSON, decoded, and handed back to you below. Only you see your records, and nothing is sent until you have answered the consent banner.

Your records

Make something happen, and watch it arrive within a couple of seconds. Each row opens to show its attributes, trace and span ids, and the resource it came with. The source column says who recorded it: browser (the script), wasm (the .NET pipeline running in this tab) or server (the demo server's own pipeline).

Liveyour session
The live feed starts once the page is interactive.

Nothing yet. Click around the site - page views, clicks, web vitals and your .NET logs land here within a few seconds.

What each button records

ButtonCallWhat arrivesFeature
Track an event IBelemetry.TrackEvent("demo.live.event", ...) A wasm log record with the event name and your attributes Events
Log with ILogger Logger.LogInformation(...) A wasm log record: the category as its scope, the template's values as attributes, and the trace context Logs
Throw a JS error blDemo.throwError(...) - an uncaught TypeError A browser exception event with exception.type, exception.message and exception.stacktrace Errors
Fetch /api/demo/echo blDemo.fetch("/api/demo/echo") - a plain fetch A browser client span GET, and - because the script added a traceparent - the server's request span and log in the same trace, marked correlated Dependencies

How this works

  1. Export. The browser script and the WebAssembly app's .NET pipeline both export OTLP/HTTP with the JSON encoding to this site's own server - /otlp/v1/traces, /otlp/v1/logs and /otlp/v1/metrics. JSON rather than protobuf so the payloads are readable in your browser's network tab.
  2. Receive. An in-process receiver decodes each request and files every record under its session.id, keeping the last few hundred per session in memory. Nothing is ever written to disk or a database, and a session idle for an hour is forgotten.
  3. Attribute. Some records carry no session. Metric points never do (it would multiply their cardinality by the number of visitors), so they are matched to you through the service.instance.id of the WebAssembly app that sent them. The server's own request spans and logs go into one shared bucket.
  4. Read back. This page asks the script for your session id (BitBelemetry.getContext()) and polls GET /api/telemetry?sessionId=... for what is new. The server can only be asked for a session id your browser generated, so nobody sees anyone else's clicks.
  5. Correlate. A server span from the shared bucket is shown to you when it shares a trace id with one of your records - your fetch span's traceparent made it that span's child. Include server telemetry shows the rest of the shared bucket too.

Nothing showing up? The feed says so when nothing is granted. Answer the banner, or choose per feature on the consent page. Before you decide, records are held in memory in the page - and they arrive the moment you grant their feature.

The demo's configuration

This is the real setup of this site, copied from its source. Both projects call AddBitBelemetryServices(DemoTelemetry.ConfigureShared), and each adds its own exporter on top; every configure callback runs, in order, on the same options.

// Client/Services/DemoTelemetry.cs - called by both projects through AddDemoServices:
//     services.AddBitBelemetryServices(DemoTelemetry.ConfigureShared);
public static void ConfigureShared(BelemetryOptions options)
{
    options.ServiceName = ServiceName;            // "belemetry-demo"
    options.ServiceNamespace = ServiceNamespace;  // "bitplatform"

    // Opt-in: nothing leaves the browser until the reader answers the consent banner. This is
    // the library default, written out because the whole consent flow is part of the demo.
    options.Consent.Default = BelemetryFeatures.None;

    // Short intervals so a click shows up on the Live telemetry page within a couple of
    // seconds. The defaults (5 s, 60 s for metrics) are the right ones for a real app.
    options.Browser.FlushInterval = TimeSpan.FromSeconds(2);
    options.Batch.FlushInterval = TimeSpan.FromSeconds(2);
    options.Metrics.ExportInterval = TimeSpan.FromSeconds(10);

    // Off by default; on here so the browser page can demonstrate them.
    options.Browser.LongTasks = true;
    options.Browser.CaptureConsole = true;

    // The demo's own instrumentation (the ".NET instrumentation" page's buttons).
    options.Traces.Sources.Add(DemoDiagnostics.Name);
    options.Metrics.Meters.Add(DemoDiagnostics.Name);

    foreach (var url in SelfUrls) options.Browser.IgnoreUrls.Add(url);   // "/otlp/", "/api/telemetry"

    options.AddProcessor(new DemoNoiseFilter());
}

The WebAssembly client exports its .NET records to its own origin. The endpoint is absolute, because a .NET exporter has no page to resolve a relative URL against:

// Client/Program.cs
var builder = WebAssemblyHostBuilder.CreateDefault(args);

builder.Services.AddDemoServices(builder.HostEnvironment.BaseAddress);

builder.Services.AddBitBelemetryServices(options =>
{
    options.AddOtlpExporter(builder.HostEnvironment.BaseAddress + "otlp", otlp =>
    {
        otlp.Protocol = BelemetryOtlpProtocol.HttpJson;
    });
});

await builder.Build().RunAsync();

The server renders <BelemetryScript />, so the browser script's exporter comes from its registration: BrowserEndpoint is relative, so the script posts to whatever origin it was served from. The server also turns on its own pipeline, which is where the correlated request spans come from.

// Server/Program.cs
builder.Services.AddDemoServices();

builder.Services.AddBitBelemetryServices(options =>
{
    // The server's own pipeline posts to its listening address over loopback; the browser
    // script posts to the origin it was served from.
    options.AddOtlpExporter(DemoEndpoints.SelfOtlpEndpoint(builder.Configuration), otlp =>
    {
        otlp.Protocol = BelemetryOtlpProtocol.HttpJson;
        otlp.BrowserEndpoint = new Uri("/otlp", UriKind.Relative);
    });

    // Request spans, logs and metrics of the server, governed by Server.Features.
    options.Server.CollectDotnetTelemetry = true;

    if (builder.Configuration["ApplicationInsights:ConnectionString"] is { Length: > 0 } connectionString)
    {
        options.AddAzureMonitorExporter(connectionString);
    }
});
An unhandled error has occurred. Reload 🗙