---
title: Laravel AI SDK with OpenTelemetry
description: Send laravel/ai agent traces to AgentPing over OTLP with the laravel-ai-otel package. Per-step tokens, tool calls, failover and sub-agents, priced server-side.
section: integrations
order: 2
---

# Laravel AI SDK with OpenTelemetry

There are two ways to get [`laravel/ai`](https://github.com/laravel/ai)
agents into AgentPing. The [Laravel SDK](/docs/sdks/laravel) 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`](https://github.com/cristea-iulian/laravel-ai-otel)
and pointing them at the AgentPing [OTLP endpoint](/docs/integrations/opentelemetry).
Pick it when OpenTelemetry is already how your app reports, or when you
want the same spans in a second backend.

## Install

```bash
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:

```bash
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:

```env
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:

```env
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:

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

- [OpenTelemetry](/docs/integrations/opentelemetry) for the endpoint,
  authentication and attribute conventions AgentPing accepts.
- [Laravel SDK](/docs/sdks/laravel) for the direct route.
- [OpenTelemetry for AI agents](/blog/opentelemetry-for-ai-agents) for
  what agent spans contain and what OTel leaves to the backend.
