lmnr 0.6.16__py3-none-any.whl → 0.7.26__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 (113) hide show
  1. lmnr/__init__.py +6 -15
  2. lmnr/cli/__init__.py +270 -0
  3. lmnr/cli/datasets.py +371 -0
  4. lmnr/{cli.py → cli/evals.py} +20 -102
  5. lmnr/cli/rules.py +42 -0
  6. lmnr/opentelemetry_lib/__init__.py +9 -2
  7. lmnr/opentelemetry_lib/decorators/__init__.py +274 -168
  8. lmnr/opentelemetry_lib/litellm/__init__.py +352 -38
  9. lmnr/opentelemetry_lib/litellm/utils.py +82 -0
  10. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/__init__.py +849 -0
  11. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/config.py +13 -0
  12. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/event_emitter.py +211 -0
  13. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/event_models.py +41 -0
  14. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/span_utils.py +401 -0
  15. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/streaming.py +425 -0
  16. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/utils.py +332 -0
  17. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/version.py +1 -0
  18. lmnr/opentelemetry_lib/opentelemetry/instrumentation/claude_agent/__init__.py +451 -0
  19. lmnr/opentelemetry_lib/opentelemetry/instrumentation/claude_agent/proxy.py +144 -0
  20. lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_agent/__init__.py +100 -0
  21. lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_computer/__init__.py +476 -0
  22. lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_computer/utils.py +12 -0
  23. lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/__init__.py +191 -129
  24. lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/schema_utils.py +26 -0
  25. lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/utils.py +126 -41
  26. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/__init__.py +488 -0
  27. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/config.py +8 -0
  28. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/event_emitter.py +143 -0
  29. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/event_models.py +41 -0
  30. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/span_utils.py +229 -0
  31. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/utils.py +92 -0
  32. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/version.py +1 -0
  33. lmnr/opentelemetry_lib/opentelemetry/instrumentation/kernel/__init__.py +381 -0
  34. lmnr/opentelemetry_lib/opentelemetry/instrumentation/kernel/utils.py +36 -0
  35. lmnr/opentelemetry_lib/opentelemetry/instrumentation/langgraph/__init__.py +16 -16
  36. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/__init__.py +61 -0
  37. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/__init__.py +472 -0
  38. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/chat_wrappers.py +1185 -0
  39. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/completion_wrappers.py +305 -0
  40. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/config.py +16 -0
  41. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/embeddings_wrappers.py +312 -0
  42. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/event_emitter.py +100 -0
  43. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/event_models.py +41 -0
  44. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/image_gen_wrappers.py +68 -0
  45. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/utils.py +197 -0
  46. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v0/__init__.py +176 -0
  47. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/__init__.py +368 -0
  48. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/assistant_wrappers.py +325 -0
  49. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/event_handler_wrapper.py +135 -0
  50. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/responses_wrappers.py +786 -0
  51. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/version.py +1 -0
  52. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openhands_ai/__init__.py +388 -0
  53. lmnr/opentelemetry_lib/opentelemetry/instrumentation/opentelemetry/__init__.py +69 -0
  54. lmnr/opentelemetry_lib/opentelemetry/instrumentation/skyvern/__init__.py +59 -61
  55. lmnr/opentelemetry_lib/opentelemetry/instrumentation/threading/__init__.py +197 -0
  56. lmnr/opentelemetry_lib/tracing/__init__.py +119 -18
  57. lmnr/opentelemetry_lib/tracing/_instrument_initializers.py +124 -25
  58. lmnr/opentelemetry_lib/tracing/attributes.py +4 -0
  59. lmnr/opentelemetry_lib/tracing/context.py +200 -0
  60. lmnr/opentelemetry_lib/tracing/exporter.py +109 -15
  61. lmnr/opentelemetry_lib/tracing/instruments.py +22 -5
  62. lmnr/opentelemetry_lib/tracing/processor.py +128 -30
  63. lmnr/opentelemetry_lib/tracing/span.py +398 -0
  64. lmnr/opentelemetry_lib/tracing/tracer.py +40 -1
  65. lmnr/opentelemetry_lib/tracing/utils.py +62 -0
  66. lmnr/opentelemetry_lib/utils/package_check.py +9 -0
  67. lmnr/opentelemetry_lib/utils/wrappers.py +11 -0
  68. lmnr/sdk/browser/background_send_events.py +158 -0
  69. lmnr/sdk/browser/browser_use_cdp_otel.py +100 -0
  70. lmnr/sdk/browser/browser_use_otel.py +12 -12
  71. lmnr/sdk/browser/bubus_otel.py +71 -0
  72. lmnr/sdk/browser/cdp_utils.py +518 -0
  73. lmnr/sdk/browser/inject_script.js +514 -0
  74. lmnr/sdk/browser/patchright_otel.py +18 -44
  75. lmnr/sdk/browser/playwright_otel.py +104 -187
  76. lmnr/sdk/browser/pw_utils.py +249 -210
  77. lmnr/sdk/browser/recorder/record.umd.min.cjs +84 -0
  78. lmnr/sdk/browser/utils.py +1 -1
  79. lmnr/sdk/client/asynchronous/async_client.py +47 -15
  80. lmnr/sdk/client/asynchronous/resources/__init__.py +2 -7
  81. lmnr/sdk/client/asynchronous/resources/browser_events.py +1 -0
  82. lmnr/sdk/client/asynchronous/resources/datasets.py +131 -0
  83. lmnr/sdk/client/asynchronous/resources/evals.py +122 -18
  84. lmnr/sdk/client/asynchronous/resources/evaluators.py +85 -0
  85. lmnr/sdk/client/asynchronous/resources/tags.py +4 -10
  86. lmnr/sdk/client/synchronous/resources/__init__.py +2 -2
  87. lmnr/sdk/client/synchronous/resources/datasets.py +131 -0
  88. lmnr/sdk/client/synchronous/resources/evals.py +83 -17
  89. lmnr/sdk/client/synchronous/resources/evaluators.py +85 -0
  90. lmnr/sdk/client/synchronous/resources/tags.py +4 -10
  91. lmnr/sdk/client/synchronous/sync_client.py +47 -15
  92. lmnr/sdk/datasets/__init__.py +94 -0
  93. lmnr/sdk/datasets/file_utils.py +91 -0
  94. lmnr/sdk/decorators.py +103 -23
  95. lmnr/sdk/evaluations.py +122 -33
  96. lmnr/sdk/laminar.py +816 -333
  97. lmnr/sdk/log.py +7 -2
  98. lmnr/sdk/types.py +124 -143
  99. lmnr/sdk/utils.py +115 -2
  100. lmnr/version.py +1 -1
  101. {lmnr-0.6.16.dist-info → lmnr-0.7.26.dist-info}/METADATA +71 -78
  102. lmnr-0.7.26.dist-info/RECORD +116 -0
  103. lmnr-0.7.26.dist-info/WHEEL +4 -0
  104. lmnr-0.7.26.dist-info/entry_points.txt +3 -0
  105. lmnr/opentelemetry_lib/tracing/context_properties.py +0 -65
  106. lmnr/sdk/browser/rrweb/rrweb.umd.min.cjs +0 -98
  107. lmnr/sdk/client/asynchronous/resources/agent.py +0 -329
  108. lmnr/sdk/client/synchronous/resources/agent.py +0 -323
  109. lmnr/sdk/datasets.py +0 -60
  110. lmnr-0.6.16.dist-info/LICENSE +0 -75
  111. lmnr-0.6.16.dist-info/RECORD +0 -61
  112. lmnr-0.6.16.dist-info/WHEEL +0 -4
  113. lmnr-0.6.16.dist-info/entry_points.txt +0 -3
