---
title: "OpenTelemetry ingestion"
url: "https://bicho.fyi/docs/otlp"
description: "Send error logs to Bicho over OpenTelemetry OTLP/HTTP using JSON or protobuf. Configure exception fields, batching, request limits, and retries for your app."
---

Bicho accepts exception and error logs at `POST /v1/logs` over OTLP/HTTP JSON or binary protobuf. It is not a general log archive. It does not accept gRPC, traces, or metrics, including `/v1/traces` and `/v1/metrics`. An OpenTelemetry Collector is optional and is not bundled with Bicho.

## Configure a logs exporter

First [register a project](/docs/) and save its generated `otlp.endpoint` and `otlp.headers`. For exporters that honor the standard environment variables:

```
export OTEL_EXPORTER_OTLP_LOGS_ENDPOINT=https://your-bicho-worker.example/v1/logs
export OTEL_EXPORTER_OTLP_LOGS_PROTOCOL=http/protobuf
export OTEL_EXPORTER_OTLP_LOGS_HEADERS=X-Bicho-Key=YOUR_PUBLIC_KEY
```

The logs-specific endpoint is the complete URL and is used as supplied. A generic `OTEL_EXPORTER_OTLP_ENDPOINT` is a base URL: HTTP exporters normally append `/v1/logs`. Do not configure the full logs path as a generic base endpoint or assume a gRPC exporter will work. `http/json` is also supported where your exporter implements it.

The headers variable uses comma-separated `name=value` pairs, not JSON or colon-separated HTTP header lines. A secret-enabled backend project requires both generated headers:

```
export OTEL_EXPORTER_OTLP_LOGS_HEADERS="X-Bicho-Key=YOUR_PUBLIC_KEY,X-Bicho-Secret=YOUR_INGEST_SECRET"
```

`X-Bicho-Key` selects the project. Resource attributes and service names never select or authenticate it. Never put the ingest secret in a browser. Neither ingest credential grants API reads, issue updates, source-map uploads, or MCP access. See [privacy and credentials](/docs/privacy/).

Environment variables configure transport, not capture. Install and initialize your language’s OpenTelemetry logs SDK and exporter, or a logging bridge that emits exception records. Some instrumentation records exceptions only as trace events; those do not reach this logs endpoint. Flush the exporter during orderly shutdown according to its SDK’s instructions.

