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,398 @@
1
+ from logging import Logger
2
+ from inspect import Traceback
3
+ from typing import Any, Literal
4
+ import orjson
5
+ import uuid
6
+
7
+ from opentelemetry.sdk.resources import Resource
8
+ from opentelemetry.sdk.trace import Event, ReadableSpan, Span as SDKSpan
9
+ from opentelemetry.sdk.util.instrumentation import (
10
+ InstrumentationInfo,
11
+ InstrumentationScope,
12
+ )
13
+ from opentelemetry.trace import Link, Span, SpanContext, SpanKind, Status
14
+ from opentelemetry.util.types import AttributeValue
15
+ from opentelemetry.context import detach
16
+
17
+ from lmnr.opentelemetry_lib.tracing.attributes import (
18
+ ASSOCIATION_PROPERTIES,
19
+ METADATA,
20
+ SESSION_ID,
21
+ SPAN_IDS_PATH,
22
+ SPAN_INPUT,
23
+ SPAN_OUTPUT,
24
+ SPAN_PATH,
25
+ TRACE_TYPE,
26
+ USER_ID,
27
+ )
28
+ from lmnr.opentelemetry_lib.tracing.context import (
29
+ detach_context,
30
+ pop_span_context,
31
+ )
32
+ from lmnr.sdk.log import get_default_logger
33
+ from lmnr.sdk.types import LaminarSpanContext
34
+ from lmnr.sdk.utils import is_otel_attribute_value_type, json_dumps
35
+
36
+ MAX_MANUAL_SPAN_PAYLOAD_SIZE = 1024 * 1024 * 10 # 10MB
37
+
38
+
39
+ class LaminarSpanInterfaceMixin:
40
+ """Mixin providing Laminar-specific span methods and properties."""
41
+
42
+ span: SDKSpan
43
+ logger: Logger
44
+
45
+ def set_trace_session_id(self, session_id: str | None = None) -> None:
46
+ """Set the session id for the current trace. Must be called at most once per trace.
47
+
48
+ Args:
49
+ session_id (str | None): Session id to set for the span.
50
+ """
51
+ if session_id is not None:
52
+ self.set_attribute(f"{ASSOCIATION_PROPERTIES}.session_id", session_id)
53
+
54
+ def set_trace_user_id(self, user_id: str | None = None) -> None:
55
+ """Set the user id for the current trace. Must be called at most once per trace.
56
+
57
+ Args:
58
+ user_id (str | None): User id to set for the span.
59
+ """
60
+ if user_id is not None:
61
+ self.span.set_attribute(f"{ASSOCIATION_PROPERTIES}.user_id", user_id)
62
+
63
+ def set_trace_metadata(self, metadata: dict[str, AttributeValue]) -> None:
64
+ """Set the metadata for the current trace, merging with any global metadata.
65
+ Must be called at most once per trace.
66
+
67
+ Args:
68
+ metadata (dict[str, AttributeValue]): Metadata to set for the trace.
69
+ """
70
+ formatted_metadata = {}
71
+ for key, value in metadata.items():
72
+ if is_otel_attribute_value_type(value):
73
+ formatted_metadata[f"{ASSOCIATION_PROPERTIES}.metadata.{key}"] = value
74
+ else:
75
+ formatted_metadata[f"{ASSOCIATION_PROPERTIES}.metadata.{key}"] = (
76
+ json_dumps(value)
77
+ )
78
+ self.span.set_attributes(formatted_metadata)
79
+
80
+ def get_laminar_span_context(self) -> LaminarSpanContext:
81
+ span_path = []
82
+ span_ids_path = []
83
+ user_id = None
84
+ session_id = None
85
+ trace_type = None
86
+ metadata = {}
87
+ if hasattr(self.span, "attributes"):
88
+ span_path = list(self.span.attributes.get(SPAN_PATH, tuple()))
89
+ span_ids_path = list(self.span.attributes.get(SPAN_IDS_PATH, tuple()))
90
+ user_id = self.span.attributes.get(
91
+ f"{ASSOCIATION_PROPERTIES}.{USER_ID}", None
92
+ )
93
+ session_id = self.span.attributes.get(
94
+ f"{ASSOCIATION_PROPERTIES}.{SESSION_ID}", None
95
+ )
96
+ trace_type = self.span.attributes.get(
97
+ f"{ASSOCIATION_PROPERTIES}.{TRACE_TYPE}", None
98
+ )
99
+ metadata = {
100
+ k.replace(f"{ASSOCIATION_PROPERTIES}.{METADATA}.", ""): v
101
+ for k, v in self.span.attributes.items()
102
+ if k.startswith(f"{ASSOCIATION_PROPERTIES}.{METADATA}.")
103
+ }
104
+ for k, v in metadata.items():
105
+ try:
106
+ metadata[k] = orjson.loads(v)
107
+ except Exception:
108
+ metadata[k] = v
109
+ else:
110
+ self.logger.warning(
111
+ "Attributes object is not available. Most likely the span is not a LaminarSpan "
112
+ "and not an OpenTelemetry default SDK span. Span path and ids path will be empty.",
113
+ )
114
+ return LaminarSpanContext(
115
+ trace_id=uuid.UUID(int=self.span.get_span_context().trace_id),
116
+ span_id=uuid.UUID(int=self.span.get_span_context().span_id),
117
+ is_remote=self.span.get_span_context().is_remote,
118
+ span_path=span_path,
119
+ span_ids_path=span_ids_path,
120
+ user_id=user_id,
121
+ session_id=session_id,
122
+ trace_type=trace_type,
123
+ metadata=metadata,
124
+ )
125
+
126
+ def span_id(self, format: Literal["int", "uuid"] = "int") -> int | uuid.UUID:
127
+ if format == "int":
128
+ return self.span.get_span_context().span_id
129
+ elif format == "uuid":
130
+ return uuid.UUID(int=self.span.get_span_context().span_id)
131
+ self.logger.warning(f"Invalid format: {format}. Returning int.")
132
+ return self.span.get_span_context().span_id
133
+
134
+ def trace_id(self, format: Literal["int", "uuid"] = "int") -> int | uuid.UUID:
135
+ if format == "int":
136
+ return self.span.get_span_context().trace_id
137
+ elif format == "uuid":
138
+ return uuid.UUID(int=self.span.get_span_context().trace_id)
139
+ self.logger.warning(f"Invalid format: {format}. Returning int.")
140
+ return self.span.get_span_context().trace_id
141
+
142
+ def parent_span_id(
143
+ self, format: Literal["int", "uuid"] = "int"
144
+ ) -> int | uuid.UUID | None:
145
+ parent_span_id = self.span.parent.span_id if self.span.parent else None
146
+ if parent_span_id is None:
147
+ return None
148
+ if format == "int":
149
+ return parent_span_id
150
+ elif format == "uuid":
151
+ return uuid.UUID(int=parent_span_id)
152
+ self.logger.warning(f"Invalid format: {format}. Returning int.")
153
+ return parent_span_id
154
+
155
+ def set_output(self, output: Any = None) -> None:
156
+ if output is not None:
157
+ serialized_output = json_dumps(output)
158
+ if len(serialized_output) > MAX_MANUAL_SPAN_PAYLOAD_SIZE:
159
+ self.span.set_attribute(
160
+ SPAN_OUTPUT,
161
+ "Laminar: output too large to record",
162
+ )
163
+ else:
164
+ self.span.set_attribute(SPAN_OUTPUT, serialized_output)
165
+
166
+ def set_input(self, input: Any = None) -> None:
167
+ if input is not None:
168
+ serialized_input = json_dumps(input)
169
+ if len(serialized_input) > MAX_MANUAL_SPAN_PAYLOAD_SIZE:
170
+ self.span.set_attribute(
171
+ SPAN_INPUT,
172
+ "Laminar: input too large to record",
173
+ )
174
+ else:
175
+ self.span.set_attribute(SPAN_INPUT, serialized_input)
176
+
177
+ def add_tags(self, tags: list[str]) -> None:
178
+ if not isinstance(tags, list) or not all(isinstance(tag, str) for tag in tags):
179
+ self.logger.warning("Tags must be a list of strings. Tags will be ignored.")
180
+ return
181
+ current_tags = self.tags
182
+ if current_tags is None:
183
+ current_tags = []
184
+ current_tags.extend(tags)
185
+ self.span.set_attribute(
186
+ f"{ASSOCIATION_PROPERTIES}.tags", list(set(current_tags))
187
+ )
188
+
189
+ def set_tags(self, tags: list[str]) -> None:
190
+ """Set the tags for the current span.
191
+
192
+ Args:
193
+ tags (list[str]): Tags to set for the span.
194
+ """
195
+ if not isinstance(tags, list) or not all(isinstance(tag, str) for tag in tags):
196
+ self.logger.warning("Tags must be a list of strings. Tags will be ignored.")
197
+ return
198
+ self.span.set_attribute(f"{ASSOCIATION_PROPERTIES}.tags", list(set(tags)))
199
+
200
+ @property
201
+ def tags(self) -> list[str]:
202
+ if not hasattr(self.span, "attributes"):
203
+ self.logger.debug(
204
+ "[LaminarSpan.tags] WARNING. Current span does not have attributes object. "
205
+ "Perhaps, the span was created with a custom OTel SDK. Returning an empty list. "
206
+ "Help: OpenTelemetry API does not guarantee reading attributes from a span, but OTel SDK "
207
+ "allows it by default. Laminar SDK allows to read attributes too.",
208
+ )
209
+ return []
210
+ try:
211
+ return list(self.span.attributes.get(f"{ASSOCIATION_PROPERTIES}.tags", []))
212
+ except Exception:
213
+ return []
214
+
215
+ @property
216
+ def laminar_association_properties(self) -> dict[str, Any]:
217
+ if not hasattr(self.span, "attributes"):
218
+ self.logger.debug(
219
+ "[LaminarSpan.laminar_association_properties] WARNING. Current span "
220
+ "does not have attributes object. Perhaps, the span was created with a "
221
+ "custom OTel SDK. Returning an empty dictionary."
222
+ "Help: OpenTelemetry API does not guarantee reading attributes from a span, but OTel SDK "
223
+ "allows it by default. Laminar SDK allows to read attributes too.",
224
+ )
225
+ return {}
226
+ try:
227
+ values = {}
228
+ for key, value in self.span.attributes.items():
229
+ if key.startswith(f"{ASSOCIATION_PROPERTIES}."):
230
+ if key.startswith(f"{ASSOCIATION_PROPERTIES}.metadata."):
231
+ meta_key = key.replace(
232
+ f"{ASSOCIATION_PROPERTIES}.metadata.", ""
233
+ )
234
+ try:
235
+ values[meta_key] = orjson.loads(value)
236
+ except Exception:
237
+ values[meta_key] = value
238
+ else:
239
+ values[key] = value
240
+ return values
241
+ except Exception:
242
+ return {}
243
+
244
+
245
+ class SpanDelegationMixin:
246
+ """Mixin providing delegation to the wrapped SDK span for standard OpenTelemetry methods."""
247
+
248
+ span: SDKSpan
249
+
250
+ def get_span_context(self) -> SpanContext:
251
+ return self.span.get_span_context()
252
+
253
+ def set_attributes(self, attributes: dict[str, AttributeValue]) -> None:
254
+ self.span.set_attributes(attributes)
255
+
256
+ def set_attribute(self, key: str, value: AttributeValue) -> None:
257
+ self.span.set_attribute(key, value)
258
+
259
+ def add_event(
260
+ self,
261
+ name: str,
262
+ attributes: dict[str, AttributeValue] = None,
263
+ timestamp: int | None = None,
264
+ ) -> None:
265
+ self.span.add_event(name, attributes, timestamp)
266
+
267
+ def add_link(
268
+ self, context: SpanContext, attributes: dict[str, AttributeValue] = None
269
+ ) -> None:
270
+ self.span.add_link(context, attributes)
271
+
272
+ def update_name(self, name: str) -> None:
273
+ self.span.update_name(name)
274
+
275
+ def is_recording(self) -> bool:
276
+ return self.span.is_recording()
277
+
278
+ def set_status(self, status: Status, description: str | None = None) -> None:
279
+ self.span.set_status(status, description)
280
+
281
+ def record_exception(
282
+ self,
283
+ exception: BaseException,
284
+ attributes: dict[str, AttributeValue] = None,
285
+ timestamp: int | None = None,
286
+ escaped: bool = False,
287
+ ) -> None:
288
+ self.span.record_exception(exception, attributes, timestamp, escaped)
289
+
290
+ def _readable_span(self) -> ReadableSpan:
291
+ return self.span._readable_span()
292
+
293
+ @property
294
+ def name(self) -> str:
295
+ return self.span.name
296
+
297
+ @property
298
+ def context(self) -> SpanContext:
299
+ return self.span.context
300
+
301
+ @property
302
+ def start_time(self) -> int | None:
303
+ return self.span.start_time
304
+
305
+ @property
306
+ def end_time(self) -> int | None:
307
+ return self.span.end_time
308
+
309
+ @property
310
+ def dropped_attributes(self) -> int:
311
+ return self.span.dropped_attributes
312
+
313
+ @property
314
+ def dropped_events(self) -> int:
315
+ return self.span.dropped_events
316
+
317
+ @property
318
+ def dropped_links(self) -> int:
319
+ return self.span.dropped_links
320
+
321
+ @property
322
+ def attributes(self) -> dict[str, AttributeValue]:
323
+ return self.span.attributes
324
+
325
+ @property
326
+ def events(self) -> list[Event]:
327
+ return self.span.events
328
+
329
+ @property
330
+ def links(self) -> list[Link]:
331
+ return self.span.links
332
+
333
+ @property
334
+ def status(self) -> Status:
335
+ return self.span.status
336
+
337
+ @property
338
+ def kind(self) -> SpanKind:
339
+ return self.span.kind
340
+
341
+ @property
342
+ def resource(self) -> Resource:
343
+ return self.span.resource
344
+
345
+ @property
346
+ def instrumentation_scope(self) -> InstrumentationScope:
347
+ return self.span.instrumentation_scope
348
+
349
+ @property
350
+ def instrumentation_info(self) -> InstrumentationInfo:
351
+ return self.span.instrumentation_info
352
+
353
+ def to_json(self) -> str:
354
+ return self.span.to_json()
355
+
356
+
357
+ class LaminarSpan(LaminarSpanInterfaceMixin, SpanDelegationMixin, Span, ReadableSpan):
358
+ """
359
+ Laminar's span wrapper that complies with OpenTelemetry's Span and ReadableSpan interfaces.
360
+
361
+ We wrap the SDK span instead of inheriting from it, because OpenTelemetry discourages
362
+ direct initialization of SdkSpan objects. Instead, we rely on the tracer to create
363
+ the span for us, and then we wrap it in a LaminarSpan.
364
+ """
365
+
366
+ span: SDKSpan
367
+ _popped: bool = False
368
+
369
+ def __init__(self, span: SDKSpan):
370
+ if isinstance(span, LaminarSpan):
371
+ span = span.span
372
+ self.logger = get_default_logger(__name__)
373
+ self.span = span
374
+
375
+ def end(self, end_time: int | None = None) -> None:
376
+ self.span.end(end_time)
377
+ if hasattr(self, "_lmnr_ctx_token") and not self._popped:
378
+ try:
379
+ pop_span_context()
380
+ detach(self._lmnr_ctx_token)
381
+ self._popped = True
382
+ except Exception:
383
+ pass
384
+ if hasattr(self, "_lmnr_isolated_ctx_token"):
385
+ detach_context(self._lmnr_isolated_ctx_token)
386
+ if hasattr(self, "_lmnr_assoc_props_token") and self._lmnr_assoc_props_token:
387
+ detach_context(self._lmnr_assoc_props_token)
388
+
389
+ def __enter__(self) -> "LaminarSpan":
390
+ return self
391
+
392
+ def __exit__(
393
+ self,
394
+ exc_type: type[BaseException] | None,
395
+ exc_value: BaseException | None,
396
+ exc_tb: Traceback | None,
397
+ ) -> None:
398
+ self.end()
@@ -1,7 +1,10 @@
1
1
  from contextlib import contextmanager
