Skip to content

OTLP

OTLP is OpenTelemetry's own wire protocol, and the one exporter that reaches almost every backend: the OpenTelemetry Collector, the .NET Aspire dashboard, Grafana, Jaeger, Honeycomb, Datadog, New Relic, Elastic and more. Belemetry writes it by hand - HTTP with protobuf or JSON, and gRPC, without the OpenTelemetry packages - from both the .NET pipeline and the browser script, with one configuration.

Adding the exporter

AddOtlpExporter takes the base endpoint, or a callback for everything else, or both. Any number of exporters can be added; each record goes to all of them. The browser script exports with the same settings unless IncludeInBrowser is off.

builder.Services.AddBitBelemetryServices(options =>
{
    options.ServiceName = "MyApp";

    // The base endpoint: /v1/traces, /v1/logs and /v1/metrics are appended.
    options.AddOtlpExporter("http://localhost:4318");

    // Or everything at once.
    options.AddOtlpExporter(otlp =>
    {
        otlp.Endpoint = new Uri("https://otel.example.com");
        otlp.Protocol = BelemetryOtlpProtocol.HttpProtobuf;
        otlp.Compression = BelemetryCompression.Gzip;
    });
});
This site exports to itself: the WebAssembly app and the browser script post OTLP/JSON to an in-process receiver at /otlp, which is what the Live telemetry page and the feeds on every page read from. JSON was chosen so every payload is readable in the browser's network tab.

Protobuf, JSON or gRPC

Protocol picks the encoding. HttpProtobuf (the default, application/x-protobuf) is the most widely supported and the smallest on the wire. HttpJson (application/json) is the OTLP JSON mapping - ids as hex, 64-bit integers as strings, enums as numbers - and is easier to inspect and to receive without generated code. Grpc is OTLP/gRPC over HTTP/2, spoken without a gRPC library: a unary call is an HTTP/2 POST with the 5-byte message prefix, and the status comes back in the trailers. It is what the OpenTelemetry SDKs default to and what the Aspire AppHost hands a resource (OTEL_EXPORTER_OTLP_PROTOCOL=grpc, port 4317); Compression becomes the gRPC message compression (grpc-encoding: gzip).

A browser cannot speak gRPC - fetch has no HTTP/2 trailers - so a gRPC exporter reaches the page only through an OTLP/HTTP BrowserEndpoint; in a WebAssembly app the .NET pipeline cannot either, so there the whole exporter becomes OTLP/HTTP to that endpoint (which must be absolute), and one without is skipped with a startup warning in the Bit.Belemetry log category. On Android and iOS gRPC goes through a SocketsHttpHandler of its own, since the native handlers cannot do HTTP/2 without TLS.

Endpoints

Endpoint is the base URL; /v1/traces, /v1/logs and /v1/metrics are appended to it, exactly as OTEL_EXPORTER_OTLP_ENDPOINT works. TracesEndpoint, LogsEndpoint and MetricsEndpoint are full URLs, used as they are - for backends whose routes do not follow the convention (with gRPC they are base addresses the service method is appended to, as the specification defines the variables). Without any endpoint, the exporter posts to http://localhost:4318 (4317 for gRPC).

BrowserEndpoint is the base URL the browser script uses instead of Endpoint: a public, CORS-enabled route to the collector while the server talks to it over an internal address. The per-signal endpoints apply to the browser only when it shares Endpoint. Signals restricts what is sent - a traces-only backend such as Jaeger gets BelemetrySignals.Traces.

Headers, compression and timeout

Headers are added to every request - typically the vendor's API key. Compression = BelemetryCompression.Gzip sends Content-Encoding: gzip (in the browser through CompressionStream, except for the final batch sent while the page unloads, which cannot wait for it). Timeout bounds one export request (10 s, the OTLP default). Failed requests are retried with backoff, honoring Retry-After, and kept for later with the offline persistence. MinimumLogLevel is the per-exporter counterpart of a logging provider's level: this exporter gets the plain log records at or above it while the others get everything - one backend for errors, another for the full picture, from one pipeline. Events (page views, interactions, web vitals, custom events, exceptions), spans and metrics are not logs to a log level and always pass.

Headers the browser script uses are visible to anyone who opens the page. Never put a credential in an exporter the browser exports to: set IncludeInBrowser = false on it and give the browser a credential-free route (a collector in front of the vendor), or use the Relay transport, which hands browser records to .NET. Relay only hides the key when .NET runs outside the browser - Blazor Server or Hybrid. In WebAssembly the .NET pipeline is itself in the browser, and so is its configuration.