@@ -0,0 +1,197 @@
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._lmnr_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
+ # Genearally, this must be set in __wrap_threading_start, but it is
163
+ # possible to Thread().run() without Thread().start(), so in that case,
164
+ # we need to capture the context here.
165
+ # We still want to capture the context in __wrap_threading_start,
166
+ # in order to stay close to the original implementation.
167
+ if not hasattr(instance, "_lmnr_otel_context"):
168
+ instance._lmnr_otel_context = get_current_context()
169
+ token = attach_context(instance._lmnr_otel_context)
170
+ return call_wrapped(*args, **kwargs)
171
+ finally:
172
+ if token is not None:
173
+ detach_context(token)
174
+
175
+ @staticmethod
176
+ def __wrap_thread_pool_submit(
177
+ call_wrapped: Callable[..., R],
178
+ instance: futures.ThreadPoolExecutor,
179
+ args: tuple[Callable[..., Any], ...],
180
+ kwargs: dict[str, Any],
181
+ ) -> R:
182
+ # obtain the original function and wrapped kwargs
183
+ original_func = args[0]
184
+ otel_context = get_current_context()
185
+
186
+ def wrapped_func(*func_args: Any, **func_kwargs: Any) -> R:
187
+ token = None
188
+ try:
189
+ token = attach_context(otel_context)
190
+ return original_func(*func_args, **func_kwargs)
191
+ finally:
192
+ if token is not None:
193
+ detach_context(token)
194
+
195
+ # replace the original function with the wrapped function
196
+ new_args: tuple[Callable[..., Any], ...] = (wrapped_func,) + args[1:]
197
+ return call_wrapped(*new_args, **kwargs)
@@ -4,15 +4,28 @@ import threading
4
4
 
