lmnr 0.6.20__py3-none-any.whl → 0.7.0__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 +211 -151
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/__init__.py +678 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/config.py +13 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/event_emitter.py +211 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/event_models.py +41 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/span_utils.py +256 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/streaming.py +295 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/utils.py +179 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/version.py +1 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/__init__.py +4 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/__init__.py +488 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/config.py +8 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/event_emitter.py +143 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/event_models.py +41 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/span_utils.py +229 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/utils.py +92 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/version.py +1 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/langgraph/__init__.py +16 -16
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/chat_wrappers.py +3 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/completion_wrappers.py +3 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/utils.py +3 -3
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/assistant_wrappers.py +3 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/responses_wrappers.py +7 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/threading/__init__.py +190 -0
- lmnr/opentelemetry_lib/tracing/__init__.py +90 -2
- lmnr/opentelemetry_lib/tracing/_instrument_initializers.py +12 -7
- lmnr/opentelemetry_lib/tracing/context.py +109 -0
- lmnr/opentelemetry_lib/tracing/processor.py +6 -7
- lmnr/opentelemetry_lib/tracing/tracer.py +29 -0
- lmnr/opentelemetry_lib/utils/package_check.py +9 -0
- lmnr/sdk/browser/browser_use_otel.py +9 -7
- lmnr/sdk/browser/patchright_otel.py +14 -26
- lmnr/sdk/browser/playwright_otel.py +72 -73
- lmnr/sdk/browser/pw_utils.py +436 -119
- lmnr/sdk/client/asynchronous/resources/browser_events.py +1 -0
- lmnr/sdk/decorators.py +39 -4
- lmnr/sdk/evaluations.py +23 -9
- lmnr/sdk/laminar.py +181 -209
- lmnr/sdk/types.py +0 -6
- lmnr/version.py +1 -1
- {lmnr-0.6.20.dist-info → lmnr-0.7.0.dist-info}/METADATA +10 -8
- {lmnr-0.6.20.dist-info → lmnr-0.7.0.dist-info}/RECORD +45 -29
- {lmnr-0.6.20.dist-info → lmnr-0.7.0.dist-info}/WHEEL +1 -1
- lmnr/opentelemetry_lib/tracing/context_properties.py +0 -65
- {lmnr-0.6.20.dist-info → lmnr-0.7.0.dist-info}/entry_points.txt +0 -0
@@ -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,
|
@@ -45,7 +59,7 @@ class TracerWrapper(object):
|
|
45
59
|
project_api_key: str | None = None,
|
46
60
|
max_export_batch_size: int | None = None,
|
47
61
|
force_http: bool = False,
|
48
|
-
timeout_seconds: int =
|
62
|
+
timeout_seconds: int = 30,
|
49
63
|
set_global_tracer_provider: bool = True,
|
50
64
|
otel_logger_level: int = logging.ERROR,
|
51
65
|
) -> "TracerWrapper":
|
@@ -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:
|
@@ -2,7 +2,10 @@ import abc
|
|
2
2
|
|
3
3
|
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
|
4
4
|
|
5
|
-
from lmnr.opentelemetry_lib.utils.package_check import
|
5
|
+
from lmnr.opentelemetry_lib.utils.package_check import (
|
6
|
+
get_package_version,
|
7
|
+
is_package_installed,
|
8
|
+
)
|
6
9
|
|
7
10
|
|
8
11
|
class InstrumentorInitializer(abc.ABC):
|
@@ -27,10 +30,8 @@ class AnthropicInstrumentorInitializer(InstrumentorInitializer):
|
|
27
30
|
def init_instrumentor(self, *args, **kwargs) -> BaseInstrumentor | None:
|
28
31
|
if not is_package_installed("anthropic"):
|
29
32
|
return None
|
30
|
-
if not is_package_installed("opentelemetry-instrumentation-anthropic"):
|
31
|
-
return None
|
32
33
|
|
33
|
-
from opentelemetry.instrumentation.anthropic import AnthropicInstrumentor
|
34
|
+
from ..opentelemetry.instrumentation.anthropic import AnthropicInstrumentor
|
34
35
|
|
35
36
|
return AnthropicInstrumentor(
|
36
37
|
upload_base64_image=None,
|
@@ -54,6 +55,12 @@ class BrowserUseInstrumentorInitializer(InstrumentorInitializer):
|
|
54
55
|
if not is_package_installed("browser-use"):
|
55
56
|
return None
|
56
57
|
|
58
|
+
version = get_package_version("browser-use")
|
59
|
+
from packaging.version import parse
|
60
|
+
|
61
|
+
if version and parse(version) >= parse("0.5.0"):
|
62
|
+
return None
|
63
|
+
|
57
64
|
from lmnr.sdk.browser.browser_use_otel import BrowserUseInstrumentor
|
58
65
|
|
59
66
|
return BrowserUseInstrumentor()
|
@@ -127,10 +134,8 @@ class GroqInstrumentorInitializer(InstrumentorInitializer):
|
|
127
134
|
def init_instrumentor(self, *args, **kwargs) -> BaseInstrumentor | None:
|
128
135
|
if not is_package_installed("groq"):
|
129
136
|
return None
|
130
|
-
if not is_package_installed("opentelemetry-instrumentation-groq"):
|
131
|
-
return None
|
132
137
|
|
133
|
-
from opentelemetry.instrumentation.groq import GroqInstrumentor
|
138
|
+
from ..opentelemetry.instrumentation.groq import GroqInstrumentor
|
134
139
|
|
135
140
|
return GroqInstrumentor()
|
136
141
|
|
@@ -0,0 +1,109 @@
|
|
1
|
+
import threading
|
2
|
+
|
3
|
+
from abc import ABC, abstractmethod
|
4
|
+
from contextvars import ContextVar
|
5
|
+
from opentelemetry.context import Context, Token
|
6
|
+
|
7
|
+
|
8
|
+
class _IsolatedRuntimeContext(ABC):
|
9
|
+
"""The isolated RuntimeContext interface, identical to OpenTelemetry's _RuntimeContext
|
10
|
+
but isolated from the global context.
|
11
|
+
"""
|
12
|
+
|
13
|
+
@abstractmethod
|
14
|
+
def attach(self, context: Context) -> Token[Context]:
|
15
|
+
"""Sets the current `Context` object. Returns a
|
16
|
+
token that can be used to reset to the previous `Context`.
|
17
|
+
|
18
|
+
Args:
|
19
|
+
context: The Context to set.
|
20
|
+
"""
|
21
|
+
|
22
|
+
@abstractmethod
|
23
|
+
def get_current(self) -> Context:
|
24
|
+
"""Returns the current `Context` object."""
|
25
|
+
|
26
|
+
@abstractmethod
|
27
|
+
def detach(self, token: Token[Context]) -> None:
|
28
|
+
"""Resets Context to a previous value
|
29
|
+
|
30
|
+
Args:
|
31
|
+
token: A reference to a previous Context.
|
32
|
+
"""
|
33
|
+
|
34
|
+
|
35
|
+
class IsolatedContextVarsRuntimeContext(_IsolatedRuntimeContext):
|
36
|
+
"""An isolated implementation of the RuntimeContext interface which wraps ContextVar
|
37
|
+
but uses its own ContextVar instead of the global one.
|
38
|
+
"""
|
39
|
+
|
40
|
+
def __init__(self) -> None:
|
41
|
+
self._current_context = ContextVar(
|
42
|
+
"isolated_current_context", default=Context()
|
43
|
+
)
|
44
|
+
|
45
|
+
def attach(self, context: Context) -> Token[Context]:
|
46
|
+
"""Sets the current `Context` object. Returns a
|
47
|
+
token that can be used to reset to the previous `Context`.
|
48
|
+
|
49
|
+
Args:
|
50
|
+
context: The Context to set.
|
51
|
+
"""
|
52
|
+
return self._current_context.set(context)
|
53
|
+
|
54
|
+
def get_current(self) -> Context:
|
55
|
+
"""Returns the current `Context` object."""
|
56
|
+
return self._current_context.get()
|
57
|
+
|
58
|
+
def detach(self, token: Token[Context]) -> None:
|
59
|
+
"""Resets Context to a previous value
|
60
|
+
|
61
|
+
Args:
|
62
|
+
token: A reference to a previous Context.
|
63
|
+
"""
|
64
|
+
self._current_context.reset(token)
|
65
|
+
|
66
|
+
|
67
|
+
# Create the isolated runtime context
|
68
|
+
_ISOLATED_RUNTIME_CONTEXT = IsolatedContextVarsRuntimeContext()
|
69
|
+
|
70
|
+
# Token stack for push/pop API compatibility - much lighter than copying contexts
|
71
|
+
_isolated_token_stack: ContextVar[list[Token[Context]]] = ContextVar(
|
72
|
+
"isolated_token_stack", default=[]
|
73
|
+
)
|
74
|
+
|
75
|
+
# Thread-local storage for threading support
|
76
|
+
_isolated_token_stack_storage = threading.local()
|
77
|
+
|
78
|
+
|
79
|
+
def get_token_stack() -> list[Token[Context]]:
|
80
|
+
"""Get the token stack, supporting both asyncio and threading."""
|
81
|
+
try:
|
82
|
+
return _isolated_token_stack.get()
|
83
|
+
except LookupError:
|
84
|
+
if not hasattr(_isolated_token_stack_storage, "token_stack"):
|
85
|
+
_isolated_token_stack_storage.token_stack = []
|
86
|
+
return _isolated_token_stack_storage.token_stack
|
87
|
+
|
88
|
+
|
89
|
+
def set_token_stack(stack: list[Token[Context]]) -> None:
|
90
|
+
"""Set the token stack, supporting both asyncio and threading."""
|
91
|
+
try:
|
92
|
+
_isolated_token_stack.set(stack)
|
93
|
+
except LookupError:
|
94
|
+
_isolated_token_stack_storage.token_stack = stack
|
95
|
+
|
96
|
+
|
97
|
+
def get_current_context() -> Context:
|
98
|
+
"""Get the current isolated context."""
|
99
|
+
return _ISOLATED_RUNTIME_CONTEXT.get_current()
|
100
|
+
|
101
|
+
|
102
|
+
def attach_context(context: Context) -> Token[Context]:
|
103
|
+
"""Attach a context to the isolated runtime context."""
|
104
|
+
return _ISOLATED_RUNTIME_CONTEXT.attach(context)
|
105
|
+
|
106
|
+
|
107
|
+
def detach_context(token: Token[Context]) -> None:
|
108
|
+
"""Detach a context from the isolated runtime context."""
|
109
|
+
_ISOLATED_RUNTIME_CONTEXT.detach(token)
|
@@ -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
|
|
@@ -35,7 +32,7 @@ class LaminarSpanProcessor(SpanProcessor):
|
|
35
32
|
api_key: str | None = None,
|
36
33
|
timeout_seconds: int = 30,
|
37
34
|
force_http: bool = False,
|
38
|
-
max_export_batch_size: int =
|
35
|
+
max_export_batch_size: int = 64,
|
39
36
|
disable_batch: bool = False,
|
40
37
|
exporter: SpanExporter | None = None,
|
41
38
|
):
|
@@ -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):
|
@@ -1,6 +1,8 @@
|
|
1
1
|
from contextlib import contextmanager
|
2
|
+
from typing import Generator, Tuple
|
2
3
|
|
3
4
|
from opentelemetry import trace
|
5
|
+
from opentelemetry.context import Context
|
4
6
|
from lmnr.opentelemetry_lib.tracing import TracerWrapper
|
5
7
|
|
6
8
|
|
@@ -16,3 +18,30 @@ def get_tracer(flush_on_exit: bool = False):
|
|
16
18
|
finally:
|
17
19
|
if flush_on_exit:
|
18
20
|
wrapper.flush()
|
21
|
+
|
22
|
+
|
23
|
+
@contextmanager
|
24
|
+
def get_tracer_with_context(
|
25
|
+
flush_on_exit: bool = False,
|
26
|
+
) -> Generator[Tuple[trace.Tracer, Context], None, None]:
|
27
|
+
"""Get tracer with isolated context. Returns (tracer, context) tuple."""
|
28
|
+
wrapper = TracerWrapper()
|
29
|
+
try:
|
30
|
+
tracer = wrapper.get_tracer()
|
31
|
+
context = wrapper.get_isolated_context()
|
32
|
+
yield tracer, context
|
33
|
+
finally:
|
34
|
+
if flush_on_exit:
|
35
|
+
wrapper.flush()
|
36
|
+
|
37
|
+
|
38
|
+
def copy_current_context() -> Context:
|
39
|
+
"""Copy the current isolated context for use in threads/tasks."""
|
40
|
+
wrapper = TracerWrapper()
|
41
|
+
return wrapper.get_isolated_context()
|
42
|
+
|
43
|
+
|
44
|
+
def set_context_for_thread(context: Context) -> None:
|
45
|
+
"""Set the isolated context for the current thread."""
|
46
|
+
wrapper = TracerWrapper()
|
47
|
+
wrapper.set_isolated_context(context)
|
@@ -1,5 +1,7 @@
|
|
1
1
|
from importlib.metadata import distributions
|
2
2
|
|
3
|
+
from typing import Optional
|
4
|
+
|
3
5
|
installed_packages = {
|
4
6
|
(dist.name or dist.metadata.get("Name", "")).lower() for dist in distributions()
|
5
7
|
}
|
@@ -7,3 +9,10 @@ installed_packages = {
|
|
7
9
|
|
8
10
|
def is_package_installed(package_name: str) -> bool:
|
9
11
|
return package_name.lower() in installed_packages
|
12
|
+
|
13
|
+
|
14
|
+
def get_package_version(package_name: str) -> Optional[str]:
|
15
|
+
for dist in distributions():
|
16
|
+
if (dist.name or dist.metadata.get("Name", "")).lower() == package_name.lower():
|
17
|
+
return dist.version
|
18
|
+
return None
|
@@ -1,4 +1,5 @@
|
|
1
1
|
from lmnr.opentelemetry_lib.decorators import json_dumps
|
2
|
+
from lmnr.sdk.laminar import Laminar
|
2
3
|
from lmnr.sdk.browser.utils import with_tracer_wrapper
|
3
4
|
from lmnr.sdk.utils import get_input_from_func_args
|
4
5
|
from lmnr.version import __version__
|
@@ -19,7 +20,7 @@ except ImportError as e:
|
|
19
20
|
"to install Browser Use or remove this import."
|
20
21
|
) from e
|
21
22
|
|
22
|
-
_instruments = ("browser-use
|
23
|
+
_instruments = ("browser-use >= 0.1.0",)
|
23
24
|
|
24
25
|
WRAPPED_METHODS = [
|
25
26
|
{
|
@@ -27,8 +28,8 @@ WRAPPED_METHODS = [
|
|
27
28
|
"object": "Agent",
|
28
29
|
"method": "run",
|
29
30
|
"span_name": "agent.run",
|
30
|
-
"ignore_input":
|
31
|
-
"ignore_output":
|
31
|
+
"ignore_input": True,
|
32
|
+
"ignore_output": True,
|
32
33
|
"span_type": "DEFAULT",
|
33
34
|
},
|
34
35
|
{
|
@@ -46,15 +47,15 @@ WRAPPED_METHODS = [
|
|
46
47
|
"method": "act",
|
47
48
|
"span_name": "controller.act",
|
48
49
|
"ignore_input": True,
|
49
|
-
"ignore_output":
|
50
|
+
"ignore_output": True,
|
50
51
|
"span_type": "DEFAULT",
|
51
52
|
},
|
52
53
|
{
|
53
54
|
"package": "browser_use.controller.registry.service",
|
54
55
|
"object": "Registry",
|
55
56
|
"method": "execute_action",
|
56
|
-
"ignore_input":
|
57
|
-
"ignore_output":
|
57
|
+
"ignore_input": True,
|
58
|
+
"ignore_output": True,
|
58
59
|
"span_type": "TOOL",
|
59
60
|
},
|
60
61
|
]
|
@@ -86,7 +87,8 @@ async def _wrap(tracer: Tracer, to_wrap, wrapped, instance, args, kwargs):
|
|
86
87
|
step_info = kwargs.get("step_info", args[0] if len(args) > 0 else None)
|
87
88
|
if step_info and hasattr(step_info, "step_number"):
|
88
89
|
span_name = f"agent.step.{step_info.step_number}"
|
89
|
-
|
90
|
+
|
91
|
+
with Laminar.start_as_current_span(span_name) as span:
|
90
92
|
span.set_attributes(attributes)
|
91
93
|
result = await wrapped(*args, **kwargs)
|
92
94
|
if not to_wrap.get("ignore_output"):
|