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
@@ -14,13 +14,17 @@ SPAN_OUTPUT = "lmnr.span.output"
14
14
  SPAN_TYPE = "lmnr.span.type"
15
15
  SPAN_PATH = "lmnr.span.path"
16
16
  SPAN_IDS_PATH = "lmnr.span.ids_path"
17
+ PARENT_SPAN_PATH = "lmnr.span.parent_path"
18
+ PARENT_SPAN_IDS_PATH = "lmnr.span.parent_ids_path"
17
19
  SPAN_INSTRUMENTATION_SOURCE = "lmnr.span.instrumentation_source"
18
20
  SPAN_SDK_VERSION = "lmnr.span.sdk_version"
19
21
  SPAN_LANGUAGE_VERSION = "lmnr.span.language_version"
22
+ HUMAN_EVALUATOR_OPTIONS = "lmnr.span.human_evaluator_options"
20
23
 
21
24
  ASSOCIATION_PROPERTIES = "lmnr.association.properties"
22
25
  SESSION_ID = "session_id"
23
26
  USER_ID = "user_id"
27
+ METADATA = "metadata"
24
28
  TRACE_TYPE = "trace_type"
25
29
  TRACING_LEVEL = "tracing_level"
26
30
 
@@ -0,0 +1,200 @@
1
+ import threading
2
+
3
+ from abc import ABC, abstractmethod
4
+ from contextvars import ContextVar
5
+ from typing import Any
6
+ from opentelemetry.context import Context, Token, create_key, get_value, set_value
7
+
8
+ from lmnr.opentelemetry_lib.tracing.attributes import (
9
+ METADATA,
10
+ SESSION_ID,
11
+ TRACE_TYPE,
12
+ USER_ID,
13
+ )
14
+ from lmnr.sdk.types import TraceType
15
+
16
+
17
+ class _IsolatedRuntimeContext(ABC):
18
+ """The isolated RuntimeContext interface, identical to OpenTelemetry's _RuntimeContext
19
+ but isolated from the global context.
20
+ """
21
+
22
+ @abstractmethod
23
+ def attach(self, context: Context) -> Token[Context]:
24
+ """Sets the current `Context` object. Returns a
25
+ token that can be used to reset to the previous `Context`.
26
+
27
+ Args:
28
+ context: The Context to set.
29
+ """
30
+
31
+ @abstractmethod
32
+ def get_current(self) -> Context:
33
+ """Returns the current `Context` object."""
34
+
35
+ @abstractmethod
36
+ def detach(self, token: Token[Context]) -> None:
37
+ """Resets Context to a previous value
38
+
39
+ Args:
40
+ token: A reference to a previous Context.
41
+ """
42
+
43
+
44
+ class IsolatedContextVarsRuntimeContext(_IsolatedRuntimeContext):
45
+ """An isolated implementation of the RuntimeContext interface which wraps ContextVar
46
+ but uses its own ContextVar instead of the global one.
47
+ """
48
+
49
+ def __init__(self) -> None:
50
+ self._current_context = ContextVar(
51
+ "isolated_current_context", default=Context()
52
+ )
53
+
54
+ def attach(self, context: Context) -> Token[Context]:
55
+ """Sets the current `Context` object. Returns a
56
+ token that can be used to reset to the previous `Context`.
57
+
58
+ Args:
59
+ context: The Context to set.
60
+ """
61
+ return self._current_context.set(context)
62
+
63
+ def get_current(self) -> Context:
64
+ """Returns the current `Context` object."""
65
+ return self._current_context.get()
66
+
67
+ def detach(self, token: Token[Context]) -> None:
68
+ """Resets Context to a previous value
69
+
70
+ Args:
71
+ token: A reference to a previous Context.
72
+ """
73
+ self._current_context.reset(token)
74
+
75
+
76
+ # Create the isolated runtime context
77
+ _ISOLATED_RUNTIME_CONTEXT = IsolatedContextVarsRuntimeContext()
78
+
79
+ # Token stack for push/pop API compatibility - much lighter than copying contexts
80
+ _isolated_token_stack: ContextVar[list[Token[Context]]] = ContextVar(
81
+ "isolated_token_stack", default=[]
82
+ )
83
+
84
+ # Thread-local storage for threading support
85
+ _isolated_token_stack_storage = threading.local()
86
+
87
+
88
+ def get_token_stack() -> list[Token[Context]]:
89
+ """Get the token stack, supporting both asyncio and threading."""
90
+ try:
91
+ return _isolated_token_stack.get()
92
+ except LookupError:
93
+ if not hasattr(_isolated_token_stack_storage, "token_stack"):
94
+ _isolated_token_stack_storage.token_stack = []
95
+ return _isolated_token_stack_storage.token_stack
96
+
97
+
98
+ def set_token_stack(stack: list[Token[Context]]) -> None:
99
+ """Set the token stack, supporting both asyncio and threading."""
100
+ try:
101
+ _isolated_token_stack.set(stack)
102
+ except LookupError:
103
+ _isolated_token_stack_storage.token_stack = stack
104
+
105
+
106
+ def get_current_context() -> Context:
107
+ """Get the current isolated context."""
108
+ return _ISOLATED_RUNTIME_CONTEXT.get_current()
109
+
110
+
111
+ def attach_context(context: Context) -> Token[Context]:
112
+ """Attach a context to the isolated runtime context."""
113
+ return _ISOLATED_RUNTIME_CONTEXT.attach(context)
114
+
115
+
116
+ def detach_context(token: Token[Context]) -> None:
117
+ """Detach a context from the isolated runtime context."""
118
+ _ISOLATED_RUNTIME_CONTEXT.detach(token)
119
+
120
+
121
+ CONTEXT_USER_ID_KEY = create_key(f"lmnr.{USER_ID}")
122
+ CONTEXT_SESSION_ID_KEY = create_key(f"lmnr.{SESSION_ID}")
123
+ CONTEXT_METADATA_KEY = create_key(f"lmnr.{METADATA}")
124
+ CONTEXT_TRACE_TYPE_KEY = create_key(f"lmnr.{TRACE_TYPE}")
125
+
126
+
127
+ def get_event_attributes_from_context(context: Context | None = None) -> dict[str, str]:
128
+ """Get the event attributes from the context."""
129
+ context = context or get_current_context()
130
+ attributes = {}
131
+ if session_id := get_value(CONTEXT_SESSION_ID_KEY, context):
132
+ attributes["lmnr.event.session_id"] = session_id
133
+ if user_id := get_value(CONTEXT_USER_ID_KEY, context):
134
+ attributes["lmnr.event.user_id"] = user_id
135
+ return attributes
136
+
137
+
138
+ def set_association_prop_context(
139
+ user_id: str | None = None,
140
+ session_id: str | None = None,
141
+ trace_type: TraceType | None = None,
142
+ context: Context | None = None,
143
+ metadata: dict[str, Any] | None = None,
144
+ attach: bool = True,
145
+ ) -> Context:
146
+ context = context or get_current_context()
147
+ if user_id is not None:
148
+ context = set_value(CONTEXT_USER_ID_KEY, user_id, context)
149
+ if session_id is not None:
150
+ context = set_value(CONTEXT_SESSION_ID_KEY, session_id, context)
151
+ if trace_type is not None:
152
+ context = set_value(CONTEXT_TRACE_TYPE_KEY, trace_type.value, context)
153
+ if metadata is not None:
154
+ context = set_value(CONTEXT_METADATA_KEY, metadata, context)
155
+ if attach:
156
+ attach_context(context)
157
+ return context
158
+
159
+
160
+ def pop_span_context() -> None:
161
+ """Pop the current span context from the stack."""
162
+ current_stack = get_token_stack().copy()
163
+ if current_stack:
164
+ token = current_stack.pop()
165
+ set_token_stack(current_stack)
166
+ detach_context(token)
167
+
168
+
169
+ def push_span_context(context: Context) -> None:
170
+ """Push a new span context onto the stack."""
171
+ token = attach_context(context)
172
+ token_stack = get_token_stack().copy()
173
+ token_stack.append(token)
174
+ set_token_stack(token_stack)
175
+
176
+
177
+ def clear_context() -> None:
178
+ """Clear the isolated context and token stack.
179
+
180
+ This is primarily used during force_flush operations in Lambda-like
181
+ environments to ensure subsequent invocations don't continue traces
182
+ from previous invocations.
183
+
184
+ Warning: This should only be called when you're certain no spans are
185
+ actively being processed, as it will reset all context state.
186
+ """
187
+ # Clear the token stack first
188
+ try:
189
+ _isolated_token_stack.set([])
190
+ except LookupError:
191
+ pass
192
+
193
+ # Clear thread-local storage if it exists
194
+ if hasattr(_isolated_token_stack_storage, "token_stack"):
195
+ _isolated_token_stack_storage.token_stack = []
196
+
197
+ # Reset the context to a fresh empty context
198
+ # This doesn't require manually detaching tokens since we're
199
+ # intentionally resetting everything to a clean state
200
+ _ISOLATED_RUNTIME_CONTEXT._current_context.set(Context())
@@ -1,20 +1,30 @@
1
1
  import grpc