5
5
  from lmnr.opentelemetry_lib.tracing.processor import LaminarSpanProcessor
6
6
  from lmnr.sdk.client.asynchronous.async_client import AsyncLaminarClient
7
- from lmnr.sdk.client.synchronous.sync_client import LaminarClient
7
+ from lmnr.sdk.types import SessionRecordingOptions
8
8
  from lmnr.sdk.log import VerboseColorfulFormatter
9
9
  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
+ clear_context,
16
+ pop_span_context as ctx_pop_span_context,
17
+ get_current_context,
18
+ get_token_stack,
19
+ push_span_context as ctx_push_span_context,
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
@@ -25,13 +38,14 @@ MAX_EVENTS_OR_ATTRIBUTES_PER_SPAN = 5000
25
38
  class TracerWrapper(object):
26
39
  resource_attributes: dict = {}
27
40
  enable_content_tracing: bool = True
41
+ session_recording_options: SessionRecordingOptions = {}
28
42
  _lock = threading.Lock()
29
43
  _tracer_provider: TracerProvider | None = None
30
44
  _logger: logging.Logger
31
- _client: LaminarClient
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,
@@ -39,33 +53,36 @@ class TracerWrapper(object):
39
53
  exporter: SpanExporter | None = None,
40
54
  instruments: set[Instruments] | None = None,
41
55
  block_instruments: set[Instruments] | None = None,
42
- base_url: str = "https://api.lmnr.ai",
56
+ base_url: str | None = None,
43
57
  port: int = 8443,
44
58
  http_port: int = 443,
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 = 10,
62
+ timeout_seconds: int = 30,
49
63
  set_global_tracer_provider: bool = True,
50
64
  otel_logger_level: int = logging.ERROR,
65
+ session_recording_options: SessionRecordingOptions | None = None,
51
66
  ) -> "TracerWrapper":
52
67
  # Silence some opentelemetry warnings
53
68
  logging.getLogger("opentelemetry.trace").setLevel(otel_logger_level)
54
69
 
55
- base_http_url = f"{base_url}:{http_port}"
70
+ base_http_url = f"{base_url}:{http_port}" if base_url else None
56
71
  with cls._lock:
57
72
  if not hasattr(cls, "instance"):
58
73
  cls._initialize_logger(cls)
59
74
  obj = super(TracerWrapper, cls).__new__(cls)
60
75
 
61
- obj._client = LaminarClient(
62
- base_url=base_http_url,
63
- project_api_key=project_api_key,
64
- )
65
- obj._async_client = AsyncLaminarClient(
66
- base_url=base_http_url,
67
- project_api_key=project_api_key,
68
- )
76
+ # Store session recording options
77
+ cls.session_recording_options = session_recording_options or {}
78
+
79
+ if project_api_key:
80
+ obj._async_client = AsyncLaminarClient(
81
+ base_url=base_http_url or "https://api.lmnr.ai",
82
+ project_api_key=project_api_key,
83
+ )
84
+ else:
85
+ obj._async_client = None
69
86
 
70
87
  obj._resource = Resource(attributes=TracerWrapper.resource_attributes)
71
88
 
@@ -91,6 +108,9 @@ class TracerWrapper(object):
91
108
 
92
109
  obj._tracer_provider.add_span_processor(obj._span_processor)
93
110
 
111
+ # Setup threading context inheritance
112
+ obj._setup_threading_inheritance()
113
+
94
114
  # This is not a real instrumentation and does not generate telemetry
