Phoenix
Arize Phoenix is an open-source LLM observability platform with a tracing UI built around the OpenInference span convention. It runs as a single container, takes ~5 seconds to start, and pretty-prints JSON previews.
Signals: traces only. Deployment: local (single container) or Arize AX cloud. Cost: OSS (self-host) / commercial (cloud).
Local (Docker)
A ready-to-use compose file ships with the plugin:
cd ~/.hermes/plugins/hermes_otel
docker compose -f docker-compose/phoenix/docker-compose.yaml up -d
Then:
export OTEL_PHOENIX_ENDPOINT="http://localhost:6006/v1/traces"
export OTEL_PROJECT_NAME="hermes-agent"
Open the UI at http://localhost:6006. Traces show up in the hermes-agent project.
Arize AX Cloud
Phoenix also runs as a hosted service. Point at your cloud endpoint and attach the ingest key:
export OTEL_PHOENIX_ENDPOINT="https://app.phoenix.arize.com/v1/traces"
# Headers can be passed via config.yaml:
# ~/.hermes/hermes_otel.yaml
backends:
- type: phoenix
endpoint: https://app.phoenix.arize.com/v1/traces
headers:
api_key: ${PHOENIX_API_KEY}
What you'll see
Phoenix is built around LLM-specific spans, so the UI understands the plugin's span types natively:
agent/cronroot spans appear as top-level traces with the turn summary on them (tool count, skills, final status).llm.*spans show the user message in the Input panel and the assistant response in the Output panel (pretty-printed JSON when conversation capture is on).api.*spans carry the token counts (llm.token_count.prompt,llm.token_count.completion,llm.token_count.total), thefinish_reason, and the HTTP duration.tool.*spans show the arguments (Input) and the result (Output). Errors map toStatusCode.ERRORso the Phoenix error filter works.
Attribute convention
Phoenix uses OpenInference. hermes-otel emits that convention on every span (llm.token_count.*, input.value, output.value) alongside the gen_ai.* convention for other backends.
See Attribute conventions for the full dual-convention table.
Metrics and logs
Phoenix ingests traces only: its server answers 405 on /v1/metrics and /v1/logs (checked against arizephoenix/phoenix:latest). The plugin therefore creates no metrics or logs exporter for a phoenix entry, and the startup banner reads ✓ Phoenix at … (traces only). Token, tool and cost metrics need a second backend that takes OTLP metrics (SigNoz, LGTM, OpenObserve) via multi-backend fan-out; Phoenix computes token totals and cost from the span attributes itself. If you front Phoenix with a collector that does accept metrics, set metrics: true on the entry explicitly.
Dashboard
The bundled dashboard's Phoenix adapter shows the project named by project_name (on the backend entry, else the top-level key). If that project does not exist on the server the dashboard reports Phoenix project '…' not found; available: … instead of quietly showing another project's traces. With no project_name configured it shows the first project that has traces and says which one in its status (project_resolved, project_fallback: true).
Phoenix 20 removed the rootSpansOnly / orphanSpanAsRootSpan arguments of Project.spans; the adapter introspects the schema once per query URL and sends parent_id is None in the filter condition instead, so older and current builds both work (verified on 20.20.0, 2026-10-09).
Troubleshooting
"No traces show up in Phoenix"
- Check the endpoint includes
/v1/traces— Phoenix doesn't redirect from the collector root. - Confirm the container is listening:
curl -I http://localhost:6006should return200. - Turn on debug logging:
export HERMES_OTEL_DEBUG=true, run a Hermes turn, check~/.hermes/plugins/hermes_otel/debug.logfor theexport Phoenix: … -> SUCCESS|FAILUREline and, on failure, the[sdk] … Failed to export span batch code: <status>line with the reason.
"Spans are missing input/output previews"
- If
capture_previews: falseorHERMES_OTEL_CAPTURE_PREVIEWS=falseis set, previews are intentionally suppressed. Remove the setting, restart Hermes.
"Tokens show as zero"
- Phoenix keys off
llm.token_count.*— the plugin emits these onapi.*spans (notllm.*). Check the childapi.*span, not the parent.