lmnr 0.6.21__py3-none-any.whl → 0.7.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. lmnr/__init__.py +0 -4
  2. lmnr/opentelemetry_lib/decorators/__init__.py +81 -32
  3. lmnr/opentelemetry_lib/litellm/__init__.py +5 -2
  4. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/__init__.py +6 -2
  5. lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/__init__.py +11 -2
  6. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/__init__.py +3 -0
  7. lmnr/opentelemetry_lib/opentelemetry/instrumentation/langgraph/__init__.py +16 -16
  8. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/__init__.py +6 -0
  9. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/chat_wrappers.py +141 -9
  10. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/completion_wrappers.py +10 -2
  11. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/embeddings_wrappers.py +6 -2
  12. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/assistant_wrappers.py +8 -2
  13. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/event_handler_wrapper.py +4 -1
  14. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/responses_wrappers.py +20 -4
  15. lmnr/opentelemetry_lib/opentelemetry/instrumentation/threading/__init__.py +190 -0
  16. lmnr/opentelemetry_lib/tracing/__init__.py +89 -1
  17. lmnr/opentelemetry_lib/tracing/context.py +126 -0
  18. lmnr/opentelemetry_lib/tracing/processor.py +5 -6
  19. lmnr/opentelemetry_lib/tracing/tracer.py +29 -0
  20. lmnr/sdk/browser/browser_use_otel.py +5 -5
  21. lmnr/sdk/browser/patchright_otel.py +14 -0
  22. lmnr/sdk/browser/playwright_otel.py +32 -6
  23. lmnr/sdk/browser/pw_utils.py +119 -112
  24. lmnr/sdk/browser/recorder/record.umd.min.cjs +84 -0
  25. lmnr/sdk/client/asynchronous/resources/browser_events.py +1 -0
  26. lmnr/sdk/laminar.py +156 -186
  27. lmnr/sdk/types.py +17 -11
  28. lmnr/version.py +1 -1
  29. {lmnr-0.6.21.dist-info → lmnr-0.7.1.dist-info}/METADATA +3 -2
  30. {lmnr-0.6.21.dist-info → lmnr-0.7.1.dist-info}/RECORD +32 -31
  31. {lmnr-0.6.21.dist-info → lmnr-0.7.1.dist-info}/WHEEL +1 -1
  32. lmnr/opentelemetry_lib/tracing/context_properties.py +0 -65
  33. lmnr/sdk/browser/rrweb/rrweb.umd.min.cjs +0 -98
  34. {lmnr-0.6.21.dist-info → lmnr-0.7.1.dist-info}/entry_points.txt +0 -0
@@ -27,6 +27,10 @@ from ..utils import (
27
27
  should_emit_events,
28
28
  should_send_prompts,
29
29
  )
30
+ from lmnr.opentelemetry_lib.tracing.context import (
31
+ get_current_context,
32
+ get_event_attributes_from_context,
33
+ )
30
34
  from opentelemetry.instrumentation.utils import _SUPPRESS_INSTRUMENTATION_KEY
31
35
  from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE
32
36
  from opentelemetry.semconv_ai import (
@@ -55,6 +59,7 @@ def completion_wrapper(tracer, wrapped, instance, args, kwargs):
55
59
  SPAN_NAME,
56
60
  kind=SpanKind.CLIENT,
57
61
  attributes={SpanAttributes.LLM_REQUEST_TYPE: LLM_REQUEST_TYPE.value},
62
+ context=get_current_context(),
58
63
  )
59
64
 
60
65
  _handle_request(span, kwargs, instance)
@@ -63,7 +68,8 @@ def completion_wrapper(tracer, wrapped, instance, args, kwargs):
63
68
  response = wrapped(*args, **kwargs)
64
69
  except Exception as e:
65
70
  span.set_attribute(ERROR_TYPE, e.__class__.__name__)
66
- span.record_exception(e)
71
+ attributes = get_event_attributes_from_context()
72
+ span.record_exception(e, attributes=attributes)
67
73
  span.set_status(Status(StatusCode.ERROR, str(e)))
68
74
  span.end()
69
75
  raise
