lmnr 0.4.53.dev0__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 (133) hide show
  1. lmnr/__init__.py +32 -11
  2. lmnr/cli/__init__.py +270 -0
  3. lmnr/cli/datasets.py +371 -0
  4. lmnr/cli/evals.py +111 -0
  5. lmnr/cli/rules.py +42 -0
  6. lmnr/opentelemetry_lib/__init__.py +70 -0
  7. lmnr/opentelemetry_lib/decorators/__init__.py +337 -0
  8. lmnr/opentelemetry_lib/litellm/__init__.py +685 -0
  9. lmnr/opentelemetry_lib/litellm/utils.py +100 -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 +599 -0
  24. lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/config.py +9 -0
  25. lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/schema_utils.py +26 -0
  26. lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/utils.py +330 -0
  27. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/__init__.py +488 -0
  28. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/config.py +8 -0
  29. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/event_emitter.py +143 -0
  30. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/event_models.py +41 -0
  31. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/span_utils.py +229 -0
  32. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/utils.py +92 -0
  33. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/version.py +1 -0
  34. lmnr/opentelemetry_lib/opentelemetry/instrumentation/kernel/__init__.py +381 -0
  35. lmnr/opentelemetry_lib/opentelemetry/instrumentation/kernel/utils.py +36 -0
  36. lmnr/opentelemetry_lib/opentelemetry/instrumentation/langgraph/__init__.py +121 -0
  37. lmnr/opentelemetry_lib/opentelemetry/instrumentation/langgraph/utils.py +60 -0
  38. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/__init__.py +61 -0
  39. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/__init__.py +472 -0
  40. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/chat_wrappers.py +1185 -0
  41. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/completion_wrappers.py +305 -0
  42. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/config.py +16 -0
  43. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/embeddings_wrappers.py +312 -0
  44. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/event_emitter.py +100 -0
  45. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/event_models.py +41 -0
  46. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/image_gen_wrappers.py +68 -0
  47. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/utils.py +197 -0
  48. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v0/__init__.py +176 -0
  49. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/__init__.py +368 -0
  50. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/assistant_wrappers.py +325 -0
  51. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/event_handler_wrapper.py +135 -0
  52. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/responses_wrappers.py +786 -0
  53. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/version.py +1 -0
  54. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openhands_ai/__init__.py +388 -0
  55. lmnr/opentelemetry_lib/opentelemetry/instrumentation/opentelemetry/__init__.py +69 -0
  56. lmnr/opentelemetry_lib/opentelemetry/instrumentation/skyvern/__init__.py +191 -0
  57. lmnr/opentelemetry_lib/opentelemetry/instrumentation/threading/__init__.py +197 -0
  58. lmnr/opentelemetry_lib/tracing/__init__.py +263 -0
  59. lmnr/opentelemetry_lib/tracing/_instrument_initializers.py +516 -0
  60. lmnr/{openllmetry_sdk → opentelemetry_lib}/tracing/attributes.py +21 -8
  61. lmnr/opentelemetry_lib/tracing/context.py +200 -0
  62. lmnr/opentelemetry_lib/tracing/exporter.py +153 -0
  63. lmnr/opentelemetry_lib/tracing/instruments.py +140 -0
  64. lmnr/opentelemetry_lib/tracing/processor.py +193 -0
  65. lmnr/opentelemetry_lib/tracing/span.py +398 -0
  66. lmnr/opentelemetry_lib/tracing/tracer.py +57 -0
  67. lmnr/opentelemetry_lib/tracing/utils.py +62 -0
  68. lmnr/opentelemetry_lib/utils/package_check.py +18 -0
  69. lmnr/opentelemetry_lib/utils/wrappers.py +11 -0
  70. lmnr/sdk/browser/__init__.py +0 -0
  71. lmnr/sdk/browser/background_send_events.py +158 -0
  72. lmnr/sdk/browser/browser_use_cdp_otel.py +100 -0
  73. lmnr/sdk/browser/browser_use_otel.py +142 -0
  74. lmnr/sdk/browser/bubus_otel.py +71 -0
  75. lmnr/sdk/browser/cdp_utils.py +518 -0
  76. lmnr/sdk/browser/inject_script.js +514 -0
  77. lmnr/sdk/browser/patchright_otel.py +151 -0
  78. lmnr/sdk/browser/playwright_otel.py +322 -0
  79. lmnr/sdk/browser/pw_utils.py +363 -0
  80. lmnr/sdk/browser/recorder/record.umd.min.cjs +84 -0
  81. lmnr/sdk/browser/utils.py +70 -0
  82. lmnr/sdk/client/asynchronous/async_client.py +180 -0
  83. lmnr/sdk/client/asynchronous/resources/__init__.py +6 -0
  84. lmnr/sdk/client/asynchronous/resources/base.py +32 -0
  85. lmnr/sdk/client/asynchronous/resources/browser_events.py +41 -0
  86. lmnr/sdk/client/asynchronous/resources/datasets.py +131 -0
  87. lmnr/sdk/client/asynchronous/resources/evals.py +266 -0
  88. lmnr/sdk/client/asynchronous/resources/evaluators.py +85 -0
  89. lmnr/sdk/client/asynchronous/resources/tags.py +83 -0
  90. lmnr/sdk/client/synchronous/resources/__init__.py +6 -0
  91. lmnr/sdk/client/synchronous/resources/base.py +32 -0
  92. lmnr/sdk/client/synchronous/resources/browser_events.py +40 -0
  93. lmnr/sdk/client/synchronous/resources/datasets.py +131 -0
  94. lmnr/sdk/client/synchronous/resources/evals.py +263 -0
  95. lmnr/sdk/client/synchronous/resources/evaluators.py +85 -0
  96. lmnr/sdk/client/synchronous/resources/tags.py +83 -0
  97. lmnr/sdk/client/synchronous/sync_client.py +191 -0
  98. lmnr/sdk/datasets/__init__.py +94 -0
  99. lmnr/sdk/datasets/file_utils.py +91 -0
  100. lmnr/sdk/decorators.py +163 -26
  101. lmnr/sdk/eval_control.py +3 -2
  102. lmnr/sdk/evaluations.py +403 -191
  103. lmnr/sdk/laminar.py +1080 -549
  104. lmnr/sdk/log.py +7 -2
  105. lmnr/sdk/types.py +246 -134
  106. lmnr/sdk/utils.py +151 -7
  107. lmnr/version.py +46 -0
  108. {lmnr-0.4.53.dev0.dist-info → lmnr-0.7.26.dist-info}/METADATA +152 -106
  109. lmnr-0.7.26.dist-info/RECORD +116 -0
  110. lmnr-0.7.26.dist-info/WHEEL +4 -0
  111. lmnr-0.7.26.dist-info/entry_points.txt +3 -0
  112. lmnr/cli.py +0 -101
  113. lmnr/openllmetry_sdk/.python-version +0 -1
  114. lmnr/openllmetry_sdk/__init__.py +0 -72
  115. lmnr/openllmetry_sdk/config/__init__.py +0 -9
  116. lmnr/openllmetry_sdk/decorators/base.py +0 -185
  117. lmnr/openllmetry_sdk/instruments.py +0 -38
  118. lmnr/openllmetry_sdk/tracing/__init__.py +0 -1
  119. lmnr/openllmetry_sdk/tracing/content_allow_list.py +0 -24
  120. lmnr/openllmetry_sdk/tracing/context_manager.py +0 -13
  121. lmnr/openllmetry_sdk/tracing/tracing.py +0 -884
  122. lmnr/openllmetry_sdk/utils/in_memory_span_exporter.py +0 -61
  123. lmnr/openllmetry_sdk/utils/package_check.py +0 -7
  124. lmnr/openllmetry_sdk/version.py +0 -1
  125. lmnr/sdk/datasets.py +0 -55
  126. lmnr-0.4.53.dev0.dist-info/LICENSE +0 -75
  127. lmnr-0.4.53.dev0.dist-info/RECORD +0 -33
  128. lmnr-0.4.53.dev0.dist-info/WHEEL +0 -4
  129. lmnr-0.4.53.dev0.dist-info/entry_points.txt +0 -3
  130. /lmnr/{openllmetry_sdk → opentelemetry_lib}/.flake8 +0 -0
  131. /lmnr/{openllmetry_sdk → opentelemetry_lib}/utils/__init__.py +0 -0
  132. /lmnr/{openllmetry_sdk → opentelemetry_lib}/utils/json_encoder.py +0 -0
  133. /lmnr/{openllmetry_sdk/decorators/__init__.py → py.typed} +0 -0
