Skip to main content
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 further down.

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

If your framework, language, or tooling already emits OpenTelemetry spans — OpenLLMetry/Traceloop, 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:
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, optionally gzip-compressed (Content-Encoding: gzip).
header
x-api-key: <your project API key> — the same key the SDKs use — or Authorization: Bearer <your project API key> (for exporters and collectors that only offer a bearer token). If both are sent, x-api-key is used. Bearer is accepted only for project keys (zsp_...) and only on the OTLP paths.
status
200 when the request was accepted. Look at partialSuccess.rejectedSpans in the body: spans counted there (more than 512 in one request, unusable trace or span ids, no valid start time) were not stored, and exporters do not retry them. A request that cannot be parsed as OTLP is answered 400 and nothing is stored. A successful protobuf request gets a protobuf response and a successful JSON request gets JSON; error replies are always JSON. In OTLP/JSON, kind and status.code can be numbers or enum names ("SPAN_KIND_SERVER", "server"), and trace and span ids can be hex or base64.
This endpoint accepts traces only — /v1/metrics and /v1/logs return 501. Requests are capped at 8 MiB after decompression and 512 spans, rate-limited to 3,000 requests/minute per project by default, and count against your organization’s monthly event quota exactly like the native SDK ingest path (these limits are for this endpoint only; the native SDK ingest keeps its own). A body over the size limit is answered 413, which OTLP exporters do not retry, so a small gzip upload that inflates past 8 MiB is refused: lower your exporter’s batch size (for example max_export_batch_size). Over the rate limit the answer is 429 with Retry-After, which exporters do retry. Self-hosted: OTLP_MAX_BODY_BYTES and OTLP_RATE_PER_MIN change them (see environment variables).
Protobuf exports sent before this release were dropped or stored with corrupted ids. Every span sent as application/x-protobuf (the default for the Python opentelemetry-exporter-otlp-proto-http example above, the OpenTelemetry Collector’s otlphttp exporter, Kong, and LiteLLM’s otlp_http exporter) was either dropped or stored with wrong trace and span ids, while the endpoint answered 202. Spans sent as JSON were not affected. If you exported protobuf to Zespan before this release, those traces are missing or have unusable ids and cannot be recovered; new exports land normally.

Attributes Zespan reads

Zespan maps the standard OpenTelemetry GenAI semantic conventions — the attribute names OpenLLMetry and spec-compliant framework instrumentation emit. Nothing custom to add for basic tracing to work. Older GenAI names, OpenInference names and the attribute names AI gateways publish are read too (the compatibility mapping, on by default): they only fill a value the standard names left empty, a value read from a standard name is never replaced. Zespan prices each call from its own table (and your organization’s custom prices), not from a cost attribute on your spans. The one exception is the opt-in gateway fallback described in AI gateways.
Self-hosted: OTEL_GENAI_COMPAT=off turns the compatibility mapping off, and OTEL_GATEWAY_MAPPING=off turns it off together with the gateway handling below. Spans with only the older names, OpenInference names or gateway names then land as before, with zero tokens and no model-call kind. See environment variables.
Spans stored before the compatibility mapping existed are not rewritten. After it, a span that carries only the older names (for example gen_ai.usage.prompt_tokens) is stored with its tokens, cost, model-call kind and, where your capture settings allow, its text, so token and cost totals for those sources rise from that point.

AI gateways

