OTLP ingest
A separate surface from the Sentry-compatible envelope protocol: Bucker also accepts OpenTelemetry traces and logs over OTLP/HTTP, so a stock OTel SDK or collector can point at Bucker with nothing but an endpoint and a header — no Bucker-specific exporter required.
/v1/metrics is deliberately not registered. A route that accepted metrics and
silently dropped them would be worse than the 404 an exporter gets today.
Endpoints
POST /v1/traces
POST /v1/logs
POST /api/<projectId>/otlp/v1/traces
POST /api/<projectId>/otlp/v1/logs
The bare /v1/* pair is what an unmodified OTel exporter reaches when
OTEL_EXPORTER_OTLP_ENDPOINT points at Bucker. The project-scoped pair is for anyone
who prefers an explicit project in the URL; both pairs run the same handler.
Authentication and project resolution
OTLP has no DSN-shaped credential and no project segment in its own protocol, so both are recovered from the request, in this order:
- The
:projectIdroute parameter (project-scoped pair only). Authorization: DSN <full dsn>— the simplest option for a stock exporter, since it supplies the public key and the project id together.- The
x-bucker-project-idheader, paired with a public key fromX-Sentry-Auth,Authorization: Sentry sentry_key=<publicKey>, or?sentry_key=<publicKey>. - A
project_idquery parameter, paired with a public key the same way.
A key that does not belong to the resolved project is refused, exactly as on the envelope route. The recommended exporter configuration is:
OTEL_EXPORTER_OTLP_ENDPOINT=https://<region>.<ingest-host>
OTEL_EXPORTER_OTLP_HEADERS=Authorization=DSN <full dsn>
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
Encodings
Both OTLP/HTTP encodings are accepted, decided from Content-Type:
| Content-Type | Encoding |
|---|---|
application/x-protobuf or application/protobuf |
protobuf |
application/json, or no Content-Type at all |
JSON |
http/protobuf is OTLP's spec default and what an SDK sends unless told otherwise, so
both are first-class rather than JSON-only. An absent Content-Type is treated as
JSON rather than refused: a bare curl -d @batch.json with no -H is a common first
attempt, and guessing protobuf from bytes would be worse. The response is encoded in
whatever was received — a protobuf request gets a protobuf response, including on
partial success — because several exporters log a deserialization warning on every
successful export otherwise.
Protobuf decoding is hand-written rather than pulled from a library: it produces the same OTLP/JSON object shape the JSON path validates, so a protobuf batch and a JSON batch converge on one validated object before anything downstream runs, and quota, scrubbing and outcome accounting cannot fork between the two encodings.
Compression
Content-Encoding: gzip (or x-gzip), deflate or br — the same bounded
decompressor the envelope route uses, so a decompression bomb is refused as 413
(too large) rather than expanded.
Limits
| Limit | Value | On exceeding |
|---|---|---|
| Request body (received or decompressed) | 4 MiB | 413, whole request refused |
| Records per request (spans + log records) | 5,000 | 400, whole request refused |
Resource wrapper objects (ResourceSpans/ResourceLogs) |
512 | 400 (schema validation), whole request refused |
Scope wrapper objects, per resource (ScopeSpans/ScopeLogs) |
512 | 400 (schema validation), whole request refused |
| Scope wrapper objects, total across the request | 4,096 | 400 (schema validation), whole request refused |
The wrapper ceilings exist because a wrapper is allocated and walked whether or not it
holds any records — a batch of a million empty wrappers costs the same CPU as a
million empty spans, with no per-record charge to catch it. The per-resource and
total scope caps are enforced identically on both encodings, so an exporter cannot be
accepted on http/json and refused on http/protobuf for the same batch.
Unknown fields — either encoding — are always skipped rather than refused, because
opentelemetry-proto adds fields in minor releases and an exporter one version ahead
must not start failing.
Responses
Success is an empty JSON object (or a zero-byte protobuf body) unless some records were rejected on content grounds, in which case the OTLP partial-success shape is used:
{ "partialSuccess": { "rejectedSpans": 3, "errorMessage": "…" } }
{ "partialSuccess": { "rejectedLogRecords": 1, "errorMessage": "…" } }
An exporter logs a partial success and does not retry, which is correct for
records refused on content grounds — retrying would only reproduce the same
rejection. Authentication, rate-limit and decompression failures instead raise the
ordinary HTTP error statuses (400, 401, 413, 429) described in
ingest-protocol.md; those are transport failures,
which an exporter should retry, and partial success is not overloaded to mean both.
Rate limits and quota
Ingested through the same auth, rate-limiting and quota machinery as the envelope
route: a coarse per-IP ceiling ahead of any database lookup, then a per-key token
bucket keyed on the resolved DSN key (not a separate per-IP limit — a NAT egress or a
collector fronting a fleet is not an attacker). Over quota, traces and log records
degrade to stratified sampling rather than an outright block, exactly as ingest does
for Sentry events, and every drop is recorded against a data category
(transaction for spans, log for log records) the same way.
What this document does not cover
Grouping an exception-carrying log record into an issue reuses the same
ingestNormalizedEvent + essence pipeline the envelope path uses — see
error-essence.md — rather than a second one for OTLP. This
document is the wire layer only: encodings, auth, limits, and what a refusal looks
like.