Skip to content
bucker

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:

  1. The :projectId route parameter (project-scoped pair only).
  2. Authorization: DSN <full dsn> — the simplest option for a stock exporter, since it supplies the public key and the project id together.
  3. The x-bucker-project-id header, paired with a public key from X-Sentry-Auth, Authorization: Sentry sentry_key=<publicKey>, or ?sentry_key=<publicKey>.
  4. A project_id query 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.