Skip to content

Commit 12c4df7

Browse files
init laminar span context from env var (#221)
* init laminar span context from env var * small fixes and unit tests * please the bots in tests * bump version to 0.7.24 --------- Co-authored-by: Din <[email protected]>
1 parent 09531ce commit 12c4df7

File tree

6 files changed

+143
-3
lines changed

6 files changed

+143
-3
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
[project]
88
name = "lmnr"
9-
version = "0.7.23"
9+
version = "0.7.24"
1010
description = "Python SDK for Laminar"
1111
authors = [
1212
{ name = "lmnr.ai", email = "[email protected]" }

src/lmnr/opentelemetry_lib/tracing/processor.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,3 +140,13 @@ def clear(self):
140140
with self._paths_lock:
141141
self.__span_id_to_path = {}
142142
self.__span_id_lists = {}
143+
144+
def set_parent_path_info(
145+
self,
146+
parent_span_id: int,
147+
span_path: list[str],
148+
span_ids_path: list[str],
149+
):
150+
with self._paths_lock:
151+
self.__span_id_to_path[parent_span_id] = span_path
152+
self.__span_id_lists[parent_span_id] = span_ids_path

src/lmnr/sdk/laminar.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@
88
CONTEXT_USER_ID_KEY,
99
attach_context,
1010
get_event_attributes_from_context,
11+
push_span_context,
1112
)
1213
from lmnr.opentelemetry_lib.tracing.instruments import Instruments
14+
from lmnr.opentelemetry_lib.tracing.processor import LaminarSpanProcessor
1315
from lmnr.opentelemetry_lib.tracing.tracer import get_tracer_with_context
1416
from lmnr.opentelemetry_lib.tracing.attributes import (
1517
ASSOCIATION_PROPERTIES,
@@ -203,6 +205,46 @@ def initialize(
203205
force_http=force_http,
204206
)
205207

208+
cls._initialize_context_from_env()
209+
210+
@classmethod
211+
def _initialize_context_from_env(cls) -> None:
212+
"""Attach upstream Laminar context from the environment, if provided."""
213+
env_context = os.getenv("LMNR_SPAN_CONTEXT")
214+
if not env_context:
215+
return
216+
217+
try:
218+
laminar_context = LaminarSpanContext.deserialize(env_context)
219+
except Exception as exc: # pylint: disable=broad-exception-caught
220+
cls.__logger.warning(
221+
"LMNR_SPAN_CONTEXT is set but could not be deserialized: %s", exc
222+
)
223+
return
224+
225+
try:
226+
otel_span_context = LaminarSpanContext.try_to_otel_span_context(
227+
laminar_context, cls.__logger
228+
)
229+
except ValueError as exc:
230+
cls.__logger.warning(
231+
"LMNR_SPAN_CONTEXT is set but invalid span context provided: %s", exc
232+
)
233+
return
234+
235+
base_context = trace.set_span_in_context(
236+
trace.NonRecordingSpan(otel_span_context), get_current_context()
237+
)
238+
processor = TracerWrapper.instance._span_processor
239+
if isinstance(processor, LaminarSpanProcessor):
240+
processor.set_parent_path_info(
241+
otel_span_context.span_id,
242+
laminar_context.span_path,
243+
laminar_context.span_ids_path,
244+
)
245+
push_span_context(base_context)
246+
cls.__logger.debug("Initialized Laminar parent context from LMNR_SPAN_CONTEXT.")
247+
206248
@classmethod
207249
def is_initialized(cls):
208250
"""Check if Laminar is initialized. A utility to make sure other

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.23"
6+
__version__ = "0.7.24"
77
PYTHON_VERSION = f"{sys.version_info.major}.{sys.version_info.minor}"
88

99

tests/test_observe.py

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import json
2+
import os
23
import uuid
34
import pytest
45

5-
from lmnr import Laminar, observe
6+
from lmnr import Laminar, observe, LaminarSpanContext
67
from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter
78
from opentelemetry import trace
89

@@ -1563,3 +1564,48 @@ def func2():
15631564
str(uuid.UUID(int=func1_span.get_span_context().span_id)),
15641565
str(uuid.UUID(int=func2_span.get_span_context().span_id)),
15651566
)
1567+
1568+
1569+
def test_span_context_from_env_variables_observe(span_exporter: InMemorySpanExporter):
1570+
test_trace_id = "01234567-89ab-cdef-0123-456789abcdef"
1571+
test_span_id = "00000000-0000-0000-0123-456789abcdef"
1572+
test_span_id2 = "00000000-0000-0000-fedc-ba9876543210"
1573+
old_val = os.getenv("LMNR_SPAN_CONTEXT")
1574+
test_context = LaminarSpanContext(
1575+
trace_id=test_trace_id,
1576+
span_id=test_span_id2,
1577+
span_path=["grandparent", "parent"],
1578+
span_ids_path=[test_span_id, test_span_id2],
1579+
)
1580+
1581+
os.environ["LMNR_SPAN_CONTEXT"] = str(test_context)
1582+
1583+
Laminar._initialize_context_from_env()
1584+
1585+
@observe()
1586+
def test():
1587+
pass
1588+
1589+
test()
1590+
1591+
spans = span_exporter.get_finished_spans()
1592+
assert len(spans) == 1
1593+
span_id = spans[0].get_span_context().span_id
1594+
assert spans[0].name == "test"
1595+
assert spans[0].attributes["lmnr.span.instrumentation_source"] == "python"
1596+
assert spans[0].attributes["lmnr.span.path"] == (
1597+
"grandparent",
1598+
"parent",
1599+
"test",
1600+
)
1601+
assert spans[0].attributes["lmnr.span.ids_path"] == (
1602+
str(uuid.UUID(test_span_id)),
1603+
str(uuid.UUID(test_span_id2)),
1604+
str(uuid.UUID(int=span_id)),
1605+
)
1606+
assert spans[0].get_span_context().trace_id == uuid.UUID(test_trace_id).int
1607+
assert spans[0].parent.span_id == uuid.UUID(test_span_id2).int
1608+
if old_val:
1609+
os.environ["LMNR_SPAN_CONTEXT"] = old_val
1610+
else:
1611+
os.environ.pop("LMNR_SPAN_CONTEXT", None)

tests/test_tracing.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import json
2+
import os
23
import pytest
34
import uuid
45

@@ -671,6 +672,47 @@ def foo(context: LaminarSpanContext):
671672
assert inner_span.parent.span_id == outer_span.get_span_context().span_id
672673

673674

675+
def test_span_context_from_env_variables(span_exporter: InMemorySpanExporter):
676+
test_trace_id = "01234567-89ab-cdef-0123-456789abcdef"
677+
test_span_id = "00000000-0000-0000-0123-456789abcdef"
678+
test_span_id2 = "00000000-0000-0000-fedc-ba9876543210"
679+
old_val = os.getenv("LMNR_SPAN_CONTEXT")
680+
test_context = LaminarSpanContext(
681+
trace_id=test_trace_id,
682+
span_id=test_span_id2,
683+
span_path=["grandparent", "parent"],
684+
span_ids_path=[test_span_id, test_span_id2],
685+
)
686+
687+
os.environ["LMNR_SPAN_CONTEXT"] = str(test_context)
688+
689+
Laminar._initialize_context_from_env()
690+
with Laminar.start_as_current_span("test"):
691+
pass
692+
693+
spans = span_exporter.get_finished_spans()
694+
assert len(spans) == 1
695+
span_id = spans[0].get_span_context().span_id
696+
assert spans[0].name == "test"
697+
assert spans[0].attributes["lmnr.span.instrumentation_source"] == "python"
698+
assert spans[0].attributes["lmnr.span.path"] == (
699+
"grandparent",
700+
"parent",
701+
"test",
702+
)
703+
assert spans[0].attributes["lmnr.span.ids_path"] == (
704+
str(uuid.UUID(test_span_id)),
705+
str(uuid.UUID(test_span_id2)),
706+
str(uuid.UUID(int=span_id)),
707+
)
708+
assert spans[0].get_span_context().trace_id == uuid.UUID(test_trace_id).int
709+
assert spans[0].parent.span_id == uuid.UUID(test_span_id2).int
710+
if old_val:
711+
os.environ["LMNR_SPAN_CONTEXT"] = old_val
712+
else:
713+
os.environ.pop("LMNR_SPAN_CONTEXT", None)
714+
715+
674716
def test_tags_deduplication(span_exporter: InMemorySpanExporter):
675717
with Laminar.start_as_current_span("test"):
676718
Laminar.set_span_tags(["foo", "bar", "foo"])

0 commit comments

Comments
 (0)