Environment variables

Outside the browser, with options.UseEnvironmentVariables (on by default), the standard OTLP variables are read - which is how the Aspire AppHost and most container platforms wire telemetry into an app without code changes. OTEL_EXPORTER_OTLP_ENDPOINT alone adds an OTLP exporter when none is configured, and fills Endpoint on those that have none - as does any of the per-signal endpoint variables. Headers from the variable are added unless the code set the same name. A protocol of http/protobuf, http/json or grpc sets Protocol; any other value is reported at startup and ignored.

VariableEffect
OTEL_SERVICE_NAMEservice.name, when ServiceName is not set
OTEL_RESOURCE_ATTRIBUTESExtra resource attributes (k=v,k2=v2)
OTEL_EXPORTER_OTLP_ENDPOINTAdds an OTLP exporter when none is configured, and fills unset endpoints
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, _LOGS_ENDPOINT, _METRICS_ENDPOINTPer-signal endpoints, when the code set none; any of them adds the exporter too
OTEL_EXPORTER_OTLP_HEADERSRequest headers (k=v,k2=v2)
OTEL_EXPORTER_OTLP_PROTOCOLhttp/protobuf, http/json or grpc, unless Protocol is set in code
OTEL_EXPORTER_OTLP_COMPRESSIONgzip or none, unless Compression is set in code
OTEL_EXPORTER_OTLP_TIMEOUTThe request timeout in milliseconds, unless Timeout is set in code

.NET Aspire dashboard

The dashboard is an OTLP receiver with a UI - the quickest way to see traces, logs and metrics locally. Use its OTLP/HTTP endpoint with protobuf. The browser posts to it from your app's origin, so allow that origin with DASHBOARD__OTLP__CORS__ALLOWEDORIGINS (and DASHBOARD__OTLP__CORS__ALLOWEDHEADERS if you send headers). If the dashboard requires an OTLP API key, the browser would have to carry it - keep the browser on the Relay transport or on an unsecured local endpoint.

Under an Aspire AppHost, WithOtlpExporter() sets OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_EXPORTER_OTLP_PROTOCOL=grpc for the projects it runs, and Belemetry follows them: leave the protocol unset in code and the .NET side speaks gRPC to the dashboard. An exporter whose protocol is fixed to HTTP in code would post HTTP to the gRPC port and fail quietly - give the AppHost WithOtlpExporter(OtlpProtocol.HttpProtobuf) in that case. The browser still needs the dashboard's HTTP endpoint (as BrowserEndpoint) and its CORS origin.

OpenTelemetry Collector

The Collector is the usual production setup: the app sends to a collector you control, and the collector holds the vendor credentials, batches, samples and fans out. The browser posts to it cross-origin, so the http receiver needs a cors section; traceparent does not need to be allowed there - it is sent to your API, not to the collector.

receivers:
  otlp:
    protocols:
      http:
        endpoint: 0.0.0.0:4318
        cors:
          allowed_origins: ["https://myapp.example.com"]
          allowed_headers: ["*"]

processors:
  batch: {}

exporters:
  otlphttp:
    endpoint: https://otlp.vendor.example
    headers:
      x-api-key: ${env:VENDOR_API_KEY}

service:
  pipelines:
    traces:  { receivers: [otlp], processors: [batch], exporters: [otlphttp] }
    logs:    { receivers: [otlp], processors: [batch], exporters: [otlphttp] }
    metrics: { receivers: [otlp], processors: [batch], exporters: [otlphttp] }

Grafana

Grafana Alloy is a Collector distribution: its otelcol.receiver.otlp component takes the same http endpoint and cors block, then forwards to Tempo, Loki and Mimir. Grafana Cloud also exposes an OTLP gateway directly; it authenticates with Basic auth (instance id and access token), which is a credential - so the browser should go through Alloy or a collector rather than to the gateway itself.

Jaeger

Jaeger receives OTLP natively on port 4318 - traces only, so restrict the exporter to BelemetrySignals.Traces or its logs and metrics requests are rejected. For the browser, allow the app's origin: Jaeger v2 is built on the Collector and takes the same cors block as above; Jaeger v1 uses --collector.otlp.http.cors.allowed-origins.

Honeycomb

Honeycomb accepts OTLP/HTTP at https://api.honeycomb.io (https://api.eu1.honeycomb.io in the EU) with the API key in the x-honeycomb-team header; metrics also need x-honeycomb-dataset. That key is a credential: keep this exporter out of the browser and route browser telemetry through a collector, or use the Relay transport on Blazor Server and Hybrid.

