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.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
gatewayspans, 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=onis 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=offkeeps 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.
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..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 readserror.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)
WithOTEL_CAPTURE_V2=on on the server, Zespan also reads, from documented attribute names only:
- Tool definitions from
gen_ai.tool.definitionsand from OpenInferencellm.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.idonly. 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.typewhen that is a short code, and the fixed value it always was otherwise.
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: setOTEL_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.
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
Spanobject — for example, to set attributes withspan.setAttribute()or interoperate with another library that expects a native OTel span. - You’re building an integration that needs a real
TracerProviderregistered globally (some auto-instrumentation libraries look for one).
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.
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.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.
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.
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.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.
