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
@@ -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 MAX_MANUAL_SPAN_PAYLOAD_SIZE
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
- SPAN_INPUT,
18
- SPAN_OUTPUT,
20
+ METADATA,
19
21
  SPAN_TYPE,
20
22
  )
21
23
  from lmnr.opentelemetry_lib.tracing import TracerWrapper
22
- from lmnr.opentelemetry_lib.utils.json_encoder import JSONEncoder
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
- return json.dumps(data, cls=CustomJSONEncoder)
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
- # Log the exception and return a placeholder if serialization completely fails
40
- logging.warning("Failed to serialize data to JSON, type: %s", type(data))
41
- return "{}" # Return an empty JSON object as a fallback
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 entity_method(
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
- def decorate(fn):
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
- with get_tracer() as tracer:
61
- span = tracer.start_span(span_name, attributes={SPAN_TYPE: span_type})
62
- if association_properties is not None:
63
- for key, value in association_properties.items():
64
- span.set_attribute(f"{ASSOCIATION_PROPERTIES}.{key}", value)
65
-
66
- ctx = trace.set_span_in_context(span, context_api.get_current())
67
- ctx_token = context_api.attach(ctx)
68
-
69
- try:
70
- if not ignore_input:
71
- inp = json_dumps(
72
- get_input_from_func_args(
73
- fn,
74
- is_method=is_method(fn),
75
- func_args=args,
76
- func_kwargs=kwargs,
77
- ignore_inputs=ignore_inputs,
78
- )
79
- )
80
- if len(inp) > MAX_MANUAL_SPAN_PAYLOAD_SIZE:
81
- span.set_attribute(
82
- SPAN_INPUT, "Laminar: input too large to record"
83
- )
84
- else:
85
- span.set_attribute(SPAN_INPUT, inp)
86
- except TypeError:
87
- pass
88
-
89
- try:
90
- res = fn(*args, **kwargs)
91
- except Exception as e:
92
- _process_exception(span, e)
93
- span.end()
94
- raise e
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
- return res
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 aentity_method(
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
- def decorate(fn):
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
- with get_tracer() as tracer:
148
- span = tracer.start_span(span_name, attributes={SPAN_TYPE: span_type})
149
- if association_properties is not None:
150
- for key, value in association_properties.items():
151
- span.set_attribute(f"{ASSOCIATION_PROPERTIES}.{key}", value)
152
-
153
- ctx = trace.set_span_in_context(span, context_api.get_current())
154
- ctx_token = context_api.attach(ctx)
155
-
156
- try:
157
- if not ignore_input:
158
- inp = json_dumps(
159
- get_input_from_func_args(
160
- fn,
161
- is_method=is_method(fn),
162
- func_args=args,
163
- func_kwargs=kwargs,
164
- ignore_inputs=ignore_inputs,
165
- )
166
- )
167
- if len(inp) > MAX_MANUAL_SPAN_PAYLOAD_SIZE:
168
- span.set_attribute(
169
- SPAN_INPUT, "Laminar: input too large to record"
170
- )
171
- else:
172
- span.set_attribute(SPAN_INPUT, inp)
173
- except TypeError:
174
- pass
175
-
176
- try:
177
- res = await fn(*args, **kwargs)
178
- except Exception as e:
179
- _process_exception(span, e)
180
- span.end()
181
- raise e
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
- return res
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(span, ctx_token, res):
212
- yield from res
213
-
214
- span.end()
215
- if ctx_token is not None:
216
- context_api.detach(ctx_token)
217
-
218
-
219
- async def _ahandle_generator(span, ctx_token, res):
220
- # async with contextlib.aclosing(res) as closing_gen:
221
- async for part in res:
222
- yield part
223
-
224
- span.end()
225
- if ctx_token is not None:
226
- context_api.detach(ctx_token)
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(e, escaped=True)
334
+ span.record_exception(
335
+ e, attributes=get_event_attributes_from_context(), escaped=True
336
+ )
337
+ span.set_status(Status(StatusCode.ERROR, str(e)))