OpenTelemetry
Already instrumented? Point your exporter at Agent Ping and you are done. Agent Ping accepts standard OTLP/HTTP trace exports, protobuf or JSON, and maps them onto runs and steps. No new SDK, no code changes.
The endpoint
POST https://eu.ingest.agentping.io/v1/traces
Use the host matching your data residency (eu or us). Authenticate
with your team API key (apk_...) as a Bearer token; exporters that
cannot set Authorization can send X-AgentPing-Key instead. Per-agent
ping_ tokens do not work here; OTel traffic can create agents, so it
needs the team key.
Generic exporter setup
Any OpenTelemetry SDK works with two environment variables:
export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT="https://eu.ingest.agentping.io/v1/traces"
export OTEL_EXPORTER_OTLP_TRACES_HEADERS="authorization=Bearer apk_eu_..."
How traces map to runs
| OpenTelemetry | Agent Ping |
|---|---|
| Trace | Run (one run per trace; re-exports are deduplicated) |
| Root span | The run record; its name becomes the run's goal |
| Span with model/provider attributes | LLM call step, with tokens and server-side cost |
| Span with a tool name | Tool call step |
| Any other span | Step |
service.name |
The agent (created automatically on first sight) |
LLM calls are recognised across emitter conventions, not just the
canonical gen_ai.* set. Agent Ping reads the model from gen_ai.request.model,
gen_ai.response.model, OpenInference's llm.model_name, or the Vercel AI
SDK's ai.model.id; the provider from gen_ai.system, llm.provider, or
ai.model.provider; and token counts from any of gen_ai.usage.input_tokens
/ output_tokens, the older prompt_tokens / completion_tokens,
OpenInference's llm.token_count.*, or Vercel's ai.usage.*. An
OpenInference span tagged openinference.span.kind = LLM or TOOL is mapped
even without gen_ai.* attributes.
Two attributes are worth setting explicitly:
agentping.agenton the resource overrides the agent identity whenservice.nameis not what you want on the dashboard.agentping.goalon the root span tells evaluations what the run was supposed to achieve. Setting it materially improves evaluation quality; without it the root span's name is used.
Conversations
A trace is a run. For a chat that means a run per message, which is rarely what you want: see Conversations for why the session is the better unit.
To make a trace one turn of a longer run, name the run on the trace:
"resource": {"attributes": [
{"key": "service.name", "value": {"stringValue": "support-chat"}},
{"key": "agentping.run", "value": {"stringValue": "run_eu_018f3a2b9c1d7e8fa4b9c2d7e8f1a3b6"}},
{"key": "agentping.idle_timeout_seconds", "value": {"intValue": "900"}},
{"key": "agentping.idle_timeout_status", "value": {"stringValue": "success"}}
]}
| Attribute | Effect |
|---|---|
agentping.run |
This trace is a turn of that run, not a run of its own. Its spans become steps on it. Must be a valid run_<region>_<32 hex> id. |
agentping.idle_timeout_seconds |
Close the run once no span has arrived for this long. OTLP has no way to say "finished", so without this a conversation stays open until you call /finish yourself. |
agentping.idle_timeout_status |
What that silence meant. Defaults to timeout; send success for a chat. |
agentping.turn |
On a span: which exchange of the conversation it belongs to, counting from 1. Groups the several spans of one turn. |
Any of these can go on the resource (the whole service) or on a span (this turn); the span wins where both are set.
Naming the run changes two things. The settle window below no longer applies: the run is yours to end, not ours to guess at, and the root span becomes an ordinary step rather than the run record, because the run already exists.
Mixing is fine: report turns over OTLP and call POST /v1/runs/{id}/finish on the
same id when the user closes the chat.
Run completion
OTel delivers spans in batches with no end-of-run signal, so a run ingested this way completes once its root span has arrived and no new span has landed for two minutes. Costs, health checks, and evaluations then run exactly as they do for SDK-reported runs.
Limits
Span attributes Agent Ping does not map are kept with the step, up to 8KB per step; beyond that the largest attributes are dropped and the step is flagged, never rejected. Prompt and completion attributes count as step content with the usual 64KB allowance.
Verified emitters
Anything that speaks OTLP/HTTP works; each needs only the endpoint and header configuration above. These are the ones we test against recorded fixtures, so an upstream attribute rename is caught by us, not by you:
| Emitter | What to set | Mapped from |
|---|---|---|
| OpenLLMetry / Traceloop | TRACELOOP_BASE_URL to the endpoint, plus the auth header |
gen_ai.* |
| Pydantic AI | Enable instrumentation, point the OTLP exporter at the endpoint | gen_ai.* |
| Vercel AI SDK | experimental_telemetry: { isEnabled: true } plus an OTLP exporter |
ai.* |
| OpenInference (Arize Phoenix, OpenAI Agents SDK) | OTLP exporter to the endpoint | openinference.span.kind, llm.* |
| LangGraph (via OpenLLMetry's LangChain instrumentation) | OTLP exporter to the endpoint | gen_ai.*, node spans as steps |
| Laravel AI SDK (via laravel-ai-otel) | OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_EXPORTER_OTLP_HEADERS |
gen_ai.*, invoke_agent and execute_tool spans as steps |
CrewAI and LlamaIndex are supported through their OpenLLMetry / OpenInference instrumentation by the same path.
Further reading
For the conceptual background, what OpenTelemetry covers for AI agents, what agent spans contain, and which operational questions OTel deliberately leaves to the backend, see OpenTelemetry for AI agents.