Span Attributes#
Span attributes are the key/value metadata payload attached to every span in Phoenix. They follow OpenInference semantic conventions (e.g. input.value, output.mime_type, llm.token_count.prompt) and are persisted as a single JSON blob in the database.
Storage: JSON Column on Span#
The Span ORM model stores attributes in a single attributes: Mapped[dict[str, Any]] column. The column uses the JsonDict custom TypeDecorator, which wraps JSON_ — a dialect-aware type alias that resolves to JSONB on both SQLite and PostgreSQL . JsonDict.process_bind_param guarantees a safe write: if the value is not a dict it is stored as {} rather than raising .
Attributes are always stored in nested form (e.g. {"input": {"value": "...", "mime_type": "text/plain"}}), not flat dot-notation. The unflatten / flatten helpers in phoenix/trace/attributes.py handle conversion when spans are received over OTLP.
Attribute Key Path Constants#
models.py pre-splits dotted OpenInference attribute names into path lists at module load time :
INPUT_MIME_TYPE = SpanAttributes.INPUT_MIME_TYPE.split(".") # ["input", "mime_type"]
INPUT_VALUE = SpanAttributes.INPUT_VALUE.split(".") # ["input", "value"]
OUTPUT_MIME_TYPE = SpanAttributes.OUTPUT_MIME_TYPE.split(".")
OUTPUT_VALUE = SpanAttributes.OUTPUT_VALUE.split(".")
METADATA = SpanAttributes.METADATA.split(".")
This pays the string-split cost once and makes SQL subscript expressions like cls.attributes[INPUT_VALUE] possible.
Hybrid Properties for Attribute Access#
The Span model exposes frequently-needed attribute sub-paths as @hybrid_property fields. Each has two sides :
- Instance side — calls
get_attribute_value(self.attributes, KEY)to traverse the in-memory Python dict. - Expression side — uses the SQLAlchemy JSON subscript operator
cls.attributes[KEY]to push the lookup into SQL.
| Property | SQL Expression |
|---|---|
input_value | cls.attributes[INPUT_VALUE] |
input_mime_type | cls.attributes[INPUT_MIME_TYPE] |
output_value | cls.attributes[OUTPUT_VALUE] |
output_mime_type | cls.attributes[OUTPUT_MIME_TYPE] |
metadata_ | cls.attributes[METADATA] |
input_value_first_101_chars | func.substr(…attributes[INPUT_VALUE]…, 1, 101) |
output_value_first_101_chars | func.substr(…attributes[OUTPUT_VALUE]…, 1, 101) |
The *_first_101_chars variants avoid fetching large attribute values when only a preview is needed — the GraphQL input/output resolvers use these to prefetch a truncated snippet before lazy-loading the full value .
get_attribute_value Helper#
get_attribute_value (phoenix/trace/attributes.py) is the shared traversal utility used by hybrid property instance sides, the insertion layer, and the GraphQL resolvers. It accepts a key as either a dot-separated string or a pre-split sequence, iterates through intermediate keys requiring each to be a dict, and returns None on any missing or wrong-type intermediate .
Insertion Flow#
insert_span (db/insertion/span.py) stores span.attributes directly as the JSON dict — no transformation at this layer . Token-count attributes are extracted separately with get_attribute_value and stored in dedicated scalar columns for efficient aggregation .
Normalization happens earlier, in the OTLP decoding pipeline (phoenix/trace/otel.py):
- Unflatten — dot-notation OTLP keys become nested dicts.
load_json_strings—METADATA,TOOL_PARAMETERS,DOCUMENT_METADATA, andLLM_PROMPT_TEMPLATE_VARIABLESare JSON-parsed from strings into dicts .- Non-string input coercion — if
input.valueis not a string, it is JSON-serialized andinput.mime_typeis automatically set toapplication/json.
MIME Type Handling#
input.mime_type and output.mime_type indicate how to interpret the corresponding value field. Valid values come from trace_schemas.MimeType:
| Enum | String Value |
|---|---|
MimeType.TEXT | "text/plain" |
MimeType.JSON | "application/json" |
The GraphQL MimeType Strawberry enum mirrors these values and adds a _missing_ fallback: a falsy/empty raw value defaults to MimeType.text; any other unrecognised value returns None .
SpanIOValue surfaces mime_type alongside the span's I/O content to GraphQL clients. The Span GraphQL resolver reads the hybrid property (input_mime_type / output_mime_type) and passes it directly to MimeType(mime_type) when constructing the response .
Key Source Files#
| File | Purpose |
|---|---|
src/phoenix/db/models.py | Span ORM model, JsonDict type, hybrid properties, path constants |
src/phoenix/db/insertion/span.py | insert_span — writes attributes JSON to DB |
src/phoenix/trace/attributes.py | get_attribute_value, unflatten, flatten, load_json_strings |
src/phoenix/server/api/types/Span.py | GraphQL Span type — reads attributes via hybrid properties |
src/phoenix/server/api/types/MimeType.py | GraphQL MimeType enum with fallback handling |
src/phoenix/server/api/types/SpanIOValue.py | SpanIOValue — surfaces mime_type + value to GraphQL clients |
src/phoenix/trace/schemas.py | Base MimeType enum (TEXT/JSON) — |
src/phoenix/trace/otel.py | OTLP ingestion pipeline: coercion, unflatten, mime type auto-set |