95
115
  # data, but it is required to ensure that OpenTelemetry context
96
116
  # propagation is enabled.
@@ -102,7 +122,6 @@ class TracerWrapper(object):
102
122
  tracer_provider=obj._tracer_provider,
103
123
  instruments=instruments,
104
124
  block_instruments=block_instruments,
105
- client=obj._client,
106
125
  async_client=obj._async_client,
107
126
  )
108
127
 
@@ -113,6 +132,43 @@ class TracerWrapper(object):
113
132
 
114
133
  return cls.instance
115
134
 
135
+ def _setup_threading_inheritance(self):
136
+ """Setup threading inheritance for isolated context."""
137
+ if TracerWrapper._original_thread_init is None:
138
+ # Monkey patch Thread.__init__ to capture context inheritance
139
+ TracerWrapper._original_thread_init = threading.Thread.__init__
140
+
141
+ def patched_thread_init(thread_self, *args, **kwargs):
142
+ # Capture current isolated context and token stack for inheritance
143
+ current_context = get_current_context()
144
+ current_token_stack = get_token_stack().copy()
145
+
146
+ # Get the original target function
147
+ original_target = kwargs.get("target")
148
+ if not original_target and args:
149
+ original_target = args[0]
150
+
151
+ # Only inherit if we have a target function
152
+ if original_target:
153
+ # Create a wrapper function that sets up context
154
+ def thread_wrapper(*target_args, **target_kwargs):
155
+ # Set inherited context and token stack in the new thread
156
+ attach_context(current_context)
157
+ set_token_stack(current_token_stack)
158
+ # Run original target
159
+ return original_target(*target_args, **target_kwargs)
160
+
161
+ # Replace the target with our wrapper
162
+ if "target" in kwargs:
163
+ kwargs["target"] = thread_wrapper
164
+ elif args:
165
+ args = (thread_wrapper,) + args[1:]
166
+
167
+ # Call original init
168
+ TracerWrapper._original_thread_init(thread_self, *args, **kwargs)
169
+
170
+ threading.Thread.__init__ = patched_thread_init
171
+
116
172
  def exit_handler(self):
117
173
  if isinstance(self._span_processor, LaminarSpanProcessor):
118
174
  self._span_processor.clear()
@@ -124,6 +180,21 @@ class TracerWrapper(object):
124
180
  console_log_handler.setFormatter(VerboseColorfulFormatter())
125
181
  self._logger.addHandler(console_log_handler)
126
182
 
183
+ def get_isolated_context(self) -> Context:
184
+ """Get the current isolated context."""
185
+ return get_current_context()
186
+
187
+ def push_span_context(self, span: trace.Span) -> Context:
188
+ """Push a new context with the given span onto the stack."""
189
+ current_ctx = get_current_context()
190
+ new_context = trace.set_span_in_context(span, current_ctx)
191
+ ctx_push_span_context(new_context)
192
+ return new_context
193
+
194
+ def pop_span_context(self) -> None:
195
+ """Pop the current span context from the stack."""
196
+ ctx_pop_span_context()
197
+
127
198
  @staticmethod
