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
|
@@ -1,126 +1,218 @@
|
|
|
1
1
|
from functools import wraps
|
|
2
|
-
import json
|
|
3
|
-
import logging
|
|
4
|
-
import pydantic
|
|
5
2
|
import types
|
|
6
|
-
from typing import Any, Literal
|
|
3
|
+
from typing import Any, AsyncGenerator, Callable, Generator, Literal, TypeVar
|
|
7
4
|
|
|
8
|
-
from opentelemetry import trace
|
|
9
5
|
from opentelemetry import context as context_api
|
|
10
|
-
from opentelemetry.trace import Span
|
|
6
|
+
from opentelemetry.trace import Span, Status, StatusCode
|
|
11
7
|
|
|
8
|
+
from lmnr.opentelemetry_lib.tracing.context import (
|
|
9
|
+
CONTEXT_METADATA_KEY,
|
|
10
|
+
attach_context,
|
|
11
|
+
detach_context,
|
|
12
|
+
get_event_attributes_from_context,
|
|
13
|
+
)
|
|
14
|
+
from lmnr.opentelemetry_lib.tracing.span import LaminarSpan
|
|
15
|
+
from lmnr.opentelemetry_lib.tracing.utils import set_association_props_in_context
|
|
12
16
|
from lmnr.sdk.utils import get_input_from_func_args, is_method
|
|
13
|
-
from lmnr.opentelemetry_lib import
|
|
14
|
-
from lmnr.opentelemetry_lib.tracing.tracer import get_tracer
|
|
17
|
+
from lmnr.opentelemetry_lib.tracing.tracer import get_tracer_with_context
|
|
15
18
|
from lmnr.opentelemetry_lib.tracing.attributes import (
|
|
16
19
|
ASSOCIATION_PROPERTIES,
|
|
17
|
-
|
|
18
|
-
SPAN_OUTPUT,
|
|
20
|
+
METADATA,
|
|
19
21
|
SPAN_TYPE,
|
|
20
22
|
)
|
|
21
23
|
from lmnr.opentelemetry_lib.tracing import TracerWrapper
|
|
22
|
-
from lmnr.
|
|
24
|
+
from lmnr.sdk.log import get_default_logger
|
|
25
|
+
from lmnr.sdk.utils import is_otel_attribute_value_type, json_dumps
|
|
26
|
+
|
|
27
|
+
logger = get_default_logger(__name__)
|
|
23
28
|
|
|
29
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
24
30
|
|
|
25
|
-
class CustomJSONEncoder(JSONEncoder):
|
|
26
|
-
def default(self, o: Any) -> Any:
|
|
27
|
-
if isinstance(o, pydantic.BaseModel):
|
|
28
|
-
return o.model_dump_json()
|
|
29
|
-
try:
|
|
30
|
-
return super().default(o)
|
|
31
|
-
except TypeError:
|
|
32
|
-
return str(o) # Fallback to string representation for unsupported types
|
|
33
31
|
|
|
32
|
+
def _setup_span(
|
|
33
|
+
span_name: str,
|
|
34
|
+
span_type: str,
|
|
35
|
+
association_properties: dict[str, Any] | None,
|
|
36
|
+
preserve_global_context: bool = False,
|
|
37
|
+
metadata: dict[str, Any] | None = None,
|
|
38
|
+
):
|
|
39
|
+
"""Set up a span with the given name, type, and association properties."""
|
|
40
|
+
with get_tracer_with_context() as (tracer, isolated_context):
|
|
41
|
+
# Create span in isolated context
|
|
42
|
+
span = tracer.start_span(
|
|
43
|
+
span_name,
|
|
44
|
+
context=isolated_context if not preserve_global_context else None,
|
|
45
|
+
attributes={SPAN_TYPE: span_type},
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
ctx_metadata = context_api.get_value(CONTEXT_METADATA_KEY, isolated_context)
|
|
49
|
+
merged_metadata = {
|
|
50
|
+
**(ctx_metadata or {}),
|
|
51
|
+
**(metadata or {}),
|
|
52
|
+
}
|
|
53
|
+
for key, value in merged_metadata.items():
|
|
54
|
+
span.set_attribute(
|
|
55
|
+
f"{ASSOCIATION_PROPERTIES}.{METADATA}.{key}",
|
|
56
|
+
(value if is_otel_attribute_value_type(value) else json_dumps(value)),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
if association_properties is not None:
|
|
60
|
+
for key, value in association_properties.items():
|
|
61
|
+
span.set_attribute(f"{ASSOCIATION_PROPERTIES}.{key}", value)
|
|
62
|
+
|
|
63
|
+
return span
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _process_input(
|
|
67
|
+
span: Span,
|
|
68
|
+
fn: Callable,
|
|
69
|
+
args: tuple,
|
|
70
|
+
kwargs: dict,
|
|
71
|
+
ignore_input: bool,
|
|
72
|
+
ignore_inputs: list[str] | None,
|
|
73
|
+
input_formatter: Callable[..., str] | None,
|
|
74
|
+
):
|
|
75
|
+
"""Process and set input attributes on the span."""
|
|
76
|
+
if ignore_input:
|
|
77
|
+
return
|
|
34
78
|
|
|
35
|
-
def json_dumps(data: dict) -> str:
|
|
36
79
|
try:
|
|
37
|
-
|
|
80
|
+
if input_formatter is not None:
|
|
81
|
+
inp = input_formatter(*args, **kwargs)
|
|
82
|
+
else:
|
|
83
|
+
inp = get_input_from_func_args(
|
|
84
|
+
fn,
|
|
85
|
+
is_method=is_method(fn),
|
|
86
|
+
func_args=args,
|
|
87
|
+
func_kwargs=kwargs,
|
|
88
|
+
ignore_inputs=ignore_inputs,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
if not isinstance(span, LaminarSpan):
|
|
92
|
+
span = LaminarSpan(span)
|
|
93
|
+
span.set_input(inp)
|
|
38
94
|
except Exception:
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
95
|
+
msg = "Failed to process input, ignoring"
|
|
96
|
+
if input_formatter is not None:
|
|
97
|
+
# Only warn the user if they provided an input formatter
|
|
98
|
+
# because it's their responsibility to make sure it works.
|
|
99
|
+
logger.warning(msg, exc_info=True)
|
|
100
|
+
else:
|
|
101
|
+
logger.debug(msg, exc_info=True)
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _process_output(
|
|
106
|
+
span: Span,
|
|
107
|
+
result: Any,
|
|
108
|
+
ignore_output: bool,
|
|
109
|
+
output_formatter: Callable[..., str] | None,
|
|
110
|
+
):
|
|
111
|
+
"""Process and set output attributes on the span."""
|
|
112
|
+
if ignore_output:
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
if output_formatter is not None:
|
|
117
|
+
output = output_formatter(result)
|
|
118
|
+
else:
|
|
119
|
+
output = result
|
|
120
|
+
|
|
121
|
+
if not isinstance(span, LaminarSpan):
|
|
122
|
+
span = LaminarSpan(span)
|
|
123
|
+
span.set_output(output)
|
|
124
|
+
except Exception:
|
|
125
|
+
msg = "Failed to process output, ignoring"
|
|
126
|
+
if output_formatter is not None:
|
|
127
|
+
# Only warn the user if they provided an output formatter
|
|
128
|
+
# because it's their responsibility to make sure it works.
|
|
129
|
+
logger.warning(msg, exc_info=True)
|
|
130
|
+
else:
|
|
131
|
+
logger.debug(msg, exc_info=True)
|
|
132
|
+
pass
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _cleanup_span(span: Span, wrapper: TracerWrapper):
|
|
136
|
+
"""Clean up span and context."""
|
|
137
|
+
span.end()
|
|
138
|
+
wrapper.pop_span_context()
|
|
42
139
|
|
|
43
140
|
|
|
44
|
-
def
|
|
141
|
+
def observe_base(
|
|
142
|
+
*,
|
|
45
143
|
name: str | None = None,
|
|
46
144
|
ignore_input: bool = False,
|
|
47
145
|
ignore_inputs: list[str] | None = None,
|
|
48
146
|
ignore_output: bool = False,
|
|
49
147
|
span_type: Literal["DEFAULT", "LLM", "TOOL"] = "DEFAULT",
|
|
148
|
+
metadata: dict[str, Any] | None = None,
|
|
50
149
|
association_properties: dict[str, Any] | None = None,
|
|
51
|
-
|
|
52
|
-
|
|
150
|
+
input_formatter: Callable[..., str] | None = None,
|
|
151
|
+
output_formatter: Callable[..., str] | None = None,
|
|
152
|
+
preserve_global_context: bool = False,
|
|
153
|
+
) -> Callable[[F], F]:
|
|
154
|
+
def decorate(fn: F) -> F:
|
|
53
155
|
@wraps(fn)
|
|
54
156
|
def wrap(*args, **kwargs):
|
|
55
157
|
if not TracerWrapper.verify_initialized():
|
|
56
158
|
return fn(*args, **kwargs)
|
|
57
159
|
|
|
58
160
|
span_name = name or fn.__name__
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
# span will be ended in the generator
|
|
97
|
-
if isinstance(res, types.GeneratorType):
|
|
98
|
-
return _handle_generator(span, ctx_token, res)
|
|
99
|
-
if isinstance(res, types.AsyncGeneratorType):
|
|
100
|
-
# async def foo() -> AsyncGenerator[int, None]:
|
|
101
|
-
# is not considered async in a classical sense in Python,
|
|
102
|
-
# so we handle this inside the sync wrapper.
|
|
103
|
-
# In particular, CO_COROUTINE is different from CO_ASYNC_GENERATOR.
|
|
104
|
-
# Flags are listed from LSB here:
|
|
105
|
-
# https://docs.python.org/3/library/inspect.html#inspect-module-co-flags
|
|
106
|
-
# See also: https://groups.google.com/g/python-tulip/c/6rWweGXLutU?pli=1
|
|
107
|
-
return _ahandle_generator(span, ctx_token, res)
|
|
108
|
-
|
|
109
|
-
try:
|
|
110
|
-
if not ignore_output:
|
|
111
|
-
output = json_dumps(res)
|
|
112
|
-
if len(output) > MAX_MANUAL_SPAN_PAYLOAD_SIZE:
|
|
113
|
-
span.set_attribute(
|
|
114
|
-
SPAN_OUTPUT, "Laminar: output too large to record"
|
|
115
|
-
)
|
|
116
|
-
else:
|
|
117
|
-
span.set_attribute(SPAN_OUTPUT, output)
|
|
118
|
-
except TypeError:
|
|
119
|
-
pass
|
|
120
|
-
|
|
121
|
-
span.end()
|
|
161
|
+
wrapper = TracerWrapper()
|
|
162
|
+
|
|
163
|
+
span = _setup_span(
|
|
164
|
+
span_name,
|
|
165
|
+
span_type,
|
|
166
|
+
association_properties,
|
|
167
|
+
preserve_global_context,
|
|
168
|
+
metadata,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# Set association props in context before push_span_context
|
|
172
|
+
# so child spans inherit them
|
|
173
|
+
assoc_props_token = set_association_props_in_context(span)
|
|
174
|
+
if assoc_props_token and isinstance(span, LaminarSpan):
|
|
175
|
+
span._lmnr_assoc_props_token = assoc_props_token
|
|
176
|
+
|
|
177
|
+
new_context = wrapper.push_span_context(span)
|
|
178
|
+
# Some auto-instrumentations are not under our control, so they
|
|
179
|
+
# don't have access to our isolated context. We attach the context
|
|
180
|
+
# to the OTEL global context, so that spans know their parent
|
|
181
|
+
# span and trace_id.
|
|
182
|
+
ctx_token = context_api.attach(new_context)
|
|
183
|
+
# update our isolated context too
|
|
184
|
+
isolated_ctx_token = attach_context(new_context)
|
|
185
|
+
|
|
186
|
+
_process_input(
|
|
187
|
+
span, fn, args, kwargs, ignore_input, ignore_inputs, input_formatter
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
res = fn(*args, **kwargs)
|
|
192
|
+
except Exception as e:
|
|
193
|
+
_process_exception(span, e)
|
|
194
|
+
_cleanup_span(span, wrapper)
|
|
195
|
+
raise
|
|
196
|
+
finally:
|
|
197
|
+
# Always restore global context
|
|
122
198
|
context_api.detach(ctx_token)
|
|
123
|
-
|
|
199
|
+
detach_context(isolated_ctx_token)
|
|
200
|
+
# span will be ended in the generator
|
|
201
|
+
if isinstance(res, types.GeneratorType):
|
|
202
|
+
return _handle_generator(span, wrapper, res)
|
|
203
|
+
if isinstance(res, types.AsyncGeneratorType):
|
|
204
|
+
# async def foo() -> AsyncGenerator[int, None]:
|
|
205
|
+
# is not considered async in a classical sense in Python,
|
|
206
|
+
# so we handle this inside the sync wrapper.
|
|
207
|
+
# In particular, CO_COROUTINE is different from CO_ASYNC_GENERATOR.
|
|
208
|
+
# Flags are listed from LSB here:
|
|
209
|
+
# https://docs.python.org/3/library/inspect.html#inspect-module-co-flags
|
|
210
|
+
# See also: https://groups.google.com/g/python-tulip/c/6rWweGXLutU?pli=1
|
|
211
|
+
return _ahandle_generator(span, wrapper, res)
|
|
212
|
+
|
|
213
|
+
_process_output(span, res, ignore_output, output_formatter)
|
|
214
|
+
_cleanup_span(span, wrapper)
|
|
215
|
+
return res
|
|
124
216
|
|
|
125
217
|
return wrap
|
|
126
218
|
|
|
@@ -128,104 +220,118 @@ def entity_method(
|
|
|
128
220
|
|
|
129
221
|
|
|
130
222
|
# Async Decorators
|
|
131
|
-
def
|
|
223
|
+
def async_observe_base(
|
|
224
|
+
*,
|
|
132
225
|
name: str | None = None,
|
|
133
226
|
ignore_input: bool = False,
|
|
134
227
|
ignore_inputs: list[str] | None = None,
|
|
135
228
|
ignore_output: bool = False,
|
|
136
229
|
span_type: Literal["DEFAULT", "LLM", "TOOL"] = "DEFAULT",
|
|
230
|
+
metadata: dict[str, Any] | None = None,
|
|
137
231
|
association_properties: dict[str, Any] | None = None,
|
|
138
|
-
|
|
139
|
-
|
|
232
|
+
input_formatter: Callable[..., str] | None = None,
|
|
233
|
+
output_formatter: Callable[..., str] | None = None,
|
|
234
|
+
preserve_global_context: bool = False,
|
|
235
|
+
) -> Callable[[F], F]:
|
|
236
|
+
def decorate(fn: F) -> F:
|
|
140
237
|
@wraps(fn)
|
|
141
238
|
async def wrap(*args, **kwargs):
|
|
142
239
|
if not TracerWrapper.verify_initialized():
|
|
143
240
|
return await fn(*args, **kwargs)
|
|
144
241
|
|
|
145
242
|
span_name = name or fn.__name__
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
# span will be ended in the generator
|
|
184
|
-
if isinstance(res, types.AsyncGeneratorType):
|
|
185
|
-
# probably unreachable, read the comment in the similar
|
|
186
|
-
# part of the sync wrapper.
|
|
187
|
-
return await _ahandle_generator(span, ctx_token, res)
|
|
188
|
-
|
|
189
|
-
try:
|
|
190
|
-
if not ignore_output:
|
|
191
|
-
output = json_dumps(res)
|
|
192
|
-
if len(output) > MAX_MANUAL_SPAN_PAYLOAD_SIZE:
|
|
193
|
-
span.set_attribute(
|
|
194
|
-
SPAN_OUTPUT, "Laminar: output too large to record"
|
|
195
|
-
)
|
|
196
|
-
else:
|
|
197
|
-
span.set_attribute(SPAN_OUTPUT, output)
|
|
198
|
-
except TypeError:
|
|
199
|
-
pass
|
|
200
|
-
|
|
201
|
-
span.end()
|
|
243
|
+
wrapper = TracerWrapper()
|
|
244
|
+
|
|
245
|
+
span = _setup_span(
|
|
246
|
+
span_name,
|
|
247
|
+
span_type,
|
|
248
|
+
association_properties,
|
|
249
|
+
preserve_global_context,
|
|
250
|
+
metadata,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
# Set association props in context before push_span_context
|
|
254
|
+
# so child spans inherit them
|
|
255
|
+
assoc_props_token = set_association_props_in_context(span)
|
|
256
|
+
if assoc_props_token and isinstance(span, LaminarSpan):
|
|
257
|
+
span._lmnr_assoc_props_token = assoc_props_token
|
|
258
|
+
|
|
259
|
+
new_context = wrapper.push_span_context(span)
|
|
260
|
+
# Some auto-instrumentations are not under our control, so they
|
|
261
|
+
# don't have access to our isolated context. We attach the context
|
|
262
|
+
# to the OTEL global context, so that spans know their parent
|
|
263
|
+
# span and trace_id.
|
|
264
|
+
ctx_token = context_api.attach(new_context)
|
|
265
|
+
# update our isolated context too
|
|
266
|
+
isolated_ctx_token = attach_context(new_context)
|
|
267
|
+
|
|
268
|
+
_process_input(
|
|
269
|
+
span, fn, args, kwargs, ignore_input, ignore_inputs, input_formatter
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
try:
|
|
273
|
+
res = await fn(*args, **kwargs)
|
|
274
|
+
except Exception as e:
|
|
275
|
+
_process_exception(span, e)
|
|
276
|
+
_cleanup_span(span, wrapper)
|
|
277
|
+
raise e
|
|
278
|
+
finally:
|
|
279
|
+
# Always restore global context
|
|
202
280
|
context_api.detach(ctx_token)
|
|
281
|
+
detach_context(isolated_ctx_token)
|
|
203
282
|
|
|
204
|
-
|
|
283
|
+
# span will be ended in the generator
|
|
284
|
+
if isinstance(res, types.AsyncGeneratorType):
|
|
285
|
+
# probably unreachable, read the comment in the similar
|
|
286
|
+
# part of the sync wrapper.
|
|
287
|
+
return await _ahandle_generator(span, wrapper, res)
|
|
288
|
+
|
|
289
|
+
_process_output(span, res, ignore_output, output_formatter)
|
|
290
|
+
_cleanup_span(span, wrapper)
|
|
291
|
+
return res
|
|
205
292
|
|
|
206
293
|
return wrap
|
|
207
294
|
|
|
208
295
|
return decorate
|
|
209
296
|
|
|
210
297
|
|
|
211
|
-
def _handle_generator(
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
298
|
+
def _handle_generator(
|
|
299
|
+
span: Span,
|
|
300
|
+
wrapper: TracerWrapper,
|
|
301
|
+
res: Generator,
|
|
302
|
+
ignore_output: bool = False,
|
|
303
|
+
output_formatter: Callable[..., str] | None = None,
|
|
304
|
+
):
|
|
305
|
+
results = []
|
|
306
|
+
try:
|
|
307
|
+
for part in res:
|
|
308
|
+
results.append(part)
|
|
309
|
+
yield part
|
|
310
|
+
finally:
|
|
311
|
+
_process_output(span, results, ignore_output, output_formatter)
|
|
312
|
+
_cleanup_span(span, wrapper)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
async def _ahandle_generator(
|
|
316
|
+
span: Span,
|
|
317
|
+
wrapper: TracerWrapper,
|
|
318
|
+
res: AsyncGenerator,
|
|
319
|
+
ignore_output: bool = False,
|
|
320
|
+
output_formatter: Callable[..., str] | None = None,
|
|
321
|
+
):
|
|
322
|
+
results = []
|
|
323
|
+
try:
|
|
324
|
+
async for part in res:
|
|
325
|
+
results.append(part)
|
|
326
|
+
yield part
|
|
327
|
+
finally:
|
|
328
|
+
_process_output(span, results, ignore_output, output_formatter)
|
|
329
|
+
_cleanup_span(span, wrapper)
|
|
227
330
|
|
|
228
331
|
|
|
229
332
|
def _process_exception(span: Span, e: Exception):
|
|
230
333
|
# Note that this `escaped` is sent as a StringValue("True"), not a boolean.
|
|
231
|
-
span.record_exception(
|
|
334
|
+
span.record_exception(
|
|
335
|
+
e, attributes=get_event_attributes_from_context(), escaped=True
|
|
336
|
+
)
|
|
337
|
+
span.set_status(Status(StatusCode.ERROR, str(e)))
|