> ## Documentation Index
> Fetch the complete documentation index at: https://docs.zespan.com/llms.txt
> Use this file to discover all available pages before exploring further.

# OpenTelemetry integration

> Send traces from any OTel-instrumented app straight to Zespan with no Zespan SDK at all, or use initOTel/init_otel, getTracer/get_tracer, createSpan/create_span, and withSpan/with_span to dual-export Zespan-instrumented spans to your own OTel backend.

Zespan's SDKs are built on top of OpenTelemetry, and there are two independent ways to use that: send traces from an existing OTel setup straight to Zespan with no Zespan SDK installed at all (below), or use the Zespan SDK's own OTel primitives to also export Zespan-instrumented spans to your own backend (Jaeger, Honeycomb, a Collector) — see [Dual-export to your own OTel backend](#dual-export-to-your-own-otel-backend) further down.

## Send traces from any OTel-instrumented app (no Zespan SDK)

If your framework, language, or tooling already emits OpenTelemetry spans — [OpenLLMetry](https://github.com/traceloop/openllmetry)/Traceloop, [OpenInference](https://github.com/Arize-ai/openinference), a framework's own native OTel exporter, or a raw OTel SDK in any language — you don't need the Zespan SDK, a wrapper, or any code change to get those traces into Zespan. Point the exporter at Zespan's OTLP endpoint directly:

<CodeGroup>
  ```bash cURL theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  curl -X POST https://api.zespan.com/v1/traces \
    -H "x-api-key: $ZESPAN_API_KEY" \
    -H "Content-Type: application/json" \
    -d @export-trace-service-request.json
  ```

  ```python Python (OTel SDK, no zespan package) theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  from opentelemetry.sdk.trace import TracerProvider
  from opentelemetry.sdk.trace.export import BatchSpanProcessor
  from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter

  provider = TracerProvider()
  provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter(
      endpoint="https://api.zespan.com/v1/traces",
      headers={"x-api-key": "zsp_..."},
  )))
  ```
</CodeGroup>

<ParamField body="Endpoint" type="string">
  `https://api.zespan.com/v1/traces` (self-hosted: `<your base URL>/v1/traces`). Accepts a standard OTLP `ExportTraceServiceRequest`, either `application/json` or `application/x-protobuf`.
</ParamField>

<ParamField body="Authentication" type="header">
  `x-api-key: <your project API key>` — the same key the SDKs use.
</ParamField>

<Warning>
  This endpoint accepts **traces only** — `/v1/metrics` and `/v1/logs` return `501`. Requests are capped at 1 MB and 512 spans, rate-limited to 300 requests/minute per API key, and count against your organization's monthly event quota exactly like the native SDK ingest path.
</Warning>

### Attributes Zespan reads

Zespan maps the standard [OpenTelemetry GenAI semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/) — the same attribute names OpenLLMetry, OpenInference, and spec-compliant framework instrumentation already emit by default. Nothing custom to add for basic tracing to work:

| Field               | Attribute(s) read                                                |
| ------------------- | ---------------------------------------------------------------- |
| Provider            | `gen_ai.provider.name` (falls back to the older `gen_ai.system`) |
| Model               | `gen_ai.response.model` (falls back to `gen_ai.request.model`)   |
| Operation           | `gen_ai.operation.name`                                          |
| Input/output tokens | `gen_ai.usage.input_tokens` / `gen_ai.usage.output_tokens`       |
| Finish reason       | `gen_ai.response.finish_reason`                                  |
| Streaming           | `gen_ai.request.stream`                                          |

Agent-specific fields (`agent_id`, `agent_name`, tool calls, delegation) read the same `gen_ai.agent.*` / `gen_ai.tool.*` attributes, with a `zespan.*`-prefixed override for anything the semconv doesn't cover yet — see [Custom integrations](/sdk/integrations/custom) if you want that level of control from your own instrumentation.

## Dual-export to your own OTel backend

<Note>
  This page covers the OTel-native API — the same primitives (`Span`, `SpanKind`, `SpanStatusCode`) you'd use with the raw `@opentelemetry/api` / `opentelemetry-api` packages. If you just want to trace a custom operation and have it show up in the Zespan dashboard with evaluation scores, cost tracking, and status, use [`startSpan`](/sdk/manual-spans) instead — it's higher-level and integrates directly with Zespan's own trace model.
</Note>

## When to use this instead of `startSpan`

* You already export traces to an existing OTel backend and want Zespan's spans to appear there too, dual-exported from the same instrumentation.
* You need direct access to the underlying OTel `Span` object — for example, to set attributes with `span.setAttribute()` or interoperate with another library that expects a native OTel span.
* You're building an integration that needs a real `TracerProvider` registered globally (some auto-instrumentation libraries look for one).

If none of that applies, `startSpan` is almost always simpler — it doesn't require a separate `initOTel` call and it feeds directly into Zespan's cost, status, and evaluation-score model.

## Relationship to `enableOTel` / `otelEndpoint`

The TypeScript and Python SDKs already have a simpler, declarative way to dual-export to an OTel backend: the `enableOTel` (`enable_otel`), `otelEndpoint` (`otel_endpoint`), and `otelServiceName` (`otel_service_name`) options passed to `zespan.init()` / `zespan.init()`. See [Init options](/sdk/typescript#init-options) (TypeScript) and [Init options](/sdk/python#init-options) (Python) for those.

The functions on this page — `initOTel`/`init_otel` and friends — are the **manual, lower-level equivalent**. Use them when you need to configure the `TracerProvider` yourself (custom resource attributes, a non-default sampler, multiple span processors) or when you want to create and manage OTel spans directly rather than going through `zespan.init()`'s declarative config. The two mechanisms are independent — calling `initOTel` does not require `enableOTel` to be set, and vice versa.

## `initOTel(config)` / `init_otel(config)`

Configures a global OpenTelemetry `TracerProvider` with a `BatchSpanProcessor` and an OTLP HTTP exporter, and registers it as the active provider. Call this once at application startup, before creating any spans with `getTracer`/`get_tracer`.

<CodeGroup>
  ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  import { initOTel } from "@zespan/sdk";

  const { tracer, provider } = initOTel({
    endpoint: "https://otel-collector.internal:4318/v1/traces",
    serviceName: "checkout-service",
    environment: "production",
    apiKey: process.env.ZESPAN_API_KEY,
  });
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  from zespan import init_otel, OTelConfig

  init_otel(OTelConfig(
      endpoint="https://otel-collector.internal:4318/v1/traces",
      service_name="checkout-service",
      environment="production",
      api_key=os.environ["ZESPAN_API_KEY"],
  ))
  ```
</CodeGroup>

`initOTel` returns `{ tracer, provider }` — the registered `Tracer` and the underlying `NodeTracerProvider`. `init_otel` returns `None`; call `get_tracer()` afterward to retrieve the tracer.

### `OTelConfig`

<ParamField body="endpoint" type="string" default="https://localhost:3001/v1/traces">
  OTLP HTTP endpoint that spans are exported to. Point this at your own collector or backend.
</ParamField>

<Warning>
  Zespan's OTLP endpoint accepts **traces only**. `/v1/metrics` and `/v1/logs`
  return `501`. If your collector or SDK exports all three signals, point only
  the trace exporter at Zespan.

  Trace exports are capped at 1 MB and 512 spans per request — matching the
  OpenTelemetry SDK/Collector's own default `max_export_batch_size`, so a
  default-configured exporter isn't silently truncated — and count against
  your organization's monthly event quota, exactly like the native
  `/v1/ingest` path. Anything beyond the cap comes back in
  `partialSuccess.rejectedSpans`; per the OTLP spec this is informational
  only, so raise your exporter's batch size if you rely on it.
</Warning>

<ParamField body="serviceName" type="string" default="zespan-sdk">
  Service name attached as the `service.name` resource attribute on every exported span. TypeScript field: `serviceName`. Python field: `service_name`.
</ParamField>

<ParamField body="sampleRate" type="number" default="1.0">
  Fraction of traces to keep, decided once per trace from its trace id — the
  same enforcement the native SDKs use, not a per-event roll. TypeScript
  field: `sampleRate`. Python field: `sample_rate`.

  <Note>
    Before this was enforced, an OTel-originated trace always sampled in at
    100% regardless of this setting. If you're upgrading from an older SDK
    version and already had `sampleRate` below `1.0` configured, your ingest
    volume will drop to match it — this is the fix taking effect, not new
    data loss.
  </Note>
</ParamField>

<ParamField body="apiKey" type="string">
  When set, sent as the `x-api-key` header on every export request. TypeScript field: `apiKey`. Python field: `api_key`.
</ParamField>

<ParamField body="environment" type="string" default="production">
  Attached as the `deployment.environment` resource attribute.
</ParamField>

<Warning>
  `initOTel`/`init_otel` registers a global `TracerProvider`. Call it once per process. Calling it again re-registers the provider and can produce duplicate exporters.
</Warning>

## `getTracer()` / `get_tracer()`

Returns the `Tracer` instance created by `initOTel`/`init_otel`. Throws (TypeScript: `Error`; Python: `RuntimeError`) if called before initialization.

<CodeGroup>
  ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  import { getTracer } from "@zespan/sdk";

  const tracer = getTracer();
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  from zespan import get_tracer

  tracer = get_tracer()
  ```
</CodeGroup>

## `createSpan` / `create_span`

Starts and returns a native OTel span from the tracer configured by `initOTel`/`init_otel`. You are responsible for ending the span yourself — prefer `withSpan`/`with_span` below unless you need to manage the span lifecycle manually (for example, ending it from a different function than the one that created it).

<CodeGroup>
  ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  import { createSpan, SpanKind } from "@zespan/sdk";

  const span = createSpan({
    name: "cache-lookup",
    kind: SpanKind.CLIENT,
    attributes: { "cache.key": "user:123" },
  });

  try {
    const value = await lookupCache("user:123");
    span.setAttribute("cache.hit", value !== null);
  } finally {
    span.end();
  }
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  from zespan import create_span
  from opentelemetry.trace import SpanKind

  span = create_span(
      "cache-lookup",
      kind=SpanKind.CLIENT,
      attributes={"cache.key": "user:123"},
  )

  try:
      value = lookup_cache("user:123")
      span.set_attribute("cache.hit", value is not None)
  finally:
      span.end()
  ```
</CodeGroup>

<ParamField body="name" type="string" required>
  Span name.
</ParamField>

<ParamField body="kind" type="SpanKind" default="SpanKind.INTERNAL">
  OpenTelemetry span kind. See [`SpanKind` values](#spankind-values) below.
</ParamField>

<ParamField body="attributes" type="Record<string, string | number | boolean | string[]>">
  Attributes set on the span at creation time. TypeScript accepts `attributes` as an object property; Python accepts it as the `attributes` keyword argument (a `dict`).
</ParamField>

<Note>
  In Python, `kind` and `attributes` are keyword-only arguments — `create_span(name, *, kind=None, attributes=None)`.
</Note>

## `withSpan` / `with_span`

Wraps a function call in a span, setting `SpanStatusCode.OK` on success or `SpanStatusCode.ERROR` (with the exception recorded) on failure, and always ending the span. This is the recommended way to create spans with this API — it guarantees the span is closed exactly once regardless of outcome.

<CodeGroup>
  ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  import { withSpan, SpanKind } from "@zespan/sdk";

  const result = await withSpan(
    "process-payment",
    async (span) => {
      span.setAttribute("payment.provider", "stripe");
      return chargeCard(order);
    },
    { kind: SpanKind.CLIENT, attributes: { "order.id": order.id } }
  );
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  from zespan import with_span
  from opentelemetry.trace import SpanKind

  async with with_span(
      "process-payment",
      kind=SpanKind.CLIENT,
      attributes={"order.id": order.id},
  ) as span:
      span.set_attribute("payment.provider", "stripe")
      result = await charge_card(order)
  ```
</CodeGroup>

**TypeScript**: `withSpan<T>(name, fn, options?)` — `fn` receives the span and returns a `Promise<T>`; `withSpan` returns that same `Promise<T>`. `options` accepts `kind` and `attributes`, same shape as `createSpan`.

**Python**: `with_span(name, *, kind=None, attributes=None)` is an async context manager (`@contextlib.asynccontextmanager`) — use it with `async with`, not as a function that takes a callback. It yields the span.

<Warning>
  On error, both implementations call `record_exception`/`recordException` and set `SpanStatusCode.ERROR` before re-raising — the exception always propagates. Neither implementation swallows errors.
</Warning>

## `SpanKind` values

`SpanKind` is re-exported from `@opentelemetry/api` as-is — it's not a Zespan-specific type. The common values:

| Value               | Use for                                                                     |
| ------------------- | --------------------------------------------------------------------------- |
| `SpanKind.INTERNAL` | Default. Internal application logic with no cross-process boundary.         |
| `SpanKind.CLIENT`   | An outbound call to another service (HTTP request, DB query, cache lookup). |
| `SpanKind.SERVER`   | Handling an inbound request from another service.                           |
| `SpanKind.PRODUCER` | Sending a message to a queue or event bus.                                  |
| `SpanKind.CONSUMER` | Receiving a message from a queue or event bus.                              |

<Note>
  **TypeScript** exports `SpanKind` directly from `@zespan/sdk` — `import { SpanKind } from "@zespan/sdk"`. **Python** does not re-export `SpanKind` from the `zespan` package; import it from OpenTelemetry directly — `from opentelemetry.trace import SpanKind`.
</Note>

## `SpanStatusCode` values

Used with `span.setStatus({ code })` (TypeScript) / `span.set_status(code)` (Python) to record the outcome of the work a span covers.

| Value                  | Meaning                                                                                          |
| ---------------------- | ------------------------------------------------------------------------------------------------ |
| `SpanStatusCode.UNSET` | Default status; no explicit outcome recorded.                                                    |
| `SpanStatusCode.OK`    | The operation completed successfully. Set automatically by `withSpan`/`with_span`.               |
| `SpanStatusCode.ERROR` | The operation failed. Set automatically by `withSpan`/`with_span`, along with `recordException`. |

<Note>
  **TypeScript** exports `SpanStatusCode` directly from `@zespan/sdk`. **Python** does not — the underlying OTel API names this enum `StatusCode`, not `SpanStatusCode`. Import it yourself: `from opentelemetry.trace import StatusCode`.
</Note>

## `BaggageSpanProcessor`

<Note>
  `BaggageSpanProcessor` is currently **TypeScript-only** — there is no Python equivalent exported from the `zespan` package.
</Note>

A `SpanProcessor` that copies every entry in the current [OTel baggage](https://opentelemetry.io/docs/concepts/signals/baggage/) onto each span as a `baggage.<key>` attribute when the span starts. Zespan's [`injectAgentContext`/`extractAgentContext`](/sdk/typescript#framework-integrations) helpers use OTel baggage to carry agent delegation metadata (`agent.delegation.reason`, `agent.task`) across an HTTP call to another service. Registering `BaggageSpanProcessor` on your `TracerProvider` is what makes that propagated metadata show up as searchable span attributes on the receiving side, rather than only being available on the wire.

```typescript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
import { BaggageSpanProcessor } from "@zespan/sdk";
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";

const provider = new NodeTracerProvider({ /* ...resource, etc. */ });
provider.addSpanProcessor(new BaggageSpanProcessor());
provider.register();
```

`BaggageSpanProcessor` implements the standard OTel `SpanProcessor` interface (`onStart`, `onEnd`, `shutdown`, `forceFlush`) — add it alongside any other span processors (such as the `BatchSpanProcessor` used internally by `initOTel`) on a `TracerProvider` you construct yourself. It only acts on `onStart`; `onEnd`, `shutdown`, and `forceFlush` are no-ops.

<Note>
  `initOTel` does not register a `BaggageSpanProcessor` automatically. If you want baggage entries copied onto span attributes, add it explicitly when you construct your own `TracerProvider`.
</Note>