The [JavaScript SDK](/docs/javascript/) provides browser capture and explicit Node.js reporting. The [PHP SDK](/docs/php/) and [Python SDK](/docs/python/) provide explicit synchronous reporting, and the [Rust SDK](/docs/rust/) provides explicit awaited reporting on Tokio. The [Astro integration](/docs/astro/) uses the JavaScript SDK for opt-in browser capture and SSR request-exception reporting. All send OTLP/HTTP JSON. Direct SDK backend capture is manual; Python exception capture includes traceback locations. Other languages can send compatible OTLP logs through their own instrumentation and exporters. The core includes interoperability tests for the official OpenTelemetry JavaScript JSON and protobuf log exporters; that does not establish compatibility with every language or exporter version. See the [OpenTelemetry exporter configuration reference](https://opentelemetry.io/docs/specs/otel/protocol/exporter/) and your language’s logs documentation.

## Send a JSON record

Use a dedicated test project and replace the endpoint and key with its generated values. For a secret-enabled project, also supply `X-Bicho-Secret` from a secure backend environment.

```
curl --fail-with-body -sS https://your-bicho-worker.example/v1/logs \
  -H 'Content-Type: application/json' \
  -H 'X-Bicho-Key: YOUR_PUBLIC_KEY' \
  --data-binary '{
    "resourceLogs": [{
      "resource": {
        "attributes": [
          {"key": "service.name", "value": {"stringValue": "checkout"}},
          {"key": "service.version", "value": {"stringValue": "checkout@1.0.0"}},
          {"key": "deployment.environment.name", "value": {"stringValue": "test"}}
        ]
      },
      "scopeLogs": [{
        "scope": {"name": "checkout-errors", "version": "1.0.0"},
        "logRecords": [{
          "severityNumber": 17,
          "severityText": "ERROR",
          "body": {"stringValue": "Checkout failed"},
          "attributes": [
            {"key": "exception.type", "value": {"stringValue": "TestError"}},
            {"key": "exception.message", "value": {"stringValue": "Checkout failed"}},
            {"key": "exception.stacktrace", "value": {"stringValue": "TestError: Checkout failed\n    at checkout (app.js:20:3)"}},
            {"key": "log.record.uid", "value": {"stringValue": "bicho-docs-checkout-occurrence-1"}}
          ]
        }]
      }]
    }]
  }'
```

The fixed UID lets you resend this exact occurrence without incrementing its count. Use a new UID for a genuinely new occurrence. Verify the stored event through [authenticated event reads](/docs/api/) or [MCP](/docs/mcp/); HTTP 200 alone can include rejected records.

## Accepted records and severity

A record is an exception when at least one of `exception.type`, `exception.message`, or `exception.stacktrace` is a nonempty string attribute. Such records are accepted regardless of severity. Otherwise, only records with numeric `severityNumber` of 17 through 24, the ERROR and FATAL ranges, are accepted. Plain ERROR/FATAL messages do not need a stack trace. Ordinary lower-severity logs are rejected through partial success.

`severityText` is preserved as evidence but does not control acceptance or the indexed level:

| Numeric severity | Indexed level |
| --- | --- |
| Missing or 0 | error for an accepted exception |
| 1 through 8 | debug |
| 9 through 12 | info |
| 13 through 16 | warning |
| 17 through 20 | error |
| 21 through 24 | fatal |

The indexed message uses the string `exception.message` when present, otherwise the log body. Structured bodies become JSON text; absent bodies use `Unknown error`. The exception type defaults to `Error`. Indexed messages stop at 16,384 characters, while the sanitized OTLP evidence is stored separately within the record size limit.

## Metadata and captured evidence

Bicho preserves the sanitized resource, instrumentation scope, schema URLs, and log record under the stored event’s `otlp` field. Resource `service.version` becomes the release. `deployment.environment.name` becomes the environment, with `deployment.environment` as a fallback. `service.name` remains metadata rather than a routing key.

Up to 32 scalar resource/log attributes become indexed tags. Keys must fit 128 characters and values 512 characters; log attributes override resource attributes of the same name. Other supported structured values remain in the sanitized evidence rather than becoming searchable tags.

OTLP JSON uses lowerCamelCase field names and typed values such as `stringValue`. Supply nanosecond timestamps as decimal strings to avoid JavaScript number precision loss. Bicho uses nonzero `timeUnixNano`, then nonzero `observedTimeUnixNano`, then receipt time for the searchable timestamp. The index has millisecond precision; the stored log retains its nanosecond timestamp.

JSON `traceId` and `spanId` use hexadecimal strings of 32 and 16 characters respectively. Invalid or all-zero correlation IDs are omitted. Valid IDs remain correlation evidence, not deduplication keys. Protobuf uses the standard binary fields.

Bicho retains sanitized raw `exception.stacktrace` text even when it cannot parse frames. It recognizes common JavaScript, Python, Ruby, PHP, and Rust stack lines, deriving at most 100 frames from the first 200 input lines. Python traceback ordering is reversed for grouping. Recognizing a stack format does not mean Bicho installs that language’s instrumentation or supports native crash symbolication.

Source-map enrichment requires matching debug IDs in captured evidence and uploaded maps. Ordinary OTLP stack attributes and a release label do not supply them. The JavaScript SDK does not attach debug IDs or upload maps. See [source maps](/docs/sourcemaps/) before building an enrichment workflow.

## Deduplication and grouping

Give each occurrence a nonempty string `log.record.uid` of at most 512 characters and preserve it across retries. Bicho hashes the original UID with the project identity to derive a 32-character event ID. A retry of an already stored occurrence neither adds an event nor consumes quota, even when the quota is full. The UID returned by a client is not the server’s event lookup ID.

Without a UID, Bicho generates an ID. Resending that record can add another occurrence. Reusing one UID for unrelated errors suppresses later records rather than updating the first event.

Grouping is separate: exception type, message, and the most recent recognized stack location determine the issue fingerprint. Different occurrence UIDs can add events to the same issue. Bicho does not normalize changing IDs or URLs in messages and does not infer a root cause.

## Encoding and limits

| Input | Requirement or limit |
| --- | --- |
| JSON | Content-Type: application/json, OTLP protobuf JSON mapping |
| Binary protobuf | Content-Type: application/x-protobuf, OTLP logs export request schema |
| Compression | No encoding, identity, gzip, or deflate in Content-Encoding |
| Request bytes | 2 MiB on the wire and 2 MiB after decompression |
| Batch | At most 100 log records across all resources and scopes |
| Structure | At most 32 nesting levels and 20,000 structural values |
| Normalized event | At most 1 MiB before persistence |

Compression does not increase the decoded allowance. Unsupported content types or encodings return 415; oversized input returns 413. JSON severity values must be integers from 0 through 24, not enum names. Unknown JSON fields are ignored, so use supported OTLP fields for evidence you need to retain.

## Validation, partial success, and retries

Bicho decodes and normalizes the batch before storing any events. Malformed known fields, invalid accepted-record UIDs, or record limits reject the request before persistence. This differs from a valid record outside the error tracker’s scope, which contributes to partial rejection.

Full success returns HTTP 200 with an empty OTLP export response (`{}` for JSON). A valid batch with rejected records returns HTTP 200 with a response such as:

```
{
  "partialSuccess": {
    "rejectedLogRecords": "1",
    "errorMessage": "Ordinary logs below ERROR are outside the error-tracker scope"
  }
}
```

Do not retry a populated partial-success response. It can mean some or all records were outside scope, or that quota/storage failure stopped persistence after an accepted prefix. Replaying the accepted batch is not the recovery protocol.

If quota or storage failure occurs before any record is accepted, the whole request can return 429 or 503. Retry transient whole-request failures with backoff, keeping occurrence UIDs unchanged, and honor `Retry-After` when supplied. Quota responses specify the wait until the next UTC hour. Fix validation and authentication failures instead of retrying them unchanged.

Whole-request errors use OTLP’s `google.rpc.Status` shape, not the REST API’s `{ "error": "..." }` shape. Responses use the request encoding: JSON for JSON requests and binary protobuf for protobuf requests. See the [OTLP specification](https://opentelemetry.io/docs/specs/otlp/), [exception log conventions](https://opentelemetry.io/docs/specs/semconv/exceptions/exceptions-logs/), and [operations troubleshooting](/docs/operations/).