2
2
  import re
3
+ import threading
4
+ from urllib.parse import urlparse, urlunparse
3
5
  from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult
4
6
  from opentelemetry.sdk.trace import ReadableSpan
5
7
  from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
6
8
  OTLPSpanExporter,
7
9
  )
8
- from opentelemetry.exporter.otlp.proto.http import Compression
10
+ from opentelemetry.exporter.otlp.proto.http import Compression as HTTPCompression
9
11
  from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
10
12
  OTLPSpanExporter as HTTPOTLPSpanExporter,
11
13
  )
12
14
 
13
- from lmnr.sdk.utils import from_env
15
+ from lmnr.sdk.log import get_default_logger
16
+ from lmnr.sdk.utils import from_env, get_otel_env_var, parse_otel_headers
17
+
18
+ logger = get_default_logger(__name__)
14
19
 
15
20
 
16
21
  class LaminarSpanExporter(SpanExporter):
17
22
  instance: OTLPSpanExporter | HTTPOTLPSpanExporter
23
+ endpoint: str
24
+ headers: dict[str, str]
25
+ timeout: float
26
+ force_http: bool
27
+ _instance_lock: threading.RLock
18
28
 
19
29
  def __init__(
20
30
  self,
@@ -24,6 +34,7 @@ class LaminarSpanExporter(SpanExporter):
24
34
  timeout_seconds: int = 30,
25
35
  force_http: bool = False,
26
36
  ):
