Skip to content

Commit 06dfedb

Browse files
authored
add span path to LaminarSpanContext (#171)
* add span path to LaminarSpanContext * improve tests: checks parent id * remove debug print statements * remove breaking openai import * remove path context * bump version to 0.7.4
1 parent b2d132a commit 06dfedb

File tree

9 files changed

+183
-19
lines changed

9 files changed

+183
-19
lines changed

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
[project]
88
name = "lmnr"
9-
version = "0.7.3"
9+
version = "0.7.4"
1010
description = "Python SDK for Laminar"
1111
authors = [
1212
{ name = "lmnr.ai", email = "[email protected]" }
@@ -124,7 +124,7 @@ dev = [
124124
"pytest-asyncio>=0.26.0",
125125
"playwright>=1.52.0",
126126
"vcrpy>=7.0.0",
127-
"openai>=1.99.3",
127+
"openai>=1.99.7",
128128
"pytest-recording>=0.13.4",
129129
"patchright>=1.52.3",
130130
"google-genai>=1.19.0",

src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/chat_wrappers.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@
5656
from opentelemetry.trace.status import Status, StatusCode
5757
from wrapt import ObjectProxy
5858

59-
from openai.types.chat import ChatCompletionMessageToolCall
6059
from openai.types.chat.chat_completion_message import FunctionCall
60+
import pydantic
6161

6262
SPAN_NAME = "openai.chat"
6363
PROMPT_FILTER_KEY = "prompt_filter_results"
@@ -995,7 +995,7 @@ async def _abuild_from_streaming_response(
995995

996996

997997
def _parse_tool_calls(
998-
tool_calls: Optional[List[Union[dict, ChatCompletionMessageToolCall]]],
998+
tool_calls: Optional[List[Union[dict, pydantic.BaseModel]]],
999999
) -> Union[List[ToolCall], None]:
10001000
"""
10011001
Util to correctly parse the tool calls data from the OpenAI API to this module's

src/lmnr/opentelemetry_lib/tracing/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ def __new__(
7474
if not hasattr(cls, "instance"):
7575
cls._initialize_logger(cls)
7676
obj = super(TracerWrapper, cls).__new__(cls)
77-
77+
7878
# Store session recording options
7979
cls.session_recording_options = session_recording_options or {}
8080

@@ -249,13 +249,13 @@ def flush(self):
249249
self._logger.warning("TracerWrapper not fully initialized, cannot flush")
250250
return False
251251
return self._span_processor.force_flush()
252-
252+
253253
@classmethod
254254
def get_session_recording_options(cls) -> SessionRecordingOptions:
255255
"""Get the session recording options set during initialization."""
256256
return cls.session_recording_options
257257

258-
def get_tracer(self):
258+
def get_tracer(self) -> trace.Tracer:
259259
if self._tracer_provider is None:
260260
return trace.get_tracer_provider().get_tracer(TRACER_NAME)
261261
return self._tracer_provider.get_tracer(TRACER_NAME)

src/lmnr/opentelemetry_lib/tracing/attributes.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
SPAN_TYPE = "lmnr.span.type"
1515
SPAN_PATH = "lmnr.span.path"
1616
SPAN_IDS_PATH = "lmnr.span.ids_path"
17+
PARENT_SPAN_PATH = "lmnr.span.parent_path"
18+
PARENT_SPAN_IDS_PATH = "lmnr.span.parent_ids_path"
1719
SPAN_INSTRUMENTATION_SOURCE = "lmnr.span.instrumentation_source"
1820
SPAN_SDK_VERSION = "lmnr.span.sdk_version"
1921
SPAN_LANGUAGE_VERSION = "lmnr.span.language_version"

src/lmnr/opentelemetry_lib/tracing/processor.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@
77
SimpleSpanProcessor,
88
)
99
from opentelemetry.sdk.trace import Span
10-
from opentelemetry.context import Context, get_value, get_current, set_value
10+
from opentelemetry.context import Context, get_value
1111

1212
from lmnr.opentelemetry_lib.tracing.attributes import (
13+
PARENT_SPAN_IDS_PATH,
14+
PARENT_SPAN_PATH,
1315
SPAN_IDS_PATH,
1416
SPAN_INSTRUMENTATION_SOURCE,
1517
SPAN_LANGUAGE_VERSION,
@@ -52,20 +54,18 @@ def __init__(
5254
)
5355

5456
def on_start(self, span: Span, parent_context: Context | None = None):
55-
span_path_in_context = get_value("span_path", parent_context or get_current())
56-
parent_span_path = span_path_in_context or (
57+
parent_span_path = list(span.attributes.get(PARENT_SPAN_PATH, tuple())) or (
5758
self.__span_id_to_path.get(span.parent.span_id) if span.parent else None
5859
)
59-
parent_span_ids_path = (
60-
self.__span_id_lists.get(span.parent.span_id, []) if span.parent else []
61-
)
60+
parent_span_ids_path = list(
61+
span.attributes.get(PARENT_SPAN_IDS_PATH, tuple())
62+
) or (self.__span_id_lists.get(span.parent.span_id, []) if span.parent else [])
6263
span_path = parent_span_path + [span.name] if parent_span_path else [span.name]
6364
span_ids_path = parent_span_ids_path + [
6465
str(uuid.UUID(int=span.get_span_context().span_id))
6566
]
6667
span.set_attribute(SPAN_PATH, span_path)
6768
span.set_attribute(SPAN_IDS_PATH, span_ids_path)
68-
set_value("span_path", span_path, get_current())
6969
self.__span_id_to_path[span.get_span_context().span_id] = span_path
7070
self.__span_id_lists[span.get_span_context().span_id] = span_ids_path
7171

src/lmnr/sdk/laminar.py

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313
from lmnr.opentelemetry_lib.tracing.tracer import get_tracer_with_context
1414
from lmnr.opentelemetry_lib.tracing.attributes import (
1515
ASSOCIATION_PROPERTIES,
16+
PARENT_SPAN_IDS_PATH,
17+
PARENT_SPAN_PATH,
18+
SPAN_IDS_PATH,
19+
SPAN_PATH,
1620
USER_ID,
1721
Attributes,
1822
SPAN_TYPE,
@@ -316,9 +320,29 @@ def start_as_current_span(
316320

317321
with get_tracer_with_context() as (tracer, isolated_context):
318322
ctx = context or isolated_context
323+
path = []
324+
span_ids_path = []
319325
if parent_span_context is not None:
326+
if isinstance(parent_span_context, (dict, str)):
327+
try:
328+
laminar_span_context = LaminarSpanContext.deserialize(
329+
parent_span_context
330+
)
331+
path = laminar_span_context.span_path
332+
span_ids_path = laminar_span_context.span_ids_path
333+
except Exception:
334+
cls.__logger.warning(
335+
f"`start_as_current_span` Could not deserialize parent_span_context: {parent_span_context}. "
336+
"Will use it as is."
337+
)
338+
laminar_span_context = parent_span_context
339+
else:
340+
laminar_span_context = parent_span_context
341+
if isinstance(laminar_span_context, LaminarSpanContext):
342+
path = laminar_span_context.span_path
343+
span_ids_path = laminar_span_context.span_ids_path
320344
span_context = LaminarSpanContext.try_to_otel_span_context(
321-
parent_span_context, cls.__logger
345+
laminar_span_context, cls.__logger
322346
)
323347
ctx = trace.set_span_in_context(
324348
trace.NonRecordingSpan(span_context), ctx
@@ -352,6 +376,8 @@ def start_as_current_span(
352376
context=ctx,
353377
attributes={
354378
SPAN_TYPE: span_type,
379+
PARENT_SPAN_PATH: path,
380+
PARENT_SPAN_IDS_PATH: span_ids_path,
355381
**(label_props),
356382
**(tag_props),
357383
},
@@ -454,9 +480,29 @@ def bar():
454480

455481
with get_tracer_with_context() as (tracer, isolated_context):
456482
ctx = context or isolated_context
483+
path = []
484+
span_ids_path = []
457485
if parent_span_context is not None:
486+
if isinstance(parent_span_context, (dict, str)):
487+
try:
488+
laminar_span_context = LaminarSpanContext.deserialize(
489+
parent_span_context
490+
)
491+
path = laminar_span_context.span_path
492+
span_ids_path = laminar_span_context.span_ids_path
493+
except Exception:
494+
cls.__logger.warning(
495+
f"`start_span` Could not deserialize parent_span_context: {parent_span_context}. "
496+
"Will use it as is."
497+
)
498+
laminar_span_context = parent_span_context
499+
else:
500+
laminar_span_context = parent_span_context
501+
if isinstance(laminar_span_context, LaminarSpanContext):
502+
path = laminar_span_context.span_path
503+
span_ids_path = laminar_span_context.span_ids_path
458504
span_context = LaminarSpanContext.try_to_otel_span_context(
459-
parent_span_context, cls.__logger
505+
laminar_span_context, cls.__logger
460506
)
461507
ctx = trace.set_span_in_context(
462508
trace.NonRecordingSpan(span_context), ctx
@@ -491,6 +537,8 @@ def bar():
491537
context=ctx,
492538
attributes={
493539
SPAN_TYPE: span_type,
540+
PARENT_SPAN_PATH: path,
541+
PARENT_SPAN_IDS_PATH: span_ids_path,
494542
**(label_props),
495543
**(tag_props),
496544
},
@@ -662,6 +710,8 @@ def get_laminar_span_context(
662710
trace_id=uuid.UUID(int=span.get_span_context().trace_id),
663711
span_id=uuid.UUID(int=span.get_span_context().span_id),
664712
is_remote=span.get_span_context().is_remote,
713+
span_path=span.attributes.get(SPAN_PATH, []),
714+
span_ids_path=span.attributes.get(SPAN_IDS_PATH, []),
665715
)
666716

667717
@classmethod

src/lmnr/sdk/types.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,8 @@ class LaminarSpanContext(pydantic.BaseModel):
169169
trace_id: uuid.UUID
170170
span_id: uuid.UUID
171171
is_remote: bool = pydantic.Field(default=False)
172+
span_path: list[str] = pydantic.Field(default=[])
173+
span_ids_path: list[str] = pydantic.Field(default=[]) # stringified UUIDs
172174

173175
def __str__(self) -> str:
174176
return self.model_dump_json()
@@ -199,7 +201,7 @@ def try_to_otel_span_context(
199201
"Please use `LaminarSpanContext` instead."
200202
)
201203
return span_context
202-
elif isinstance(span_context, dict) or isinstance(span_context, str):
204+
elif isinstance(span_context, (dict, str)):
203205
try:
204206
laminar_span_context = cls.deserialize(span_context)
205207
return SpanContext(
@@ -221,6 +223,9 @@ def deserialize(cls, data: dict[str, Any] | str) -> "LaminarSpanContext":
221223
"trace_id": data.get("trace_id") or data.get("traceId"),
222224
"span_id": data.get("span_id") or data.get("spanId"),
223225
"is_remote": data.get("is_remote") or data.get("isRemote", False),
226+
"span_path": data.get("span_path") or data.get("spanPath", []),
227+
"span_ids_path": data.get("span_ids_path")
228+
or data.get("spanIdsPath", []),
224229
}
225230
return cls.model_validate(converted_data)
226231
elif isinstance(data, str):
@@ -356,5 +361,6 @@ class MaskInputOptions(TypedDict):
356361
email: bool | None
357362
tel: bool | None
358363

364+
359365
class SessionRecordingOptions(TypedDict):
360-
mask_input_options: MaskInputOptions | None
366+
mask_input_options: MaskInputOptions | None

src/lmnr/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from packaging import version
44

55

6-
__version__ = "0.7.3"
6+
__version__ = "0.7.4"
77
PYTHON_VERSION = f"{sys.version_info.major}.{sys.version_info.minor}"
88

99

0 commit comments

Comments
 (0)