Skip to content

Processors

A processor sees every record the .NET pipeline handles before it is queued, and can change it or drop it: add a tenant id to every span, rename a noisy log, drop health checks and static files, strip an attribute only your app knows is sensitive. It is one small interface with three optional methods.

IBelemetryProcessor

OnSpan, OnLog and OnMetric each receive the record - a BelemetrySpan, BelemetryLogRecord or BelemetryMetric, every member of which may be changed - and return false to drop it. All three have default implementations that keep the record, so a processor implements only what it cares about. Add processors with options.AddProcessor; they run in the order they were added, after the built-in privacy rules.

public sealed class TenantProcessor(ITenantAccessor tenants) : IBelemetryProcessor
{
    public bool OnSpan(BelemetrySpan span)
    {
        span.Attributes.Set("tenant.id", tenants.Current);
        return span.Name != "GET /health";            // false drops it
    }

    public bool OnLog(BelemetryLogRecord log) => log.Body?.Contains("noise") is not true;

    // OnMetric is not implemented: the default keeps every metric.
}

builder.Services.AddBitBelemetryServices(options =>
{
    options.AddProcessor(new TenantProcessor(tenantAccessor));
});
Processors run synchronously, on the thread that produced the record - inside the code that logged, or stopped the activity. They must be fast and must not block, await or call out over the network. A processor that throws is skipped for that record; the record itself is kept.

Which records they see

Processors run on every record of the .NET pipeline:

  • ILogger entries, Activity spans and Meter metrics.
  • IBelemetry calls on WebAssembly and Hybrid, where they go through the .NET pipeline.
  • Browser records relayed to .NET with the Relay transport (the default in Hybrid) - so one processor can enrich page views, clicks and fetch spans as well.
  • Everything the server pipeline collects, when it is on.

With the Direct transport the browser script exports its own records, and .NET never sees them. Shape those with the browser options instead - IgnoreUrls, IgnoreErrorMessages, data-bit-belemetry-ignore and the privacy rules.

A real one: DemoNoiseFilter

This site runs the processor below on both of its pipelines, the WebAssembly one and the server's. The live feeds poll /api/telemetry every two seconds with HttpClient, and the .NET side exports to /otlp; without it every reader would see a span for each of those, and the server would record a request span for every static file and every piece of the WebAssembly payload. The browser script does not need it: the same URLs are in its IgnoreUrls.

LiveDemoNoiseFilter
The live feed starts once the page is interactive.

No .NET HTTP spans yet - press the first button; the second never shows up here.

public sealed class DemoNoiseFilter : IBelemetryProcessor
{
    private static readonly string[] _noise =
    [
        .. DemoTelemetry.SelfUrls,          // "/otlp/", "/api/telemetry"
        "/_framework/", "/_content/", "/_blazor", "/images/", "/.well-known/", "/_vs/", "/browserLink",
    ];

    // The site's stylesheets, scripts and icons, served at the root under fingerprinted names.
    private static readonly string[] _staticFiles = [".css", ".js", ".map", ".svg", ".png", ".ico", ".woff2"];

    public bool OnSpan(BelemetrySpan span)
    {
        var url = span.Attributes.GetString(BelemetrySemanticConventions.UrlFull)
               ?? span.Attributes.GetString(BelemetrySemanticConventions.UrlPath)
               ?? span.Attributes.GetString(BelemetrySemanticConventions.HttpRoute);

        if (url is null) return true;

        foreach (var noise in _noise)
        {
            if (url.Contains(noise, StringComparison.OrdinalIgnoreCase)) return false;
        }

        var path = url.AsSpan();
        var end = path.IndexOfAny('?', '#');
        if (end >= 0) path = path[..end];

        foreach (var extension in _staticFiles)
        {
            if (path.EndsWith(extension, StringComparison.OrdinalIgnoreCase)) return false;
        }

        return true;
    }
}

// Shared by the client and the server (Services/DemoTelemetry.cs):
options.AddProcessor(new DemoNoiseFilter());

Enrich, rewrite, drop

Enriching is setting attributes. Rewriting is assigning members: a span's Name, a log's Body or Severity, a metric's points. Dropping is returning false. The same processor can do all three, for any of the three signals.

API

IBelemetryProcessor Interface

Inspects, enriches or drops records on the .NET side before they are queued for export - including records relayed from the browser.

Name Type and default Description
OnSpan(BelemetrySpan span) bool Called for every finished span. Return false to drop it.
OnLog(BelemetryLogRecord log) bool Called for every log record or event. Return false to drop it.
OnMetric(BelemetryMetric metric) bool Called for every collected metric. Return false to drop it.
An unhandled error has occurred. Reload 🗙