37
+ self._instance_lock = threading.RLock()
27
38
  url = base_url or from_env("LMNR_BASE_URL") or "https://api.lmnr.ai"
28
39
  url = url.rstrip("/")
29
40
  if match := re.search(r":(\d{1,5})$", url):
@@ -34,26 +45,109 @@ class LaminarSpanExporter(SpanExporter):
34
45
  port = 443 if force_http else 8443
35
46
  final_url = f"{url}:{port or 443}"
36
47
  api_key = api_key or from_env("LMNR_PROJECT_API_KEY")
37
- if force_http:
38
- self.instance = HTTPOTLPSpanExporter(
39
- endpoint=f"{final_url}/v1/traces",
40
- headers={"Authorization": f"Bearer {api_key}"},
41
- compression=Compression.Gzip,
42
- timeout=timeout_seconds,
48
+ self.endpoint = final_url
49
+ if api_key:
50
+ self.headers = (
51
+ {"Authorization": f"Bearer {api_key}"}
52
+ if force_http
53
+ else {"authorization": f"Bearer {api_key}"}
54
+ )
55
+ elif get_otel_env_var("HEADERS"):
56
+ self.headers = parse_otel_headers(get_otel_env_var("HEADERS"))
57
+ else:
58
+ self.headers = {}
59
+ self.timeout = timeout_seconds
60
+ self.force_http = force_http
61
+ if get_otel_env_var("ENDPOINT"):
62
+ if not base_url:
63
+ self.endpoint = get_otel_env_var("ENDPOINT")
64
+ else:
65
+ logger.warning(
66
+ "OTEL_ENDPOINT is set, but Laminar base URL is also set. Ignoring OTEL_ENDPOINT."
67
+ )
68
+ protocol = get_otel_env_var("PROTOCOL") or "grpc/protobuf"
69
+ exporter_type = from_env("OTEL_EXPORTER") or "otlp_grpc"
70
+ self.force_http = (
71
+ protocol in ("http/protobuf", "http/json")
72
+ or exporter_type == "otlp_http"
73
+ )
74
+ if not self.endpoint:
75
+ raise ValueError(
76
+ "Laminar base URL is not set and OTEL_ENDPOINT is not set. Please either\n"
77
+ "- set the LMNR_BASE_URL environment variable\n"
78
+ "- set the OTEL_ENDPOINT environment variable\n"
79
+ "- pass the base_url parameter to Laminar.initialize"
80
+ )
81
+ self._init_instance()
82
+
83
+ def _normalize_http_endpoint(self, endpoint: str) -> str:
84
+ """
85
+ Normalize HTTP endpoint URL by adding /v1/traces path if no path is present.
86
+
87
+ Args:
88
+ endpoint: The endpoint URL to normalize
89
+
90
+ Returns:
91
+ The normalized endpoint URL with /v1/traces path if needed
92
+ """
93
+ try:
94
+ parsed = urlparse(endpoint)
95
+ # Check if there's no path or only a trailing slash
96
+ if not parsed.path or parsed.path == "/":
97
+ # Add /v1/traces to the endpoint
98
+ new_parsed = parsed._replace(path="/v1/traces")
99
+ normalized_url = urlunparse(new_parsed)
100
+ logger.info(
101
+ f"No path found in HTTP endpoint URL. "
102
+ f"Adding default path /v1/traces: {endpoint} -> {normalized_url}"
103
+ )
104
+ return normalized_url
105
+ return endpoint
106
+ except Exception as e:
107
+ logger.warning(
108
+ f"Failed to parse endpoint URL '{endpoint}': {e}. Using as-is."
109
+ )
110
+ return endpoint
111
+
112
+ def _init_instance(self):
113
+ # Create new instance first (outside critical section for performance)
114
+ if self.force_http:
115
+ # Normalize HTTP endpoint to ensure it has a path
116
+ http_endpoint = self._normalize_http_endpoint(self.endpoint)
117
+ new_instance = HTTPOTLPSpanExporter(
118
+ endpoint=http_endpoint,
119
+ headers=self.headers,
120
+ compression=HTTPCompression.Gzip,
121
+ timeout=self.timeout,
43
122
  )
44
123
  else:
45
- self.instance = OTLPSpanExporter(
46
- endpoint=final_url,
47
- headers={"authorization": f"Bearer {api_key}"},
124
+ new_instance = OTLPSpanExporter(
125
+ endpoint=self.endpoint,
126
+ headers=self.headers,
127
+ timeout=self.timeout,
48
128
  compression=grpc.Compression.Gzip,
49
- timeout=timeout_seconds,
50
129
  )
51
130
 
131
+ # Atomic swap with proper cleanup
132
+ with self._instance_lock:
133
+ old_instance: OTLPSpanExporter | HTTPOTLPSpanExporter | None = getattr(
134
+ self, "instance", None
135
+ )
136
+ if old_instance is not None:
137
+ try:
138
+ old_instance.shutdown()
139
+ except Exception as e:
140
+ logger.warning(f"Error shutting down old exporter instance: {e}")
141
+ self.instance = new_instance
142
+
52
143
  def export(self, spans: list[ReadableSpan]) -> SpanExportResult:
53
- return self.instance.export(spans)
144
+ with self._instance_lock:
145
+ return self.instance.export(spans)
54
146
 
55
147
  def shutdown(self) -> None:
56
- return self.instance.shutdown()
148
+ with self._instance_lock:
149
+ return self.instance.shutdown()
57
150
 
58
151
  def force_flush(self, timeout_millis: int = 30000) -> bool:
59
- return self.instance.force_flush(timeout_millis)
152
+ with self._instance_lock:
153
+ return self.instance.force_flush(timeout_millis)
@@ -4,7 +4,6 @@ from enum import Enum
4
4
 
5
5
  from opentelemetry.trace import TracerProvider
6
6
  import lmnr.opentelemetry_lib.tracing._instrument_initializers as initializers
7
- from lmnr.sdk.client.synchronous.sync_client import LaminarClient
8
7
  from lmnr.sdk.client.asynchronous.async_client import AsyncLaminarClient
9
8
 
10
9
  module_logger = logging.getLogger(__name__)
@@ -17,13 +16,18 @@ class Instruments(Enum):
17
16
  ANTHROPIC = "anthropic"
18
17
  BEDROCK = "bedrock"
19
18
  BROWSER_USE = "browser_use"
19
+ BROWSER_USE_SESSION = "browser_use_session"
20
+ BUBUS = "bubus"
20
21
  CHROMA = "chroma"
22
+ CLAUDE_AGENT = "claude_agent"
21
23
  COHERE = "cohere"
22
24
  CREWAI = "crewai"
23
- GOOGLE_GENERATIVEAI = "google_generativeai"
25
+ CUA_AGENT = "cua_agent"
26
+ CUA_COMPUTER = "cua_computer"
24
27
  GOOGLE_GENAI = "google_genai"
25
28
  GROQ = "groq"
26
29
  HAYSTACK = "haystack"
30
+ KERNEL = "kernel"
27
31
  LANCEDB = "lancedb"
28
32
  LANGCHAIN = "langchain"
29
33
  LANGGRAPH = "langgraph"
@@ -34,6 +38,12 @@ class Instruments(Enum):
34
38
  MISTRAL = "mistral"
35
39
  OLLAMA = "ollama"
36
40
  OPENAI = "openai"
41
+ OPENHANDS = "openhands"
42
+ # Patch OpenTelemetry to fix DataDog's broken Span context
43
+ # See lmnr.opentelemetry_lib.opentelemetry.instrumentation.opentelemetry
44
+ # for more details.
45
+ OPENTELEMETRY = "opentelemetry"
46
+ ###
37
47
  PATCHRIGHT = "patchright"
38
48
  PINECONE = "pinecone"
39
49
  PLAYWRIGHT = "playwright"
@@ -47,6 +57,7 @@ class Instruments(Enum):
47
57
  WATSONX = "watsonx"
48
58
  WEAVIATE = "weaviate"
49
59
 
60
+
50
61
  INSTRUMENTATION_INITIALIZERS: dict[
51
62
  Instruments, initializers.InstrumentorInitializer
52
63
  ] = {
@@ -54,13 +65,18 @@ INSTRUMENTATION_INITIALIZERS: dict[
54
65
  Instruments.ANTHROPIC: initializers.AnthropicInstrumentorInitializer(),
55
66
  Instruments.BEDROCK: initializers.BedrockInstrumentorInitializer(),
56
67
  Instruments.BROWSER_USE: initializers.BrowserUseInstrumentorInitializer(),
68
+ Instruments.BROWSER_USE_SESSION: initializers.BrowserUseSessionInstrumentorInitializer(),
69
+ Instruments.BUBUS: initializers.BubusInstrumentorInitializer(),
57
70
  Instruments.CHROMA: initializers.ChromaInstrumentorInitializer(),
71
+ Instruments.CLAUDE_AGENT: initializers.ClaudeAgentInstrumentorInitializer(),
58
72
  Instruments.COHERE: initializers.CohereInstrumentorInitializer(),
59
73
  Instruments.CREWAI: initializers.CrewAIInstrumentorInitializer(),
60
- Instruments.GOOGLE_GENERATIVEAI: initializers.GoogleGenerativeAIInstrumentorInitializer(),
74
+ Instruments.CUA_AGENT: initializers.CuaAgentInstrumentorInitializer(),
75
+ Instruments.CUA_COMPUTER: initializers.CuaComputerInstrumentorInitializer(),
61
76
  Instruments.GOOGLE_GENAI: initializers.GoogleGenAIInstrumentorInitializer(),
62
77
  Instruments.GROQ: initializers.GroqInstrumentorInitializer(),
63
78
  Instruments.HAYSTACK: initializers.HaystackInstrumentorInitializer(),
79
+ Instruments.KERNEL: initializers.KernelInstrumentorInitializer(),
64
80
  Instruments.LANCEDB: initializers.LanceDBInstrumentorInitializer(),
65
81
  Instruments.LANGCHAIN: initializers.LangchainInstrumentorInitializer(),
66
82
  Instruments.LANGGRAPH: initializers.LanggraphInstrumentorInitializer(),
@@ -71,6 +87,8 @@ INSTRUMENTATION_INITIALIZERS: dict[
71
87
  Instruments.MISTRAL: initializers.MistralInstrumentorInitializer(),
72
88
  Instruments.OLLAMA: initializers.OllamaInstrumentorInitializer(),
73
89
  Instruments.OPENAI: initializers.OpenAIInstrumentorInitializer(),
90
+ Instruments.OPENHANDS: initializers.OpenHandsAIInstrumentorInitializer(),
91
+ Instruments.OPENTELEMETRY: initializers.OpenTelemetryInstrumentorInitializer(),
74
92
  Instruments.PATCHRIGHT: initializers.PatchrightInstrumentorInitializer(),
75
93
  Instruments.PINECONE: initializers.PineconeInstrumentorInitializer(),
76
94
  Instruments.PLAYWRIGHT: initializers.PlaywrightInstrumentorInitializer(),
@@ -90,7 +108,6 @@ def init_instrumentations(
90
108
  tracer_provider: TracerProvider,
91
109
  instruments: set[Instruments] | None = None,
92
110
  block_instruments: set[Instruments] | None = None,
93
- client: LaminarClient | None = None,
94
111
  async_client: AsyncLaminarClient | None = None,
95
112
  ):
96
113
  block_instruments = block_instruments or set()
@@ -109,7 +126,7 @@ def init_instrumentations(
109
126
  continue
110
127
 
111
128
  try:
112
- instrumentor = initializer.init_instrumentor(client, async_client)
129
+ instrumentor = initializer.init_instrumentor(async_client)
113
130
  if instrumentor is None:
114
131
  continue
115
132
  if not instrumentor.is_instrumented_by_opentelemetry:
@@ -1,3 +1,5 @@
1
+ import logging
2
+ import threading
1
3
  import uuid
2
4
 
3
5
  from opentelemetry.sdk.trace.export import (
@@ -7,26 +9,41 @@ from opentelemetry.sdk.trace.export import (
7
9
  SimpleSpanProcessor,
8
10
  )
9
11
  from opentelemetry.sdk.trace import Span
10
- from opentelemetry.context import Context, get_value, get_current, set_value
12
+ from opentelemetry.context import Context, get_value
11
13
 
12
14
  from lmnr.opentelemetry_lib.tracing.attributes import (
15
+ ASSOCIATION_PROPERTIES,
16
+ PARENT_SPAN_IDS_PATH,
17
+ PARENT_SPAN_PATH,
18
+ SESSION_ID,
13
19
  SPAN_IDS_PATH,
14
20
  SPAN_INSTRUMENTATION_SOURCE,
15
21
  SPAN_LANGUAGE_VERSION,
16
22
  SPAN_PATH,
17
23
  SPAN_SDK_VERSION,
24
+ TRACE_TYPE,
25
+ USER_ID,
18
26
  )
19
- from lmnr.opentelemetry_lib.tracing.exporter import LaminarSpanExporter
20
- from lmnr.opentelemetry_lib.tracing.context_properties import (
21
- _set_association_properties_attributes,
27
+ from lmnr.opentelemetry_lib.tracing.context import (
28
+ CONTEXT_METADATA_KEY,
29
+ CONTEXT_SESSION_ID_KEY,
30
+ CONTEXT_TRACE_TYPE_KEY,
31
+ CONTEXT_USER_ID_KEY,
22
32
  )
33
+ from lmnr.opentelemetry_lib.tracing.exporter import LaminarSpanExporter
34
+ from lmnr.sdk.log import get_default_logger
35
+ from lmnr.sdk.utils import is_otel_attribute_value_type, json_dumps
23
36
  from lmnr.version import PYTHON_VERSION, __version__
24
37
 
25
38
 
26
39
  class LaminarSpanProcessor(SpanProcessor):
27
40
  instance: BatchSpanProcessor | SimpleSpanProcessor
41
+ logger: logging.Logger
28
42
  __span_id_to_path: dict[int, list[str]] = {}
29
43
  __span_id_lists: dict[int, list[str]] = {}
44
+ max_export_batch_size: int
45
+ _instance_lock: threading.RLock
46
+ _paths_lock: threading.RLock
30
47
 
31
48
  def __init__(
32
49
  self,
@@ -35,10 +52,14 @@ class LaminarSpanProcessor(SpanProcessor):
35
52
  api_key: str | None = None,
36
53
  timeout_seconds: int = 30,
37
54
  force_http: bool = False,
38
- max_export_batch_size: int = 512,
55
+ max_export_batch_size: int = 64,
39
56
  disable_batch: bool = False,
40
57
  exporter: SpanExporter | None = None,
41
58
  ):
59
+ self._instance_lock = threading.RLock()
60
+ self._paths_lock = threading.RLock()
61
+ self.logger = get_default_logger(__name__)
62
+ self.max_export_batch_size = max_export_batch_size
42
63
  self.exporter = exporter or LaminarSpanExporter(
43
64
  base_url=base_url,
44
65
  port=port,
@@ -55,41 +76,118 @@ class LaminarSpanProcessor(SpanProcessor):
55
76
  )
56
77
 
57
78
  def on_start(self, span: Span, parent_context: Context | None = None):
58
- span_path_in_context = get_value("span_path", parent_context or get_current())
59
- parent_span_path = span_path_in_context or (
60
- self.__span_id_to_path.get(span.parent.span_id) if span.parent else None
61
- )
62
- parent_span_ids_path = (
63
- self.__span_id_lists.get(span.parent.span_id, []) if span.parent else []
64
- )
65
- span_path = parent_span_path + [span.name] if parent_span_path else [span.name]
66
- span_ids_path = parent_span_ids_path + [
67
- str(uuid.UUID(int=span.get_span_context().span_id))
68
- ]
69
- span.set_attribute(SPAN_PATH, span_path)
70
- span.set_attribute(SPAN_IDS_PATH, span_ids_path)
71
- set_value("span_path", span_path, get_current())
72
- self.__span_id_to_path[span.get_span_context().span_id] = span_path
73
- self.__span_id_lists[span.get_span_context().span_id] = span_ids_path
79
+ with self._paths_lock:
80
+ parent_span_path = list(span.attributes.get(PARENT_SPAN_PATH, tuple())) or (
81
+ self.__span_id_to_path.get(span.parent.span_id) if span.parent else None
82
+ )
83
+ parent_span_ids_path = list(
84
+ span.attributes.get(PARENT_SPAN_IDS_PATH, tuple())
85
+ ) or (
86
+ self.__span_id_lists.get(span.parent.span_id, []) if span.parent else []
87
+ )
88
+ span_path = (
89
+ parent_span_path + [span.name] if parent_span_path else [span.name]
90
+ )
91
+ span_ids_path = parent_span_ids_path + [
92
+ str(uuid.UUID(int=span.get_span_context().span_id))
93
+ ]
94
+ span.set_attribute(SPAN_PATH, span_path)
95
+ span.set_attribute(SPAN_IDS_PATH, span_ids_path)
96
+ self.__span_id_to_path[span.get_span_context().span_id] = span_path
97
+ self.__span_id_lists[span.get_span_context().span_id] = span_ids_path
74
98
 
75
99
  span.set_attribute(SPAN_INSTRUMENTATION_SOURCE, "python")
76
100
  span.set_attribute(SPAN_SDK_VERSION, __version__)
77
101
  span.set_attribute(SPAN_LANGUAGE_VERSION, f"python@{PYTHON_VERSION}")
78
102
 
79
- association_properties = get_value("association_properties")
80
- if association_properties is not None:
81
- _set_association_properties_attributes(span, association_properties)
82
- self.instance.on_start(span, parent_context)
103
+ if parent_context:
104
+ trace_type = get_value(CONTEXT_TRACE_TYPE_KEY, parent_context)
105
+ if trace_type:
106
+ span.set_attribute(f"{ASSOCIATION_PROPERTIES}.{TRACE_TYPE}", trace_type)
107
+ user_id = get_value(CONTEXT_USER_ID_KEY, parent_context)
108
+ if user_id:
109
+ span.set_attribute(f"{ASSOCIATION_PROPERTIES}.{USER_ID}", user_id)
110
+ session_id = get_value(CONTEXT_SESSION_ID_KEY, parent_context)
111
+ if session_id:
112
+ span.set_attribute(f"{ASSOCIATION_PROPERTIES}.{SESSION_ID}", session_id)
113
+ ctx_metadata = get_value(CONTEXT_METADATA_KEY, parent_context)
114
+ if ctx_metadata and isinstance(ctx_metadata, dict):
115
+ span_metadata = {}
116
+ if hasattr(span, "attributes") and hasattr(span.attributes, "items"):
117
+ for key, value in span.attributes.items():
118
+ if key.startswith(f"{ASSOCIATION_PROPERTIES}.metadata."):
119
+ span_metadata[
120
+ key.replace(f"{ASSOCIATION_PROPERTIES}.metadata.", "")
121
+ ] = value
122
+
123
+ for key, value in {**ctx_metadata, **span_metadata}.items():
124
+ span.set_attribute(
125
+ f"{ASSOCIATION_PROPERTIES}.metadata.{key}",
126
+ (
127
+ value
128
+ if is_otel_attribute_value_type(value)
129
+ else json_dumps(value)
130
+ ),
131
+ )
132
+
133
+ if span.name == "LangGraph.workflow":
134
+ graph_context = get_value("lmnr.langgraph.graph") or {}
135
+ for key, value in graph_context.items():
136
+ span.set_attribute(f"lmnr.association.properties.{key}", value)
137
+
138
+ with self._instance_lock:
139
+ self.instance.on_start(span, parent_context)
83
140
 
84
141
  def on_end(self, span: Span):
85
- self.instance.on_end(span)
142
+ with self._instance_lock:
143
+ self.instance.on_end(span)
86
144
 
87
145
  def force_flush(self, timeout_millis: int = 30000) -> bool:
88
- return self.instance.force_flush(timeout_millis)
146
+ with self._instance_lock:
147
+ return self.instance.force_flush(timeout_millis)
148
+
149
+ def force_reinit(self):
150
+ if not isinstance(self.exporter, LaminarSpanExporter):
151
+ self.logger.warning(
152
+ "LaminarSpanProcessor is not using LaminarSpanExporter, cannot force reinit"
153
+ )
154
+ return
155
+
156
+ # Reinitialize exporter (thread-safe, handles its own locking)
157
+ self.exporter._init_instance()
158
+
159
+ with self._instance_lock:
160
+ old_instance = self.instance
161
+ disable_batch = isinstance(old_instance, SimpleSpanProcessor)
162
+
163
+ try:
164
+ old_instance.shutdown()
165
+ except Exception as e:
166
+ self.logger.debug(f"Error shutting down old processor instance: {e}")
167
+
168
+ self.instance = (
169
+ SimpleSpanProcessor(self.exporter)
170
+ if disable_batch
171
+ else BatchSpanProcessor(
172
+ self.exporter, max_export_batch_size=self.max_export_batch_size
173
+ )
174
+ )
89
175
 
90
176
  def shutdown(self):
91
- self.instance.shutdown()
177
+ with self._instance_lock:
178
+ self.instance.shutdown()
92
179
 
93
180
  def clear(self):
94
- self.__span_id_to_path = {}
95
- self.__span_id_lists = {}
181
+ with self._paths_lock:
182
+ self.__span_id_to_path = {}
183
+ self.__span_id_lists = {}
184
+
185
+ def set_parent_path_info(
186
+ self,
187
+ parent_span_id: int,
188
+ span_path: list[str],
189
+ span_ids_path: list[str],
190
+ ):
191
+ with self._paths_lock:
192
+ self.__span_id_to_path[parent_span_id] = span_path
193
+ self.__span_id_lists[parent_span_id] = span_ids_path