Laravel AI SDK with OpenTelemetry

There are two ways to get laravel/ai agents into AgentPing. The Laravel SDK listens to the same events inside your app and reports them directly; it needs no OpenTelemetry and is the shorter path. This page covers the other route: exporting OpenTelemetry spans with cristea-iulian/laravel-ai-otel and pointing them at the AgentPing OTLP endpoint. Pick it when OpenTelemetry is already how your app reports, or when you want the same spans in a second backend.

Install

composer require cristea-iulian/laravel-ai-otel

If the app does not run OpenTelemetry yet, add the SDK and the OTLP exporter too. The package bootstraps a tracer provider with a batch processor and flushes it after every request, every queue job, and at shutdown, Octane included:

composer require open-telemetry/sdk open-telemetry/exporter-otlp

Then point the exporter at AgentPing. The package appends /v1/traces to the endpoint when it is missing, and the team API key goes in the headers as a Bearer token:

OTEL_EXPORTER_OTLP_ENDPOINT=https://eu.ingest.agentping.io
OTEL_EXPORTER_OTLP_HEADERS="authorization=Bearer apk_eu_..."
OTEL_SERVICE_NAME=my-app

Use us.ingest.agentping.io for a apk_us_ key. Per-agent ping_ tokens do not work on the OTLP endpoint; traces can create agents, so they need the team key.

If open-telemetry/opentelemetry-auto-laravel or another SDK autoloader already registers a global tracer provider, the package records through it and there is nothing more to configure. Set the two exporter variables above on that provider instead.

Requires PHP 8.3+, Laravel 12 or 13, and laravel/ai 0.11.x. The package pins the SDK minor because the SDK is pre-1.0.

What arrives in AgentPing

laravel-ai-otel emits the current GenAI semantic conventions and AgentPing reads them as they are. One trace becomes one run.

Span AgentPing event What is read
invoke_agent {Agent} step gen_ai.agent.name names the agent. Its usage attributes are the sum of the steps inside it, so this span is never priced.
chat {model} llm_call Provider, model, input and output tokens, cache read, cache creation and reasoning tokens. Priced from the rate card.
execute_tool {tool} tool_call Tool name and gen_ai.tool.call.id. Arguments and result when content capture is on.
embeddings {model} llm_call Provider, model and input tokens.

Attributes AgentPing does not map, such as gen_ai.response.finish_reasons, gen_ai.request.stream, laravel.ai.step.number and the configured provider name in laravel.ai.provider.name, stay on the step as attributes, so they are still there when you open it.

Errors carry through. A span with error status becomes a step with status: error and the exception class, and any errored step marks the run as errored.

Sub-agents fit naturally. An agent used as a tool produces its own invoke_agent span under the parent's execute_tool span, so the run timeline shows the tool call, the inner agent's step, and its model calls in order, all inside one run.

Failover is visible but reads as an error. The failed attempt's chat span keeps its error status, so the run shows an errored llm_call for the first provider followed by a successful one for the provider that answered, and the run itself is flagged as errored even though the prompt succeeded. The laravel.ai.failover span event that explains the switch is not read; span events are not part of the mapping. The Laravel SDK records failover as a step with the reason and leaves the run's status alone, which is the main reason to prefer it if failover is routine for you.

Agent identity and goal

The agent is the Agent class name from gen_ai.agent.name, so a SupportAgent class shows up as supportagent. When the resource carries agentping.agent that wins, which is how to give several classes one dashboard identity or match the slug the Laravel SDK would use. With the package's own tracer provider the OpenTelemetry SDK reads resource attributes from the environment:

OTEL_RESOURCE_ATTRIBUTES=agentping.agent=support_agent

The run's goal is the root span's name. Without other instrumentation that is invoke_agent SupportAgent; under opentelemetry-auto-laravel it is the HTTP route or queue job that triggered the agent, which is usually the more useful of the two. Set agentping.goal on the root span from your own code when neither says what the run was for.

Content capture

By default no prompt, instruction, model output, tool argument or tool result leaves the app; the package follows the GenAI conventions, which make content opt-in. AgentPing has the same posture, so what you see in the run timeline is metadata: models, providers, tokens, latency, tool names and finish reasons.

To see messages and tool payloads on the timeline, turn capture on in the package:

OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true
AI_OTEL_CAPTURE_MAX_LENGTH=8192

AgentPing then stores gen_ai.input.messages, gen_ai.output.messages and gen_ai.system_instructions as step content on the llm_call, and gen_ai.tool.call.arguments and gen_ai.tool.call.result as the tool_call input and output, within the usual 64KB step allowance. The package's redactor hook runs before anything is exported, so scrubbing happens on your side of the wire.

Run completion and queues

Spans arrive in batches with no end-of-run signal, so a run ingested over OTLP completes once its root span has landed and no new span has arrived for two minutes. Costs, checks and evaluations then run as they do for SDK-reported runs. Expect a run to appear a couple of minutes after the agent finishes rather than immediately.

Each queued agent run is its own trace, and so its own run, unless opentelemetry-auto-laravel is propagating trace context through the queue, in which case the job's spans join the request that dispatched it. Either way laravel.ai.invocation.id is on every span for correlation.

Choosing between this and the Laravel SDK

Laravel SDK OpenTelemetry
Dependencies agentping/laravel only laravel-ai-otel plus the OpenTelemetry SDK and exporter
Run grouping AgentPing::run() wraps several invocations into one run One trace per run; use opentelemetry-auto-laravel to group under a request
Run appears On flush, within seconds About two minutes after the last span
Provider failover A failover step with the reason; run status unaffected Errored llm_call followed by the successful one; run flagged as errored
Content on the timeline Tool arguments and results by default, never prompts Nothing by default; prompts, outputs and tool payloads when capture is on
Heartbeats, spend guard, manual events Yes No; OTLP is trace-only
Other backends AgentPing only Any OTLP backend from the same spans

Both price model calls server-side from the same rate card and produce the same run, step and cost records, so switching later does not change what the dashboard shows.

Further reading