@@ -89,6 +95,7 @@ async def acompletion_wrapper(tracer, wrapped, instance, args, kwargs):
89
95
  name=SPAN_NAME,
90
96
  kind=SpanKind.CLIENT,
91
97
  attributes={SpanAttributes.LLM_REQUEST_TYPE: LLM_REQUEST_TYPE.value},
98
+ context=get_current_context(),
92
99
  )
93
100
 
94
101
  _handle_request(span, kwargs, instance)
@@ -97,7 +104,8 @@ async def acompletion_wrapper(tracer, wrapped, instance, args, kwargs):
97
104
  response = await wrapped(*args, **kwargs)
98
105
  except Exception as e:
99
106
  span.set_attribute(ERROR_TYPE, e.__class__.__name__)
100
- span.record_exception(e)
107
+ attributes = get_event_attributes_from_context()
108
+ span.record_exception(e, attributes=attributes)
101
109
  span.set_status(Status(StatusCode.ERROR, str(e)))
102
110
  span.end()
103
111
  raise
@@ -3,6 +3,8 @@ import time
3
3
  from collections.abc import Iterable
4
4
 
5
5
  from opentelemetry import context as context_api
6
+
7
+ from lmnr.opentelemetry_lib.tracing.context import get_event_attributes_from_context
6
8
  from ..shared import (
7
9
  OPENAI_LLM_USAGE_TOKEN_TYPES,
8
10
  _get_openai_base_url,
@@ -91,7 +93,8 @@ def embeddings_wrapper(
91
93
  exception_counter.add(1, attributes=attributes)
92
94
 
93
95
  span.set_attribute(ERROR_TYPE, e.__class__.__name__)
94
- span.record_exception(e)
96
+ attributes = get_event_attributes_from_context()
97
+ span.record_exception(e, attributes=attributes)
95
98
  span.set_status(Status(StatusCode.ERROR, str(e)))
96
99
  span.end()
97
100
 
@@ -156,7 +159,8 @@ async def aembeddings_wrapper(
156
159
  exception_counter.add(1, attributes=attributes)
157
160
 
158
161
  span.set_attribute(ERROR_TYPE, e.__class__.__name__)
159
- span.record_exception(e)
162
+ attributes = get_event_attributes_from_context()
163
+ span.record_exception(e, attributes=attributes)
160
164
  span.set_status(Status(StatusCode.ERROR, str(e)))
161
165
  span.end()
162
166
 
@@ -17,6 +17,10 @@ from ..utils import (
17
17
  dont_throw,
18
18
  should_emit_events,
19
19
  )
20
+ from lmnr.opentelemetry_lib.tracing.context import (
21
+ get_current_context,
22
+ get_event_attributes_from_context,
23
+ )
20
24
  from opentelemetry.instrumentation.utils import _SUPPRESS_INSTRUMENTATION_KEY
21
25
  from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE
22
26
  from opentelemetry.semconv_ai import LLMRequestTypeValues, SpanAttributes
@@ -126,11 +130,12 @@ def messages_list_wrapper(tracer, wrapped, instance, args, kwargs):
126
130
  kind=SpanKind.CLIENT,
127
131
  attributes={SpanAttributes.LLM_REQUEST_TYPE: LLMRequestTypeValues.CHAT.value},
128
132
  start_time=run.get("start_time"),
133
+ context=get_current_context(),
129
134
  )
130
135
 
131
136
  if exception := run.get("exception"):
132
137
  span.set_attribute(ERROR_TYPE, exception.__class__.__name__)
133
- span.record_exception(exception)
138
+ span.record_exception(exception, attributes=get_event_attributes_from_context())
134
139
  span.set_status(Status(StatusCode.ERROR, str(exception)))
135
140
  span.end(run.get("end_time"))
136
141
 
@@ -250,6 +255,7 @@ def runs_create_and_stream_wrapper(tracer, wrapped, instance, args, kwargs):
250
255
  "openai.assistant.run_stream",
251
256
  kind=SpanKind.CLIENT,
252
257
  attributes={SpanAttributes.LLM_REQUEST_TYPE: LLMRequestTypeValues.CHAT.value},
258
+ context=get_current_context(),
253
259
  )
254
260
 
255
261
  i = 0
@@ -313,7 +319,7 @@ def runs_create_and_stream_wrapper(tracer, wrapped, instance, args, kwargs):
313
319
  return response
314
320
  except Exception as e:
315
321
  span.set_attribute(ERROR_TYPE, e.__class__.__name__)
316
- span.record_exception(e)
322
+ span.record_exception(e, attributes=get_event_attributes_from_context())
317
323
  span.set_status(Status(StatusCode.ERROR, str(e)))
318
324
  span.end()
319
325
  raise
@@ -1,3 +1,4 @@
1
+ from lmnr.opentelemetry_lib.tracing.context import get_event_attributes_from_context
1
2
  from ..shared import _set_span_attribute
2
3
  from ..shared.event_emitter import emit_event
3
4
  from ..shared.event_models import ChoiceEvent
@@ -69,7 +70,9 @@ class EventHandlerWrapper(AssistantEventHandler):
69
70
  @override
70
71
  def on_exception(self, exception: Exception):
71
72
  self._span.set_attribute(ERROR_TYPE, exception.__class__.__name__)
72
- self._span.record_exception(exception)
73
+ self._span.record_exception(
74
+ exception, attributes=get_event_attributes_from_context()
75
+ )
73
76
  self._span.set_status(Status(StatusCode.ERROR, str(exception)))
74
77
  self._original_handler.on_exception(exception)
75
78
 
@@ -36,6 +36,10 @@ except ImportError:
36
36
  ResponseOutputMessageParam = Dict[str, Any]
37
37
  RESPONSES_AVAILABLE = False
38
38
 
39
+ from lmnr.opentelemetry_lib.tracing.context import (
40
+ get_current_context,
41
+ get_event_attributes_from_context,
42
+ )
39
43
  from openai._legacy_response import LegacyAPIResponse
40
44
  from opentelemetry import context as context_api
41
45
  from opentelemetry.instrumentation.utils import _SUPPRESS_INSTRUMENTATION_KEY
@@ -429,9 +433,10 @@ def responses_get_or_create_wrapper(tracer: Tracer, wrapped, instance, args, kwa
429
433
  start_time=(
430
434
  start_time if traced_data is None else int(traced_data.start_time)
431
435
  ),
436
+ context=get_current_context(),
432
437
  )
433
438
  span.set_attribute(ERROR_TYPE, e.__class__.__name__)
434
- span.record_exception(e)
439
+ span.record_exception(e, attributes=get_event_attributes_from_context())
435
440
  span.set_status(StatusCode.ERROR, str(e))
436
441
  if traced_data:
437
442
  set_data_attributes(traced_data, span)
@@ -472,6 +477,7 @@ def responses_get_or_create_wrapper(tracer: Tracer, wrapped, instance, args, kwa
472
477
  SPAN_NAME,
473
478
  kind=SpanKind.CLIENT,
474
479
  start_time=int(traced_data.start_time),
480
+ context=get_current_context(),
475
481
  )
476
482
  set_data_attributes(traced_data, span)
477
483
  span.end()
@@ -523,9 +529,10 @@ async def async_responses_get_or_create_wrapper(
523
529
  start_time=(
524
530
  start_time if traced_data is None else int(traced_data.start_time)
525
531
  ),
532
+ context=get_current_context(),
526
533
  )
527
534
  span.set_attribute(ERROR_TYPE, e.__class__.__name__)
528
- span.record_exception(e)
535
+ span.record_exception(e, attributes=get_event_attributes_from_context())
529
536
  span.set_status(StatusCode.ERROR, str(e))
530
537
  if traced_data:
531
538
  set_data_attributes(traced_data, span)
@@ -566,6 +573,7 @@ async def async_responses_get_or_create_wrapper(
566
573
  SPAN_NAME,
567
574
  kind=SpanKind.CLIENT,
568
575
  start_time=int(traced_data.start_time),
576
+ context=get_current_context(),
569
577
  )
570
578
  set_data_attributes(traced_data, span)
571
579
  span.end()
@@ -590,8 +598,12 @@ def responses_cancel_wrapper(tracer: Tracer, wrapped, instance, args, kwargs):
590
598
  kind=SpanKind.CLIENT,
591
599
  start_time=existing_data.start_time,
592
600
  record_exception=True,
601
+ context=get_current_context(),
602
+ )
603
+ span.record_exception(
604
+ Exception("Response cancelled"),
605
+ attributes=get_event_attributes_from_context(),
593
606
  )