@@ -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())
@@ -0,0 +1,153 @@
1
+ import grpc
2
+ import re
3
+ import threading
4
+ from urllib.parse import urlparse, urlunparse
5
+ from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult
6
+ from opentelemetry.sdk.trace import ReadableSpan
7
+ from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
8
+ OTLPSpanExporter,
9
+ )
10
+ from opentelemetry.exporter.otlp.proto.http import Compression as HTTPCompression
11
+ from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
12
+ OTLPSpanExporter as HTTPOTLPSpanExporter,
13
+ )
14
+
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__)
19
+
20
+
21
+ class LaminarSpanExporter(SpanExporter):
22
+ instance: OTLPSpanExporter | HTTPOTLPSpanExporter
23
+ endpoint: str
24
+ headers: dict[str, str]
25
+ timeout: float
26
+ force_http: bool
27
+ _instance_lock: threading.RLock
28
+
29
+ def __init__(
30
+ self,
31
+ base_url: str | None = None,
32
+ port: int | None = None,
33
+ api_key: str | None = None,
34
+ timeout_seconds: int = 30,
35
+ force_http: bool = False,
36
+ ):
37
+ self._instance_lock = threading.RLock()
38
+ url = base_url or from_env("LMNR_BASE_URL") or "https://api.lmnr.ai"
39
+ url = url.rstrip("/")
40
+ if match := re.search(r":(\d{1,5})$", url):
41
+ url = url[: -len(match.group(0))]
42
+ if port is None:
43
+ port = int(match.group(1))
44
+ if port is None:
45
+ port = 443 if force_http else 8443
46
+ final_url = f"{url}:{port or 443}"
47
+ api_key = api_key or from_env("LMNR_PROJECT_API_KEY")
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,
122
+ )
123
+ else:
124
+ new_instance = OTLPSpanExporter(
125
+ endpoint=self.endpoint,
126
+ headers=self.headers,
127
+ timeout=self.timeout,
128
+ compression=grpc.Compression.Gzip,
129
+ )
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
+
143
+ def export(self, spans: list[ReadableSpan]) -> SpanExportResult:
144
+ with self._instance_lock:
145
+ return self.instance.export(spans)
146
+
147
+ def shutdown(self) -> None:
148
+ with self._instance_lock:
149
+ return self.instance.shutdown()
150
+
151
+ def force_flush(self, timeout_millis: int = 30000) -> bool:
152
+ with self._instance_lock:
153
+ return self.instance.force_flush(timeout_millis)
@@ -0,0 +1,140 @@
1
+ import logging
2
+
3
+ from enum import Enum
4
+
5
+ from opentelemetry.trace import TracerProvider
6
+ import lmnr.opentelemetry_lib.tracing._instrument_initializers as initializers
7
+ from lmnr.sdk.client.asynchronous.async_client import AsyncLaminarClient
8
+
9
+ module_logger = logging.getLogger(__name__)
10
+
11
+
12
+ class Instruments(Enum):
13
+ # The list of libraries which will be autoinstrumented
14
+ # if no specific instruments are provided to initialize()
15
+ ALEPHALPHA = "alephalpha"
16
+ ANTHROPIC = "anthropic"
17
+ BEDROCK = "bedrock"
18
+ BROWSER_USE = "browser_use"
19
+ BROWSER_USE_SESSION = "browser_use_session"
20
+ BUBUS = "bubus"
21
+ CHROMA = "chroma"
22
+ CLAUDE_AGENT = "claude_agent"
23
+ COHERE = "cohere"
24
+ CREWAI = "crewai"
25
+ CUA_AGENT = "cua_agent"
26
+ CUA_COMPUTER = "cua_computer"
27
+ GOOGLE_GENAI = "google_genai"
28
+ GROQ = "groq"
29
+ HAYSTACK = "haystack"
30
+ KERNEL = "kernel"
31
+ LANCEDB = "lancedb"
32
+ LANGCHAIN = "langchain"
33
+ LANGGRAPH = "langgraph"
34
+ LLAMA_INDEX = "llama_index"
35
+ MARQO = "marqo"
36
+ MCP = "mcp"
37
+ MILVUS = "milvus"
38
+ MISTRAL = "mistral"
39
+ OLLAMA = "ollama"
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
+ ###
47
+ PATCHRIGHT = "patchright"
48
+ PINECONE = "pinecone"
49
+ PLAYWRIGHT = "playwright"
50
+ QDRANT = "qdrant"
51
+ REPLICATE = "replicate"
52
+ SAGEMAKER = "sagemaker"
53
+ SKYVERN = "skyvern"
54
+ TOGETHER = "together"
55
+ TRANSFORMERS = "transformers"
56
+ VERTEXAI = "vertexai"
57
+ WATSONX = "watsonx"
58
+ WEAVIATE = "weaviate"
59
+
60
+
61
+ INSTRUMENTATION_INITIALIZERS: dict[
62
+ Instruments, initializers.InstrumentorInitializer
63
+ ] = {
64
+ Instruments.ALEPHALPHA: initializers.AlephAlphaInstrumentorInitializer(),
65
+ Instruments.ANTHROPIC: initializers.AnthropicInstrumentorInitializer(),
66
+ Instruments.BEDROCK: initializers.BedrockInstrumentorInitializer(),
67
+ Instruments.BROWSER_USE: initializers.BrowserUseInstrumentorInitializer(),
68
+ Instruments.BROWSER_USE_SESSION: initializers.BrowserUseSessionInstrumentorInitializer(),
69
+ Instruments.BUBUS: initializers.BubusInstrumentorInitializer(),
70
+ Instruments.CHROMA: initializers.ChromaInstrumentorInitializer(),
71
+ Instruments.CLAUDE_AGENT: initializers.ClaudeAgentInstrumentorInitializer(),
72
+ Instruments.COHERE: initializers.CohereInstrumentorInitializer(),
73
+ Instruments.CREWAI: initializers.CrewAIInstrumentorInitializer(),
74
+ Instruments.CUA_AGENT: initializers.CuaAgentInstrumentorInitializer(),
75
+ Instruments.CUA_COMPUTER: initializers.CuaComputerInstrumentorInitializer(),
76
+ Instruments.GOOGLE_GENAI: initializers.GoogleGenAIInstrumentorInitializer(),
77
+ Instruments.GROQ: initializers.GroqInstrumentorInitializer(),
78
+ Instruments.HAYSTACK: initializers.HaystackInstrumentorInitializer(),
79
+ Instruments.KERNEL: initializers.KernelInstrumentorInitializer(),
80
+ Instruments.LANCEDB: initializers.LanceDBInstrumentorInitializer(),
81
+ Instruments.LANGCHAIN: initializers.LangchainInstrumentorInitializer(),
82
+ Instruments.LANGGRAPH: initializers.LanggraphInstrumentorInitializer(),
83
+ Instruments.LLAMA_INDEX: initializers.LlamaIndexInstrumentorInitializer(),
84
+ Instruments.MARQO: initializers.MarqoInstrumentorInitializer(),
85
+ Instruments.MCP: initializers.MCPInstrumentorInitializer(),
86
+ Instruments.MILVUS: initializers.MilvusInstrumentorInitializer(),
87
+ Instruments.MISTRAL: initializers.MistralInstrumentorInitializer(),
88
+ Instruments.OLLAMA: initializers.OllamaInstrumentorInitializer(),
89
+ Instruments.OPENAI: initializers.OpenAIInstrumentorInitializer(),
90
+ Instruments.OPENHANDS: initializers.OpenHandsAIInstrumentorInitializer(),
91
+ Instruments.OPENTELEMETRY: initializers.OpenTelemetryInstrumentorInitializer(),
92
+ Instruments.PATCHRIGHT: initializers.PatchrightInstrumentorInitializer(),
93
+ Instruments.PINECONE: initializers.PineconeInstrumentorInitializer(),
94
+ Instruments.PLAYWRIGHT: initializers.PlaywrightInstrumentorInitializer(),
95
+ Instruments.QDRANT: initializers.QdrantInstrumentorInitializer(),
96
+ Instruments.REPLICATE: initializers.ReplicateInstrumentorInitializer(),
97
+ Instruments.SAGEMAKER: initializers.SageMakerInstrumentorInitializer(),
98
+ Instruments.SKYVERN: initializers.SkyvernInstrumentorInitializer(),
99
+ Instruments.TOGETHER: initializers.TogetherInstrumentorInitializer(),
100
+ Instruments.TRANSFORMERS: initializers.TransformersInstrumentorInitializer(),
101
+ Instruments.VERTEXAI: initializers.VertexAIInstrumentorInitializer(),
102
+ Instruments.WATSONX: initializers.WatsonxInstrumentorInitializer(),
103
+ Instruments.WEAVIATE: initializers.WeaviateInstrumentorInitializer(),
104
+ }
105
+
106
+
107
+ def init_instrumentations(
108
+ tracer_provider: TracerProvider,
109
+ instruments: set[Instruments] | None = None,
110
+ block_instruments: set[Instruments] | None = None,
111
+ async_client: AsyncLaminarClient | None = None,
112
+ ):
113
+ block_instruments = block_instruments or set()
114
+ if instruments is None:
115
+ instruments = set(Instruments)
116
+ if not isinstance(instruments, set):
117
+ instruments = set(instruments)
118
+
119
+ # Remove any instruments that were explicitly blocked
120
+ instruments = instruments - block_instruments
121
+
122
+ for instrument in instruments:
123
+ initializer = INSTRUMENTATION_INITIALIZERS.get(instrument)
124
+ if initializer is None:
125
+ module_logger.error(f"Invalid instrument: {instrument}")
126
+ continue
127
+
128
+ try:
129
+ instrumentor = initializer.init_instrumentor(async_client)
130
+ if instrumentor is None:
131
+ continue
132
+ if not instrumentor.is_instrumented_by_opentelemetry:
133
+ instrumentor.instrument(tracer_provider=tracer_provider)
134
+ except Exception as e:
135
+ if "No module named 'langchain_community'" in str(e):
136
+ # LangChain instrumentor does not require langchain_community,
137
+ # but throws this error if it's not installed.
138
+ continue
139
+ module_logger.error(f"Error initializing instrumentor: {e}")
140
+ continue
@@ -0,0 +1,193 @@
1
+ import logging
2
+ import threading
3
+ import uuid
4
+
5
+ from opentelemetry.sdk.trace.export import (
6
+ SpanProcessor,
7
+ SpanExporter,
8
+ BatchSpanProcessor,
9
+ SimpleSpanProcessor,
10
+ )
11
+ from opentelemetry.sdk.trace import Span
12
+ from opentelemetry.context import Context, get_value
13
+
14
+ from lmnr.opentelemetry_lib.tracing.attributes import (
15
+ ASSOCIATION_PROPERTIES,
16
+ PARENT_SPAN_IDS_PATH,
17
+ PARENT_SPAN_PATH,
18
+ SESSION_ID,
19
+ SPAN_IDS_PATH,
20
+ SPAN_INSTRUMENTATION_SOURCE,
21
+ SPAN_LANGUAGE_VERSION,
22
+ SPAN_PATH,
23
+ SPAN_SDK_VERSION,
24
+ TRACE_TYPE,
25
+ USER_ID,
26
+ )
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,
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
36
+ from lmnr.version import PYTHON_VERSION, __version__
37
+
38
+
39
+ class LaminarSpanProcessor(SpanProcessor):
40
+ instance: BatchSpanProcessor | SimpleSpanProcessor
41
+ logger: logging.Logger
42
+ __span_id_to_path: dict[int, list[str]] = {}
43
+ __span_id_lists: dict[int, list[str]] = {}
44
+ max_export_batch_size: int
45
+ _instance_lock: threading.RLock
46
+ _paths_lock: threading.RLock
47
+
48
+ def __init__(
49
+ self,
50
+ base_url: str | None = None,
51
+ port: int | None = None,
52
+ api_key: str | None = None,
53
+ timeout_seconds: int = 30,
54
+ force_http: bool = False,
55
+ max_export_batch_size: int = 64,
56
+ disable_batch: bool = False,
57
+ exporter: SpanExporter | None = None,
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
63
+ self.exporter = exporter or LaminarSpanExporter(
64
+ base_url=base_url,
65
+ port=port,
66
+ api_key=api_key,
67
+ timeout_seconds=timeout_seconds,
68
+ force_http=force_http,
69
+ )
70
+ self.instance = (
71
+ SimpleSpanProcessor(self.exporter)
72
+ if disable_batch
73
+ else BatchSpanProcessor(
74
+ self.exporter, max_export_batch_size=max_export_batch_size
75
+ )
76
+ )
77
+
78
+ def on_start(self, span: Span, parent_context: Context | None = None):
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
98
+
99
+ span.set_attribute(SPAN_INSTRUMENTATION_SOURCE, "python")
100
+ span.set_attribute(SPAN_SDK_VERSION, __version__)
101
+ span.set_attribute(SPAN_LANGUAGE_VERSION, f"python@{PYTHON_VERSION}")
102
+
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)
140
+
141
+ def on_end(self, span: Span):
142
+ with self._instance_lock:
143
+ self.instance.on_end(span)
144
+
145
+ def force_flush(self, timeout_millis: int = 30000) -> bool:
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
+ )
175
+
176
+ def shutdown(self):
177
+ with self._instance_lock:
178
+ self.instance.shutdown()
179
+
180
+ def clear(self):
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