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.
- lmnr/__init__.py +0 -4
- lmnr/opentelemetry_lib/decorators/__init__.py +81 -32
- lmnr/opentelemetry_lib/litellm/__init__.py +5 -2
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/__init__.py +6 -2
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/__init__.py +11 -2
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/__init__.py +3 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/langgraph/__init__.py +16 -16
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/__init__.py +6 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/chat_wrappers.py +141 -9
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/completion_wrappers.py +10 -2
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/embeddings_wrappers.py +6 -2
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/assistant_wrappers.py +8 -2
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/event_handler_wrapper.py +4 -1
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/responses_wrappers.py +20 -4
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/threading/__init__.py +190 -0
- lmnr/opentelemetry_lib/tracing/__init__.py +89 -1
- lmnr/opentelemetry_lib/tracing/context.py +126 -0
- lmnr/opentelemetry_lib/tracing/processor.py +5 -6
- lmnr/opentelemetry_lib/tracing/tracer.py +29 -0
- lmnr/sdk/browser/browser_use_otel.py +5 -5
- lmnr/sdk/browser/patchright_otel.py +14 -0
- lmnr/sdk/browser/playwright_otel.py +32 -6
- lmnr/sdk/browser/pw_utils.py +119 -112
- lmnr/sdk/browser/recorder/record.umd.min.cjs +84 -0
- lmnr/sdk/client/asynchronous/resources/browser_events.py +1 -0
- lmnr/sdk/laminar.py +156 -186
- lmnr/sdk/types.py +17 -11
- lmnr/version.py +1 -1
- {lmnr-0.6.21.dist-info → lmnr-0.7.1.dist-info}/METADATA +3 -2
- {lmnr-0.6.21.dist-info → lmnr-0.7.1.dist-info}/RECORD +32 -31
- {lmnr-0.6.21.dist-info → lmnr-0.7.1.dist-info}/WHEEL +1 -1
- lmnr/opentelemetry_lib/tracing/context_properties.py +0 -65
- lmnr/sdk/browser/rrweb/rrweb.umd.min.cjs +0 -98
- {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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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(
|
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.
|
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
|
-
|
80
|
-
|
81
|
-
|
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):
|