Both products consume the same OpenInference telemetry, so your instrumentors, manual spans, and semantic conventions stay exactly as they are. The only thing that changes is how you register the tracer, meaning where spans get exported and how the request authenticates.
What Stays the Same
Begin Sending Traces to AX
1
Install the AX SDK
Keep
arize-phoenix-otel installed while you work through this. You need it for the dual-write step, and uninstalling it is the last thing you do rather than the first.2
Replace register() with the AX equivalent
Credentials are the one real difference between the two. Phoenix does not require any by default, and only takes an API key when you are on Phoenix Cloud or have turned on auth in a self-hosted deployment. AX always authenticates, using an API key together with a space ID. Both are on your project’s setup page in the AX UI.
- Python
- JS/TS
Before, against a local Phoenix with no auth:On Phoenix Cloud, or a self-hosted instance with auth enabled, you also pass Everything below the
api_key=... or set PHOENIX_API_KEY, which Phoenix turns into an authorization: Bearer header.After, against AX:register() call stays as it is.3
Map the remaining arguments and environment variables
Most arguments have a direct counterpart. The two defaults that differ are called out in the rows below.
Environment variables follow the same pattern.
If your space is not in US East, set the endpoint to match. Use
Endpoint.ARIZE_EUROPE for the EU, or the string https://otlp.ca-central-1a.arize.com/v1 for Canada. It is the same region as the subdomain you log in to.Dual-Write While You Verify
A hard cutover is fine for a side project, but for production traffic you want a window where both backends see the same spans. Register AX once and attach a second exporter for Phoenix to that same tracer provider. Your instrumentation runs a single time, so the two backends receive identical spans and any difference you find is a configuration problem rather than a sampling artifact.- Trace counts should match over the same time window.
- Span trees should have the same shape. Pick a few traces by hand and check nesting depth and span kinds.
- Attributes should be equally complete, particularly inputs, outputs, token counts, and any custom metadata or tags you set.
- Sessions and users should still group correctly if you rely on
session.id, since that grouping is the easiest thing to lose in a rushed cutover.
DUAL_WRITE_PHOENIX=0 and deploy. Keeping the toggle in an environment variable means a rollback is a config change instead of a code change and a rebuild.
Retire the Phoenix Path
Once AX has been the only destination through a full traffic cycle, removearize-phoenix-otel from your dependencies, drop the PHOENIX_* variables from your deployment config, and delete the dual-write block. If you still intend to backfill, do that before you decommission Phoenix, because the migration tool reads from a running instance.
Evals and annotations need their own pass. They are separate write paths from tracing, so code that logged them to Phoenix keeps doing exactly that after you change the tracer. See Run evals on traces and Human review for the AX equivalents.
Troubleshooting
No spans arrive in AX
No spans arrive in AX
Check credentials first, since they are the usual cause. Header naming depends on transport: the HTTP endpoint expects the hyphenated
arize-space-id and arize-api-key, while gRPC expects the unprefixed space_id and api_key as metadata. Using the wrong form fails silently and you get no spans at all. See Manual instrumentation for the full breakdown. If you are using register(), leave verbose=True on and read the configuration it prints at startup.Spans land in a project called default
Spans land in a project called default
The project name never made it onto the resource attribute. In Python, pass
project_name to register() or set ARIZE_PROJECT_NAME. In JS/TS you have to set SEMRESATTRS_PROJECT_NAME on the resource yourself, since there is no argument for it to inherit.Only one of the two backends is receiving spans
Only one of the two backends is receiving spans
Confirm that you registered once and attached a processor, rather than calling both products’
register() functions. If that looks right, check that you are comparing the same time window in both UIs and give AX a minute to index recent spans.auto_instrument=True no longer works
auto_instrument=True no longer works
arize.otel.register() has no auto_instrument argument in the SDK versions most applications run, so attach each instrumentor explicitly with .instrument(tracer_provider=tracer_provider). The extra lines are worth it, because being explicit forces you to instrument the framework or model client that actually makes the LLM calls rather than only the web layer around it.Short scripts exit before spans flush
Short scripts exit before spans flush
AX batches spans by default, while Phoenix uses a simple processor that exports synchronously, so a script that exits immediately can lose its last spans. Pass
batch=False, or call tracer_provider.shutdown() before the process exits.Next step
Backfill your Phoenix history
Export your traces, evals, annotations, datasets, and experiments out of Phoenix and import them into AX.
Configure your tracer
The full parameter reference, including transports, regional endpoints, and raw OTel setup.