> ## Documentation Index
> Fetch the complete documentation index at: https://docs.bronto.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Send Pydantic AI traces to Bronto via OpenTelemetry

> Send Pydantic AI agent runs, model calls, tool calls, token usage and cost to Bronto with OpenTelemetry, with or without the Logfire SDK.

Pydantic AI emits OpenTelemetry spans and metrics for every agent run, model request and tool call. You can send them to Bronto with the plain OpenTelemetry SDK, or with the Logfire SDK pointed at Bronto. You do not need a Logfire account for either.

Already sending to [Logfire](https://pydantic.dev/logfire)? The Logfire SDK can send the same data to Logfire and Bronto at once, so you can keep your Logfire project while you try Bronto.

> [!NOTE]
>
> This page was verified on **October 28, 2026** with `pydantic-ai-slim` 2.63.0, `logfire` 5.4.0, `anthropic` 2.3.0 and the OpenTelemetry Python SDK 1.45.1. GenAI libraries and semantic conventions are evolving rapidly, so package configuration and emitted attributes can change between releases. Keep your instrumentation current and verify the fields emitted by the version you deploy.

## Prerequisites

* Python 3.11+ and Pydantic AI 2.x
* An LLM provider API key, such as Anthropic or OpenAI
* An OTel Collector configured using [Connect OpenTelemetry to Bronto](https://docs.bronto.io/agent-setup/open-telemetry.md), or a direct OTLP exporter (below)

## Instrument Pydantic AI

Choose one of the two options. Both produce the same Pydantic AI spans and metrics.

> [!TIP]
>
> In Pydantic AI 2.x, the `Agent(..., instrument=True)` constructor argument no longer exists. Use `Agent.instrument_all()` or `logfire.instrument_pydantic_ai()` as shown below.

### Option 1: OpenTelemetry SDK

```bash theme={"dark"}
pip install "pydantic-ai-slim[anthropic]" opentelemetry-sdk opentelemetry-exporter-otlp-proto-http
```

Register a tracer provider and a meter provider, then turn on instrumentation for all agents:

```python theme={"dark"}
from opentelemetry import metrics, trace
from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from pydantic_ai import Agent

tracer_provider = TracerProvider()
tracer_provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))
trace.set_tracer_provider(tracer_provider)
metrics.set_meter_provider(MeterProvider(metric_readers=[PeriodicExportingMetricReader(OTLPMetricExporter())]))

Agent.instrument_all()

agent = Agent("anthropic:claude-haiku-5-5", name="booking_agent")
result = agent.run_sync("Book Bella for Monday morning")

tracer_provider.force_flush()  # flush spans before a short-lived process exits
```

Pass `InstrumentationSettings(...)` to `Agent.instrument_all()` to change what is recorded, for example `include_content=False` (see [Prompts and responses](#prompts-and-responses)).

### Option 2: Logfire SDK

The Logfire SDK is an OpenTelemetry distribution. With `send_to_logfire=False` it sends only to the OTLP endpoint in your environment:

```bash theme={"dark"}
pip install "pydantic-ai-slim[anthropic]" logfire
```

```python theme={"dark"}
import logfire
from opentelemetry.sdk.metrics.view import DropAggregation, View
from pydantic_ai import Agent

logfire.configure(
    send_to_logfire=False,
    service_name="booking-service",
    # Replace Logfire's default views, which turn histograms into exponential histograms
    metrics=logfire.MetricsOptions(views=[View(instrument_name="otel.sdk.*", aggregation=DropAggregation())]),
)
logfire.instrument_pydantic_ai()

agent = Agent("anthropic:claude-haiku-5-5", name="booking_agent")
result = agent.run_sync("Book Bella for Monday morning")
```

By default, Logfire aggregates every histogram as an exponential histogram. Bronto accepts explicit-bucket histograms, so without the `metrics=` option above the token and cost metrics don't arrive. Traces are not affected.

To send to Logfire **and** Bronto, leave out `send_to_logfire=False` and keep the `OTEL_EXPORTER_OTLP_*` variables set. The Logfire SDK then exports to both. It exports over OTLP/HTTP with protobuf only.

## Send traces and metrics to Bronto

**Via a Collector**: point the application's OTLP exporter at the Collector, then configure the Collector using [Connect OpenTelemetry to Bronto](https://docs.bronto.io/agent-setup/open-telemetry.md). This is recommended in production.

**Direct export**: point the exporter at the Bronto ingestion endpoint and add your API key. Both options above read these variables:

```bash theme={"dark"}
export OTEL_EXPORTER_OTLP_ENDPOINT="https://ingestion.eu.bronto.io"
export OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf"
export OTEL_EXPORTER_OTLP_HEADERS="x-bronto-api-key=<YOUR_API_KEY>"
export OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE="delta"
export OTEL_SERVICE_NAME="booking-service"
```

| Region | Endpoint |
| - | - |
| EU | `https://ingestion.eu.bronto.io` |
| US | `https://ingestion.us.bronto.io` |

The SDK appends `/v1/traces` and `/v1/metrics`. Set the temporality to `delta`: with the Python SDK's default cumulative temporality, every export repeats the running total, and summing the metric in Bronto counts the same tokens and cost more than once. See [API Keys](https://docs.bronto.io/Account-Management/API-Keys.md) for how to create a key with ingestion permissions.

## What you get

### Spans

One agent run produces this trace:

```text theme={"dark"}
invoke_agent booking_agent              Pydantic AI: the run, run-level token totals
├─ chat claude-haiku-5-5                Pydantic AI: one model request, tokens and cost
│  └─ anthropic.messages.create         Anthropic SDK: the HTTP call (see below)
├─ execute_tool find_slots              Pydantic AI: one tool call
└─ chat claude-haiku-5-5
   └─ anthropic.messages.create
```

Open [Tracing](https://docs.bronto.io/tracing/explore-traces.md) and select the `invoke_agent <agent name>` operation to see each run as a waterfall, with the provider call marked as a downstream call. Select a span to see its attributes, including prompts and responses as JSON.

Fields on the Pydantic AI spans:

| Field | Span | Notes |
| - | - | - |
| `gen_ai.agent.name`, `gen_ai.agent.call.id`, `gen_ai.conversation.id` | all | `gen_ai.agent.call.id` identifies one run |
| `gen_ai.request.model`, `gen_ai.provider.name` | `chat` | |
| `gen_ai.usage.input_tokens`, `gen_ai.usage.output_tokens` | `chat` | Tokens of one model request |
| `gen_ai.aggregated_usage.input_tokens`, `gen_ai.aggregated_usage.output_tokens` | `invoke_agent` | Tokens of the whole run. Pydantic AI uses its own name here so they aren't added to the request totals |
| `operation.cost` | `chat` | Estimated cost of the request in USD, from Pydantic AI's price data. Not part of the OpenTelemetry conventions |
| `gen_ai.tool.name`, `gen_ai.tool.call.id` | `execute_tool` | |
| `pydantic_ai.tool.failure_stage`, `pydantic_ai.tool.retry.cause`, `pydantic_ai.tool.retry.attempt` | `execute_tool` | Set when the model's tool arguments failed validation and the model was asked to retry |
| `gen_ai.input.messages`, `gen_ai.output.messages` | `chat` | Prompts and responses as JSON, when content capture is on |

### Metrics

Pydantic AI records two histograms, which appear in [Metrics Explorer](https://docs.bronto.io/metrics/explore-metrics.md):

| Metric | Unit | What it measures |
| - | - | - |
| `gen_ai.client.token.usage` | tokens | Tokens per model request, by `gen_ai.token.type` (`input`, `output`) |
| `operation.cost` | USD | Cost per model request |

Both carry `gen_ai.operation.name`, `gen_ai.provider.name`, `gen_ai.request.model` and `gen_ai.response.model`, plus your resource attributes, which Metrics Explorer lists as `Resource:service.name` and so on. They don't carry the agent name.

### Provider SDK spans

The Anthropic Python SDK (2.x) creates its own `anthropic.messages.create` span for every API call whenever an OpenTelemetry tracer provider is registered. It sits under Pydantic AI's `chat` span and adds fields Pydantic AI doesn't record:

* `http.response.status_code`: the provider's HTTP status, for example `429` or `529`
* `anthropic.request.id`: the ID to quote to Anthropic support
* `gen_ai.response.time_to_first_chunk`: time to the first streamed token, in seconds

It also repeats the request's token counts on the span; it records no metrics. To turn these spans off, set `ANTHROPIC_OPEN_TELEMETRY=false`. With the Claude Platform's own telemetry enabled, the SDK's trace context also joins Anthropic's server-side spans to the same trace; see [Claude Platform](https://docs.bronto.io/ai-features/claude-platform.md).

> [!TIP]
>
> Do not add a second provider instrumentation, such as `opentelemetry-instrumentation-openai-v2`, for a client that Pydantic AI already instruments, unless you want the extra HTTP-level span. Each extra layer repeats the token counts on its own span.

## Track tokens and cost

Use the metrics for totals. In [Metrics Explorer](https://docs.bronto.io/metrics/explore-metrics.md), select the metric, set **Calculate** to **Sum**, and group by a field:

| Analysis | Metric | Calculate | Group by |
| - | - | - | - |
| Input and output tokens | `gen_ai.client.token.usage` | **Sum** | `gen_ai.token.type` |
| Tokens by model | `gen_ai.client.token.usage` | **Sum** | `gen_ai.request.model` |
| Cost by model | `operation.cost` | **Sum** | `gen_ai.request.model` |
| Cost by service | `operation.cost` | **Sum** | `Resource:service.name` |
| Most expensive requests | `operation.cost` | **P95** or **P99** | `gen_ai.request.model` |

With the instrumentation shown here, the metrics count each model request once. Additional provider instrumentation, such as `opentelemetry-instrumentation-openai-v2`, can also record `gen_ai.client.token.usage`; disable the overlapping instrumentation or filter out its token metric stream before calculating totals. On spans, the same tokens can appear three times for one request: on `invoke_agent` as the run total, on `chat`, and on the provider SDK span. For one run's tokens, read `gen_ai.aggregated_usage.*` on its `invoke_agent` span; its cost is the sum of `operation.cost` on its `chat` spans.

The metrics don't carry the agent name. To split cost by agent, run each agent in its own service, or set a resource attribute per process with `OTEL_RESOURCE_ATTRIBUTES` and group by it.

## Find failing runs

A failed run ends with an error status on its `invoke_agent` span. In [Tracing](https://docs.bronto.io/tracing/explore-traces.md), the **Errors** column of the `invoke_agent <agent name>` operation shows how many runs failed. Select the operation and the **Error** status filter to list the failed traces, then open one: the root span's status message names the exception.

| Status message on `invoke_agent` | What happened |
| - | - |
| `UnexpectedModelBehavior: Exceeded maximum output retries (n)` | The model's output kept failing validation |
| `UsageLimitExceeded: The next request would exceed the request_limit of n…` | The run hit a `UsageLimits` limit (requests, tool calls, tokens or cost) |
| `ModelHTTPError: status_code: 429, …` | The provider returned an HTTP error; the status is also in `http.response.status_code` on the provider SDK span |

A tool call with invalid arguments doesn't fail the run: the `execute_tool` span gets an error status and `pydantic_ai.tool.failure_stage` = `validation`, and the model is asked to retry. Output validation retries don't get a span of their own. The run shows two or more `chat` spans followed by the error; with content capture on, the rejected output is in `gen_ai.output.messages` of the earlier `chat` span.

To stop a runaway agent before it spends more, set limits on the run, for example `UsageLimits(request_limit=20, cost_limit=Decimal("0.50"))`. A run that hits a limit fails with `UsageLimitExceeded`.

## Alert on cost and failures

[Monitors](https://docs.bronto.io/core-features/monitors.md) evaluate log datasets and metrics, not spans. Create a metric monitor on `operation.cost` with **Sum** over a time window, for example more than \$5 in an hour, to be alerted when model spend climbs. A monitor on `gen_ai.client.token.usage` works the same way for tokens.

Pydantic AI's metrics don't record failures: a request that fails emits no token or cost data. To alert on failed runs, export the failures as logs or metrics first, for example by logging the exception your application catches around `agent.run()` through the [OpenTelemetry Python SDK](https://docs.bronto.io/opentelemetry/python.md), then create a dataset monitor on those logs.

## Prompts and responses

Pydantic AI records prompts, responses, tool arguments and tool results by default. To keep the message structure, roles and tool names but drop the text, pass `include_content=False`:

```python theme={"dark"}
from pydantic_ai.models.instrumented import InstrumentationSettings

Agent.instrument_all(InstrumentationSettings(include_content=False))
```

With the Logfire SDK, pass `include_content=False` to `logfire.instrument_pydantic_ai()`. The Anthropic SDK spans contain metadata only unless you set `ANTHROPIC_OPEN_TELEMETRY_TRACES_CONTENT_MODE=content`.

## Verify in Bronto

Open [Tracing](https://docs.bronto.io/tracing/explore-traces.md) and filter by the service you configured in `OTEL_SERVICE_NAME`. Each agent run appears as an `invoke_agent <agent name>` operation. Then open [Metrics Explorer](https://docs.bronto.io/metrics/explore-metrics.md) and select `operation.cost` or `gen_ai.client.token.usage`.

If no traces appear, check that instrumentation is set up before the first agent run, verify the OTLP endpoint and API-key header, and make sure short-lived processes flush their span processor before exit. If traces appear but metrics don't, check the temporality variable and, with the Logfire SDK, the `metrics=` option above.

## References

* [LLM Observability](https://docs.bronto.io/ai-features/llm-observability.md)
* [Python: GenAI semantic conventions](https://docs.bronto.io/opentelemetry/python.md#genai-semantic-conventions)
* [Explore Traces](https://docs.bronto.io/tracing/explore-traces.md)
* [Claude Platform](https://docs.bronto.io/ai-features/claude-platform.md)
* [Pydantic AI: Debugging and monitoring](https://pydantic.dev/docs/ai/integrations/logfire/)
* [Logfire: Alternative backends](https://logfire.pydantic.dev/docs/how-to-guides/alternative-backends/)
* [Pydantic AI: Usage limits](https://pydantic.dev/docs/ai/core-concepts/agent/#usage-limits)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.