# Observability
OpenResponses emits structured telemetry events at every significant point in the request lifecycle. These events power Prometheus metrics via the built-in PromEx plugin and can be consumed by any `:telemetry` handler.
## Telemetry events
All events are emitted under the `[:open_responses, ...]` namespace.
### Request events
| Event | Metadata |
|---|---|
| `[:open_responses, :request, :start]` | `%{model: model}` |
| `[:open_responses, :request, :stop]` | `%{model: model, status: status}` |
### Loop events
| Event | Metadata |
|---|---|
| `[:open_responses, :loop, :iteration, :start]` | `%{model: model, iteration: n}` |
| `[:open_responses, :loop, :iteration, :stop]` | `%{model: model, iteration: n, action: action}` |
### Adapter events
| Event | Metadata |
|---|---|
| `[:open_responses, :adapter, :stream, :start]` | `%{model: model, iteration: n}` |
| `[:open_responses, :adapter, :stream, :stop]` | `%{model: model}` |
| `[:open_responses, :adapter, :stream, :chunk]` | `%{type: event_type}` |
### Tool events
| Event | Metadata |
|---|---|
| `[:open_responses, :tool, :dispatch, :start]` | `%{tool: name}` |
| `[:open_responses, :tool, :dispatch, :stop]` | `%{tool: name, result: :ok | :error}` |
### Stream events
| Event | Metadata |
|---|---|
| `[:open_responses, :stream, :event]` | `%{type: event_type, response_id: id}` |
## Prometheus metrics
OpenResponses ships a PromEx plugin that wires the above telemetry to Prometheus metrics.
### Setup
Add `OpenResponses.PromEx` to your supervision tree:
```elixir
# application.ex
children = [
OpenResponses.PromEx,
# ... other children
]
```
Mount the metrics endpoint in your router:
```elixir
# router.ex
get "/metrics", PromEx.Plug, prom_ex_module: OpenResponses.PromEx
```
Add to `config/config.exs`:
```elixir
config :open_responses, OpenResponses.PromEx,
manual_metrics_start: :no_async,
drop_metrics_groups: [],
grafana: :disabled,
metrics_server: :disabled
```
### Available metrics
| Metric | Type | Description |
|---|---|---|
| `open_responses_loop_iterations_total` | Counter | Total agentic loop iterations, tagged by `model` |
| `open_responses_tool_dispatches_total` | Counter | Total internal tool calls, tagged by `tool` |
| `open_responses_stream_events_total` | Counter | Total SSE events emitted, tagged by `type` |
| `open_responses_requests_total` | Counter | Total requests received, tagged by `model` |
### Scraping
Metrics are available at `GET /metrics` in Prometheus text format. Configure your Prometheus instance:
```yaml
scrape_configs:
- job_name: open_responses
static_configs:
- targets: ['your-host:4000']
metrics_path: /metrics
```
## Custom telemetry handlers
Attach your own handler to any event:
```elixir
:telemetry.attach(
"my-loop-logger",
[:open_responses, :loop, :iteration, :start],
fn _event, _measurements, %{model: model, iteration: n}, _config ->
Logger.info("Loop iteration #{n} started", model: model)
end,
nil
)
```
Or attach to multiple events at once:
```elixir
:telemetry.attach_many(
"my-handler",
[
[:open_responses, :request, :start],
[:open_responses, :request, :stop]
],
&MyApp.Telemetry.handle/4,
%{}
)
```
## Logger metadata
Each loop process sets Logger metadata for its lifetime:
```elixir
%{
response_id: "resp_01",
model: "gpt-4o",
loop_iteration: 2
}
```
All log lines emitted during a loop carry this context automatically, making it straightforward to trace a single request through your logs.
## LiveDashboard
If you include `phoenix_live_dashboard` (enabled by default in Phoenix apps), you can view telemetry metrics in the LiveDashboard at `/dev/dashboard`. The `OpenResponsesWeb.Telemetry` module registers the relevant metrics for the dashboard.