---
title: OpenTelemetry
description: Send traces and logs from any OpenTelemetry SDK or collector without the OpenPerf SDK.
---

OpenPerf accepts OTLP over HTTP, so any OpenTelemetry SDK or the Collector can send to it. Use this for languages the [SDK](/sdk) doesn't cover, or an app that already uses OpenTelemetry.

| Signal | Path         |
| ------ | ------------ |
| Traces | `/v1/traces` |
| Logs   | `/v1/logs`   |

Metrics and gRPC are not accepted. Bodies are protobuf (`application/x-protobuf`) or JSON (`application/json`), plain or `gzip`.

## Connect

Copy the DSN from the project settings, under **Connection**. Its token, `opf_…`, authenticates the requests. The **Endpoint** shown there is the intake address with `/v1` already added.

With the standard environment variables:

```bash
OTEL_EXPORTER_OTLP_ENDPOINT="https://ingest.example.com"
OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf"
OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer%20opf_…"
OTEL_METRICS_EXPORTER="none"
OTEL_SERVICE_NAME="checkout"
OTEL_RESOURCE_ATTRIBUTES="deployment.environment.name=production,service.version=1.4.2"
```

`OTEL_EXPORTER_OTLP_ENDPOINT` takes the address without `/v1`; the SDK adds `/v1/traces` and `/v1/logs`. Per-signal variables such as `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` take the full URL.

A client that can't set headers can pass the token as `?key=opf_…` instead.

Browser exporters work too. The intake answers CORS preflights, and **Allowed origins** in the project settings limits which sites may send. Empty accepts any origin; requests from an unlisted origin get `403`.

## Send a request

```bash
curl https://ingest.example.com/v1/logs \
  -H "Authorization: Bearer opf_…" \
  -H "Content-Type: application/json" \
  -d '{
    "resourceLogs": [{
      "resource": { "attributes": [
        { "key": "service.name", "value": { "stringValue": "checkout" } }
      ] },
      "scopeLogs": [{ "logRecords": [{
        "severityNumber": 17,
        "body": { "stringValue": "Payment failed" },
        "attributes": [
          { "key": "exception.type", "value": { "stringValue": "PaymentError" } }
        ]
      }] }]
    }]
  }'
```

The log shows under **Logs** and, because it carries `exception.type`, as an error under **Errors**.

## Attributes OpenPerf reads

Resource attributes:

| Attribute                     | Used for                                                                          |
| ----------------------------- | --------------------------------------------------------------------------------- |
| `service.name`                | The service. `unknown` when missing.                                              |
| `deployment.environment.name` | The environment. `deployment.environment` also works. `default` when missing.     |
| `service.version`             | The [release](/releases). Needed for source maps and release health.              |
| `vcs.ref.head.revision`       | The commit the code was built from.                                               |
| `telemetry.sdk.language`      | `webjs`, set by OpenTelemetry's web SDK, marks the data as coming from a browser. |

Span and log attributes:

| Attribute                                                     | Used for                                             |
| ------------------------------------------------------------- | ---------------------------------------------------- |
| `exception.type`, `exception.message`, `exception.stacktrace` | The error's type, message and stack.                 |
| `openperf.error.fingerprint`                                  | Groups all errors with the same value together.      |
| `user.id`, `user.email`, `user.name`                          | The user.                                            |
| `http.route`                                                  | Names server transactions, such as `GET /users/:id`. |

## Errors

An error is created from:

- a span event named `exception`, which `recordException` adds. Its level is `error`.
- a log record with `exception.type` or `exception.stacktrace`. Its level follows the log severity: `fatal`, `error`, or `warn` for anything lower.

Errors with a similar type and stack join the same group. Set `openperf.error.fingerprint` to group them yourself.

## Transactions

Root spans, server spans and consumer spans count as transactions in **Performance**. In the browser, the `documentLoad` span of the document load instrumentation counts as a page load.

## Responses

| Status | Meaning                                                                                                         |
| ------ | --------------------------------------------------------------------------------------------------------------- |
| `200`  | Accepted. Spans without a valid trace or span ID are dropped and counted in `partialSuccess`.                   |
| `400`  | The body isn't valid OTLP.                                                                                      |
| `401`  | Unknown token.                                                                                                  |
| `403`  | The browser origin isn't in **Allowed origins**.                                                                |
| `413`  | The body is too large. The limit is 10 MB unless your operator [changed it](/self-hosting/services/api-ingest). |
| `415`  | Unsupported content type or encoding.                                                                           |
| `429`  | Rate limited, too many invalid tokens, or the intake is busy. Retry after `Retry-After` seconds.                |
| `503`  | The intake is unavailable. Retry after `Retry-After` seconds.                                                   |

A body identical to one sent in the last 15 minutes is answered with `200` and dropped, so retries are safe.
