---
title: "OpenTelemetry"
description: "Use any OpenTelemetry backend for MainSDK logs and traces - OTLP endpoint, auth header as a secret, sampling, local testing and swapping."
section: "integrations"
canonical: "https://mainsdk.dev/docs/integrations/opentelemetry"
updated: "2026-10-28"
---

# OpenTelemetry

> Use any OpenTelemetry backend for MainSDK logs and traces - OTLP endpoint, auth header as a secret, sampling, local testing and swapping.

Canonical: https://mainsdk.dev/docs/integrations/opentelemetry · Updated: 2026-10-28

OpenTelemetry is the open standard for logs, traces and metrics, and the `otel` driver is how [`log`](https://mainsdk.dev/docs/interfaces/log) sends your app's telemetry to a backend you already run. Point it at any endpoint that accepts OTLP: your own collector, or a hosted backend such as Datadog, Grafana, Honeycomb or Sentry. Pick it when your team already has an observability tool, or when telemetry must stay in infrastructure you control. Logging works with no configuration; this driver only decides where records go after they leave the terminal and the local event store.

## Interfaces it serves

### Logs, audit and telemetry (`log`)

- Status: Building for 0.2 · ships as a stable API
- Driver: `otel` (Any OpenTelemetry backend)
- Docs: https://mainsdk.dev/docs/interfaces/log.md

```yaml
telemetry:
  to: otel
```

Swap to another provider: `terminal`, `mainsdk`, `sentry`.

### Product analytics (`analytics`)

- Status: Building for 0.3 · ships as a beta API
- Driver: `otel` (Any OpenTelemetry backend)
- Docs: https://mainsdk.dev/docs/interfaces/analytics.md

```yaml
analytics:
  driver: otel
```

Swap to another provider: `builtin`, `posthog`, `plausible`.

## Before you start

You need an endpoint that receives OTLP over HTTP/protobuf, and usually a token to authenticate to it.

1. **Choose where telemetry lands.** Either run an OpenTelemetry Collector yourself, or use a backend that accepts OTLP directly. A collector is useful when you want to fan out to several backends or filter before data leaves your network.
2. **Get the OTLP HTTP endpoint.** Collectors listen for OTLP over HTTP on port 4318 by default. Hosted backends publish their own OTLP endpoint, often per region; use the one in the region where you want the data stored.
3. **Create an ingest token.** Most hosted backends issue an API or ingest key that goes in a request header. Use a key that can only write telemetry, not read or administer your account.
4. **Store the token** in your secrets manager, never in code or a committed env file.

## Configure

Two blocks: `logging` for what is recorded and redacted, `telemetry` for where it goes. Add `otel` to `telemetry.to` and give it the endpoint and headers.

```yaml title="main.yaml"
logging: { audit: true, redact: [personal, sensitive, child, phi] }
telemetry:
  to: [terminal, otel]                          # any mix; default [terminal]
  otel:
    endpoint: https://otel.pinecart.dev:4318    # OTLP over HTTP/protobuf
    headers: { authorization: secret:OTEL_TOKEN }  # suggested name; your backend's ingest token
  sample: 0.2                                   # traces only
```

The header name your backend expects may differ from `authorization`; `headers` is a map, so use the name it documents and keep the value a `secret:` reference. A literal token in `main.yaml` fails `mainsdk check` with `MAIN-CONFIG-0004`.

Keep local logs in development and export only in production with an overlay:

```yaml title="main.env.prod.yaml"
telemetry:
  to: [otel]
  sample: 0.2
```

## Use it

Your code calls `log` as usual; nothing in it names OpenTelemetry.

```ts title="app/api/orders/[id]/ship/route.ts"
import { auth, log } from "mainsdk";

export async function POST(req: Request, { params }: { params: { id: string } }) {
  const session = await auth.require(req);
  try {
    log.info("order shipped", { orderId: params.id, by: session.user.id });
    return new Response(null, { status: 204 });
  } catch (err) {
    log.error(err, { orderId: params.id });
    throw err;
  }
}
```

Records follow the OpenTelemetry logs data model, carry W3C Trace Context ids, and arrive with the resource attributes `service.name` and `mainsdk.project`, so your backend can group them by app.

## Local development and testing

- **No backend:** the default `terminal` destination prints records and keeps recent events in `.mainsdk/events/` (git-ignored) for 7 days.
- **A local collector:** run an OpenTelemetry Collector on your machine and point `telemetry.otel.endpoint` at `http://localhost:4318` to see exactly what your backend will receive.
- **Tests:** `createTestApp()` from `@mainsdk/testing` runs `log` without a network, and `chaos.on("log", { outage: true })` simulates the endpoint being down. Assertions on log and audit records in the testing kit are not defined yet [TBD].

```ts title="app/api/orders/ship.test.ts"
import { it } from "node:test";
import { createTestApp, chaos } from "@mainsdk/testing";

it("keeps serving when the telemetry endpoint is down", async () => {
  createTestApp({ main: "./main.yaml" });
  chaos.on("log", { outage: true });
  // ... call your route and assert it still returns 204
});
```

## What MainSDK adds on top

- Personal data and secrets redacted in process before any export: labelled fields, anything resolved from a `secret:` reference, and known key formats. User ids are pseudonymised with a keyed hash and IP addresses trimmed.
- Only fields listed in the `telemetry@1` contract are exported; anything else is dropped with `MAIN-LOG-0005`.
- Export never blocks a request. It is asynchronous, buffered and bounded; when the endpoint is unreachable, telemetry is dropped with a `telemetry.dropped` counter.
- Errors, policy decisions and audit records are never sampled; `sample` applies to traces only.

See [Logs, audit and telemetry](https://mainsdk.dev/docs/interfaces/log) for the full contract.

## Limits and notes

- The `otel` driver comes with the `log-telemetry` spec (phase 1).
- Export uses OTLP over HTTP/protobuf. Whether OTLP over gRPC is supported is not stated [TBD].
- Data residency is wherever your endpoint is. MainSDK regions are US, EU and India; choose a backend region to match. Telemetry sent to MainSDK Cloud is tagged with the project's region.
- `authorization`, `cookie` and `set-cookie` headers, cookies and request and response bodies are not collected by default.
- Where audit records are exported (`logging.export`) and the audit log's production home are not decided yet [TBD].

## Swap to or from it

Change the destination list. Your `log.*` calls and audit records stay the same.

```yaml title="main.yaml"
telemetry:
  to: [otel, mainsdk]   # was [otel]
```

Moving from one OpenTelemetry backend to another is a change of `endpoint` and header. Past telemetry stays in the old backend; nothing needs to migrate.

## FAQ

**Which backends work?** Any that accepts OTLP over HTTP, including your own collector. Datadog, Grafana, Honeycomb and Sentry all take the same export.

**Will a slow or broken endpoint slow my app?** No. Export is buffered and bounded, and drops with a counter rather than block a request.

**Can I send to my backend and MainSDK Cloud at once?** Yes. List both in `telemetry.to`. Nothing reaches MainSDK Cloud until you run `mainsdk login` and opt in.

## Next

- [Logs, audit and telemetry](https://mainsdk.dev/docs/interfaces/log)
- [PostHog](https://mainsdk.dev/docs/integrations/posthog)
- [Fail static](https://mainsdk.dev/docs/concepts/fail-static)
