Skip to content

Console, in-memory and custom exporters

Besides OTLP and Azure Monitor, Belemetry ships a console exporter for development and an in-memory exporter for tests and in-app viewers, and takes any IBelemetryExporter of your own. All of them can be combined with the others: every exporter receives every batch.

Console

AddConsoleExporter prints one line per span, log record and metric point - the kind, the name, the duration or severity, the ids and the attributes. The .NET side writes to the process's standard output, which in WebAssembly is the browser's console; with IncludeInBrowser (on by default) the browser script prints its own records there as well. Signals (all three by default) narrows it down when the output gets noisy. It is a development tool: keep it out of production builds.

builder.Services.AddBitBelemetryServices(options =>
{
    if (builder.Environment.IsDevelopment())
    {
        options.AddConsoleExporter(console =>
        {
            console.Signals = BelemetrySignals.Traces | BelemetrySignals.Logs;
            console.IncludeInBrowser = true;      // the default
        });
    }
});

In-memory

BelemetryInMemoryExporter keeps what it is handed in three queues - Spans, Logs and Metrics, oldest first, each capped at the capacity passed to its constructor (1000) - plus the LastResource, and raises Exported after every batch. Clear() empties it. Because it sees records exactly as the other exporters do - after consent, privacy rules and processors - it is the way to assert on telemetry in a test.

using Bit.Belemetry;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

[TestClass]
public sealed class CheckoutTelemetryTests
{
    [TestMethod]
    public async Task Checkout_records_an_event()
    {
        var memory = new BelemetryInMemoryExporter();

        var services = new ServiceCollection();
        services.AddLogging();
        services.AddBitBelemetryServices(options =>
        {
            options.ServiceName = "checkout-tests";
            options.UseEnvironmentVariables = false;       // the test machine's OTEL_* must not leak in
            options.Consent.Default = BelemetryFeatures.All; // no user to ask in a test
            options.Consent.Persist = false;
            options.Persistence.Enabled = false;
            options.AddExporter(memory);
        });

        await using var provider = services.BuildServiceProvider();

        // Building the logger factory starts the collectors, as it does when a host starts.
        _ = provider.GetRequiredService<ILoggerFactory>();

        await using var scope = provider.CreateAsyncScope();
        var belemetry = scope.ServiceProvider.GetRequiredService<IBelemetry>();

        belemetry.TrackEvent("checkout.completed", new Dictionary<string, object?> { ["cart.items"] = 3 });
        await belemetry.FlushAsync();

        var log = memory.Logs.Single(l => l.EventName == "checkout.completed");
        Assert.AreEqual(3L, log.Attributes.GetInt64("cart.items"));
        Assert.AreEqual("checkout-tests", memory.LastResource?.ServiceName);
    }
}

The same exporter backs an in-app telemetry viewer - a diagnostics page in an internal tool, or a support panel in a Hybrid app. Register the instance as a service too, so components can read it. Exported is raised by the export loop, not by the renderer, so marshal back with InvokeAsync before rendering.

Your own exporter

Implement IBelemetryExporter - a Name for diagnostics and ExportAsync - and add an instance with options.AddExporter. A BelemetryBatch holds the records that share one Resource: Spans, Logs and Metrics, already shaped like their OTLP messages (see the model types BelemetrySpan, BelemetryLogRecord and BelemetryMetric).

  • Calls are serialized: the pipeline never calls ExportAsync on one exporter concurrently, so no locking is needed inside it.
  • It runs with Belemetry's own instrumentation suppressed: the HttpClient calls it makes are not traced. In WebAssembly HttpClient goes through the browser's fetch, so add the exporter's URL to options.Browser.IgnoreUrls as well, or the script records each export as a dependency.
  • Each call gets a cancellation token that fires after 30 seconds. An exception is caught and reported in Belemetry's diagnostics (options.Debug); it never reaches the app.
  • Retries and offline persistence belong to the built-in HTTP exporters. A custom exporter's BelemetryExportResult.Failure is not retried, so retry inside ExportAsync if the records matter.
  • Custom exporters are .NET-side only. Browser records reach one when they travel through .NET - with the Relay transport - but not when the script exports them directly.
using System.Net.Http.Json;
using Bit.Belemetry;

/// <summary>Posts every error of a batch to an incident webhook.</summary>
public sealed class IncidentWebhookExporter(HttpClient http, Uri webhook) : IBelemetryExporter
{
    public string Name => "incident-webhook";

    public async ValueTask<BelemetryExportResult> ExportAsync(BelemetryBatch batch, CancellationToken cancellationToken)
    {
        var incidents = batch.Logs
            .Where(log => log.HasException || log.Severity >= BelemetrySeverity.Error)
            .Select(log => new
            {
                service = batch.Resource.ServiceName,
                time = DateTimeOffset.FromUnixTimeMilliseconds(log.TimeUnixNano / 1_000_000),
                type = log.Attributes.GetString(BelemetrySemanticConventions.ExceptionType),
                message = log.Attributes.GetString(BelemetrySemanticConventions.ExceptionMessage) ?? log.Body,
                traceId = log.TraceId,
                sessionId = log.Attributes.GetString(BelemetrySemanticConventions.SessionId),
            })
            .ToList();

        // An empty batch is a success: there was nothing to deliver.
        if (incidents.Count == 0) return BelemetryExportResult.Success;

        try
        {
            using var response = await http.PostAsJsonAsync(webhook, incidents, cancellationToken);
            return response.IsSuccessStatusCode ? BelemetryExportResult.Success : BelemetryExportResult.Failure;
        }
        catch (HttpRequestException)
        {
            return BelemetryExportResult.Failure;
        }
    }
}

API

IBelemetryExporter Interface

Sends batches of telemetry to a backend.

Name Type and default Description
Name string A short name for diagnostics.
ExportAsync(BelemetryBatch batch, CancellationToken cancellationToken) ValueTask<BelemetryExportResult> Exports batch.

BelemetryInMemoryExporter Class

Keeps exported records in memory - for tests, and for in-app telemetry viewers. Add it with BelemetryOptions.AddExporter.

Name Type and default Description
Name string
Spans IReadOnlyCollection<BelemetrySpan> The exported spans, oldest first.
Logs IReadOnlyCollection<BelemetryLogRecord> The exported log records, oldest first.
Metrics IReadOnlyCollection<BelemetryMetric> The exported metrics, oldest first.
LastResource BelemetryResource The resource of the last exported batch.
Exported Action<BelemetryBatch> Raised after every export.
ExportAsync(BelemetryBatch batch, CancellationToken cancellationToken) ValueTask<BelemetryExportResult>
Clear() void Removes every kept record.

BelemetryConsoleExporterOptions Class

Settings of the console exporter, which prints every exported record - for development only.

Name Type and default Description
IncludeInBrowser bool= true Whether the browser script prints its records to the browser console too. Defaults to true.
Signals BelemetrySignals= BelemetrySignals.All The signals printed. Defaults to all three.
An unhandled error has occurred. Reload 🗙