mantisdk 0.1.0__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.

Potentially problematic release.


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

@@ -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
+ ]