Implementation:Arize ai Phoenix OTLP Server Receiver
| Knowledge Sources | |
|---|---|
| Domains | AI Observability, Telemetry Ingestion, Data Persistence |
| Last Updated | 2026-02-14 00:00 GMT |
Overview
Concrete tool for receiving, decoding, and persisting OTLP trace data provided by the Phoenix server's gRPC and HTTP receiver implementations.
Description
The Phoenix server implements two complementary OTLP receivers that accept spans from client-side exporters and persist them for observability:
HTTP Receiver (POST /v1/traces): A FastAPI route handler that accepts protobuf-serialized ExportTraceServiceRequest payloads. It validates the content type (application/x-protobuf), decompresses the body if needed (gzip or deflate), deserializes the protobuf message, and enqueues decoded spans as background tasks.
gRPC Receiver (TraceService.Export): An async gRPC servicer that implements the standard OTLP ExportTraceService RPC. It receives ExportTraceServiceRequest messages directly via gRPC, iterates through resource spans, decodes each OTLP span, and enqueues them for persistence.
Span Decoder (decode_otlp_span): A shared function that converts OTLP protobuf span objects into Phoenix's internal Span schema. The decoder handles binary-to-hex ID conversion, nanosecond-to-datetime timestamp conversion, recursive attribute decoding (including JSON string loading and unflattening), OpenInference span kind extraction, status code mapping, and event/exception parsing.
Usage
These server-side components are used whenever:
- Running a Phoenix server instance that receives traces from instrumented applications.
- Configuring the server for HTTP-only, gRPC-only, or dual-protocol ingestion.
- Setting up TLS or authentication for secured trace ingestion.
- Diagnosing why spans are not appearing in the Phoenix UI (checking for 415, 422, or 503 errors).
Code Reference
Source Location
- Repository: Phoenix
- HTTP Receiver:
src/phoenix/server/api/routers/v1/traces.py - gRPC Receiver:
src/phoenix/server/grpc_server.py - Span Decoder:
src/phoenix/trace/otel.py
Signature
HTTP Route Handler:
@router.post("/traces")
async def post_traces(
request: Request,
background_tasks: BackgroundTasks,
content_type: Optional[str] = Header(default=None),
content_encoding: Optional[str] = Header(default=None),
) -> Response:
...
gRPC Servicer:
class Servicer(TraceServiceServicer):
def __init__(
self,
enqueue_span: Callable[[Span, ProjectName], Awaitable[None]],
) -> None:
...
async def Export(
self,
request: ExportTraceServiceRequest,
context: RpcContext,
) -> ExportTraceServiceResponse:
...
gRPC Server Manager:
class GrpcServer:
def __init__(
self,
enqueue_span: Callable[[Span, ProjectName], Awaitable[None]],
tracer_provider: Optional[TracerProvider] = None,
enable_prometheus: bool = False,
disabled: bool = False,
token_store: Optional[CanReadToken] = None,
interceptors: Iterable[ServerInterceptor] = (),
) -> None:
...
Span Decoder:
def decode_otlp_span(otlp_span: otlp.Span) -> Span:
...
Import
from phoenix.trace.otel import decode_otlp_span
from phoenix.server.grpc_server import GrpcServer, Servicer
I/O Contract
Inputs (HTTP Receiver)
| Name | Type | Required | Description |
|---|---|---|---|
| request body | bytes | Yes | Serialized ExportTraceServiceRequest protobuf message.
|
| Content-Type | str (header) | Yes | Must be application/x-protobuf. Returns HTTP 415 otherwise.
|
| Content-Encoding | str (header) | No | Optional compression: gzip or deflate. Returns HTTP 415 for unsupported encodings.
|
Inputs (gRPC Receiver)
| Name | Type | Required | Description |
|---|---|---|---|
| request | ExportTraceServiceRequest | Yes | OTLP protobuf message containing resource_spans with nested scope_spans and spans.
|
| context | RpcContext | Yes | gRPC call context (metadata, deadlines, etc.). |
Inputs (decode_otlp_span)
| Name | Type | Required | Description |
|---|---|---|---|
| otlp_span | otlp.Span | Yes | A single OTLP protobuf span object containing trace_id, span_id, name, timestamps, attributes, status, and events. |
Outputs (HTTP Receiver)
| Name | Type | Description |
|---|---|---|
| Response | HTTP Response | Serialized ExportTraceServiceResponse protobuf with Content-Type: application/x-protobuf and status 200 on success.
|
| Side effect | Background task | Decoded spans are enqueued for async database persistence via state.enqueue_span(span, project_name).
|
Outputs (gRPC Receiver)
| Name | Type | Description |
|---|---|---|
| ExportTraceServiceResponse | Protobuf message | Standard OTLP response confirming receipt. |
| Side effect | Async enqueue | Decoded spans are enqueued for database persistence via self._enqueue_span(span, project_name).
|
Outputs (decode_otlp_span)
| Name | Type | Description |
|---|---|---|
| Span | phoenix.trace.schemas.Span | Internal span object with fields: name, context (trace_id, span_id), parent_id, start_time, end_time, attributes (dict), span_kind, status_code, status_message, events (list), conversation.
|
Usage Examples
HTTP Ingestion Flow
# Client-side: spans are exported via HTTPSpanExporter
# Server-side: the POST /v1/traces route receives them
# 1. Client sends protobuf-encoded request
# POST /v1/traces
# Content-Type: application/x-protobuf
# Content-Encoding: gzip
# Body: <gzipped ExportTraceServiceRequest>
# 2. Server decompresses and parses
body = await request.body()
body = gzip.decompress(body) # if gzip
req = ExportTraceServiceRequest()
req.ParseFromString(body)
# 3. Server iterates and decodes each span
for resource_spans in req.resource_spans:
project_name = get_project_name(resource_spans.resource.attributes)
for scope_span in resource_spans.scope_spans:
for otlp_span in scope_span.spans:
span = decode_otlp_span(otlp_span)
await state.enqueue_span(span, project_name)
gRPC Ingestion Flow
# The gRPC Servicer.Export method handles incoming gRPC requests
async def Export(self, request, context):
for resource_spans in request.resource_spans:
project_name = get_project_name(
resource_spans.resource.attributes
)
for scope_span in resource_spans.scope_spans:
for otlp_span in scope_span.spans:
span = await run_in_threadpool(
decode_otlp_span, otlp_span
)
await self._enqueue_span(span, project_name)
return ExportTraceServiceResponse()
Span Decoding Details
from phoenix.trace.otel import decode_otlp_span
# Given an OTLP protobuf span, decode to internal representation
span = decode_otlp_span(otlp_span)
# Access decoded fields
print(span.context.trace_id) # e.g., "0af7651916cd43dd8448eb211c80319c"
print(span.context.span_id) # e.g., "b7ad6b7169203331"
print(span.name) # e.g., "llm-call"
print(span.start_time) # datetime(2026, 2, 14, ..., tzinfo=utc)
print(span.span_kind) # SpanKind.LLM, SpanKind.RETRIEVER, etc.
print(span.attributes) # {"input": {"value": "..."}, "output": {"value": "..."}}
print(span.status_code) # SpanStatusCode.OK
print(span.events) # [SpanEvent(...), SpanException(...)]
gRPC Server with TLS and Authentication
from phoenix.server.grpc_server import GrpcServer
# The GrpcServer is used as an async context manager
async with GrpcServer(
enqueue_span=enqueue_span_callback,
token_store=token_store, # Enables API key authentication
enable_prometheus=True, # Enables metrics
) as server:
# Server is now listening on the configured gRPC port
# TLS is auto-configured from PHOENIX_TLS_* environment variables
await serve_forever()
Error Handling (HTTP)
HTTP 415 - Unsupported Media Type:
Content-Type is not "application/x-protobuf"
Content-Encoding is not "gzip" or "deflate"
HTTP 422 - Unprocessable Entity:
Request body cannot be parsed as ExportTraceServiceRequest
HTTP 503 - Service Unavailable:
Server span queue is full (back-pressure mechanism)
Related Pages
Implements Principle
Requires Environment
- Environment:Arize_ai_Phoenix_Python_Runtime
- Environment:Arize_ai_Phoenix_OpenTelemetry_SDK
- Environment:Arize_ai_Phoenix_Phoenix_Server_Runtime