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;
});
});
/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.
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.
| Variable | Effect |
|---|---|
OTEL_SERVICE_NAME | service.name, when ServiceName is not set |
OTEL_RESOURCE_ATTRIBUTES | Extra resource attributes (k=v,k2=v2) |
OTEL_EXPORTER_OTLP_ENDPOINT | Adds an OTLP exporter when none is configured, and fills unset endpoints |
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, _LOGS_ENDPOINT, _METRICS_ENDPOINT | Per-signal endpoints, when the code set none; any of them adds the exporter too |
OTEL_EXPORTER_OTLP_HEADERS | Request headers (k=v,k2=v2) |
OTEL_EXPORTER_OTLP_PROTOCOL | http/protobuf, http/json or grpc, unless Protocol is set in code |
OTEL_EXPORTER_OTLP_COMPRESSION | gzip or none, unless Compression is set in code |
OTEL_EXPORTER_OTLP_TIMEOUT | The 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. |