mantisdk 0.1.1__py3-none-any.whl → 0.1.3__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.

Potentially problematic release.


This version of mantisdk might be problematic. Click here for more details.

@@ -0,0 +1,747 @@
1
+ # Copyright (c) Metis. All rights reserved.
2
+
3
+ """Context managers and decorators for MantisDK tracing.
4
+
5
+ This module provides the user-facing API for creating spans:
6
+ - trace(): Create a root span (trace)
7
+ - span(): Create a child span
8
+ - tool(): Create a tool execution span
9
+
10
+ Both sync and async variants are provided.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import asyncio
16
+ import functools
17
+ import inspect
18
+ import json
19
+ import logging
20
+ from contextlib import asynccontextmanager, contextmanager
21
+ from typing import Any, AsyncGenerator, Callable, Generator, Optional, TypeVar, Union
22
+
23
+ from opentelemetry import trace as otel_trace
24
+ from opentelemetry.trace import Span, SpanKind, Status, StatusCode, Tracer
25
+
26
+ from . import semconv
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+ # Type variable for decorators
31
+ F = TypeVar("F", bound=Callable[..., Any])
32
+
33
+ # Tracer name for MantisDK spans
34
+ TRACER_NAME = "mantisdk.tracing"
35
+
36
+
37
+ # =============================================================================
38
+ # Serialization helpers for I/O capture
39
+ # =============================================================================
40
+
41
+
42
+ def _safe_serialize(value: Any) -> Any:
43
+ """Recursively serialize a value to JSON-compatible format."""
44
+ if value is None or isinstance(value, (bool, int, float, str)):
45
+ return value
46
+ if isinstance(value, (list, tuple)):
47
+ return [_safe_serialize(v) for v in value]
48
+ if isinstance(value, dict):
49
+ return {str(k): _safe_serialize(v) for k, v in value.items()}
50
+ # Fallback for complex objects (Pydantic models, dataclasses, etc.)
51
+ if hasattr(value, "model_dump"):
52
+ return _safe_serialize(value.model_dump())
53
+ if hasattr(value, "__dict__"):
54
+ return _safe_serialize(vars(value))
55
+ return str(value)
56
+
57
+
58
+ def _serialize_value(value: Any) -> str:
59
+ """Safely serialize a value to a JSON string for span attributes."""
60
+ if value is None:
61
+ return "null"
62
+ if isinstance(value, str):
63
+ return value
64
+ if isinstance(value, (bool, int, float)):
65
+ return json.dumps(value)
66
+ try:
67
+ return json.dumps(_safe_serialize(value))
68
+ except Exception:
69
+ return str(value)
70
+
71
+
72
+ def _capture_function_input(fn: Callable[..., Any], args: tuple[Any, ...], kwargs: dict[str, Any]) -> str:
73
+ """Capture function arguments as a JSON string."""
74
+ try:
75
+ sig = inspect.signature(fn)
76
+ bound = sig.bind(*args, **kwargs)
77
+ bound.apply_defaults()
78
+
79
+ # Filter out 'self' and 'cls' parameters
80
+ input_dict = {
81
+ k: _safe_serialize(v)
82
+ for k, v in bound.arguments.items()
83
+ if k not in ("self", "cls")
84
+ }
85
+
86
+ return json.dumps(input_dict) if input_dict else "{}"
87
+ except Exception:
88
+ # Fallback: simple representation
89
+ return json.dumps({
90
+ "args": [str(a) for a in args],
91
+ "kwargs": {k: str(v) for k, v in kwargs.items()}
92
+ })
93
+
94
+
95
+ def _get_tracer() -> Tracer:
96
+ """Get the MantisDK tracer instance."""
97
+ return otel_trace.get_tracer(TRACER_NAME)
98
+
99
+
100
+ class trace:
101
+ """Create a trace span - works as both a decorator and context manager.
102
+
103
+ As a decorator, automatically captures function inputs and outputs::
104
+
105
+ @tracing.trace
106
+ def my_function(query: str) -> dict:
107
+ return {"result": query.upper()}
108
+
109
+ @tracing.trace(name="custom-name", capture_output=False)
110
+ def another_function(data):
111
+ process(data)
112
+
113
+ As a context manager, allows explicit input/output control::
114
+
115
+ with tracing.trace("my-workflow", input=query) as span:
116
+ result = do_work()
117
+ span.set_attribute(tracing.semconv.TRACE_OUTPUT, result)
118
+
119
+ Args:
120
+ name_or_func: Either the span name (str) or the function to decorate.
121
+ name: Explicit span name (for decorator with custom name).
122
+ input: Input value to capture (context manager mode).
123
+ kind: The span kind (default: INTERNAL).
124
+ attributes: Dictionary of attributes to set on the span.
125
+ capture_input: Auto-capture function input (decorator mode, default: True).
126
+ capture_output: Auto-capture function output (decorator mode, default: True).
127
+ record_exception: If True, exceptions are recorded on the span.
128
+ set_status_on_exception: If True, span status is set to ERROR on exception.
129
+ """
130
+
131
+ def __init__(
132
+ self,
133
+ name_or_func: Optional[Union[str, Callable[..., Any]]] = None,
134
+ *,
135
+ name: Optional[str] = None,
136
+ input: Optional[Any] = None,
137
+ kind: SpanKind = SpanKind.INTERNAL,
138
+ attributes: Optional[dict[str, Any]] = None,
139
+ capture_input: bool = True,
140
+ capture_output: bool = True,
141
+ record_exception: bool = True,
142
+ set_status_on_exception: bool = True,
143
+ ):
144
+ self._name_or_func = name_or_func
145
+ self._explicit_name = name
146
+ self._input = input
147
+ self._kind = kind
148
+ self._attributes = attributes
149
+ self._capture_input = capture_input
150
+ self._capture_output = capture_output
151
+ self._record_exception = record_exception
152
+ self._set_status_on_exception = set_status_on_exception
153
+ self._span: Optional[Span] = None
154
+ self._token: Optional[Any] = None
155
+
156
+ # If called as @trace without parentheses, name_or_func is the function
157
+ if callable(name_or_func):
158
+ self._func = name_or_func
159
+ self._span_name = name or name_or_func.__name__
160
+ else:
161
+ self._func = None
162
+ self._span_name = name_or_func or name or "trace"
163
+
164
+ def __call__(self, *args: Any, **kwargs: Any) -> Any:
165
+ """Handle decorator invocation."""
166
+ # If we already have a function, this is the actual call
167
+ if self._func is not None:
168
+ return self._invoke_decorated(self._func, args, kwargs)
169
+
170
+ # If first arg is a callable, we're being used as @trace(...) decorator
171
+ if args and callable(args[0]) and not kwargs:
172
+ func = args[0]
173
+ self._func = func
174
+ self._span_name = self._explicit_name or func.__name__
175
+
176
+ # Return a wrapper that will be called when the function is invoked
177
+ if asyncio.iscoroutinefunction(func):
178
+ @functools.wraps(func)
179
+ async def async_wrapper(*a: Any, **kw: Any) -> Any:
180
+ return await self._invoke_decorated_async(func, a, kw)
181
+ return async_wrapper
182
+ else:
183
+ @functools.wraps(func)
184
+ def sync_wrapper(*a: Any, **kw: Any) -> Any:
185
+ return self._invoke_decorated(func, a, kw)
186
+ return sync_wrapper
187
+
188
+ raise TypeError("trace() requires a name string or a function")
189
+
190
+ def _invoke_decorated(self, func: Callable[..., Any], args: tuple[Any, ...], kwargs: dict[str, Any]) -> Any:
191
+ """Invoke a decorated sync function with tracing."""
192
+ tracer = _get_tracer()
193
+
194
+ with tracer.start_as_current_span(
195
+ name=self._span_name,
196
+ kind=self._kind,
197
+ attributes=self._attributes,
198
+ record_exception=self._record_exception,
199
+ set_status_on_exception=self._set_status_on_exception,
200
+ ) as span:
201
+ # Capture input
202
+ if self._capture_input:
203
+ input_val = _capture_function_input(func, args, kwargs)
204
+ span.set_attribute(semconv.OBSERVATION_INPUT, input_val)
205
+
206
+ result = func(*args, **kwargs)
207
+
208
+ # Capture output
209
+ if self._capture_output:
210
+ span.set_attribute(semconv.OBSERVATION_OUTPUT, _serialize_value(result))
211
+
212
+ return result
213
+
214
+ async def _invoke_decorated_async(self, func: Callable[..., Any], args: tuple[Any, ...], kwargs: dict[str, Any]) -> Any:
215
+ """Invoke a decorated async function with tracing."""
216
+ tracer = _get_tracer()
217
+
218
+ with tracer.start_as_current_span(
219
+ name=self._span_name,
220
+ kind=self._kind,
221
+ attributes=self._attributes,
222
+ record_exception=self._record_exception,
223
+ set_status_on_exception=self._set_status_on_exception,
224
+ ) as span:
225
+ # Capture input
226
+ if self._capture_input:
227
+ input_val = _capture_function_input(func, args, kwargs)
228
+ span.set_attribute(semconv.OBSERVATION_INPUT, input_val)
229
+
230
+ result = await func(*args, **kwargs)
231
+
232
+ # Capture output
233
+ if self._capture_output:
234
+ span.set_attribute(semconv.OBSERVATION_OUTPUT, _serialize_value(result))
235
+
236
+ return result
237
+
238
+ def __enter__(self) -> Span:
239
+ """Context manager entry - start the span."""
240
+ tracer = _get_tracer()
241
+ self._span = tracer.start_span(
242
+ name=self._span_name,
243
+ kind=self._kind,
244
+ attributes=self._attributes,
245
+ record_exception=self._record_exception,
246
+ set_status_on_exception=self._set_status_on_exception,
247
+ )
248
+ # Store the context manager to properly exit later
249
+ self._use_span_cm = otel_trace.use_span(self._span, end_on_exit=False)
250
+ self._use_span_cm.__enter__()
251
+
252
+ # Set input if provided
253
+ if self._input is not None:
254
+ self._span.set_attribute(semconv.TRACE_INPUT, _serialize_value(self._input))
255
+
256
+ return self._span
257
+
258
+ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
259
+ """Context manager exit - end the span."""
260
+ try:
261
+ if self._span is not None:
262
+ if exc_val is not None and self._record_exception:
263
+ self._span.record_exception(exc_val)
264
+ if exc_val is not None and self._set_status_on_exception:
265
+ self._span.set_status(Status(StatusCode.ERROR, str(exc_val)))
266
+ self._span.end()
267
+ finally:
268
+ if hasattr(self, '_use_span_cm') and self._use_span_cm is not None:
269
+ self._use_span_cm.__exit__(exc_type, exc_val, exc_tb)
270
+
271
+
272
+ @contextmanager
273
+ def span(
274
+ name: str,
275
+ *,
276
+ input: Optional[Any] = None,
277
+ kind: SpanKind = SpanKind.INTERNAL,
278
+ attributes: Optional[dict[str, Any]] = None,
279
+ record_exception: bool = True,
280
+ set_status_on_exception: bool = True,
281
+ **extra_attributes: Any,
282
+ ) -> Generator[Span, None, None]:
283
+ """Context manager to create a child span.
284
+
285
+ This creates a span that will be a child of the current active span.
286
+ If no active span exists, it becomes a root span.
287
+
288
+ Args:
289
+ name: The name of the span.
290
+ input: Input value to capture (displayed in Insight UI).
291
+ kind: The span kind (default: INTERNAL).
292
+ attributes: Dictionary of attributes to set on the span.
293
+ record_exception: If True, exceptions are recorded on the span.
294
+ set_status_on_exception: If True, span status is set to ERROR on exception.
295
+ **extra_attributes: Additional attributes as keyword arguments.
296
+
297
+ Yields:
298
+ The OpenTelemetry Span object.
299
+
300
+ Example::
301
+
302
+ import mantisdk.tracing as tracing
303
+
304
+ with tracing.trace("my-workflow"):
305
+ with tracing.span("step.load_data", input=dataset_name) as s:
306
+ data = load_data()
307
+ s.set_attribute(tracing.semconv.OBSERVATION_OUTPUT, str(len(data)))
308
+
309
+ with tracing.span("step.process"):
310
+ process(data)
311
+ """
312
+ tracer = _get_tracer()
313
+ all_attributes = {**(attributes or {}), **extra_attributes}
314
+
315
+ with tracer.start_as_current_span(
316
+ name=name,
317
+ kind=kind,
318
+ attributes=all_attributes if all_attributes else None,
319
+ record_exception=record_exception,
320
+ set_status_on_exception=set_status_on_exception,
321
+ ) as otel_span:
322
+ # Set input if provided
323
+ if input is not None:
324
+ otel_span.set_attribute(semconv.OBSERVATION_INPUT, _serialize_value(input))
325
+ try:
326
+ yield otel_span
327
+ except Exception:
328
+ raise
329
+
330
+
331
+ @contextmanager
332
+ def tool(
333
+ name: str,
334
+ *,
335
+ input: Optional[Any] = None,
336
+ tool_name: Optional[str] = None,
337
+ attributes: Optional[dict[str, Any]] = None,
338
+ record_exception: bool = True,
339
+ set_status_on_exception: bool = True,
340
+ **extra_attributes: Any,
341
+ ) -> Generator[Span, None, None]:
342
+ """Context manager to trace a tool execution.
343
+
344
+ This creates a span with TOOL kind and semantic attributes for tool calls.
345
+ Useful for tracing function calls, API calls, or other discrete operations.
346
+
347
+ Args:
348
+ name: The span name (often the function/tool name).
349
+ input: Input value to capture (displayed in Insight UI).
350
+ tool_name: Explicit tool name attribute (defaults to name).
351
+ attributes: Dictionary of attributes to set on the span.
352
+ record_exception: If True, exceptions are recorded on the span.
353
+ set_status_on_exception: If True, span status is set to ERROR on exception.
354
+ **extra_attributes: Additional attributes as keyword arguments.
355
+
356
+ Yields:
357
+ The OpenTelemetry Span object.
358
+
359
+ Example::
360
+
361
+ import mantisdk.tracing as tracing
362
+
363
+ with tracing.tool("search_database", input=query) as s:
364
+ results = db.execute(query)
365
+ s.set_attribute(tracing.semconv.OBSERVATION_OUTPUT, str(len(results)))
366
+ """
367
+ tracer = _get_tracer()
368
+ all_attributes = {
369
+ "tool.name": tool_name or name,
370
+ **(attributes or {}),
371
+ **extra_attributes,
372
+ }
373
+
374
+ # Use CLIENT kind for tool calls (external service calls)
375
+ with tracer.start_as_current_span(
376
+ name=name,
377
+ kind=SpanKind.CLIENT,
378
+ attributes=all_attributes,
379
+ record_exception=record_exception,
380
+ set_status_on_exception=set_status_on_exception,
381
+ ) as otel_span:
382
+ # Set input if provided
383
+ if input is not None:
384
+ otel_span.set_attribute(semconv.OBSERVATION_INPUT, _serialize_value(input))
385
+ try:
386
+ yield otel_span
387
+ except Exception:
388
+ raise
389
+
390
+
391
+ @asynccontextmanager
392
+ async def atrace(
393
+ name: str,
394
+ *,
395
+ input: Optional[Any] = None,
396
+ kind: SpanKind = SpanKind.INTERNAL,
397
+ attributes: Optional[dict[str, Any]] = None,
398
+ record_exception: bool = True,
399
+ set_status_on_exception: bool = True,
400
+ **extra_attributes: Any,
401
+ ) -> AsyncGenerator[Span, None]:
402
+ """Async context manager to create a trace (root span).
403
+
404
+ Async variant of trace() for use in async code.
405
+
406
+ Args:
407
+ name: The name of the trace/span.
408
+ input: Input value to capture (displayed in Insight UI).
409
+ kind: The span kind (default: INTERNAL).
410
+ attributes: Dictionary of attributes to set on the span.
411
+ record_exception: If True, exceptions are recorded on the span.
412
+ set_status_on_exception: If True, span status is set to ERROR on exception.
413
+ **extra_attributes: Additional attributes as keyword arguments.
414
+
415
+ Yields:
416
+ The OpenTelemetry Span object.
417
+
418
+ Example::
419
+
420
+ import mantisdk.tracing as tracing
421
+
422
+ async with tracing.atrace("my-async-workflow", input=query) as span:
423
+ result = await async_operation()
424
+ span.set_attribute(tracing.semconv.TRACE_OUTPUT, result)
425
+ """
426
+ tracer = _get_tracer()
427
+ all_attributes = {**(attributes or {}), **extra_attributes}
428
+
429
+ with tracer.start_as_current_span(
430
+ name=name,
431
+ kind=kind,
432
+ attributes=all_attributes if all_attributes else None,
433
+ record_exception=record_exception,
434
+ set_status_on_exception=set_status_on_exception,
435
+ ) as otel_span:
436
+ # Set input if provided
437
+ if input is not None:
438
+ otel_span.set_attribute(semconv.TRACE_INPUT, _serialize_value(input))
439
+ try:
440
+ yield otel_span
441
+ except Exception:
442
+ raise
443
+
444
+
445
+ @asynccontextmanager
446
+ async def aspan(
447
+ name: str,
448
+ *,
449
+ input: Optional[Any] = None,
450
+ kind: SpanKind = SpanKind.INTERNAL,
451
+ attributes: Optional[dict[str, Any]] = None,
452
+ record_exception: bool = True,
453
+ set_status_on_exception: bool = True,
454
+ **extra_attributes: Any,
455
+ ) -> AsyncGenerator[Span, None]:
456
+ """Async context manager to create a child span.
457
+
458
+ Async variant of span() for use in async code.
459
+
460
+ Args:
461
+ name: The name of the span.
462
+ input: Input value to capture (displayed in Insight UI).
463
+ kind: The span kind (default: INTERNAL).
464
+ attributes: Dictionary of attributes to set on the span.
465
+ record_exception: If True, exceptions are recorded on the span.
466
+ set_status_on_exception: If True, span status is set to ERROR on exception.
467
+ **extra_attributes: Additional attributes as keyword arguments.
468
+
469
+ Yields:
470
+ The OpenTelemetry Span object.
471
+
472
+ Example::
473
+
474
+ import mantisdk.tracing as tracing
475
+
476
+ async with tracing.atrace("workflow", input=query):
477
+ async with tracing.aspan("fetch_data") as s:
478
+ data = await fetch()
479
+ s.set_attribute(tracing.semconv.OBSERVATION_OUTPUT, str(len(data)))
480
+ """
481
+ tracer = _get_tracer()
482
+ all_attributes = {**(attributes or {}), **extra_attributes}
483
+
484
+ with tracer.start_as_current_span(
485
+ name=name,
486
+ kind=kind,
487
+ attributes=all_attributes if all_attributes else None,
488
+ record_exception=record_exception,
489
+ set_status_on_exception=set_status_on_exception,
490
+ ) as otel_span:
491
+ # Set input if provided
492
+ if input is not None:
493
+ otel_span.set_attribute(semconv.OBSERVATION_INPUT, _serialize_value(input))
494
+ try:
495
+ yield otel_span
496
+ except Exception:
497
+ raise
498
+
499
+
500
+ def trace_decorator(
501
+ func: Optional[F] = None,
502
+ *,
503
+ name: Optional[str] = None,
504
+ kind: SpanKind = SpanKind.INTERNAL,
505
+ attributes: Optional[dict[str, Any]] = None,
506
+ record_exception: bool = True,
507
+ set_status_on_exception: bool = True,
508
+ ) -> Union[F, Callable[[F], F]]:
509
+ """Decorator to trace a function execution.
510
+
511
+ Can be used with or without parentheses:
512
+ @trace_decorator
513
+ def my_func(): ...
514
+
515
+ @trace_decorator(name="custom-name")
516
+ def my_func(): ...
517
+
518
+ Works with both sync and async functions.
519
+
520
+ Args:
521
+ func: The function to decorate (when used without parentheses).
522
+ name: Custom span name. Defaults to the function name.
523
+ kind: The span kind (default: INTERNAL).
524
+ attributes: Dictionary of attributes to set on the span.
525
+ record_exception: If True, exceptions are recorded on the span.
526
+ set_status_on_exception: If True, span status is set to ERROR on exception.
527
+
528
+ Returns:
529
+ Decorated function.
530
+
531
+ Example::
532
+
533
+ import mantisdk.tracing_claude as tracing
534
+
535
+ @tracing.trace
536
+ def process_data(items):
537
+ return [process(item) for item in items]
538
+
539
+ @tracing.trace(name="custom-operation", attributes={"version": "1.0"})
540
+ async def async_operation():
541
+ return await do_something()
542
+ """
543
+ def decorator(fn: F) -> F:
544
+ span_name = name or fn.__name__
545
+ span_attributes = attributes or {}
546
+
547
+ if asyncio.iscoroutinefunction(fn):
548
+ @functools.wraps(fn)
549
+ async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
550
+ tracer = _get_tracer()
551
+ with tracer.start_as_current_span(
552
+ name=span_name,
553
+ kind=kind,
554
+ attributes=span_attributes if span_attributes else None,
555
+ record_exception=record_exception,
556
+ set_status_on_exception=set_status_on_exception,
557
+ ):
558
+ return await fn(*args, **kwargs)
559
+ return async_wrapper # type: ignore
560
+ else:
561
+ @functools.wraps(fn)
562
+ def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
563
+ tracer = _get_tracer()
564
+ with tracer.start_as_current_span(
565
+ name=span_name,
566
+ kind=kind,
567
+ attributes=span_attributes if span_attributes else None,
568
+ record_exception=record_exception,
569
+ set_status_on_exception=set_status_on_exception,
570
+ ):
571
+ return fn(*args, **kwargs)
572
+ return sync_wrapper # type: ignore
573
+
574
+ if func is None:
575
+ # Called with parentheses: @trace_decorator(...)
576
+ return decorator
577
+ else:
578
+ # Called without parentheses: @trace_decorator
579
+ return decorator(func)
580
+
581
+
582
+ def tool_decorator(
583
+ func: Optional[F] = None,
584
+ *,
585
+ name: Optional[str] = None,
586
+ tool_name: Optional[str] = None,
587
+ attributes: Optional[dict[str, Any]] = None,
588
+ record_exception: bool = True,
589
+ set_status_on_exception: bool = True,
590
+ ) -> Union[F, Callable[[F], F]]:
591
+ """Decorator to trace a tool/function execution.
592
+
593
+ Similar to trace_decorator but uses CLIENT span kind and adds tool.name attribute.
594
+
595
+ Args:
596
+ func: The function to decorate (when used without parentheses).
597
+ name: Custom span name. Defaults to the function name.
598
+ tool_name: Tool name attribute. Defaults to span name.
599
+ attributes: Dictionary of attributes to set on the span.
600
+ record_exception: If True, exceptions are recorded on the span.
601
+ set_status_on_exception: If True, span status is set to ERROR on exception.
602
+
603
+ Returns:
604
+ Decorated function.
605
+
606
+ Example::
607
+
608
+ import mantisdk.tracing_claude as tracing
609
+
610
+ @tracing.tool
611
+ def search_database(query: str) -> list:
612
+ return db.execute(query)
613
+ """
614
+ def decorator(fn: F) -> F:
615
+ span_name = name or fn.__name__
616
+ span_attributes = {
617
+ "tool.name": tool_name or span_name,
618
+ **(attributes or {}),
619
+ }
620
+
621
+ if asyncio.iscoroutinefunction(fn):
622
+ @functools.wraps(fn)
623
+ async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
624
+ tracer = _get_tracer()
625
+ with tracer.start_as_current_span(
626
+ name=span_name,
627
+ kind=SpanKind.CLIENT,
628
+ attributes=span_attributes,
629
+ record_exception=record_exception,
630
+ set_status_on_exception=set_status_on_exception,
631
+ ):
632
+ return await fn(*args, **kwargs)
633
+ return async_wrapper # type: ignore
634
+ else:
635
+ @functools.wraps(fn)
636
+ def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
637
+ tracer = _get_tracer()
638
+ with tracer.start_as_current_span(
639
+ name=span_name,
640
+ kind=SpanKind.CLIENT,
641
+ attributes=span_attributes,
642
+ record_exception=record_exception,
643
+ set_status_on_exception=set_status_on_exception,
644
+ ):
645
+ return fn(*args, **kwargs)
646
+ return sync_wrapper # type: ignore
647
+
648
+ if func is None:
649
+ return decorator
650
+ else:
651
+ return decorator(func)
652
+
653
+
654
+ def record_exception(
655
+ span: Span,
656
+ exception: BaseException,
657
+ *,
658
+ attributes: Optional[dict[str, Any]] = None,
659
+ escaped: bool = False,
660
+ ) -> None:
661
+ """Record an exception on a span.
662
+
663
+ Helper function for recording exceptions with proper attributes.
664
+
665
+ Args:
666
+ span: The span to record the exception on.
667
+ exception: The exception to record.
668
+ attributes: Additional attributes to record with the exception.
669
+ escaped: Whether the exception escaped the span's scope.
670
+
671
+ Example::
672
+
673
+ import mantisdk.tracing_claude as tracing
674
+
675
+ with tracing.span("operation") as s:
676
+ try:
677
+ risky_operation()
678
+ except ValueError as e:
679
+ tracing.record_exception(s, e, attributes={"input": "bad"})
680
+ s.set_status(StatusCode.ERROR, str(e))
681
+ # Handle or re-raise
682
+ """
683
+ span.record_exception(exception, attributes=attributes, escaped=escaped)
684
+
685
+
686
+ def set_span_error(
687
+ span: Span,
688
+ message: str,
689
+ *,
690
+ exception: Optional[BaseException] = None,
691
+ ) -> None:
692
+ """Set a span's status to ERROR with a description.
693
+
694
+ Convenience function for marking a span as failed.
695
+
696
+ Args:
697
+ span: The span to mark as error.
698
+ message: Error description.
699
+ exception: Optional exception to record.
700
+
701
+ Example::
702
+
703
+ import mantisdk.tracing_claude as tracing
704
+
705
+ with tracing.span("operation") as s:
706
+ result = do_work()
707
+ if not result.success:
708
+ tracing.set_span_error(s, f"Operation failed: {result.error}")
709
+ """
710
+ span.set_status(Status(StatusCode.ERROR, message))
711
+ if exception is not None:
712
+ span.record_exception(exception)
713
+
714
+
715
+ def set_span_ok(span: Span, message: Optional[str] = None) -> None:
716
+ """Set a span's status to OK.
717
+
718
+ Convenience function for marking a span as successful.
719
+
720
+ Args:
721
+ span: The span to mark as OK.
722
+ message: Optional success message.
723
+ """
724
+ span.set_status(Status(StatusCode.OK, message))
725
+
726
+
727
+ def get_current_span() -> Span:
728
+ """Get the currently active span.
729
+
730
+ Returns:
731
+ The current span, or a non-recording span if none is active.
732
+
733
+ Example::
734
+
735
+ import mantisdk.tracing_claude as tracing
736
+
737
+ def inner_function():
738
+ span = tracing.get_current_span()
739
+ span.set_attribute("inner_data", "value")
740
+ """
741
+ return otel_trace.get_current_span()
742
+
743
+
744
+ # Convenience aliases for decorator-style usage
745
+ # These allow: @tracing.trace instead of @tracing.trace_decorator
746
+ # The context manager `trace` takes precedence, but when used as decorator
747
+ # it works because the context manager returns a span, not a wrapper