API

BelemetryOtlpExporterOptions Class

Settings of an OTLP exporter - the vendor-neutral OpenTelemetry protocol understood by the OpenTelemetry Collector, the Aspire dashboard, Grafana (Tempo/Loki/Mimir), Jaeger, Honeycomb, Datadog, New Relic, Elastic and many more - over HTTP (protobuf or JSON) or gRPC.

Name Type and default Description
Endpoint Uri The base endpoint; /v1/traces, /v1/logs and /v1/metrics (or the gRPC service methods) are appended to it. Defaults to OTEL_EXPORTER_OTLP_ENDPOINT when set (outside the browser), else http://localhost:4318 for OTLP/HTTP and http://localhost:4317 for gRPC.
TracesEndpoint Uri A full traces endpoint, used verbatim instead of BelemetryOtlpExporterOptions.Endpoint + /v1/traces. Defaults to OTEL_EXPORTER_OTLP_TRACES_ENDPOINT when set (outside the browser). With gRPC it is a base address (the service method is appended), as the specification defines the variable.
LogsEndpoint Uri A full logs endpoint, used verbatim instead of BelemetryOtlpExporterOptions.Endpoint + /v1/logs. Defaults to OTEL_EXPORTER_OTLP_LOGS_ENDPOINT when set.
MetricsEndpoint Uri A full metrics endpoint, used verbatim instead of BelemetryOtlpExporterOptions.Endpoint + /v1/metrics. Defaults to OTEL_EXPORTER_OTLP_METRICS_ENDPOINT when set.
BrowserEndpoint Uri The endpoint the browser script exports to, when it differs from BelemetryOtlpExporterOptions.Endpoint - typically a public, CORS-enabled route of a collector while the .NET side talks to it over an internal address. With BelemetryOtlpProtocol.Grpc this is the only way the browser exports: it posts OTLP/HTTP protobuf here, since a page cannot speak gRPC; in a WebAssembly app the .NET pipeline does the same, so the endpoint must be absolute there.
Protocol BelemetryOtlpProtocol= BelemetryOtlpProtocol.HttpProtobuf The payload encoding. Defaults to OTEL_EXPORTER_OTLP_PROTOCOL when set (outside the browser), else BelemetryOtlpProtocol.HttpProtobuf. A value set in code always wins over the variable.
Headers Dictionary<string, string>= {} Extra request headers (e.g. an API key). Merged with OTEL_EXPORTER_OTLP_HEADERS outside the browser.
Compression BelemetryCompression= BelemetryCompression.None The request-body compression (for gRPC, the message compression with grpc-encoding). Defaults to OTEL_EXPORTER_OTLP_COMPRESSION when set (outside the browser), else none.
Timeout TimeSpan= 00:00:10 The maximum time one export request may take. Defaults to OTEL_EXPORTER_OTLP_TIMEOUT (milliseconds) when set (outside the browser), else 10 seconds (the OTLP default).
IncludeInBrowser bool= true Whether the browser script exports to this endpoint too (with BelemetryBrowserTransport.Direct). Defaults to true.
Signals BelemetrySignals= BelemetrySignals.All The signals sent to this endpoint. Defaults to all three.
MinimumLogLevel LogLevel? The least severe log level this exporter receives from the .NET pipeline (its own logs and the records the browser relays); plain log records below it are left out of this exporter's batches only. Events - page views, interactions, web vitals, custom events, exceptions - are not logs to a log level and always pass, as do spans and metrics. null (the default) sends every collected record. Use it when one backend should get errors only while another gets everything, as a per-provider logging level would with two logging providers.

BelemetryOtlpProtocol Enum

The OTLP transport encoding.

Value Number Description
HttpProtobuf 0 OTLP/HTTP with binary protobuf payloads (application/x-protobuf) - the most widely supported encoding.
HttpJson 1 OTLP/HTTP with JSON payloads (application/json).
Grpc 2 OTLP/gRPC: protobuf payloads over HTTP/2 (application/grpc), the default of the OpenTelemetry SDKs and what the .NET Aspire AppHost hands its resources. Belemetry speaks it without a gRPC library: unary calls are plain HTTP/2 POSTs with a 5-byte message prefix, and the status comes back in the response trailers. .NET side only - a browser cannot send gRPC, so the browser script posts to BelemetryOtlpExporterOptions.BrowserEndpoint over OTLP/HTTP instead, and gets nothing when that is unset.
An unhandled error has occurred. Reload 🗙