A gateway (for example LiteLLM, OpenRouter, Portkey, Kong, Vercel AI Gateway, Cloudflare AI Gateway or Bifrost) that exports OTLP to /v1/traces is recognised from the span itself: the service name, the span names and the attribute names the gateway publishes. If the Gateway and OTLP data card lists the data as OpenTelemetry, the gateway was not recognised and its data is stored as plain OpenTelemetry; set the resource attribute zespan.gateway to one of litellm, openrouter, portkey, kong, vercel, cloudflare, bifrost or envoy to label it. Envoy AI Gateway is recognised only through that attribute (zespan.gateway=envoy), and the same attribute overrides detection for any other gateway.
  • One model call per request. The gateway’s proxy, routing, infrastructure and retry or fallback attempt spans are stored as gateway spans, which are never counted as model calls (see span kinds). Only the span that carries the model call is a model call. Failed attempts keep their HTTP status, error type and provider so you can see why a request was retried or fell back. A failed LiteLLM request span is treated as an attempt only when the same export also holds a successful call of the same request (same trace and same parent span); a request that failed for good stays a failed model call, also when several requests share one trace.
  • Cost uses Zespan’s prices. The gateway’s own figure is kept as evidence next to the span. It replaces Zespan’s price only for a model Zespan has no price for and only when GATEWAY_COST_FALLBACK=on is set on a self-hosted deployment. A gateway’s cache-hit or zero-cost figure never makes a call free. The two figures are never added.
  • Gateway identity metadata is dropped at ingest. Some gateways (LiteLLM, Portkey) attach the end user’s e-mail, the requester’s IP address, key hashes and ids, team and key aliases or request headers to their spans. Zespan removes them before anything is stored, whatever your content capture settings are. The end-user id you set yourself (for example llm.user) stays. Self-hosted: GATEWAY_META_DENYLIST=off keeps them.
  • Prompts and completions follow your capture settings. Many gateways export prompts and completions by default (LiteLLM, Portkey, Cloudflare), so check your project’s content capture and PII redaction settings before you point a gateway at Zespan: with prompt capture off none of that text is stored; with it on, redaction applies like it does to SDK data.
  • A call your SDK also sees can have its tokens and cost counted once. If your app uses the Zespan SDK and calls the model through one of these gateways that also exports to Zespan, the same call arrives twice. When merging is on for your project, Zespan keeps both rows, nests the gateway’s span under the SDK span when they matched by trace link, and sets the tokens and cost of the row that arrived second to 0 (the gateway’s row is held back for a short time so the SDK’s row can arrive first). Call counts are not all merged: counts built from hourly totals still count both rows as calls. Matching uses the trace link when you turn on the SDK option below, and otherwise the provider’s response id (only an id that is long and random enough: a short or numeric id, such as some local OpenAI-compatible servers return, is not matched). A row without tokens or cost is never the one kept. When Zespan cannot tell, it counts both. Until merging is on, both rows are counted. See Using the SDK too.
  • Check what arrived. Settings → Instrumentation → Gateway and OTLP data lists, per source, when data last arrived, how (JSON or Protobuf, gzip), and what share of model calls came with tokens, a span kind and text, with a one-line fix for each problem. Send one request, then press Refresh. See Instrumentation.
Setup pages: LiteLLM Proxy, OpenRouter, Portkey, Kong AI Gateway, Vercel AI Gateway, Cloudflare AI Gateway, Bifrost, Envoy AI Gateway.

Using the SDK too

If a gateway and the Zespan SDK both see the same call, turn on the correlation option so the gateway’s span becomes a child of the SDK span. It is off by default; with it off nothing is added to your requests.
For a call whose client base URL host is listed (exact name, or a leading dot such as .internal), the wrapped calls add traceparent (and Cloudflare’s cf-aig-otel-trace-id and cf-aig-otel-parent-span-id) to the provider request: OpenAI, Anthropic and OpenRouter in TypeScript; OpenAI, Anthropic and LiteLLM in Python. A header you set yourself is never replaced (if you set traceparent, none of the three is added). Whether a gateway uses an inbound traceparent differs, see each gateway page. Without it these wrappers still record the provider’s response id on every call (gen_ai.response.id, no content), which is the fallback match. Self-hosted: merging is controlled by GATEWAY_DEDUPE, see environment variables.

Finish and failure outcome

From the finish and failure attributes Zespan derives how each call ended (stopped, cut off at the length limit, blocked, refused, empty, stream ended without a finish) and, for failed calls, a failure kind such as rate limit, timeout or authentication. It reads error.type (a number is an HTTP status), http.response.status_code, rpc.grpc.status_code, and the exception type on exception events. It never reads exception.message or status.message, and stores only fixed values (no text). See the finish outcome reference. A span that records an empty answer with no tool calls is marked Empty response; a span that did not record the answer at all is not. 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 if you want that level of control from your own instrumentation.

Tool spans from OTel frameworks