128
199
  def set_static_params(
129
200
  resource_attributes: dict,
@@ -134,8 +205,19 @@ class TracerWrapper(object):
134
205
 
135
206
  @classmethod
136
207
  def verify_initialized(cls) -> bool:
137
- with cls._lock:
138
- return hasattr(cls, "instance") and hasattr(cls.instance, "_span_processor")
208
+ # This is not using lock, but it is fine to return False from here
209
+ # even if initialization is going on.
210
+
211
+ # If we try to acquire the lock here, it may deadlock if an automatic
212
+ # instrumentation is importing a file that (at the top level) has a
213
+ # function annotated with Laminar's `observe` decorator.
214
+ # The decorator is evaluated at import time, inside `init_instrumentations`,
215
+ # which is called by the `TracerWrapper` constructor while holding the lock.
216
+ # Without the lock here, we will simply return False, which will cause
217
+ # the decorator to return the original function. This is fine, at runtime,
218
+ # the next import statement will re-evaluate the decorator, and Laminar will
219
+ # have been initialized by that time.
220
+ return hasattr(cls, "instance") and hasattr(cls.instance, "_span_processor")
139
221
 
140
222
  @classmethod
141
223
  def clear(cls):
@@ -144,6 +226,8 @@ class TracerWrapper(object):
144
226
  # Any state cleanup. Now used in between tests
145
227
  if isinstance(cls.instance._span_processor, LaminarSpanProcessor):
146
228
  cls.instance._span_processor.clear()
229
+ # Clear the isolated context state for clean test state
230
+ clear_context()
147
231
 
148
232
  def shutdown(self):
149
233
  if self._tracer_provider is None:
@@ -156,7 +240,24 @@ class TracerWrapper(object):
156
240
  return False
157
241
  return self._span_processor.force_flush()
158
242
 
159
- def get_tracer(self):
243
+ def force_reinit_processor(self):
244
+ if isinstance(self._span_processor, LaminarSpanProcessor):
245
+ self._span_processor.force_flush()
246
+ self._span_processor.force_reinit()
247
+ # Clear the isolated context to prevent subsequent invocations
248
+ # (e.g., in Lambda) from continuing traces from previous invocations
249
+ clear_context()
250
+ else:
251
+ self._logger.warning(
252
+ "Not using LaminarSpanProcessor, cannot force reinit processor"
253
+ )
254
+
255
+ @classmethod
256
+ def get_session_recording_options(cls) -> SessionRecordingOptions:
257
+ """Get the session recording options set during initialization."""
258
+ return cls.session_recording_options
259
+
260
+ def get_tracer(self) -> trace.Tracer:
160
261
  if self._tracer_provider is None:
161
262
  return trace.get_tracer_provider().get_tracer(TRACER_NAME)
162
263
  return self._tracer_provider.get_tracer(TRACER_NAME)
@@ -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 is_package_installed
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,
@@ -50,13 +51,62 @@ class BedrockInstrumentorInitializer(InstrumentorInitializer):
50
51
 
51
52
 
52
53
  class BrowserUseInstrumentorInitializer(InstrumentorInitializer):
54
+ """Instruments for different versions of browser-use:
55
+
56
+ - browser-use < 0.5: BrowserUseLegacyInstrumentor to track agent_step and
57
+ other structure spans. Session instrumentation is controlled by
58
+ Instruments.PLAYWRIGHT (or Instruments.PATCHRIGHT for several versions
59
+ in 0.4.* that used patchright)
60
+ - browser-use ~= 0.5: Structure spans live in browser_use package itself.
61
+ Session instrumentation is controlled by Instruments.PLAYWRIGHT
62
+ - browser-use >= 0.6.0rc1: BubusInstrumentor to keep spans structure.
63
+ Session instrumentation is controlled by Instruments.BROWSER_USE_SESSION
64
+ """
65
+
53
66
  def init_instrumentor(self, *args, **kwargs) -> BaseInstrumentor | None:
54
67
  if not is_package_installed("browser-use"):
55
68
  return None
56
69
 
57
- from lmnr.sdk.browser.browser_use_otel import BrowserUseInstrumentor
70
+ version = get_package_version("browser-use")
71
+ from packaging.version import parse
72
+
73
+ if version and parse(version) < parse("0.5.0"):
74
+ from lmnr.sdk.browser.browser_use_otel import BrowserUseLegacyInstrumentor
75
+
76
+ return BrowserUseLegacyInstrumentor()
77
+
78
+ return None
79
+
80
+
81
+ class BrowserUseSessionInstrumentorInitializer(InstrumentorInitializer):
82
+ def init_instrumentor(
83
+ self, async_client, *args, **kwargs
84
+ ) -> BaseInstrumentor | None:
85
+ if not is_package_installed("browser-use"):
86
+ return None
87
+
88
+ version = get_package_version("browser-use")
89
+ from packaging.version import parse
90
+
91
+ if version and parse(version) >= parse("0.6.0rc1"):
92
+ from lmnr.sdk.browser.browser_use_cdp_otel import BrowserUseInstrumentor
93
+
94
+ if async_client is None:
95
+ return None
96
+
97
+ return BrowserUseInstrumentor(async_client)
58
98
 
59
- return BrowserUseInstrumentor()
99
+ return None
100
+
101
+
102
+ class BubusInstrumentorInitializer(InstrumentorInitializer):
103
+ def init_instrumentor(self, *args, **kwargs) -> BaseInstrumentor | None:
104
+ if not is_package_installed("bubus"):
105
+ return None
106
+
107
+ from lmnr.sdk.browser.bubus_otel import BubusInstrumentor
108
+
109
+ return BubusInstrumentor()
60
110
 
61
111
 
62
112
  class ChromaInstrumentorInitializer(InstrumentorInitializer):
@@ -71,6 +121,19 @@ class ChromaInstrumentorInitializer(InstrumentorInitializer):
71
121
  return ChromaInstrumentor()
72
122
 
73
123
 
124
+ class ClaudeAgentInstrumentorInitializer(InstrumentorInitializer):
125
+ def init_instrumentor(self, *args, **kwargs) -> BaseInstrumentor | None:
126
+ if not is_package_installed("claude-agent-sdk"):
127
+ return None
128
+
129
+ if not is_package_installed("lmnr-claude-code-proxy"):
130
+ return None
131
+
132
+ from ..opentelemetry.instrumentation.claude_agent import ClaudeAgentInstrumentor
133
+
134
+ return ClaudeAgentInstrumentor()
135
+
136
+
74
137
  class CohereInstrumentorInitializer(InstrumentorInitializer):
75
138
  def init_instrumentor(self, *args, **kwargs) -> BaseInstrumentor | None:
76
139
  if not is_package_installed("cohere"):
@@ -95,20 +158,24 @@ class CrewAIInstrumentorInitializer(InstrumentorInitializer):
95
158
  return CrewAiInstrumentor()
96
159
 
97
160
 
98
- class GoogleGenerativeAIInstrumentorInitializer(InstrumentorInitializer):
161
+ class CuaAgentInstrumentorInitializer(InstrumentorInitializer):
99
162
  def init_instrumentor(self, *args, **kwargs) -> BaseInstrumentor | None:
100
- if not is_package_installed("google-generativeai"):
163
+ if not is_package_installed("cua-agent"):
101
164
  return None
102
- if not is_package_installed(
103
- "opentelemetry-instrumentation-google-generativeai"
104
- ):
165
+
166
+ from ..opentelemetry.instrumentation.cua_agent import CuaAgentInstrumentor
167
+
168
+ return CuaAgentInstrumentor()
169
+
170
+
171
+ class CuaComputerInstrumentorInitializer(InstrumentorInitializer):
172
+ def init_instrumentor(self, *args, **kwargs) -> BaseInstrumentor | None:
173
+ if not is_package_installed("cua-computer"):
105
174
  return None
106
175
 
107
- from opentelemetry.instrumentation.google_generativeai import (
108
- GoogleGenerativeAiInstrumentor,
109
- )
176
+ from ..opentelemetry.instrumentation.cua_computer import CuaComputerInstrumentor
110
177
 
111
- return GoogleGenerativeAiInstrumentor()
178
+ return CuaComputerInstrumentor()
112
179
 
113
180
 
114
181
  class GoogleGenAIInstrumentorInitializer(InstrumentorInitializer):
@@ -127,10 +194,8 @@ class GroqInstrumentorInitializer(InstrumentorInitializer):
127
194
  def init_instrumentor(self, *args, **kwargs) -> BaseInstrumentor | None:
128
195
  if not is_package_installed("groq"):
129
196
  return None
130
- if not is_package_installed("opentelemetry-instrumentation-groq"):
131
- return None
132
197
 
133
- from opentelemetry.instrumentation.groq import GroqInstrumentor
198
+ from ..opentelemetry.instrumentation.groq import GroqInstrumentor
134
199
 
135
200
  return GroqInstrumentor()
136
201
 
@@ -147,6 +212,16 @@ class HaystackInstrumentorInitializer(InstrumentorInitializer):
147
212
  return HaystackInstrumentor()
148
213
 
149
214
 
215
+ class KernelInstrumentorInitializer(InstrumentorInitializer):
216
+ def init_instrumentor(self, *args, **kwargs) -> BaseInstrumentor | None:
217
+ if not is_package_installed("kernel"):
218
+ return None
219
+
220
+ from ..opentelemetry.instrumentation.kernel import KernelInstrumentor
221
+
222
+ return KernelInstrumentor()
223
+
224
+
150
225
  class LanceDBInstrumentorInitializer(InstrumentorInitializer):
151
226
  def init_instrumentor(self, *args, **kwargs) -> BaseInstrumentor | None:
152
227
  if not is_package_installed("lancedb"):
@@ -261,10 +336,8 @@ class OpenAIInstrumentorInitializer(InstrumentorInitializer):
261
336
  def init_instrumentor(self, *args, **kwargs) -> BaseInstrumentor | None:
262
337
  if not is_package_installed("openai"):
263
338
  return None
264
- if not is_package_installed("opentelemetry-instrumentation-openai"):
265
- return None
266
339
 
267
- from opentelemetry.instrumentation.openai import OpenAIInstrumentor
340
+ from ..opentelemetry.instrumentation.openai import OpenAIInstrumentor
268
341
 
269
342
  return OpenAIInstrumentor(
270
343
  # Default in the package provided is an empty function, which
@@ -274,16 +347,37 @@ class OpenAIInstrumentorInitializer(InstrumentorInitializer):
274
347
  )
275
348
 
276
349
 
350
+ class OpenHandsAIInstrumentorInitializer(InstrumentorInitializer):
351
+ def init_instrumentor(self, *args, **kwargs) -> BaseInstrumentor | None:
352
+ if not is_package_installed("openhands-ai"):
353
+ return None
354
+ from ..opentelemetry.instrumentation.openhands_ai import OpenHandsInstrumentor
355
+
356
+ return OpenHandsInstrumentor()
357
+
358
+
359
+ class OpenTelemetryInstrumentorInitializer(InstrumentorInitializer):
360
+ def init_instrumentor(self, *args, **kwargs) -> BaseInstrumentor | None:
361
+ from ..opentelemetry.instrumentation.opentelemetry import (
362
+ OpentelemetryInstrumentor,
363
+ )
364
+
365
+ return OpentelemetryInstrumentor()
366
+
367
+
277
368
  class PatchrightInstrumentorInitializer(InstrumentorInitializer):
278
369
  def init_instrumentor(
279
- self, client, async_client, *args, **kwargs
370
+ self, async_client, *args, **kwargs
280
371
  ) -> BaseInstrumentor | None:
281
372
  if not is_package_installed("patchright"):
282
373
  return None
283
374
 
284
375
  from lmnr.sdk.browser.patchright_otel import PatchrightInstrumentor
285
376
 
286
- return PatchrightInstrumentor(client, async_client)
377
+ if async_client is None:
378
+ return None
379
+
380
+ return PatchrightInstrumentor(async_client)
287
381
 
288
382
 
289
383
  class PineconeInstrumentorInitializer(InstrumentorInitializer):
@@ -300,14 +394,17 @@ class PineconeInstrumentorInitializer(InstrumentorInitializer):
300
394
 
301
395
  class PlaywrightInstrumentorInitializer(InstrumentorInitializer):
302
396
  def init_instrumentor(
303
- self, client, async_client, *args, **kwargs
397
+ self, async_client, *args, **kwargs
304
398
  ) -> BaseInstrumentor | None:
305
399
  if not is_package_installed("playwright"):
306
400
  return None
307
401
 
308
402
  from lmnr.sdk.browser.playwright_otel import PlaywrightInstrumentor
309
403
 
310
- return PlaywrightInstrumentor(client, async_client)
404
+ if async_client is None:
405
+ return None
406
+
407
+ return PlaywrightInstrumentor(async_client)
311
408
 
312
409
 
313
410
  class QdrantInstrumentorInitializer(InstrumentorInitializer):
@@ -345,6 +442,7 @@ class SageMakerInstrumentorInitializer(InstrumentorInitializer):
345
442
 
346
443
  return SageMakerInstrumentor()
347
444
 
445
+
348
446
  class SkyvernInstrumentorInitializer(InstrumentorInitializer):
349
447
  def init_instrumentor(self, *args, **kwargs) -> BaseInstrumentor | None:
350
448
  if not is_package_installed("skyvern"):
@@ -354,6 +452,7 @@ class SkyvernInstrumentorInitializer(InstrumentorInitializer):
354
452
 
355
453
  return SkyvernInstrumentor()
356
454
 
455
+
357
456
  class TogetherInstrumentorInitializer(InstrumentorInitializer):
358
457
  def init_instrumentor(self, *args, **kwargs) -> BaseInstrumentor | None:
359
458
  if not is_package_installed("together"):