Migrate from Arize Phoenix to Rius
Phoenix is built on OpenTelemetry and OpenInference, and Rius understands OpenInference spans. Migrating from Phoenix is therefore a routing change: the OpenInference instrumentors you already run keep producing the same spans, and you point their exporter at Rius instead of at your Phoenix collector. This guide maps the concepts, walks the two migration paths, and ends with a cutover checklist.
TL;DR: Keep your OpenInference instrumentors. Re-point register() or
your OTLP exporter at Rius (Path A), or replace register() with
rius.init(), which wires the same instrumentors for you (Path B). Run both
backends in parallel, then cut over.
Why migrate from Phoenix to Rius
Rius is agent observability. The same OpenInference spans you send Phoenix today get session and user grouping, server-side cost, alerts, and an MCP server so your coding agent can query production traces. With the Rius SDK (Path B) you also see which agent processes are alive, and, with partial spans turned on, runs still in progress. Rius is a managed service, so there is no Phoenix server or database to run. What Phoenix offers beyond tracing is listed under what does not carry over.
Concept mapping: Phoenix vs Rius
| Phoenix | Rius | Notes |
|---|---|---|
Project (project_name, openinference.project.name) | Agent (named by service.name) | Rius groups traces by agent name, which defaults to the service.name resource attribute, within the workspace your API key belongs to. Phoenix’s project attribute is stored but does not drive grouping. If you use separate projects per environment, give each environment (production, staging) its own workspace and API key instead. |
| Trace | Trace | Same OpenTelemetry trace, same tree view. |
Span kind (openinference.span.kind) | Span kind | AGENT, LLM, TOOL, CHAIN, RETRIEVER, EMBEDDING and RERANKER are read as-is. See the attribute reference. |
LLM span (llm.model_name, llm.token_count.*) | Generation | Read as a generation, so the span gets model, token and cost analytics. |
Session (session.id, using_session) | Session | Same session.id attribute. Set it on every span of the trace, the root included; see the note under Path A. |
User (user.id, using_user) | User | Same user.id attribute. See users. |
Metadata and tags (using_metadata, using_tags) | Span attributes | Stored as span attributes, filterable by key. No dedicated views. See the attribute reference. |
| Cost (Phoenix’s model pricing table) | Cost | Computed server-side from model and token counts at the providers’ public list prices. You send tokens, never cost. |
| Annotations and feedback | Not available | See what does not carry over. |
| Evals, datasets, experiments | Not available | See what does not carry over. |
| Prompts and prompt playground | Not available | See what does not carry over. |
Choose your path
- You want the smallest possible diff: keep
phoenix.otel.register()or your ownTracerProvider, and re-point the exporter. This is also how you run both backends side by side. - You want the Rius defaults (raised span attribute limit, privacy
controls, sessions and users as SDK scopes) or the live views (which agent
processes are alive, and, with partial spans turned on, runs still in
progress):
replace
register()withrius.init(). Your OpenInference instrumentors stay, because the Rius SDK ships the same ones.
Path A: keep your instrumentation (re-point the exporter)
Phoenix’s collector and Rius both take OTLP over HTTP with a Bearer token, so the change is the endpoint, the key, and a service name. The Rius endpoint takes OTLP/HTTP (protobuf or JSON), not gRPC.
If you use phoenix.otel.register()
register() from arize-phoenix-otel can export to Rius. Pass the full
traces URL, the protocol, and your Rius API key; register() sends the key
as authorization: Bearer <key>:
# Before: pointing at Phoenix
from phoenix.otel import register
tracer_provider = register(
project_name="my-agent",
auto_instrument=True,
)# After: pointing at Rius
import os
from opentelemetry.sdk.resources import Resource
from phoenix.otel import register
tracer_provider = register(
endpoint="https://ingest.eu.console.rius-glassflow.com/v1/traces",
protocol="http/protobuf",
api_key=os.environ["RIUS_API_KEY"],
batch=True,
resource=Resource.create({"service.name": "my-agent"}),
auto_instrument=True,
)Three details make the difference between this working and silently not:
- Pass
endpointandprotocol="http/protobuf"in code. Setting onlyPHOENIX_COLLECTOR_ENDPOINTto the Rius URL sends spans to the wrong port. - Set
service.name.register()stampsopeninference.project.name, which Rius stores but does not group by. Without aservice.name, your traces arrive asunknown_service. Passing aresourceas above, or settingOTEL_SERVICE_NAME, fixes it. - Use
batch=Truein production.register()defaults to aSimpleSpanProcessor, which exports each span synchronously as it ends.
If you wire OpenInference instrumentors yourself
If you already build a plain OpenTelemetry TracerProvider, or you want to
drop arize-phoenix-otel entirely, use a standard OTLP/HTTP exporter. This
is the same setup as the OpenInference page:
import os
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import SpanLimits, TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from openinference.instrumentation.openai import OpenAIInstrumentor
provider = TracerProvider(
resource=Resource.create({"service.name": "my-agent"}),
span_limits=SpanLimits(max_span_attributes=4096),
)
provider.add_span_processor(
BatchSpanProcessor(
OTLPSpanExporter(
endpoint="https://ingest.eu.console.rius-glassflow.com/v1/traces",
headers={"Authorization": f"Bearer {os.environ['RIUS_API_KEY']}"},
)
)
)
trace.set_tracer_provider(provider)
OpenAIInstrumentor().instrument(tracer_provider=provider)Your using_session, using_user and using_attributes context managers
from openinference-instrumentation keep working on either setup: they stamp
session.id and user.id, which Rius reads directly. Set both on every
span of the trace, the root span included: the top-level span of the
trace, usually your request handler. The trace list shows a trace under the
session and user of its root span. The catch is that using_session and
using_user only stamp spans created by OpenInference: the instrumentors,
and the tracer you get from the provider register() returns
(tracer_provider.get_tracer(__name__)). A root span you start from a plain
OpenTelemetry tracer misses them, so the trace shows up as a
single-trace session with no user, even though
the LLM span inside carries the right ids. Start your root spans from that
OpenInference tracer, or set the attributes on the root yourself:
with tracer.start_as_current_span(
"handle-turn",
attributes={"session.id": conversation_id, "user.id": user_id},
):
run_agent(message)Raise the span attribute limit on either setup. OpenInference writes one
attribute per message and tool field, and OpenTelemetry keeps 128 per span
by default, so a long agent loop silently loses the model, usage or output
on later turns. Set OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT=4096, or pass
SpanLimits as above. See
raise the span attribute limit.
Side by side: send to Phoenix and Rius
For an evaluation period, keep register() pointed at Phoenix and add Rius
as a second span processor on the same provider:
import os
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from phoenix.otel import register
# Phoenix, configured as today (PHOENIX_COLLECTOR_ENDPOINT, PHOENIX_API_KEY)
tracer_provider = register(
project_name="my-agent",
batch=True,
resource=Resource.create({"service.name": "my-agent"}),
auto_instrument=True,
)
# Rius, as a second exporter
tracer_provider.add_span_processor(
BatchSpanProcessor(
OTLPSpanExporter(
endpoint="https://ingest.eu.console.rius-glassflow.com/v1/traces",
headers={"Authorization": f"Bearer {os.environ['RIUS_API_KEY']}"},
)
),
replace_default_processor=False,
)Pass replace_default_processor=False. The TracerProvider that
register() returns removes its Phoenix exporter when you add a span
processor, unless you tell it not to, and your Phoenix project goes quiet.
Both backends now receive identical spans, so you can compare trace trees,
token counts and cost on the same traffic. If you already route through an
OpenTelemetry Collector, fan out there instead with one extra otlphttp
exporter; the Collector page
has the config.
Path B: replace register() with rius.init()
The Rius SDK for Python installs and enables the same
OpenInference instrumentors as optional extras, for example
openinference-instrumentation-openai behind glassflow-rius[openai]. On
top of the exporter, it sets the 4096 span attribute limit, adds
privacy controls, gives you session
and user scopes, and sends heartbeats
so you see which agent processes are alive. Turn on
partial spans to also see runs still
in progress.
pip uninstall arize-phoenix-otel
pip install "glassflow-rius[openai]"
export RIUS_API_KEY="<your API key>"# Before
from phoenix.otel import register
from openinference.instrumentation import using_session
register(project_name="my-agent", auto_instrument=True)
with using_session(conversation_id):
run_agent(message)# After
import rius
rius.init(service_name="my-agent")
with rius.session(conversation_id):
run_agent(message)rius.init() instruments every supported library it finds installed, so
your own OpenAIInstrumentor().instrument() calls and the
auto_instrument=True flag go away with register().
The endpoint defaults to EU ingest, so RIUS_API_KEY is usually the only
setting. See installation for every option and the
integrations list for which libraries are covered.
Mapping the rest of the Phoenix tracing API:
| Phoenix / OpenInference | Rius SDK |
|---|---|
register(project_name=...) | rius.init(service_name=...) |
using_session(session_id) | with rius.session(session_id): |
using_user(user_id) | with rius.user(user_id): |
tracer.start_as_current_span(..., openinference_span_kind="chain") | rius.start_as_current_span(name); pass kind= for other span kinds |
@tracer.chain | @rius.observe |
@tracer.agent | @rius.observe(kind=rius.SpanKind.AGENT) |
@tracer.tool | @rius.observe(kind=rius.SpanKind.TOOL) |
Manual LLM span with llm.* attributes | rius.start_as_current_generation(model=..., provider=...), or let the instrumentor capture it |
Switch using_session and using_user to rius.session and rius.user in
the same change. The OpenInference context managers do not stamp spans you
start with rius.start_as_current_span or @rius.observe, which are usually
the root, so the trace loses its session and user (see the note under
Path A). rius.session
and rius.user stamp every span in scope, the root and the instrumentors’
spans included. The full API is in the tracing guide.
What does not carry over from Phoenix
Rius has no equivalent today for:
- Evals: no LLM-as-judge or code evaluators, and no eval results on
spans. If you run
phoenix.evalstoday, keep that workflow in Phoenix or a dedicated eval tool. - Datasets and experiments: no datasets built from traces or uploads, and no experiment runs comparing app versions.
- Prompt management and the prompt playground: no versioned prompts, no prompt deployment, and no playground or span replay. Prompt content on spans is captured and inspectable, but there is no registry.
- Annotations: no human labels or feedback on spans, and no annotation API.
- Self-hosting: Rius is a managed service only. Phoenix can be self-hosted.
- Custom model prices: Phoenix lets you add or override model prices. Rius prices calls at the providers’ public list prices, and a model it does not know stays unpriced (empty, not $0). See track LLM cost.
- Historical data: Existing traces stay in Phoenix; Rius starts from the first span you send it. There is no import of historical Phoenix data.
- Phoenix-only attributes:
openinference.project.name, metadata and tags are stored and can be filtered on by key, but they do not drive grouping, views or analytics.
If evals, datasets or annotations are load-bearing in your workflow, run the side-by-side setup and keep Phoenix for those pieces rather than cutting over completely.
Phoenix to Rius cutover checklist
- Create a Rius workspace and API key (quickstart). New organizations start with a free trial (30 days or 500,000 spans, whichever comes first), then a paid plan; see Plans and billing.
- Pick your path: re-point the exporter
or switch to
rius.init(). - Run both backends in parallel (side by side).
- Set
service.nameon every service, so traces do not land underunknown_service. - Raise the span attribute limit if you are not on the Rius SDK (see Path A).
- Verify one traced request end to end: the trace tree looks right, the LLM span shows as a generation with model and token counts, cost appears, and the trace sits in the session and under the user you expect (see the root span note). If fields are empty, check the supported-conventions matrix.
- Compare both backends on real traffic for a few days: trace counts, token totals, cost.
- Move anything that reads from Phoenix (dashboards, eval jobs, internal tools) to its Rius equivalent, or deliberately leave it in Phoenix.
- Remove the Phoenix exporter, uninstall
arize-phoenix-otelif nothing else uses it, and revoke the Phoenix API key once nothing sends to it.
Frequently asked questions
Do I have to re-instrument my code to migrate from Phoenix?
No. Phoenix and Rius both read OpenInference spans, so your instrumentors
stay. You change where the exporter sends spans, and add a service.name.
Can I keep using phoenix.otel.register() with Rius?
phoenix.otel.register() with Rius?Yes, as long as you pass endpoint (the full /v1/traces URL),
protocol="http/protobuf" and your Rius key as api_key in code. Setting
only PHOENIX_COLLECTOR_ENDPOINT sends spans to the wrong port.
Can I run Phoenix and Rius at the same time?
Yes. Add Rius as a second span processor with
replace_default_processor=False, or fan out from an OpenTelemetry
Collector. The migration is reversible until you remove the Phoenix exporter.
Can I import my existing Phoenix traces into Rius?
No. Rius ingests traces as they are exported. Keep your Phoenix instance, or an export of it, for as long as you need the history.