Dosu LogoDosu Logo
Ask
Join our Discord
Arize PhoenixPublic
Arize
DocumentsArize Phoenix
OTLP Attribute Serialization
OTLP Attribute Serialization
Type
Topic
Status
Published
Created
Jul 31, 2026
Updated
Jul 31, 2026

OTLP Attribute Serialization#

Phoenix ingests spans over OTLP, where attributes arrive as a flat list of (string_key, value) pairs using dot-separated keys (e.g. "llm.token_count.completion"). Internally, Phoenix stores and works with attributes as nested dicts (e.g. {"llm": {"token_count": {"completion": 123}}}). The conversion layer lives in two files:

  • src/phoenix/trace/attributes.py — core conversion primitives
  • src/phoenix/trace/otel.py — OTLP decode/encode pipeline that uses those primitives

Decode path: flat → nested#

decode_otlp_span in otel.py applies the following pipeline to incoming OTLP attributes:

_decode_key_values → coerce_otlp_span_attributes → load_json_strings → unflatten
  1. _decode_key_values — converts KeyValue protobufs to (str, Any) tuples using _decode_value, which handles all OTLP scalar and container types (string, bool, int, double, array, kvlist, bytes) .

  2. coerce_otlp_span_attributes — converts LLM token count values to int (they may arrive as floats or strings).

  3. load_json_strings — for attributes whose keys end with any of DOCUMENT_METADATA, LLM_PROMPT_TEMPLATE_VARIABLES, METADATA, or TOOL_PARAMETERS, the string value is JSON-parsed into a dict. Failures leave the raw string intact .

  4. unflatten — the primary conversion: builds a trie from the dot-split key pairs and walks it into a nested dict.

After unflattening, one more coercion is applied: if input.value is not a string, it is JSON-serialized and input.mime_type is automatically set to application/json .


unflatten internals: trie + indexed arrays#

unflatten is built on a two-step process :

  1. _build_trie — splits each key by the separator (default .), inserting each part as a node in a _Trie. Numeric segments (all-digit key parts) are treated as array indices rather than dict keys, collected in the trie node's indices set. Non-numeric segments become branches.

  2. _walk — traverses the trie and materializes it: nodes with indices become lists of dicts; nodes with branches become dicts.

Key rule for index promotion: A digit segment becomes an index only if the trie node at that point doesn't already have a scalar value — if a value already exists, the digit is demoted to a regular branch . This prevents ambiguous overlap between a leaf value and an indexed namespace.

Example: The pairs ("retrieval.documents.0.document.content", "A") and ("retrieval.documents.1.document.content", "B") unflatten to :

{"retrieval": {"documents": [{"document": {"content": "A"}}, {"document": {"content": "B"}}]}}

Prefix exclusions: unflatten accepts a prefix_exclusions parameter (typically populated with all OpenInference semantic convention strings, sorted longest-first). _partition_with_prefix_exclusion checks these before splitting, keeping known multi-segment attribute names (e.g. "llm.token_count.total") intact as a single trie branch rather than splitting them letter by letter.

has_mapping — a guard used by both unflatten and flatten: digit sub-keys are only promoted to array indices if the resulting array would contain dicts (not plain scalars like integers or strings).


Encode path: nested → flat#

encode_span_to_otlp reverses the process for outbound spans. For each attribute value that is a Mapping, it calls flatten with recurse_on_sequence=True. For sequences that contain mappings, flatten is also called. JSON_STRING_ATTRIBUTES values that are dicts are re-serialized to JSON strings during flattening .

flatten dispatches to _flatten_mapping or _flatten_sequence depending on input type. _flatten_sequence emits indexed keys (prefix.0.key, prefix.1.key, …) for sequences of dicts, regenerating the numeric-segment format consumed by unflatten.


Key functions reference#

FunctionFilePurpose
unflattenattributes.pyFlat key-value pairs → nested dict
flattenattributes.pyNested dict → flat key-value pairs
load_json_stringsattributes.pyParse JSON string attributes into dicts
get_attribute_valueattributes.pySafe traversal of nested attribute dict
has_mappingattributes.pyGuard: sequence contains dicts?
_build_trieattributes.pyTrie construction with index detection
_walkattributes.pyTrie → nested dict materialization
decode_otlp_spanotel.pyFull OTLP protobuf → Span decode pipeline
encode_span_to_otlpotel.pySpan → OTLP protobuf encode pipeline
coerce_otlp_span_attributesotel.pyToken count type coercion
Documents
Agent Session Persistence
Database Schema Management
Dataset Evaluator
Experiment Tracing
OTLP Attribute Serialization
Phoenix CLI
Prompt Version Tagging
PXI Approval Flow
PXI Terminal UI
Reverse Proxy Sub-Path Deployment
Session Filtering
Span Attributes
Trace Filter DSL
Trace Tree UI