mantisdk 0.1.1__py3-none-any.whl → 0.1.2__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.
- mantisdk/__init__.py +1 -1
- mantisdk/tracing/__init__.py +57 -0
- mantisdk/tracing/api.py +546 -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 +371 -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-0.1.1.dist-info → mantisdk-0.1.2.dist-info}/METADATA +1 -1
- {mantisdk-0.1.1.dist-info → mantisdk-0.1.2.dist-info}/RECORD +16 -6
- {mantisdk-0.1.1.dist-info → mantisdk-0.1.2.dist-info}/WHEEL +0 -0
- {mantisdk-0.1.1.dist-info → mantisdk-0.1.2.dist-info}/entry_points.txt +0 -0
- {mantisdk-0.1.1.dist-info → mantisdk-0.1.2.dist-info}/licenses/LICENSE +0 -0
mantisdk/tracing/init.py
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
# Copyright (c) Metis. All rights reserved.
|
|
2
|
+
|
|
3
|
+
"""Initialization and lifecycle management for MantisDK tracing.
|
|
4
|
+
|
|
5
|
+
This module provides the core init(), shutdown(), and flush() functions
|
|
6
|
+
for managing the OpenTelemetry TracerProvider lifecycle.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
import threading
|
|
13
|
+
from typing import Any, Dict, List, Optional, TYPE_CHECKING
|
|
14
|
+
|
|
15
|
+
from opentelemetry import trace
|
|
16
|
+
from opentelemetry.sdk.resources import Resource, SERVICE_NAME
|
|
17
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
18
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor, SpanExporter
|
|
19
|
+
|
|
20
|
+
from .exporters.insight import insight, is_insight_configured
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from openinference.instrumentation import TraceConfig
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
# Module-level state
|
|
28
|
+
_initialized = False
|
|
29
|
+
_init_lock = threading.Lock()
|
|
30
|
+
_provider: Optional[TracerProvider] = None
|
|
31
|
+
_processors: List[BatchSpanProcessor] = []
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def init(
|
|
35
|
+
*,
|
|
36
|
+
trace_name: Optional[str] = None,
|
|
37
|
+
service_name: Optional[str] = None,
|
|
38
|
+
instrument_default: bool = True,
|
|
39
|
+
instrument_agentops: bool = False,
|
|
40
|
+
exporters: Optional[List[SpanExporter]] = None,
|
|
41
|
+
trace_config: Optional["TraceConfig"] = None,
|
|
42
|
+
force: bool = False,
|
|
43
|
+
integrations: Optional[Dict[str, Any]] = None,
|
|
44
|
+
) -> None:
|
|
45
|
+
"""Initialize MantisDK standalone tracing.
|
|
46
|
+
|
|
47
|
+
This function configures OpenTelemetry with a connection to Mantis Insight
|
|
48
|
+
(if configured via environment variables), and optionally sets up
|
|
49
|
+
auto-instrumentation for common AI libraries.
|
|
50
|
+
|
|
51
|
+
Provider Reuse Behavior:
|
|
52
|
+
- If no TracerProvider exists, creates one.
|
|
53
|
+
- If a TracerProvider exists and force=False, reuses it and adds
|
|
54
|
+
processors/exporters (avoiding duplicates).
|
|
55
|
+
- If force=True, replaces the existing TracerProvider.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
trace_name: Optional name for the trace (mostly for AgentOps compatibility).
|
|
59
|
+
service_name: Name of the service (mapped to resource attributes).
|
|
60
|
+
Defaults to "mantisdk-tracing".
|
|
61
|
+
instrument_default: Whether to automatically instrument the "core set"
|
|
62
|
+
of libraries (OpenAI, Anthropic, LangChain, LlamaIndex) if installed.
|
|
63
|
+
instrument_agentops: Whether to enable AgentOps helpers (decorators)
|
|
64
|
+
without exporting to AgentOps platform.
|
|
65
|
+
exporters: List of custom OpenTelemetry SpanExporters. If None and
|
|
66
|
+
Insight env vars are configured, auto-creates an Insight exporter.
|
|
67
|
+
trace_config: OpenInference TraceConfig for controlling capture behavior
|
|
68
|
+
(hiding inputs/outputs, etc.). Default captures all.
|
|
69
|
+
force: If True, replaces any existing global TracerProvider. If False
|
|
70
|
+
(default), reuses the existing provider and appends processors.
|
|
71
|
+
integrations: Dictionary for fine-grained control over integrations.
|
|
72
|
+
Example: {"agentops": {"enabled": True, "helpers_only": True}}
|
|
73
|
+
|
|
74
|
+
Example::
|
|
75
|
+
|
|
76
|
+
import mantisdk.tracing_claude as tracing
|
|
77
|
+
|
|
78
|
+
# Minimal: auto-detect Insight from env vars, auto-instrument
|
|
79
|
+
tracing.init()
|
|
80
|
+
|
|
81
|
+
# Explicit configuration
|
|
82
|
+
tracing.init(
|
|
83
|
+
service_name="my-agent",
|
|
84
|
+
exporters=[tracing.insight_exporter(
|
|
85
|
+
host="https://insight.withmetis.ai",
|
|
86
|
+
public_key="pk-lf-...",
|
|
87
|
+
secret_key="sk-lf-...",
|
|
88
|
+
)],
|
|
89
|
+
instrument_default=True,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# Ensure cleanup on shutdown
|
|
93
|
+
import atexit
|
|
94
|
+
atexit.register(tracing.shutdown)
|
|
95
|
+
"""
|
|
96
|
+
global _initialized, _provider, _processors
|
|
97
|
+
|
|
98
|
+
with _init_lock:
|
|
99
|
+
# Check existing provider
|
|
100
|
+
current_provider = trace.get_tracer_provider()
|
|
101
|
+
has_sdk_provider = isinstance(current_provider, TracerProvider)
|
|
102
|
+
|
|
103
|
+
if _initialized and not force:
|
|
104
|
+
logger.debug("MantisDK tracing already initialized. Use force=True to reinitialize.")
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
if has_sdk_provider and not force:
|
|
108
|
+
# Reuse existing provider
|
|
109
|
+
logger.info("Reusing existing TracerProvider")
|
|
110
|
+
_provider = current_provider
|
|
111
|
+
else:
|
|
112
|
+
# Create new provider
|
|
113
|
+
resource_attrs = {
|
|
114
|
+
SERVICE_NAME: service_name or "mantisdk-tracing",
|
|
115
|
+
}
|
|
116
|
+
if trace_name:
|
|
117
|
+
resource_attrs["trace.name"] = trace_name
|
|
118
|
+
|
|
119
|
+
resource = Resource.create(resource_attrs)
|
|
120
|
+
_provider = TracerProvider(resource=resource)
|
|
121
|
+
|
|
122
|
+
# Set as global provider
|
|
123
|
+
trace.set_tracer_provider(_provider)
|
|
124
|
+
logger.info("Created new TracerProvider with service_name=%s", service_name or "mantisdk-tracing")
|
|
125
|
+
|
|
126
|
+
# Configure exporters
|
|
127
|
+
configured_exporters = exporters or []
|
|
128
|
+
|
|
129
|
+
# Auto-detect Insight if no exporters provided
|
|
130
|
+
if not configured_exporters and is_insight_configured():
|
|
131
|
+
try:
|
|
132
|
+
insight_exporter = insight()
|
|
133
|
+
configured_exporters.append(insight_exporter)
|
|
134
|
+
logger.info("Auto-configured Insight exporter from environment variables")
|
|
135
|
+
except ValueError as e:
|
|
136
|
+
logger.warning("Could not auto-configure Insight exporter: %s", e)
|
|
137
|
+
|
|
138
|
+
# Add processors for each exporter (idempotent)
|
|
139
|
+
for exporter in configured_exporters:
|
|
140
|
+
_add_processor_idempotent(_provider, exporter)
|
|
141
|
+
|
|
142
|
+
# Run instrumentation
|
|
143
|
+
if instrument_default:
|
|
144
|
+
instrument()
|
|
145
|
+
|
|
146
|
+
if instrument_agentops:
|
|
147
|
+
_setup_agentops_helpers(integrations)
|
|
148
|
+
|
|
149
|
+
_initialized = True
|
|
150
|
+
logger.info("MantisDK tracing initialized successfully")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def instrument(
|
|
154
|
+
names: Optional[List[str]] = None,
|
|
155
|
+
skip: Optional[List[str]] = None,
|
|
156
|
+
) -> None:
|
|
157
|
+
"""Manually enable specific instrumentations.
|
|
158
|
+
|
|
159
|
+
This function looks up instrumentors by name from the registry and
|
|
160
|
+
activates them. If no names are provided and init() was called with
|
|
161
|
+
instrument_default=True, this enables the "core set".
|
|
162
|
+
|
|
163
|
+
Core Set (enabled by default):
|
|
164
|
+
- openai
|
|
165
|
+
- anthropic
|
|
166
|
+
- langchain
|
|
167
|
+
- llama_index
|
|
168
|
+
- litellm
|
|
169
|
+
|
|
170
|
+
Additional Available:
|
|
171
|
+
- google_adk
|
|
172
|
+
- mistral
|
|
173
|
+
- groq
|
|
174
|
+
- bedrock
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
names: List of instrumentor names to enable. If None and called from
|
|
178
|
+
init() with instrument_default=True, enables the core set.
|
|
179
|
+
skip: List of instrumentor names to explicitly skip.
|
|
180
|
+
|
|
181
|
+
Example::
|
|
182
|
+
|
|
183
|
+
import mantisdk.tracing_claude as tracing
|
|
184
|
+
|
|
185
|
+
# Enable only specific instrumentors
|
|
186
|
+
tracing.init(instrument_default=False)
|
|
187
|
+
tracing.instrument(names=["openai", "anthropic"])
|
|
188
|
+
|
|
189
|
+
# Enable all except langchain
|
|
190
|
+
tracing.instrument(skip=["langchain"])
|
|
191
|
+
"""
|
|
192
|
+
from .instrumentors.registry import get_registry
|
|
193
|
+
|
|
194
|
+
registry = get_registry()
|
|
195
|
+
skip_set = set(skip or [])
|
|
196
|
+
|
|
197
|
+
# Determine which instrumentors to enable
|
|
198
|
+
if names is None:
|
|
199
|
+
# Default core set (includes claude_agent_sdk for automatic tracing)
|
|
200
|
+
target_names = ["claude_agent_sdk", "openai", "anthropic", "langchain", "llama_index", "litellm"]
|
|
201
|
+
else:
|
|
202
|
+
target_names = names
|
|
203
|
+
|
|
204
|
+
# Filter out skipped instrumentors
|
|
205
|
+
target_names = [name for name in target_names if name not in skip_set]
|
|
206
|
+
|
|
207
|
+
# Activate each instrumentor
|
|
208
|
+
for name in target_names:
|
|
209
|
+
instrumentor = registry.get(name)
|
|
210
|
+
if instrumentor is not None:
|
|
211
|
+
try:
|
|
212
|
+
instrumentor.instrument()
|
|
213
|
+
logger.debug("Instrumented: %s", name)
|
|
214
|
+
except Exception as e:
|
|
215
|
+
logger.debug("Could not instrument %s: %s", name, e)
|
|
216
|
+
else:
|
|
217
|
+
logger.debug("Instrumentor not available (not installed?): %s", name)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def shutdown(timeout_millis: int = 30000) -> None:
|
|
221
|
+
"""Force flush all pending spans and shutdown the tracer provider.
|
|
222
|
+
|
|
223
|
+
This function should be called before process exit to ensure all spans
|
|
224
|
+
are exported. It is safe to call multiple times.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
timeout_millis: Maximum time to wait for flush completion in milliseconds.
|
|
228
|
+
|
|
229
|
+
Example::
|
|
230
|
+
|
|
231
|
+
import mantisdk.tracing_claude as tracing
|
|
232
|
+
import atexit
|
|
233
|
+
|
|
234
|
+
tracing.init()
|
|
235
|
+
atexit.register(tracing.shutdown)
|
|
236
|
+
|
|
237
|
+
# ... run application ...
|
|
238
|
+
"""
|
|
239
|
+
global _initialized, _provider, _processors
|
|
240
|
+
|
|
241
|
+
with _init_lock:
|
|
242
|
+
if not _initialized or _provider is None:
|
|
243
|
+
logger.debug("Tracing not initialized, nothing to shutdown")
|
|
244
|
+
return
|
|
245
|
+
|
|
246
|
+
try:
|
|
247
|
+
# Shutdown the provider (which flushes and shuts down processors)
|
|
248
|
+
_provider.shutdown()
|
|
249
|
+
logger.info("TracerProvider shutdown complete")
|
|
250
|
+
except Exception as e:
|
|
251
|
+
logger.warning("Error during TracerProvider shutdown: %s", e)
|
|
252
|
+
|
|
253
|
+
_processors.clear()
|
|
254
|
+
_initialized = False
|
|
255
|
+
_provider = None
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def flush(timeout_millis: int = 30000) -> bool:
|
|
259
|
+
"""Force flush all pending spans without shutting down.
|
|
260
|
+
|
|
261
|
+
This is useful for ensuring spans are exported at specific points
|
|
262
|
+
(e.g., after completing a batch of work) without ending the tracing session.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
timeout_millis: Maximum time to wait for flush completion in milliseconds.
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
True if flush completed successfully, False otherwise.
|
|
269
|
+
|
|
270
|
+
Example::
|
|
271
|
+
|
|
272
|
+
import mantisdk.tracing_claude as tracing
|
|
273
|
+
|
|
274
|
+
tracing.init()
|
|
275
|
+
|
|
276
|
+
# Process batch
|
|
277
|
+
for item in batch:
|
|
278
|
+
process(item)
|
|
279
|
+
|
|
280
|
+
# Ensure all spans from this batch are exported
|
|
281
|
+
tracing.flush()
|
|
282
|
+
"""
|
|
283
|
+
global _provider
|
|
284
|
+
|
|
285
|
+
with _init_lock:
|
|
286
|
+
if _provider is None:
|
|
287
|
+
logger.debug("No provider to flush")
|
|
288
|
+
return True
|
|
289
|
+
|
|
290
|
+
try:
|
|
291
|
+
success = _provider.force_flush(timeout_millis=timeout_millis)
|
|
292
|
+
if success:
|
|
293
|
+
logger.debug("Flush completed successfully")
|
|
294
|
+
else:
|
|
295
|
+
logger.warning("Flush timed out after %d ms", timeout_millis)
|
|
296
|
+
return success
|
|
297
|
+
except Exception as e:
|
|
298
|
+
logger.warning("Error during flush: %s", e)
|
|
299
|
+
return False
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _add_processor_idempotent(provider: TracerProvider, exporter: SpanExporter) -> None:
|
|
303
|
+
"""Add a BatchSpanProcessor for the exporter, avoiding duplicates.
|
|
304
|
+
|
|
305
|
+
Checks if a processor for the same exporter type/endpoint already exists
|
|
306
|
+
to prevent duplicate exports.
|
|
307
|
+
"""
|
|
308
|
+
global _processors
|
|
309
|
+
|
|
310
|
+
# Check for existing processor with same exporter type
|
|
311
|
+
exporter_id = _get_exporter_id(exporter)
|
|
312
|
+
for existing in _processors:
|
|
313
|
+
if _get_exporter_id(existing.span_exporter) == exporter_id:
|
|
314
|
+
logger.debug("Processor already exists for exporter: %s", exporter_id)
|
|
315
|
+
return
|
|
316
|
+
|
|
317
|
+
# Create and add new processor
|
|
318
|
+
processor = BatchSpanProcessor(exporter)
|
|
319
|
+
provider.add_span_processor(processor)
|
|
320
|
+
_processors.append(processor)
|
|
321
|
+
logger.debug("Added BatchSpanProcessor for: %s", exporter_id)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _get_exporter_id(exporter: SpanExporter) -> str:
|
|
325
|
+
"""Generate a unique identifier for an exporter instance.
|
|
326
|
+
|
|
327
|
+
Uses class name + endpoint (if available) to identify exporters.
|
|
328
|
+
"""
|
|
329
|
+
class_name = exporter.__class__.__name__
|
|
330
|
+
|
|
331
|
+
# Try to get endpoint for OTLP exporters
|
|
332
|
+
endpoint = getattr(exporter, "_endpoint", None)
|
|
333
|
+
if endpoint:
|
|
334
|
+
return f"{class_name}:{endpoint}"
|
|
335
|
+
|
|
336
|
+
return class_name
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _setup_agentops_helpers(integrations: Optional[Dict[str, Any]] = None) -> None:
|
|
340
|
+
"""Enable AgentOps helpers (decorators) without AgentOps export.
|
|
341
|
+
|
|
342
|
+
This sets up AgentOps instrumentation in "helpers only" mode,
|
|
343
|
+
where spans are created but exported through MantisDK's exporter
|
|
344
|
+
rather than AgentOps's backend.
|
|
345
|
+
"""
|
|
346
|
+
config = (integrations or {}).get("agentops", {})
|
|
347
|
+
if not config.get("enabled", True):
|
|
348
|
+
return
|
|
349
|
+
|
|
350
|
+
try:
|
|
351
|
+
import agentops
|
|
352
|
+
# AgentOps init with no API key = helpers only mode
|
|
353
|
+
# The spans will go through our TracerProvider
|
|
354
|
+
logger.debug("AgentOps helpers mode enabled")
|
|
355
|
+
except ImportError:
|
|
356
|
+
logger.debug("AgentOps not installed, skipping helpers setup")
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def get_tracer(name: str = "mantisdk.tracing") -> trace.Tracer:
|
|
360
|
+
"""Get a tracer instance for creating spans.
|
|
361
|
+
|
|
362
|
+
This is primarily for internal use. Users should prefer the
|
|
363
|
+
trace() and span() context managers.
|
|
364
|
+
|
|
365
|
+
Args:
|
|
366
|
+
name: The tracer name (instrumentation scope).
|
|
367
|
+
|
|
368
|
+
Returns:
|
|
369
|
+
OpenTelemetry Tracer instance.
|
|
370
|
+
"""
|
|
371
|
+
return trace.get_tracer(name)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Copyright (c) Metis. All rights reserved.
|
|
2
|
+
|
|
3
|
+
"""Instrumentors for MantisDK tracing.
|
|
4
|
+
|
|
5
|
+
This module provides a registry-based system for managing OpenTelemetry
|
|
6
|
+
instrumentors from various sources (OpenInference, AgentOps, etc.).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .registry import get_registry, InstrumentorRegistry, BaseInstrumentor
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"get_registry",
|
|
13
|
+
"InstrumentorRegistry",
|
|
14
|
+
"BaseInstrumentor",
|
|
15
|
+
]
|