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
ExportAsyncon one exporter concurrently, so no locking is needed inside it. -
It runs with Belemetry's own instrumentation suppressed: the
HttpClientcalls it makes are not traced. In WebAssemblyHttpClientgoes through the browser'sfetch, so add the exporter's URL tooptions.Browser.IgnoreUrlsas 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.Failureis not retried, so retry insideExportAsyncif 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. |