Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions py/src/braintrust/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -4627,6 +4627,9 @@ def log_internal(self, event: dict[str, Any] | None = None, internal_data: dict[
metadata=serializable_partial_record.get("metadata"),
span_parents=self.span_parents,
span_attributes=serializable_partial_record.get("span_attributes"),
error=serializable_partial_record.get("error"),
metrics=serializable_partial_record.get("metrics"),
tags=serializable_partial_record.get("tags"),
)
self.state.span_cache.queue_write(self.root_span_id, self.span_id, cached_span)

Expand Down
49 changes: 25 additions & 24 deletions py/src/braintrust/span_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from typing import Any

from braintrust.types import Metadata
from braintrust.util import merge_dicts
from braintrust.util import clean_nones, merge_dicts


# Global registry of active span caches for process exit cleanup
Expand All @@ -23,7 +23,12 @@


class CachedSpan:
"""Cached span data structure."""
"""A span held in the local cache, before it has been flushed to the server.

Carries the subset of span fields that scorers can filter on, so that a trace can be
queried without a round-trip. Fields the server has but this does not are simply not
filterable locally.
"""

def __init__(
self,
Expand All @@ -33,40 +38,36 @@ def __init__(
metadata: Metadata | None = None,
span_parents: list[str] | None = None,
span_attributes: dict[str, Any] | None = None,
error: Any | None = None,
metrics: dict[str, Any] | None = None,
tags: list[str] | None = None,
):
self.span_id = span_id
self.input = input
self.output = output
self.metadata = metadata
self.span_parents = span_parents
self.span_attributes = span_attributes
self.error = error
self.metrics = metrics
self.tags = tags

def to_dict(self) -> dict[str, Any]:
"""Convert to dictionary for serialization."""
result = {"span_id": self.span_id}
if self.input is not None:
result["input"] = self.input
if self.output is not None:
result["output"] = self.output
if self.metadata is not None:
result["metadata"] = self.metadata
if self.span_parents is not None:
result["span_parents"] = self.span_parents
if self.span_attributes is not None:
result["span_attributes"] = self.span_attributes
return result
"""Return the span's set fields, dropping those left as None.

Unset fields are omitted rather than written as null to keep the on-disk record
small; span_id is always present, so it survives the stripping.
"""
return clean_nones(self.__dict__)

@classmethod
def from_dict(cls, data: dict[str, Any]) -> "CachedSpan":
"""Create from dictionary."""
return cls(
span_id=data["span_id"],
input=data.get("input"),
output=data.get("output"),
metadata=data.get("metadata"),
span_parents=data.get("span_parents"),
span_attributes=data.get("span_attributes"),
)
"""Rebuild a span from a record produced by to_dict().

The cache file is written and read by one process, so `data` always has exactly the
fields this class defines and can be passed straight through.
"""
return cls(**data)


class DiskSpanRecord:
Expand Down
7 changes: 7 additions & 0 deletions py/src/braintrust/test_span_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ def test_span_cache_write_and_read():
span_id="span-1",
input={"text": "hello"},
output={"response": "world"},
error={"message": "retryable"},
metrics={"start": 1, "end": 3},
tags=["production"],
)
span2 = CachedSpan(
span_id="span-2",
Expand All @@ -30,6 +33,10 @@ def test_span_cache_write_and_read():
span_ids = {s.span_id for s in spans}
assert "span-1" in span_ids
assert "span-2" in span_ids
stored_span1 = next(span for span in spans if span.span_id == "span-1")
assert stored_span1.error == {"message": "retryable"}
assert stored_span1.metrics == {"start": 1, "end": 3}
assert stored_span1.tags == ["production"]

cache.stop()
cache.dispose()
Expand Down
Loading