> ## 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 — bring your own OTel backend

> Use initOTel/init_otel, getTracer/get_tracer, createSpan/create_span, and withSpan/with_span to export spans directly to your own OpenTelemetry backend, alongside or instead of Zespan's own pipeline.

Zespan's SDKs are built on top of OpenTelemetry, and both SDKs expose the underlying OTel API directly. Use this when you already run OTel infrastructure — Jaeger, Honeycomb, an OTel Collector, or any OTLP-compatible backend — and want Zespan-instrumented spans to also flow there, or when you need lower-level manual control over span creation than [manual spans](/sdk/manual-spans) provides.

<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>