Zespan reads a span as a tool call when it matches one of these shapes. The tool name is taken from the first attribute present, in this order: gen_ai.tool.name, tool.name, traceloop.entity.name, ai.toolCall.name, then the span name without a leading execute_tool or a trailing .tool. A span with an error status, or an error.type attribute, is a failed tool call. An OpenAI Agents span named handoff to <agent> is a hand-off, not a tool. The OpenInference, OpenLLMetry and Laminar rows apply when your project’s OTel tool spans setting is on (otelToolSpanKind on the project; on for projects created from now on, off for existing projects so their tool counts do not change). With the setting off those spans keep their arguments and results but are not counted as tool calls.

Tool definitions and Google ADK native spans (self-hosted, off by default)

With OTEL_CAPTURE_V2=on on the server, Zespan also reads, from documented attribute names only:
  • Tool definitions from gen_ai.tool.definitions and from OpenInference llm.tools.N.tool.json_schema, recorded with their parameter schemas (defaults, examples and constants removed, descriptions limited to 500 characters, never a partial list).
  • Google ADK’s own spans: the request (gcp.vertex.agent.llm_request: the conversation, the system instructions, the tools, the response schema and whether JSON was requested) and the response (gcp.vertex.agent.llm_response: the text and the function calls). ADK’s own spans carry no GenAI content attributes unless your exporter adds them, so without this the answer and the definitions are not recorded; the callback handler in the Python or TypeScript SDK is more complete.
  • The tool call id is gen_ai.tool.call.id only. It is no longer filled in with the tool name, because a name is not an id and breaks matching a request with its execution.
  • The error code of a failed span is its own error.type when that is a short code, and the fixed value it always was otherwise.
Prompt and tool storage and redaction apply as for SDK spans. See Capture fields.

Stack span kinds

Zespan can store retrieval, rerank, embedding, agent, chain and guardrail spans under their own kinds, so a failure is shown at the right step. This is off by default: set OTEL_MAP_STACK_SPAN_KINDS=on on the server (see environment variables) and these map as follows. LLM, TOOL and EVALUATOR spans, and a span that sets zespan.span_kind itself, are not affected. With the switch off, spans are stored as before. The SDK helps you set the right attributes: createSpan and withSpan (TypeScript, on the client and as standalone functions) take spanKind, and create_span and with_span (Python) take span_kind, one of retriever, reranker, embedding, agent, chain or guardrail. The SDK adds openinference.span.kind (and gen_ai.operation.name where GenAI defines an operation for it). Attributes you pass yourself win.
In TypeScript, spans forwarded with captureExternalSpans also keep their kind when OTEL_MAP_STACK_SPAN_KINDS=on is set in your app’s environment.

Dual-export to your own OTel backend

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 instead — it’s higher-level and integrates directly with Zespan’s own trace model.

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 (TypeScript) and Initialization (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.
initOTel returns { tracer, provider } — the registered Tracer and the underlying NodeTracerProvider. init_otel returns None; call get_tracer() afterward to retrieve the tracer.

OTelConfig

string
default:"https://localhost:3001/v1/traces"
OTLP HTTP endpoint that spans are exported to. Point this at your own collector or backend.
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 8 MiB (after decompression) 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.
string
default:"zespan-sdk"
Service name attached as the service.name resource attribute on every exported span. TypeScript field: serviceName. Python field: service_name.
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.
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.
string
When set, sent as the x-api-key header on every export request. TypeScript field: apiKey. Python field: api_key.
string
default:"production"
Attached as the deployment.environment resource attribute.
initOTel/init_otel registers a global TracerProvider. Call it once per process. Calling it again re-registers the provider and can produce duplicate exporters.

getTracer() / get_tracer()

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

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).
string
required
Span name.
SpanKind
default:"SpanKind.INTERNAL"
OpenTelemetry span kind. See SpanKind values below.
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).
In Python, kind and attributes are keyword-only arguments — create_span(name, *, kind=None, attributes=None).

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

SpanKind values

SpanKind is re-exported from @opentelemetry/api as-is — it’s not a Zespan-specific type. The common values:
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.

SpanStatusCode values

Used with span.setStatus({ code }) (TypeScript) / span.set_status(code) (Python) to record the outcome of the work a span covers.
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.

BaggageSpanProcessor

BaggageSpanProcessor is currently TypeScript-only — there is no Python equivalent exported from the zespan package.
A SpanProcessor that copies every entry in the current OTel baggage onto each span as a baggage.<key> attribute when the span starts. Zespan’s injectAgentContext/extractAgentContext 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.
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.
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.