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.
- lmnr/__init__.py +6 -15
- lmnr/cli/__init__.py +270 -0
- lmnr/cli/datasets.py +371 -0
- lmnr/{cli.py → cli/evals.py} +20 -102
- lmnr/cli/rules.py +42 -0
- lmnr/opentelemetry_lib/__init__.py +9 -2
- lmnr/opentelemetry_lib/decorators/__init__.py +274 -168
- lmnr/opentelemetry_lib/litellm/__init__.py +352 -38
- lmnr/opentelemetry_lib/litellm/utils.py +82 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/__init__.py +849 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/config.py +13 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/event_emitter.py +211 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/event_models.py +41 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/span_utils.py +401 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/streaming.py +425 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/utils.py +332 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/version.py +1 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/claude_agent/__init__.py +451 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/claude_agent/proxy.py +144 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_agent/__init__.py +100 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_computer/__init__.py +476 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_computer/utils.py +12 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/__init__.py +191 -129
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/schema_utils.py +26 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/utils.py +126 -41
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/__init__.py +488 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/config.py +8 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/event_emitter.py +143 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/event_models.py +41 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/span_utils.py +229 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/utils.py +92 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/version.py +1 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/kernel/__init__.py +381 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/kernel/utils.py +36 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/langgraph/__init__.py +16 -16
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/__init__.py +61 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/__init__.py +472 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/chat_wrappers.py +1185 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/completion_wrappers.py +305 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/config.py +16 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/embeddings_wrappers.py +312 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/event_emitter.py +100 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/event_models.py +41 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/image_gen_wrappers.py +68 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/utils.py +197 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v0/__init__.py +176 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/__init__.py +368 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/assistant_wrappers.py +325 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/event_handler_wrapper.py +135 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/responses_wrappers.py +786 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/version.py +1 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openhands_ai/__init__.py +388 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/opentelemetry/__init__.py +69 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/skyvern/__init__.py +59 -61
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/threading/__init__.py +197 -0
- lmnr/opentelemetry_lib/tracing/__init__.py +119 -18
- lmnr/opentelemetry_lib/tracing/_instrument_initializers.py +124 -25
- lmnr/opentelemetry_lib/tracing/attributes.py +4 -0
- lmnr/opentelemetry_lib/tracing/context.py +200 -0
- lmnr/opentelemetry_lib/tracing/exporter.py +109 -15
- lmnr/opentelemetry_lib/tracing/instruments.py +22 -5
- lmnr/opentelemetry_lib/tracing/processor.py +128 -30
- lmnr/opentelemetry_lib/tracing/span.py +398 -0
- lmnr/opentelemetry_lib/tracing/tracer.py +40 -1
- lmnr/opentelemetry_lib/tracing/utils.py +62 -0
- lmnr/opentelemetry_lib/utils/package_check.py +9 -0
- lmnr/opentelemetry_lib/utils/wrappers.py +11 -0
- lmnr/sdk/browser/background_send_events.py +158 -0
- lmnr/sdk/browser/browser_use_cdp_otel.py +100 -0
- lmnr/sdk/browser/browser_use_otel.py +12 -12
- lmnr/sdk/browser/bubus_otel.py +71 -0
- lmnr/sdk/browser/cdp_utils.py +518 -0
- lmnr/sdk/browser/inject_script.js +514 -0
- lmnr/sdk/browser/patchright_otel.py +18 -44
- lmnr/sdk/browser/playwright_otel.py +104 -187
- lmnr/sdk/browser/pw_utils.py +249 -210
- lmnr/sdk/browser/recorder/record.umd.min.cjs +84 -0
- lmnr/sdk/browser/utils.py +1 -1
- lmnr/sdk/client/asynchronous/async_client.py +47 -15
- lmnr/sdk/client/asynchronous/resources/__init__.py +2 -7
- lmnr/sdk/client/asynchronous/resources/browser_events.py +1 -0
- lmnr/sdk/client/asynchronous/resources/datasets.py +131 -0
- lmnr/sdk/client/asynchronous/resources/evals.py +122 -18
- lmnr/sdk/client/asynchronous/resources/evaluators.py +85 -0
- lmnr/sdk/client/asynchronous/resources/tags.py +4 -10
- lmnr/sdk/client/synchronous/resources/__init__.py +2 -2
- lmnr/sdk/client/synchronous/resources/datasets.py +131 -0
- lmnr/sdk/client/synchronous/resources/evals.py +83 -17
- lmnr/sdk/client/synchronous/resources/evaluators.py +85 -0
- lmnr/sdk/client/synchronous/resources/tags.py +4 -10
- lmnr/sdk/client/synchronous/sync_client.py +47 -15
- lmnr/sdk/datasets/__init__.py +94 -0
- lmnr/sdk/datasets/file_utils.py +91 -0
- lmnr/sdk/decorators.py +103 -23
- lmnr/sdk/evaluations.py +122 -33
- lmnr/sdk/laminar.py +816 -333
- lmnr/sdk/log.py +7 -2
- lmnr/sdk/types.py +124 -143
- lmnr/sdk/utils.py +115 -2
- lmnr/version.py +1 -1
- {lmnr-0.6.16.dist-info → lmnr-0.7.26.dist-info}/METADATA +71 -78
- lmnr-0.7.26.dist-info/RECORD +116 -0
- lmnr-0.7.26.dist-info/WHEEL +4 -0
- lmnr-0.7.26.dist-info/entry_points.txt +3 -0
- lmnr/opentelemetry_lib/tracing/context_properties.py +0 -65
- lmnr/sdk/browser/rrweb/rrweb.umd.min.cjs +0 -98
- lmnr/sdk/client/asynchronous/resources/agent.py +0 -329
- lmnr/sdk/client/synchronous/resources/agent.py +0 -323
- lmnr/sdk/datasets.py +0 -60
- lmnr-0.6.16.dist-info/LICENSE +0 -75
- lmnr-0.6.16.dist-info/RECORD +0 -61
- lmnr-0.6.16.dist-info/WHEEL +0 -4
- 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
|