2
+ from typing import Generator, Iterator, Tuple
2
3
 
3
4
  from opentelemetry import trace
5
+ from opentelemetry.context import Context
4
6
  from lmnr.opentelemetry_lib.tracing import TracerWrapper
7
+ from lmnr.opentelemetry_lib.tracing.span import LaminarSpan
5
8
 
6
9
 
7
10
  def get_laminar_tracer_provider() -> trace.TracerProvider:
@@ -12,7 +15,43 @@ def get_laminar_tracer_provider() -> trace.TracerProvider:
12
15
  def get_tracer(flush_on_exit: bool = False):
13
16
  wrapper = TracerWrapper()
14
17
  try:
15
- yield wrapper.get_tracer()
18
+ yield LaminarTracer(wrapper.get_tracer())
16
19
  finally:
17
20
  if flush_on_exit:
18
21
  wrapper.flush()
22
+
23
+
24
+ @contextmanager
25
+ def get_tracer_with_context(
26
+ flush_on_exit: bool = False,
27
+ ) -> Generator[Tuple[trace.Tracer, Context], None, None]:
28
+ """Get tracer with isolated context. Returns (tracer, context) tuple."""
29
+ wrapper = TracerWrapper()
30
+ try:
31
+ tracer = LaminarTracer(wrapper.get_tracer())
32
+ context = wrapper.get_isolated_context()
33
+ yield tracer, context
34
+ finally:
35
+ if flush_on_exit:
36
+ wrapper.flush()
37
+
38
+
39
+ class LaminarTracer(trace.Tracer):
40
+ _instance: trace.Tracer
41
+
42
+ def __init__(self, instance: trace.Tracer):
43
+ self._instance = instance
44
+
45
+ def start_span(self, *args, **kwargs) -> trace.Span:
46
+ span = LaminarSpan(self._instance.start_span(*args, **kwargs))
47
+ return span
48
+
49
+ @contextmanager
50
+ def start_as_current_span(self, *args, **kwargs) -> Iterator[trace.Span]:
51
+ wrapper = TracerWrapper()
52
+ with self._instance.start_as_current_span(*args, **kwargs) as span:
53
+ wrapper.push_span_context(span)
54
+ try:
55
+ yield LaminarSpan(span)
56
+ finally:
57
+ wrapper.pop_span_context()
@@ -0,0 +1,62 @@
1
+ from opentelemetry.trace import Span
2
+ from lmnr.opentelemetry_lib.tracing.span import LaminarSpan
3
+ from lmnr.opentelemetry_lib.tracing.attributes import (
4
+ ASSOCIATION_PROPERTIES,
5
+ USER_ID,
6
+ SESSION_ID,
7
+ TRACE_TYPE,
8
+ )
9
+ from lmnr.opentelemetry_lib.tracing.context import (
10
+ get_current_context,
11
+ attach_context,
12
+ set_value,
13
+ CONTEXT_USER_ID_KEY,
14
+ CONTEXT_SESSION_ID_KEY,
15
+ CONTEXT_TRACE_TYPE_KEY,
16
+ CONTEXT_METADATA_KEY,
17
+ )
18
+
19
+
20
+ def set_association_props_in_context(span: Span):
21
+ """Set association properties from span in context before push_span_context.
22
+
23
+ Returns the token that needs to be detached when the span ends.
24
+ """
25
+ if not isinstance(span, LaminarSpan):
26
+ return None
27
+
28
+ props = span.laminar_association_properties
29
+ user_id_key = f"{ASSOCIATION_PROPERTIES}.{USER_ID}"
30
+ session_id_key = f"{ASSOCIATION_PROPERTIES}.{SESSION_ID}"
31
+ trace_type_key = f"{ASSOCIATION_PROPERTIES}.{TRACE_TYPE}"
32
+
33
+ # Extract values from props
34
+ extracted_user_id = props.get(user_id_key)
35
+ extracted_session_id = props.get(session_id_key)
36
+ extracted_trace_type = props.get(trace_type_key)
37
+
38
+ # Extract metadata from props (keys without ASSOCIATION_PROPERTIES prefix)
39
+ metadata_dict = {}
40
+ for key, value in props.items():
41
+ if not key.startswith(f"{ASSOCIATION_PROPERTIES}."):
42
+ metadata_dict[key] = value
43
+
44
+ # Set context with association props
45
+ current_ctx = get_current_context()
46
+ ctx_with_props = current_ctx
47
+ if extracted_user_id:
48
+ ctx_with_props = set_value(
49
+ CONTEXT_USER_ID_KEY, extracted_user_id, ctx_with_props
50
+ )
51
+ if extracted_session_id:
52
+ ctx_with_props = set_value(
53
+ CONTEXT_SESSION_ID_KEY, extracted_session_id, ctx_with_props
54
+ )
55
+ if extracted_trace_type:
56
+ ctx_with_props = set_value(
57
+ CONTEXT_TRACE_TYPE_KEY, extracted_trace_type, ctx_with_props
58
+ )
59
+ if metadata_dict:
60
+ ctx_with_props = set_value(CONTEXT_METADATA_KEY, metadata_dict, ctx_with_props)
61
+
62
+ return attach_context(ctx_with_props)
@@ -1,5 +1,7 @@
1
1
  from importlib.metadata import distributions
2
2
 
3
+ from typing import Optional
4
+
3
5
  installed_packages = {
4
6
  (dist.name or dist.metadata.get("Name", "")).lower() for dist in distributions()
5
7
  }
@@ -7,3 +9,10 @@ installed_packages = {
7
9
 
8
10
  def is_package_installed(package_name: str) -> bool:
9
11
  return package_name.lower() in installed_packages
12
+
13
+
14
+ def get_package_version(package_name: str) -> Optional[str]:
15
+ for dist in distributions():
16
+ if (dist.name or dist.metadata.get("Name", "")).lower() == package_name.lower():
17
+ return dist.version
18
+ return None
@@ -0,0 +1,11 @@
1
+ # TODO: Remove the same thing from openai, anthropic, etc, and use this instead
2
+
3
+
4
+ def _with_tracer_wrapper(func):
5
+ def _with_tracer(tracer, to_wrap):
6
+ def wrapper(wrapped, instance, args, kwargs):
7
+ return func(tracer, to_wrap, wrapped, instance, args, kwargs)
8
+
9
+ return wrapper
10
+
11
+ return _with_tracer