# Logfire and other OTLP backends

> Export a Pyrula runtime's OpenTelemetry spans to Logfire or any backend that takes OTLP over HTTP, next to the Langfuse, LangSmith and Datadog shortcuts.

Source: https://docs.pyrula.io/getting-started/recipes/otlp-backends/

A Pyrula runtime emits OpenTelemetry spans for every turn, model call, tool call and durable
step ([Observability](https://docs.pyrula.io/agents/observability.md)). `configure()` sends them to any backend that
accepts OTLP over HTTP. `configure_langfuse`, `configure_langsmith` and `configure_datadog` are
shortcuts that fill in the endpoint and authentication for those three; for any other backend,
Logfire included, you give `configure()` the endpoint and headers yourself.

There is no Logfire shortcut. Take the OTLP endpoint and the write token from Logfire's own
documentation; this page does not carry them, and none of its snippets has been sent to Logfire.

Install the extra: `pip install "pyrula[agents,otel]"`. Call `configure()` once, before the worker
starts.

## An OTLP backend by endpoint and header

```python
from pyrula.agents.observability import configure

configure(
    service_name="invoice-agent",
    endpoint="https://otlp.example.com",     # the OTLP/HTTP base URL; /v1/traces is appended
    headers={"Authorization": "write-token-from-your-backend"},
    metrics=False,                            # leave True if the backend also takes metrics
    capture_content=False,                    # True records prompts and completions on spans
)
```

The same through the standard variables, with no arguments in code:

```bash
export OTEL_EXPORTER_OTLP_ENDPOINT=https://otlp.example.com
export OTEL_EXPORTER_OTLP_HEADERS="authorization=write-token-from-your-backend"
```

- **OTLP over HTTP only.** The exporter is the `http/protobuf` one. A backend that offers only
  gRPC cannot be reached this way; put an OpenTelemetry Collector between them.
- **Content is off unless you turn it on.** Prompts, completions and tool arguments reach the
  backend only with `capture_content=True` or
  `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT`. What is captured is secret-redacted and
  cut at 32 KB first ([Prompt and completion content](https://docs.pyrula.io/agents/observability.md#prompt-and-completion-content)).
- **Metrics** are on by default (`gen_ai.client.token.usage`, `gen_ai.client.operation.duration`).
  Pass `metrics=False` for a backend that ingests traces only.

## When the app already sets up OpenTelemetry

If your application registers a tracer provider before Pyrula starts, do not call `configure()`:
Pyrula's spans go through the global provider and arrive wherever it exports
([Setup](https://docs.pyrula.io/agents/observability.md#setup)). Logfire's SDK is built on OpenTelemetry; if
`logfire.configure()` registers the global provider in your version (confirm in its documentation),
an app that uses Logfire makes that call and none from Pyrula.

```python
import logfire

logfire.configure()  # Logfire's own setup; Pyrula's spans use the provider it registers
# no pyrula.agents.observability.configure() call
```

Which of the two you want depends on who owns the provider. A Pyrula worker started with
`configure()` owns it; an app that already has one keeps it.

## Next to the shortcuts

| Backend | Call |
|---|---|
| Langfuse | `configure_langfuse()` reads `LANGFUSE_PUBLIC_KEY`, `LANGFUSE_SECRET_KEY`, `LANGFUSE_HOST`; content on, metrics off |
| LangSmith | `configure_langsmith()` reads `LANGSMITH_API_KEY` and `LANGSMITH_PROJECT` |
| Datadog | `configure_datadog()` exports to the local Agent's OTLP intake |
| Logfire or any other OTLP/HTTP backend | `configure(endpoint=..., headers=...)` or the OTEL variables |
| Your own spans on the console | `configure(console=True)` |

## Not Pyrula Cloud

Pointing this exporter at Pyrula Cloud's intake is possible and is usually wrong for a Pyrula
runtime: the runtime already reports its runs to Cloud through its connector, and importing the
same work as traces would count each run twice. Traces carrying a Pyrula run id are skipped for
that reason. The Cloud intake is for apps that have OpenTelemetry and no Pyrula runtime; see
[Observe-only frameworks](https://docs.pyrula.io/getting-started/recipes/observe-only.md).
