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.
- mantisdk/__init__.py +1 -1
- mantisdk/tracing/__init__.py +66 -0
- mantisdk/tracing/api.py +747 -0
- mantisdk/tracing/attributes.py +191 -0
- mantisdk/tracing/exporters/__init__.py +10 -0
- mantisdk/tracing/exporters/insight.py +202 -0
- mantisdk/tracing/init.py +344 -0
- mantisdk/tracing/instrumentors/__init__.py +15 -0
- mantisdk/tracing/instrumentors/claude_agent_sdk.py +591 -0
- mantisdk/tracing/instrumentors/instrumentation_principles.md +289 -0
- mantisdk/tracing/instrumentors/registry.py +313 -0
- mantisdk/tracing/semconv.py +82 -0
- {mantisdk-0.1.1.dist-info → mantisdk-0.1.3.dist-info}/METADATA +1 -2
- {mantisdk-0.1.1.dist-info → mantisdk-0.1.3.dist-info}/RECORD +17 -6
- {mantisdk-0.1.1.dist-info → mantisdk-0.1.3.dist-info}/WHEEL +0 -0
- {mantisdk-0.1.1.dist-info → mantisdk-0.1.3.dist-info}/entry_points.txt +0 -0
- {mantisdk-0.1.1.dist-info → mantisdk-0.1.3.dist-info}/licenses/LICENSE +0 -0
mantisdk/tracing/api.py
ADDED
|
@@ -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
|