594
- span.record_exception(Exception("Response cancelled"))
595
607
  set_data_attributes(existing_data, span)
596
608
  span.end()
597
609
  return response
@@ -616,8 +628,12 @@ async def async_responses_cancel_wrapper(
616
628
  kind=SpanKind.CLIENT,
617
629
  start_time=existing_data.start_time,
618
630
  record_exception=True,
631
+ context=get_current_context(),
632
+ )
633
+ span.record_exception(
634
+ Exception("Response cancelled"),
635
+ attributes=get_event_attributes_from_context(),
619
636
  )
620
- span.record_exception(Exception("Response cancelled"))
621
637
  set_data_attributes(existing_data, span)
622
638
  span.end()
623
639
  return response
@@ -0,0 +1,190 @@
1
+ # Copyright The OpenTelemetry Authors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """
15
+ Instrument threading to propagate OpenTelemetry context.
16
+
17
+ Copied from opentelemetry-instrumentation-threading at commit:
18
+ ad2fe813abb2ab0b6e25bedeebef5041ca3189f7
19
+ https://github.com/open-telemetry/opentelemetry-python-contrib/blob/ad2fe813abb2ab0b6e25bedeebef5041ca3189f7/instrumentation/opentelemetry-instrumentation-threading/src/opentelemetry/instrumentation/threading/__init__.py
20
+
21
+ Modified to use the Laminar isolated context.
22
+
23
+ Usage
24
+ -----
25
+
26
+ .. code-block:: python
27
+
28
+ from opentelemetry.instrumentation.threading import ThreadingInstrumentor
29
+
30
+ ThreadingInstrumentor().instrument()
31
+
32
+ This library provides instrumentation for the `threading` module to ensure that
33
+ the OpenTelemetry context is propagated across threads. It is important to note
34
+ that this instrumentation does not produce any telemetry data on its own. It
35
+ merely ensures that the context is correctly propagated when threads are used.
36
+
37
+
38
+ When instrumented, new threads created using threading.Thread, threading.Timer,
39
+ or within futures.ThreadPoolExecutor will have the current OpenTelemetry
40
+ context attached, and this context will be re-activated in the thread's
41
+ run method or the executor's worker thread."
42
+ """
43
+
44
+ from __future__ import annotations
45
+
46
+ import threading
47
+ from concurrent import futures
48
+ from typing import TYPE_CHECKING, Any, Callable, Collection
49
+
50
+ from wrapt import (
51
+ wrap_function_wrapper, # type: ignore[reportUnknownVariableType]
52
+ )
53
+
54
+ from lmnr.opentelemetry_lib.tracing.context import (
55
+ get_current_context,
56
+ attach_context,
57
+ detach_context,
58
+ )
59
+ from opentelemetry import context
60
+ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
61
+ from opentelemetry.instrumentation.utils import unwrap
62
+
63
+ _instruments = ()
64
+
65
+ if TYPE_CHECKING:
66
+ from typing import Protocol, TypeVar
67
+
68
+ R = TypeVar("R")
69
+
70
+ class HasOtelContext(Protocol):
71
+ _otel_context: context.Context
72
+
73
+
74
+ class ThreadingInstrumentor(BaseInstrumentor):
75
+ __WRAPPER_START_METHOD = "start"
76
+ __WRAPPER_RUN_METHOD = "run"
77
+ __WRAPPER_SUBMIT_METHOD = "submit"
78
+
79
+ def instrumentation_dependencies(self) -> Collection[str]:
80
+ return _instruments
81
+
82
+ def _instrument(self, **kwargs: Any):
83
+ self._instrument_thread()
84
+ self._instrument_timer()
85
+ self._instrument_thread_pool()
86
+
87
+ def _uninstrument(self, **kwargs: Any):
88
+ self._uninstrument_thread()
89
+ self._uninstrument_timer()
90
+ self._uninstrument_thread_pool()
91
+
92
+ @staticmethod
93
+ def _instrument_thread():
94
+ wrap_function_wrapper(
95
+ threading.Thread,
96
+ ThreadingInstrumentor.__WRAPPER_START_METHOD,
97
+ ThreadingInstrumentor.__wrap_threading_start,
98
+ )
99
+ wrap_function_wrapper(
100
+ threading.Thread,
101
+ ThreadingInstrumentor.__WRAPPER_RUN_METHOD,
102
+ ThreadingInstrumentor.__wrap_threading_run,
103
+ )
104
+
105
+ @staticmethod
106
+ def _instrument_timer():
107
+ wrap_function_wrapper(
108
+ threading.Timer,
109
+ ThreadingInstrumentor.__WRAPPER_START_METHOD,
110
+ ThreadingInstrumentor.__wrap_threading_start,
111
+ )
112
+ wrap_function_wrapper(
113
+ threading.Timer,
114
+ ThreadingInstrumentor.__WRAPPER_RUN_METHOD,
115
+ ThreadingInstrumentor.__wrap_threading_run,
116
+ )
117
+
118
+ @staticmethod
119
+ def _instrument_thread_pool():
120
+ wrap_function_wrapper(
121
+ futures.ThreadPoolExecutor,
122
+ ThreadingInstrumentor.__WRAPPER_SUBMIT_METHOD,
123
+ ThreadingInstrumentor.__wrap_thread_pool_submit,
124
+ )
125
+
126
+ @staticmethod
127
+ def _uninstrument_thread():
128
+ unwrap(threading.Thread, ThreadingInstrumentor.__WRAPPER_START_METHOD)
129
+ unwrap(threading.Thread, ThreadingInstrumentor.__WRAPPER_RUN_METHOD)
130
+
131
+ @staticmethod
132
+ def _uninstrument_timer():
133
+ unwrap(threading.Timer, ThreadingInstrumentor.__WRAPPER_START_METHOD)
134
+ unwrap(threading.Timer, ThreadingInstrumentor.__WRAPPER_RUN_METHOD)
135
+
136
+ @staticmethod
137
+ def _uninstrument_thread_pool():
138
+ unwrap(
139
+ futures.ThreadPoolExecutor,
140
+ ThreadingInstrumentor.__WRAPPER_SUBMIT_METHOD,
141
+ )
142
+
143
+ @staticmethod
144
+ def __wrap_threading_start(
145
+ call_wrapped: Callable[[], None],
146
+ instance: HasOtelContext,
147
+ args: tuple[()],
148
+ kwargs: dict[str, Any],
149
+ ) -> None:
150
+ instance._otel_context = get_current_context()
151
+ return call_wrapped(*args, **kwargs)
152
+
153
+ @staticmethod
154
+ def __wrap_threading_run(
155
+ call_wrapped: Callable[..., R],
156
+ instance: HasOtelContext,
157
+ args: tuple[Any, ...],
158
+ kwargs: dict[str, Any],
159
+ ) -> R:
160
+ token = None
161
+ try:
162
+ token = attach_context(instance._otel_context)
163
+ return call_wrapped(*args, **kwargs)
164
+ finally:
165
+ if token is not None:
166
+ detach_context(token)
167
+
168
+ @staticmethod
169
+ def __wrap_thread_pool_submit(
170
+ call_wrapped: Callable[..., R],
171
+ instance: futures.ThreadPoolExecutor,
172
+ args: tuple[Callable[..., Any], ...],
173
+ kwargs: dict[str, Any],
174
+ ) -> R:
175
+ # obtain the original function and wrapped kwargs
176
+ original_func = args[0]
177
+ otel_context = get_current_context()
178
+
179
+ def wrapped_func(*func_args: Any, **func_kwargs: Any) -> R:
180
+ token = None
181
+ try:
182
+ token = attach_context(otel_context)
183
+ return original_func(*func_args, **func_kwargs)
184
+ finally:
185
+ if token is not None:
186
+ detach_context(token)
187
+
188
+ # replace the original function with the wrapped function
189
+ new_args: tuple[Callable[..., Any], ...] = (wrapped_func,) + args[1:]
190
+ return call_wrapped(*new_args, **kwargs)
@@ -10,9 +10,22 @@ from lmnr.opentelemetry_lib.tracing.instruments import (
10
10
  Instruments,
11
11
  init_instrumentations,
12
12
  )
13
+ from lmnr.opentelemetry_lib.tracing.context import (
14
+ attach_context,
15
+ detach_context,
16
+ get_current_context,
17
+ get_token_stack,
18
+ _isolated_token_stack,
19
+ _isolated_token_stack_storage,
20
+ set_token_stack,
21
+ )
13
22
 
14
23
  from opentelemetry import trace
15
- from opentelemetry.instrumentation.threading import ThreadingInstrumentor
24
+ from opentelemetry.context import Context
25
+
26
+ # instead of importing from opentelemetry.instrumentation.threading,
27
+ # we import from our modified copy to use Laminar's isolated context.
28
+ from ..opentelemetry.instrumentation.threading import ThreadingInstrumentor
16
29
  from opentelemetry.sdk.resources import Resource
17
30
  from opentelemetry.sdk.trace import TracerProvider, SpanProcessor
18
31
  from opentelemetry.sdk.trace.export import SpanExporter
@@ -32,6 +45,7 @@ class TracerWrapper(object):
32
45
  _async_client: AsyncLaminarClient
33
46
  _resource: Resource
34
47
  _span_processor: SpanProcessor
48
+ _original_thread_init = None
35
49
 
36
50
  def __new__(
37
51
  cls,
@@ -91,6 +105,9 @@ class TracerWrapper(object):
91
105
 
92
106
  obj._tracer_provider.add_span_processor(obj._span_processor)
93
107
 
108
+ # Setup threading context inheritance
109
+ obj._setup_threading_inheritance()
110
+
94
111
  # This is not a real instrumentation and does not generate telemetry
95
112
  # data, but it is required to ensure that OpenTelemetry context
96
113
  # propagation is enabled.
@@ -113,6 +130,43 @@ class TracerWrapper(object):
113
130
 
114
131
  return cls.instance
115
132
 
133
+ def _setup_threading_inheritance(self):
134
+ """Setup threading inheritance for isolated context."""
135
+ if TracerWrapper._original_thread_init is None:
136
+ # Monkey patch Thread.__init__ to capture context inheritance
137
+ TracerWrapper._original_thread_init = threading.Thread.__init__
138
+
139
+ def patched_thread_init(thread_self, *args, **kwargs):
140
+ # Capture current isolated context and token stack for inheritance
141
+ current_context = get_current_context()
142
+ current_token_stack = get_token_stack().copy()
143
+
144
+ # Get the original target function
145
+ original_target = kwargs.get("target")
146
+ if not original_target and args:
147
+ original_target = args[0]
148
+
149
+ # Only inherit if we have a target function
150
+ if original_target:
151
+ # Create a wrapper function that sets up context
152
+ def thread_wrapper(*target_args, **target_kwargs):
153
+ # Set inherited context and token stack in the new thread
154
+ attach_context(current_context)
155
+ set_token_stack(current_token_stack)
156
+ # Run original target
157
+ return original_target(*target_args, **target_kwargs)
158
+
159
+ # Replace the target with our wrapper
160
+ if "target" in kwargs:
161
+ kwargs["target"] = thread_wrapper
162
+ elif args:
163
+ args = (thread_wrapper,) + args[1:]
164
+
165
+ # Call original init
166
+ TracerWrapper._original_thread_init(thread_self, *args, **kwargs)
167
+
168
+ threading.Thread.__init__ = patched_thread_init
169
+
116
170
  def exit_handler(self):
117
171
  if isinstance(self._span_processor, LaminarSpanProcessor):
118
172
  self._span_processor.clear()
@@ -124,6 +178,31 @@ class TracerWrapper(object):
124
178
  console_log_handler.setFormatter(VerboseColorfulFormatter())
125
179
  self._logger.addHandler(console_log_handler)
126
180
 
181
+ def get_isolated_context(self) -> Context:
182
+ """Get the current isolated context."""
183
+ return get_current_context()
184
+
185
+ def push_span_context(self, span: trace.Span) -> Context:
186
+ """Push a new context with the given span onto the stack."""
187
+ current_ctx = get_current_context()
188
+ new_context = trace.set_span_in_context(span, current_ctx)
189
+ token = attach_context(new_context)
190
+
191
+ # Store the token for later detachment - tokens are much lighter than contexts
192
+ current_stack = get_token_stack().copy()
193
+ current_stack.append(token)
194
+ set_token_stack(current_stack)
195
+
196
+ return new_context
197
+
198
+ def pop_span_context(self) -> None:
199
+ """Pop the current span context from the stack."""
200
+ current_stack = get_token_stack().copy()
201
+ if current_stack:
202
+ token = current_stack.pop()
203
+ set_token_stack(current_stack)
204
+ detach_context(token)
205
+
127
206
  @staticmethod
128
207
  def set_static_params(
129
208
  resource_attributes: dict,
@@ -144,6 +223,15 @@ class TracerWrapper(object):
144
223
  # Any state cleanup. Now used in between tests
145
224
  if isinstance(cls.instance._span_processor, LaminarSpanProcessor):
146
225
  cls.instance._span_processor.clear()
226
+ # Clear the isolated context state for clean test state
227
+ try:
228
+ _isolated_token_stack.set([])
229
+ except LookupError:
230
+ pass
231
+ if hasattr(_isolated_token_stack_storage, "token_stack"):
232
+ _isolated_token_stack_storage.token_stack = []
233
+ # Reset the isolated context to a fresh state
234
+ attach_context(Context())
147
235
 
148
236
  def shutdown(self):
149
237
  if self._tracer_provider is None:
@@ -0,0 +1,126 @@
1
+ import threading
2
+
3
+ from abc import ABC, abstractmethod
4
+ from contextvars import ContextVar
5
+ from opentelemetry.context import Context, Token, create_key, get_value
6
+
7
+ from lmnr.opentelemetry_lib.tracing.attributes import SESSION_ID, USER_ID
8
+
9
+
10
+ class _IsolatedRuntimeContext(ABC):
11
+ """The isolated RuntimeContext interface, identical to OpenTelemetry's _RuntimeContext
12
+ but isolated from the global context.
13
+ """
14
+
15
+ @abstractmethod
16
+ def attach(self, context: Context) -> Token[Context]:
17
+ """Sets the current `Context` object. Returns a
18
+ token that can be used to reset to the previous `Context`.
19
+
20
+ Args:
21
+ context: The Context to set.
22
+ """
23
+
24
+ @abstractmethod
25
+ def get_current(self) -> Context:
26
+ """Returns the current `Context` object."""
27
+
28
+ @abstractmethod
29
+ def detach(self, token: Token[Context]) -> None:
30
+ """Resets Context to a previous value
31
+
32
+ Args:
33
+ token: A reference to a previous Context.
34
+ """
35
+
36
+
37
+ class IsolatedContextVarsRuntimeContext(_IsolatedRuntimeContext):
38
+ """An isolated implementation of the RuntimeContext interface which wraps ContextVar
39
+ but uses its own ContextVar instead of the global one.
40
+ """
41
+
42
+ def __init__(self) -> None:
43
+ self._current_context = ContextVar(
44
+ "isolated_current_context", default=Context()
45
+ )
46
+
47
+ def attach(self, context: Context) -> Token[Context]:
48
+ """Sets the current `Context` object. Returns a
49
+ token that can be used to reset to the previous `Context`.
50
+
51
+ Args:
52
+ context: The Context to set.
53
+ """
54
+ return self._current_context.set(context)
55
+
56
+ def get_current(self) -> Context:
57
+ """Returns the current `Context` object."""
58
+ return self._current_context.get()
59
+
60
+ def detach(self, token: Token[Context]) -> None:
61
+ """Resets Context to a previous value
62
+
63
+ Args:
64
+ token: A reference to a previous Context.
65
+ """
66
+ self._current_context.reset(token)
67
+
68
+
69
+ # Create the isolated runtime context
70
+ _ISOLATED_RUNTIME_CONTEXT = IsolatedContextVarsRuntimeContext()
71
+
72
+ # Token stack for push/pop API compatibility - much lighter than copying contexts
73
+ _isolated_token_stack: ContextVar[list[Token[Context]]] = ContextVar(
74
+ "isolated_token_stack", default=[]
75
+ )
76
+
77
+ # Thread-local storage for threading support
78
+ _isolated_token_stack_storage = threading.local()
79
+
80
+
81
+ def get_token_stack() -> list[Token[Context]]:
82
+ """Get the token stack, supporting both asyncio and threading."""
83
+ try:
84
+ return _isolated_token_stack.get()
85
+ except LookupError:
86
+ if not hasattr(_isolated_token_stack_storage, "token_stack"):
87
+ _isolated_token_stack_storage.token_stack = []
88
+ return _isolated_token_stack_storage.token_stack
89
+
90
+
91
+ def set_token_stack(stack: list[Token[Context]]) -> None:
92
+ """Set the token stack, supporting both asyncio and threading."""
93
+ try:
94
+ _isolated_token_stack.set(stack)
95
+ except LookupError:
96
+ _isolated_token_stack_storage.token_stack = stack
97
+
98
+
99
+ def get_current_context() -> Context:
100
+ """Get the current isolated context."""
101
+ return _ISOLATED_RUNTIME_CONTEXT.get_current()
102
+
103
+
104
+ def attach_context(context: Context) -> Token[Context]:
105
+ """Attach a context to the isolated runtime context."""
106
+ return _ISOLATED_RUNTIME_CONTEXT.attach(context)
107
+
108
+
109
+ def detach_context(token: Token[Context]) -> None:
110
+ """Detach a context from the isolated runtime context."""
111
+ _ISOLATED_RUNTIME_CONTEXT.detach(token)
112
+
113
+
114
+ CONTEXT_USER_ID_KEY = create_key(f"lmnr.{USER_ID}")
115
+ CONTEXT_SESSION_ID_KEY = create_key(f"lmnr.{SESSION_ID}")
116
+
117
+
118
+ def get_event_attributes_from_context(context: Context | None = None) -> dict[str, str]:
119
+ """Get the event attributes from the context."""
120
+ context = context or get_current_context()
121
+ attributes = {}
122
+ if session_id := get_value(CONTEXT_SESSION_ID_KEY, context):
123
+ attributes["lmnr.event.session_id"] = session_id
124
+ if user_id := get_value(CONTEXT_USER_ID_KEY, context):
125
+ attributes["lmnr.event.user_id"] = user_id
126
+ return attributes
@@ -17,9 +17,6 @@ from lmnr.opentelemetry_lib.tracing.attributes import (
17
17
  SPAN_SDK_VERSION,
18
18
  )
19
19
  from lmnr.opentelemetry_lib.tracing.exporter import LaminarSpanExporter
20
- from lmnr.opentelemetry_lib.tracing.context_properties import (
21
- _set_association_properties_attributes,
22
- )
23
20
  from lmnr.version import PYTHON_VERSION, __version__
24
21
 
25
22
 
@@ -76,9 +73,11 @@ class LaminarSpanProcessor(SpanProcessor):
76
73
  span.set_attribute(SPAN_SDK_VERSION, __version__)
77
74
  span.set_attribute(SPAN_LANGUAGE_VERSION, f"python@{PYTHON_VERSION}")
78
75
 
79
- association_properties = get_value("association_properties")
80
- if association_properties is not None:
81
- _set_association_properties_attributes(span, association_properties)
76
+ if span.name == "LangGraph.workflow":
77
+ graph_context = get_value("lmnr.langgraph.graph") or {}
78
+ for key, value in graph_context.items():
79
+ span.set_attribute(f"lmnr.association.properties.{key}", value)
80
+
82
81
  self.instance.on_start(span, parent_context)
83
82
 
84
83
  def on_end(self, span: Span):