genai-otel-instrument 0.1.24__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 (69) hide show
  1. genai_otel/__init__.py +132 -0
  2. genai_otel/__version__.py +34 -0
  3. genai_otel/auto_instrument.py +602 -0
  4. genai_otel/cli.py +92 -0
  5. genai_otel/config.py +333 -0
  6. genai_otel/cost_calculator.py +467 -0
  7. genai_otel/cost_enriching_exporter.py +207 -0
  8. genai_otel/cost_enrichment_processor.py +174 -0
  9. genai_otel/evaluation/__init__.py +76 -0
  10. genai_otel/evaluation/bias_detector.py +364 -0
  11. genai_otel/evaluation/config.py +261 -0
  12. genai_otel/evaluation/hallucination_detector.py +525 -0
  13. genai_otel/evaluation/pii_detector.py +356 -0
  14. genai_otel/evaluation/prompt_injection_detector.py +262 -0
  15. genai_otel/evaluation/restricted_topics_detector.py +316 -0
  16. genai_otel/evaluation/span_processor.py +962 -0
  17. genai_otel/evaluation/toxicity_detector.py +406 -0
  18. genai_otel/exceptions.py +17 -0
  19. genai_otel/gpu_metrics.py +516 -0
  20. genai_otel/instrumentors/__init__.py +71 -0
  21. genai_otel/instrumentors/anthropic_instrumentor.py +134 -0
  22. genai_otel/instrumentors/anyscale_instrumentor.py +27 -0
  23. genai_otel/instrumentors/autogen_instrumentor.py +394 -0
  24. genai_otel/instrumentors/aws_bedrock_instrumentor.py +94 -0
  25. genai_otel/instrumentors/azure_openai_instrumentor.py +69 -0
  26. genai_otel/instrumentors/base.py +919 -0
  27. genai_otel/instrumentors/bedrock_agents_instrumentor.py +398 -0
  28. genai_otel/instrumentors/cohere_instrumentor.py +140 -0
  29. genai_otel/instrumentors/crewai_instrumentor.py +311 -0
  30. genai_otel/instrumentors/dspy_instrumentor.py +661 -0
  31. genai_otel/instrumentors/google_ai_instrumentor.py +310 -0
  32. genai_otel/instrumentors/groq_instrumentor.py +106 -0
  33. genai_otel/instrumentors/guardrails_ai_instrumentor.py +510 -0
  34. genai_otel/instrumentors/haystack_instrumentor.py +503 -0
  35. genai_otel/instrumentors/huggingface_instrumentor.py +399 -0
  36. genai_otel/instrumentors/hyperbolic_instrumentor.py +236 -0
  37. genai_otel/instrumentors/instructor_instrumentor.py +425 -0
  38. genai_otel/instrumentors/langchain_instrumentor.py +340 -0
  39. genai_otel/instrumentors/langgraph_instrumentor.py +328 -0
  40. genai_otel/instrumentors/llamaindex_instrumentor.py +36 -0
  41. genai_otel/instrumentors/mistralai_instrumentor.py +315 -0
  42. genai_otel/instrumentors/ollama_instrumentor.py +197 -0
  43. genai_otel/instrumentors/ollama_server_metrics_poller.py +336 -0
  44. genai_otel/instrumentors/openai_agents_instrumentor.py +291 -0
  45. genai_otel/instrumentors/openai_instrumentor.py +260 -0
  46. genai_otel/instrumentors/pydantic_ai_instrumentor.py +362 -0
  47. genai_otel/instrumentors/replicate_instrumentor.py +87 -0
  48. genai_otel/instrumentors/sambanova_instrumentor.py +196 -0
  49. genai_otel/instrumentors/togetherai_instrumentor.py +146 -0
  50. genai_otel/instrumentors/vertexai_instrumentor.py +106 -0
  51. genai_otel/llm_pricing.json +1676 -0
  52. genai_otel/logging_config.py +45 -0
  53. genai_otel/mcp_instrumentors/__init__.py +14 -0
  54. genai_otel/mcp_instrumentors/api_instrumentor.py +144 -0
  55. genai_otel/mcp_instrumentors/base.py +105 -0
  56. genai_otel/mcp_instrumentors/database_instrumentor.py +336 -0
  57. genai_otel/mcp_instrumentors/kafka_instrumentor.py +31 -0
  58. genai_otel/mcp_instrumentors/manager.py +139 -0
  59. genai_otel/mcp_instrumentors/redis_instrumentor.py +31 -0
  60. genai_otel/mcp_instrumentors/vector_db_instrumentor.py +265 -0
  61. genai_otel/metrics.py +148 -0
  62. genai_otel/py.typed +2 -0
  63. genai_otel/server_metrics.py +197 -0
  64. genai_otel_instrument-0.1.24.dist-info/METADATA +1404 -0
  65. genai_otel_instrument-0.1.24.dist-info/RECORD +69 -0
  66. genai_otel_instrument-0.1.24.dist-info/WHEEL +5 -0
  67. genai_otel_instrument-0.1.24.dist-info/entry_points.txt +2 -0
  68. genai_otel_instrument-0.1.24.dist-info/licenses/LICENSE +680 -0
  69. genai_otel_instrument-0.1.24.dist-info/top_level.txt +1 -0
@@ -0,0 +1,602 @@
1
+ """Module for setting up OpenTelemetry auto-instrumentation for GenAI applications."""
2
+
3
+ # isort: skip_file
4
+
5
+ import logging
6
+ import sys
7
+
8
+ from opentelemetry import metrics, trace
9
+ from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter
10
+ from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
11
+ from opentelemetry.sdk.metrics import MeterProvider
12
+ from opentelemetry.sdk.metrics.export import ConsoleMetricExporter, PeriodicExportingMetricReader
13
+ from opentelemetry.sdk.metrics.view import View
14
+ from opentelemetry.sdk.metrics._internal.aggregation import ExplicitBucketHistogramAggregation
15
+ from opentelemetry.sdk.resources import Resource
16
+ from opentelemetry.sdk.trace import TracerProvider
17
+ from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter
18
+
19
+ from .config import OTelConfig
20
+ from .cost_calculator import CostCalculator
21
+ from .cost_enrichment_processor import CostEnrichmentSpanProcessor
22
+ from .cost_enriching_exporter import CostEnrichingSpanExporter
23
+ from .evaluation.config import (
24
+ BiasConfig,
25
+ HallucinationConfig,
26
+ PIIConfig,
27
+ PIIEntityType,
28
+ PIIMode,
29
+ PromptInjectionConfig,
30
+ RestrictedTopicsConfig,
31
+ ToxicityConfig,
32
+ )
33
+ from .evaluation.span_processor import EvaluationSpanProcessor
34
+ from .gpu_metrics import GPUMetricsCollector
35
+ from .mcp_instrumentors import MCPInstrumentorManager
36
+ from .server_metrics import initialize_server_metrics
37
+ from .metrics import (
38
+ _GEN_AI_CLIENT_OPERATION_DURATION_BUCKETS,
39
+ _GEN_AI_CLIENT_TOKEN_USAGE_BUCKETS,
40
+ _GEN_AI_SERVER_TBT,
41
+ _GEN_AI_SERVER_TFTT,
42
+ _MCP_CLIENT_OPERATION_DURATION_BUCKETS,
43
+ _MCP_PAYLOAD_SIZE_BUCKETS,
44
+ )
45
+
46
+ # Import semantic conventions
47
+ try:
48
+ from openlit.semcov import SemanticConvention as SC
49
+ except ImportError:
50
+ # Fallback if openlit not available
51
+ class SC:
52
+ GEN_AI_CLIENT_OPERATION_DURATION = "gen_ai.client.operation.duration"
53
+ GEN_AI_SERVER_TTFT = "gen_ai.server.ttft"
54
+ GEN_AI_SERVER_TBT = "gen_ai.server.tbt"
55
+
56
+
57
+ # Import instrumentors - fix the import path based on your actual structure
58
+ try:
59
+ from .instrumentors import (
60
+ AnthropicInstrumentor,
61
+ AnyscaleInstrumentor,
62
+ AutoGenInstrumentor,
63
+ AWSBedrockInstrumentor,
64
+ AzureOpenAIInstrumentor,
65
+ BedrockAgentsInstrumentor,
66
+ CohereInstrumentor,
67
+ CrewAIInstrumentor,
68
+ DSPyInstrumentor,
69
+ GoogleAIInstrumentor,
70
+ GroqInstrumentor,
71
+ GuardrailsAIInstrumentor,
72
+ HaystackInstrumentor,
73
+ HuggingFaceInstrumentor,
74
+ HyperbolicInstrumentor,
75
+ InstructorInstrumentor,
76
+ LangChainInstrumentor,
77
+ LangGraphInstrumentor,
78
+ LlamaIndexInstrumentor,
79
+ MistralAIInstrumentor,
80
+ OllamaInstrumentor,
81
+ OpenAIInstrumentor,
82
+ OpenAIAgentsInstrumentor,
83
+ PydanticAIInstrumentor,
84
+ ReplicateInstrumentor,
85
+ SambaNovaInstrumentor,
86
+ TogetherAIInstrumentor,
87
+ VertexAIInstrumentor,
88
+ )
89
+ except ImportError:
90
+ # Fallback for testing or if instrumentors are in different structure
91
+ from genai_otel.instrumentors import (
92
+ AnthropicInstrumentor,
93
+ AnyscaleInstrumentor,
94
+ AutoGenInstrumentor,
95
+ AWSBedrockInstrumentor,
96
+ AzureOpenAIInstrumentor,
97
+ BedrockAgentsInstrumentor,
98
+ CohereInstrumentor,
99
+ CrewAIInstrumentor,
100
+ DSPyInstrumentor,
101
+ GoogleAIInstrumentor,
102
+ GroqInstrumentor,
103
+ GuardrailsAIInstrumentor,
104
+ HaystackInstrumentor,
105
+ HuggingFaceInstrumentor,
106
+ HyperbolicInstrumentor,
107
+ InstructorInstrumentor,
108
+ LangChainInstrumentor,
109
+ LangGraphInstrumentor,
110
+ LlamaIndexInstrumentor,
111
+ MistralAIInstrumentor,
112
+ OllamaInstrumentor,
113
+ OpenAIInstrumentor,
114
+ OpenAIAgentsInstrumentor,
115
+ PydanticAIInstrumentor,
116
+ ReplicateInstrumentor,
117
+ SambaNovaInstrumentor,
118
+ TogetherAIInstrumentor,
119
+ VertexAIInstrumentor,
120
+ )
121
+
122
+ logger = logging.getLogger(__name__)
123
+
124
+ # Optional OpenInference instrumentors (requires Python >= 3.10)
125
+ try:
126
+ from openinference.instrumentation.litellm import LiteLLMInstrumentor # noqa: E402
127
+ from openinference.instrumentation.mcp import MCPInstrumentor # noqa: E402
128
+ from openinference.instrumentation.smolagents import ( # noqa: E402
129
+ SmolagentsInstrumentor,
130
+ )
131
+
132
+ OPENINFERENCE_AVAILABLE = True
133
+ except ImportError:
134
+ LiteLLMInstrumentor = None
135
+ MCPInstrumentor = None
136
+ SmolagentsInstrumentor = None
137
+ OPENINFERENCE_AVAILABLE = False
138
+
139
+ # Defines the available instrumentors. This is now at the module level for easier mocking in tests.
140
+ INSTRUMENTORS = {
141
+ "openai": OpenAIInstrumentor,
142
+ "agents": OpenAIAgentsInstrumentor, # OpenAI Agents SDK
143
+ "anthropic": AnthropicInstrumentor,
144
+ "google.generativeai": GoogleAIInstrumentor,
145
+ "boto3": AWSBedrockInstrumentor,
146
+ "azure.ai.openai": AzureOpenAIInstrumentor,
147
+ "autogen": AutoGenInstrumentor, # AutoGen multi-agent framework
148
+ "bedrock_agents": BedrockAgentsInstrumentor, # AWS Bedrock Agents
149
+ "cohere": CohereInstrumentor,
150
+ "crewai": CrewAIInstrumentor, # CrewAI multi-agent framework
151
+ "dspy": DSPyInstrumentor, # DSPy declarative LM programming framework
152
+ "mistralai": MistralAIInstrumentor,
153
+ "together": TogetherAIInstrumentor,
154
+ "groq": GroqInstrumentor,
155
+ "guardrails": GuardrailsAIInstrumentor, # Guardrails AI validation framework
156
+ "haystack": HaystackInstrumentor, # Haystack NLP pipeline framework
157
+ "instructor": InstructorInstrumentor, # Instructor structured output extraction
158
+ "ollama": OllamaInstrumentor,
159
+ "vertexai": VertexAIInstrumentor,
160
+ "replicate": ReplicateInstrumentor,
161
+ "anyscale": AnyscaleInstrumentor,
162
+ "sambanova": SambaNovaInstrumentor,
163
+ "hyperbolic": HyperbolicInstrumentor,
164
+ "langchain": LangChainInstrumentor,
165
+ "langgraph": LangGraphInstrumentor, # LangGraph stateful workflow framework
166
+ "llama_index": LlamaIndexInstrumentor,
167
+ "pydantic_ai": PydanticAIInstrumentor, # Pydantic AI type-safe agent framework
168
+ "transformers": HuggingFaceInstrumentor,
169
+ }
170
+
171
+ # Add OpenInference instrumentors if available (requires Python >= 3.10)
172
+ # IMPORTANT: Order matters! Load in this specific sequence:
173
+ # 1. smolagents - instruments the agent framework
174
+ # 2. litellm - instruments LLM calls made by agents
175
+ # 3. mcp - instruments Model Context Protocol tools
176
+ if OPENINFERENCE_AVAILABLE:
177
+ INSTRUMENTORS.update(
178
+ {
179
+ "smolagents": SmolagentsInstrumentor,
180
+ "litellm": LiteLLMInstrumentor,
181
+ "mcp": MCPInstrumentor,
182
+ }
183
+ )
184
+
185
+
186
+ # Global list to store OTLP exporter sessions that should not be instrumented
187
+ _OTLP_EXPORTER_SESSIONS = []
188
+
189
+
190
+ def setup_auto_instrumentation(config: OTelConfig):
191
+ """
192
+ Set up OpenTelemetry with auto-instrumentation for LLM frameworks and MCP tools.
193
+
194
+ Args:
195
+ config: OTelConfig instance with configuration parameters.
196
+ """
197
+ global _OTLP_EXPORTER_SESSIONS
198
+ logger.info("Starting auto-instrumentation setup...")
199
+
200
+ # Configure OpenTelemetry SDK (TracerProvider, MeterProvider, etc.)
201
+ import os
202
+
203
+ service_instance_id = os.getenv("OTEL_SERVICE_INSTANCE_ID")
204
+ environment = os.getenv("OTEL_ENVIRONMENT")
205
+ resource_attributes = {"service.name": config.service_name}
206
+ if service_instance_id:
207
+ resource_attributes["service.instance.id"] = service_instance_id
208
+ if environment:
209
+ resource_attributes["environment"] = environment
210
+ resource = Resource.create(resource_attributes)
211
+
212
+ # Configure Tracing
213
+ tracer_provider = TracerProvider(resource=resource)
214
+ trace.set_tracer_provider(tracer_provider)
215
+ from opentelemetry.propagate import set_global_textmap
216
+ from opentelemetry.trace.propagation.tracecontext import (
217
+ TraceContextTextMapPropagator,
218
+ )
219
+
220
+ set_global_textmap(TraceContextTextMapPropagator())
221
+
222
+ # Add cost enrichment processor for custom instrumentors (OpenAI, Ollama, etc.)
223
+ # These instrumentors set cost attributes directly, so processor is mainly for logging
224
+ # Also attempts to enrich OpenInference spans (smolagents, litellm, mcp), though
225
+ # the processor can't modify ReadableSpan - the exporter below handles that
226
+ cost_calculator = None
227
+ if config.enable_cost_tracking:
228
+ try:
229
+ cost_calculator = CostCalculator()
230
+ cost_processor = CostEnrichmentSpanProcessor(cost_calculator)
231
+ tracer_provider.add_span_processor(cost_processor)
232
+ logger.info("Cost enrichment processor added")
233
+ except Exception as e:
234
+ logger.warning(f"Failed to add cost enrichment processor: {e}", exc_info=True)
235
+
236
+ # Add evaluation and safety span processor (v0.2.0)
237
+ if any(
238
+ [
239
+ config.enable_pii_detection,
240
+ config.enable_toxicity_detection,
241
+ config.enable_bias_detection,
242
+ config.enable_prompt_injection_detection,
243
+ config.enable_restricted_topics,
244
+ config.enable_hallucination_detection,
245
+ ]
246
+ ):
247
+ try:
248
+ # Build PII config from OTelConfig
249
+ pii_config = None
250
+ if config.enable_pii_detection:
251
+ pii_config = PIIConfig(
252
+ enabled=True,
253
+ mode=PIIMode(config.pii_mode),
254
+ threshold=config.pii_threshold,
255
+ gdpr_mode=config.pii_gdpr_mode,
256
+ hipaa_mode=config.pii_hipaa_mode,
257
+ pci_dss_mode=config.pii_pci_dss_mode,
258
+ )
259
+
260
+ # Build Toxicity config
261
+ toxicity_config = None
262
+ if config.enable_toxicity_detection:
263
+ toxicity_config = ToxicityConfig(
264
+ enabled=True,
265
+ threshold=config.toxicity_threshold,
266
+ use_perspective_api=config.toxicity_use_perspective_api,
267
+ perspective_api_key=config.toxicity_perspective_api_key,
268
+ )
269
+
270
+ # Build Bias config
271
+ bias_config = None
272
+ if config.enable_bias_detection:
273
+ bias_config = BiasConfig(
274
+ enabled=True,
275
+ threshold=config.bias_threshold,
276
+ )
277
+
278
+ # Build Prompt Injection config
279
+ prompt_injection_config = None
280
+ if config.enable_prompt_injection_detection:
281
+ prompt_injection_config = PromptInjectionConfig(
282
+ enabled=True,
283
+ threshold=config.prompt_injection_threshold,
284
+ )
285
+
286
+ # Build Restricted Topics config
287
+ restricted_topics_config = None
288
+ if config.enable_restricted_topics:
289
+ restricted_topics_config = RestrictedTopicsConfig(
290
+ enabled=True,
291
+ threshold=config.restricted_topics_threshold,
292
+ )
293
+
294
+ # Build Hallucination config
295
+ hallucination_config = None
296
+ if config.enable_hallucination_detection:
297
+ hallucination_config = HallucinationConfig(
298
+ enabled=True,
299
+ threshold=config.hallucination_threshold,
300
+ )
301
+
302
+ # Create and add evaluation processor
303
+ evaluation_processor = EvaluationSpanProcessor(
304
+ pii_config=pii_config,
305
+ toxicity_config=toxicity_config,
306
+ bias_config=bias_config,
307
+ prompt_injection_config=prompt_injection_config,
308
+ restricted_topics_config=restricted_topics_config,
309
+ hallucination_config=hallucination_config,
310
+ )
311
+ tracer_provider.add_span_processor(evaluation_processor)
312
+ logger.info("Evaluation and safety span processor added")
313
+ except Exception as e:
314
+ logger.warning(f"Failed to add evaluation span processor: {e}", exc_info=True)
315
+
316
+ logger.debug(f"OTelConfig endpoint: {config.endpoint}")
317
+ if config.endpoint:
318
+ # Use timeout from config (already validated as int)
319
+ timeout = config.exporter_timeout
320
+
321
+ # CRITICAL FIX: Set endpoint in environment variable so exporters can append correct paths
322
+ # The exporters only call _append_trace_path() when reading from env vars
323
+ from urllib.parse import urlparse
324
+
325
+ # Set the base endpoint in environment variable
326
+ os.environ["OTEL_EXPORTER_OTLP_ENDPOINT"] = config.endpoint
327
+
328
+ parsed = urlparse(config.endpoint)
329
+ base_url = f"{parsed.scheme}://{parsed.netloc}"
330
+
331
+ # Build list of URLs to exclude from instrumentation
332
+ excluded_urls = [
333
+ base_url,
334
+ config.endpoint,
335
+ f"{base_url}/v1/traces",
336
+ f"{base_url}/v1/metrics",
337
+ config.endpoint.rstrip("/") + "/v1/traces",
338
+ config.endpoint.rstrip("/") + "/v1/metrics",
339
+ ]
340
+
341
+ # Add to environment variable (comma-separated)
342
+ existing = os.environ.get("OTEL_PYTHON_REQUESTS_EXCLUDED_URLS", "")
343
+ if existing:
344
+ excluded_urls.append(existing)
345
+ os.environ["OTEL_PYTHON_REQUESTS_EXCLUDED_URLS"] = ",".join(excluded_urls)
346
+ logger.info(f"Excluded OTLP endpoints from instrumentation: {base_url}")
347
+
348
+ # Set timeout in environment variable as integer string (OTLP exporters expect int)
349
+ os.environ["OTEL_EXPORTER_OTLP_TIMEOUT"] = str(timeout)
350
+
351
+ # Create exporters WITHOUT passing endpoint (let them read from env vars)
352
+ # This ensures they call _append_trace_path() correctly
353
+ span_exporter = OTLPSpanExporter(
354
+ headers=config.headers,
355
+ )
356
+ tracer_provider.add_span_processor(BatchSpanProcessor(span_exporter))
357
+ logger.info(
358
+ f"OpenTelemetry tracing configured with OTLP endpoint: {span_exporter._endpoint}"
359
+ )
360
+
361
+ # Configure Metrics with Views for histogram buckets
362
+ metric_exporter = OTLPMetricExporter(
363
+ headers=config.headers,
364
+ )
365
+ metric_reader = PeriodicExportingMetricReader(exporter=metric_exporter)
366
+
367
+ # Create Views to configure histogram buckets for GenAI operation duration
368
+ duration_view = View(
369
+ instrument_name=SC.GEN_AI_CLIENT_OPERATION_DURATION,
370
+ aggregation=ExplicitBucketHistogramAggregation(
371
+ boundaries=_GEN_AI_CLIENT_OPERATION_DURATION_BUCKETS
372
+ ),
373
+ )
374
+
375
+ # Create Views for MCP metrics histograms
376
+ mcp_duration_view = View(
377
+ instrument_name="mcp.client.operation.duration",
378
+ aggregation=ExplicitBucketHistogramAggregation(
379
+ boundaries=_MCP_CLIENT_OPERATION_DURATION_BUCKETS
380
+ ),
381
+ )
382
+
383
+ mcp_request_size_view = View(
384
+ instrument_name="mcp.request.size",
385
+ aggregation=ExplicitBucketHistogramAggregation(boundaries=_MCP_PAYLOAD_SIZE_BUCKETS),
386
+ )
387
+
388
+ mcp_response_size_view = View(
389
+ instrument_name="mcp.response.size",
390
+ aggregation=ExplicitBucketHistogramAggregation(boundaries=_MCP_PAYLOAD_SIZE_BUCKETS),
391
+ )
392
+
393
+ # Create Views for streaming metrics (Phase 3.4)
394
+ ttft_view = View(
395
+ instrument_name=SC.GEN_AI_SERVER_TTFT,
396
+ aggregation=ExplicitBucketHistogramAggregation(boundaries=_GEN_AI_SERVER_TFTT),
397
+ )
398
+
399
+ tbt_view = View(
400
+ instrument_name=SC.GEN_AI_SERVER_TBT,
401
+ aggregation=ExplicitBucketHistogramAggregation(boundaries=_GEN_AI_SERVER_TBT),
402
+ )
403
+
404
+ # Create Views for token distribution histograms
405
+ prompt_tokens_view = View(
406
+ instrument_name="gen_ai.client.token.usage.prompt",
407
+ aggregation=ExplicitBucketHistogramAggregation(
408
+ boundaries=_GEN_AI_CLIENT_TOKEN_USAGE_BUCKETS
409
+ ),
410
+ )
411
+
412
+ completion_tokens_view = View(
413
+ instrument_name="gen_ai.client.token.usage.completion",
414
+ aggregation=ExplicitBucketHistogramAggregation(
415
+ boundaries=_GEN_AI_CLIENT_TOKEN_USAGE_BUCKETS
416
+ ),
417
+ )
418
+
419
+ meter_provider = MeterProvider(
420
+ resource=resource,
421
+ metric_readers=[metric_reader],
422
+ views=[
423
+ duration_view,
424
+ mcp_duration_view,
425
+ mcp_request_size_view,
426
+ mcp_response_size_view,
427
+ ttft_view,
428
+ tbt_view,
429
+ prompt_tokens_view,
430
+ completion_tokens_view,
431
+ ],
432
+ )
433
+ metrics.set_meter_provider(meter_provider)
434
+ logger.info(
435
+ f"OpenTelemetry metrics configured with OTLP endpoint: {metric_exporter._endpoint}"
436
+ )
437
+ else:
438
+ # Configure Console Exporters if no OTLP endpoint is set
439
+ span_exporter = ConsoleSpanExporter()
440
+ tracer_provider.add_span_processor(BatchSpanProcessor(span_exporter))
441
+ logger.info("No OTLP endpoint configured, traces will be exported to console.")
442
+
443
+ metric_exporter = ConsoleMetricExporter()
444
+ metric_reader = PeriodicExportingMetricReader(exporter=metric_exporter)
445
+
446
+ # Create Views to configure histogram buckets (same as OTLP path)
447
+ duration_view = View(
448
+ instrument_name=SC.GEN_AI_CLIENT_OPERATION_DURATION,
449
+ aggregation=ExplicitBucketHistogramAggregation(
450
+ boundaries=_GEN_AI_CLIENT_OPERATION_DURATION_BUCKETS
451
+ ),
452
+ )
453
+
454
+ # Create Views for MCP metrics histograms
455
+ mcp_duration_view = View(
456
+ instrument_name="mcp.client.operation.duration",
457
+ aggregation=ExplicitBucketHistogramAggregation(
458
+ boundaries=_MCP_CLIENT_OPERATION_DURATION_BUCKETS
459
+ ),
460
+ )
461
+
462
+ mcp_request_size_view = View(
463
+ instrument_name="mcp.request.size",
464
+ aggregation=ExplicitBucketHistogramAggregation(boundaries=_MCP_PAYLOAD_SIZE_BUCKETS),
465
+ )
466
+
467
+ mcp_response_size_view = View(
468
+ instrument_name="mcp.response.size",
469
+ aggregation=ExplicitBucketHistogramAggregation(boundaries=_MCP_PAYLOAD_SIZE_BUCKETS),
470
+ )
471
+
472
+ # Create Views for streaming metrics (Phase 3.4)
473
+ ttft_view = View(
474
+ instrument_name=SC.GEN_AI_SERVER_TTFT,
475
+ aggregation=ExplicitBucketHistogramAggregation(boundaries=_GEN_AI_SERVER_TFTT),
476
+ )
477
+
478
+ tbt_view = View(
479
+ instrument_name=SC.GEN_AI_SERVER_TBT,
480
+ aggregation=ExplicitBucketHistogramAggregation(boundaries=_GEN_AI_SERVER_TBT),
481
+ )
482
+
483
+ # Create Views for token distribution histograms
484
+ prompt_tokens_view = View(
485
+ instrument_name="gen_ai.client.token.usage.prompt",
486
+ aggregation=ExplicitBucketHistogramAggregation(
487
+ boundaries=_GEN_AI_CLIENT_TOKEN_USAGE_BUCKETS
488
+ ),
489
+ )
490
+
491
+ completion_tokens_view = View(
492
+ instrument_name="gen_ai.client.token.usage.completion",
493
+ aggregation=ExplicitBucketHistogramAggregation(
494
+ boundaries=_GEN_AI_CLIENT_TOKEN_USAGE_BUCKETS
495
+ ),
496
+ )
497
+
498
+ meter_provider = MeterProvider(
499
+ resource=resource,
500
+ metric_readers=[metric_reader],
501
+ views=[
502
+ duration_view,
503
+ mcp_duration_view,
504
+ mcp_request_size_view,
505
+ mcp_response_size_view,
506
+ ttft_view,
507
+ tbt_view,
508
+ prompt_tokens_view,
509
+ completion_tokens_view,
510
+ ],
511
+ )
512
+ metrics.set_meter_provider(meter_provider)
513
+ logger.info("No OTLP endpoint configured, metrics will be exported to console.")
514
+
515
+ # OpenInference instrumentors that use different API (no config parameter)
516
+ # Only include if OpenInference is available (Python >= 3.10)
517
+ OPENINFERENCE_INSTRUMENTORS = (
518
+ {"smolagents", "mcp", "litellm"} if OPENINFERENCE_AVAILABLE else set()
519
+ )
520
+
521
+ # Auto-instrument LLM libraries based on the configuration
522
+ for name in config.enabled_instrumentors:
523
+ if name in INSTRUMENTORS:
524
+ try:
525
+ instrumentor_class = INSTRUMENTORS[name]
526
+ instrumentor = instrumentor_class()
527
+
528
+ # OpenInference instrumentors don't take config parameter
529
+ if name in OPENINFERENCE_INSTRUMENTORS:
530
+ instrumentor.instrument()
531
+ else:
532
+ instrumentor.instrument(config=config)
533
+
534
+ logger.info(f"{name} instrumentation enabled")
535
+ except Exception as e:
536
+ logger.error(f"Failed to instrument {name}: {e}", exc_info=True)
537
+ if config.fail_on_error:
538
+ raise
539
+ else:
540
+ logger.warning(f"Unknown instrumentor '{name}' requested.")
541
+
542
+ # Auto-instrument MCP tools (databases, APIs, etc.)
543
+ # NOTE: OTLP endpoints are excluded via OTEL_PYTHON_REQUESTS_EXCLUDED_URLS set above
544
+ if config.enable_mcp_instrumentation:
545
+ try:
546
+ mcp_manager = MCPInstrumentorManager(config)
547
+ mcp_manager.instrument_all(config.fail_on_error)
548
+ logger.info("MCP tools instrumentation enabled and set up.")
549
+ except Exception as e:
550
+ logger.error(f"Failed to set up MCP tools instrumentation: {e}", exc_info=True)
551
+ if config.fail_on_error:
552
+ raise
553
+
554
+ # Start GPU metrics collection if enabled
555
+ if config.enable_gpu_metrics:
556
+ try:
557
+ meter_provider = metrics.get_meter_provider()
558
+ gpu_collector = GPUMetricsCollector(
559
+ meter_provider.get_meter("genai.gpu"),
560
+ config,
561
+ interval=config.gpu_collection_interval,
562
+ )
563
+ gpu_collector.start()
564
+ logger.info(
565
+ f"GPU metrics collection started (interval: {config.gpu_collection_interval}s)."
566
+ )
567
+ except Exception as e:
568
+ logger.error(f"Failed to start GPU metrics collection: {e}", exc_info=True)
569
+ if config.fail_on_error:
570
+ raise
571
+
572
+ # Initialize server metrics collector (KV cache, request queue, etc.)
573
+ try:
574
+ meter_provider = metrics.get_meter_provider()
575
+ initialize_server_metrics(meter_provider.get_meter("genai.server"))
576
+ logger.info("Server metrics collector initialized (KV cache, request queue)")
577
+ except Exception as e:
578
+ logger.error(f"Failed to initialize server metrics: {e}", exc_info=True)
579
+ if config.fail_on_error:
580
+ raise
581
+
582
+ logger.info("Auto-instrumentation setup complete")
583
+
584
+
585
+ def instrument(**kwargs):
586
+ """
587
+ Convenience wrapper for setup_auto_instrumentation that accepts kwargs.
588
+
589
+ Set up OpenTelemetry with auto-instrumentation for LLM frameworks and MCP tools.
590
+
591
+ Args:
592
+ **kwargs: Keyword arguments to configure OTelConfig. These will override
593
+ environment variables.
594
+
595
+ Example:
596
+ >>> instrument(service_name="my-app", endpoint="http://localhost:4318")
597
+ """
598
+ # Load configuration from environment variables or use provided kwargs
599
+ config = OTelConfig(**kwargs)
600
+
601
+ # Call the main setup function
602
+ setup_auto_instrumentation(config)
genai_otel/cli.py ADDED
@@ -0,0 +1,92 @@
1
+ """CLI tool for running instrumented applications"""
2
+
3
+ import argparse
4
+ import logging
5
+ import os
6
+ import runpy
7
+ import sys
8
+
9
+ from genai_otel import instrument
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ def main():
15
+ """Main entry point for the genai-instrument CLI tool.
16
+
17
+ Parses command-line arguments, initializes OpenTelemetry instrumentation,
18
+ and then executes the specified command/script with its arguments.
19
+
20
+ Supports two usage patterns:
21
+ 1. genai-instrument python script.py [args...]
22
+ 2. genai-instrument script.py [args...]
23
+
24
+ In both cases, the Python script is executed in the same process to ensure
25
+ instrumentation hooks are active.
26
+ """
27
+ parser = argparse.ArgumentParser(
28
+ description=("Run a Python script with GenAI OpenTelemetry instrumentation.")
29
+ )
30
+ parser.add_argument(
31
+ "command",
32
+ nargs=argparse.REMAINDER,
33
+ help="The command to run (python script.py or script.py)",
34
+ )
35
+
36
+ args = parser.parse_args()
37
+
38
+ if not args.command:
39
+ parser.print_help()
40
+ sys.exit(1)
41
+
42
+ # Load configuration from environment variables
43
+ # The `instrument` function will handle loading config.
44
+ try:
45
+ # Initialize instrumentation. This reads env vars like OTEL_SERVICE_NAME, etc.
46
+ # If GENAI_FAIL_ON_ERROR is true and setup fails, it will raise an exception.
47
+ instrument()
48
+ except Exception as e:
49
+ logger.error(f"Failed to initialize instrumentation: {e}", exc_info=True)
50
+ sys.exit(1) # Exit if instrumentation setup fails and fail_on_error is true
51
+
52
+ # Parse the command to extract the Python script and its arguments
53
+ script_path = None
54
+ script_args = []
55
+
56
+ # Check if command starts with 'python' or 'python3' or 'python.exe'
57
+ if args.command[0].lower() in [
58
+ "python",
59
+ "python3",
60
+ "python.exe",
61
+ "python3.exe",
62
+ ] or os.path.basename(args.command[0]).lower().startswith("python"):
63
+ # Format: genai-instrument python script.py [args...]
64
+ if len(args.command) < 2:
65
+ logger.error("No Python script specified after 'python' command")
66
+ sys.exit(1)
67
+ script_path = args.command[1]
68
+ script_args = args.command[2:]
69
+ elif args.command[0].endswith(".py"):
70
+ # Format: genai-instrument script.py [args...]
71
+ script_path = args.command[0]
72
+ script_args = args.command[1:]
73
+ else:
74
+ logger.error(
75
+ f"Invalid command format. Expected 'python script.py' or 'script.py', got: {' '.join(args.command)}"
76
+ )
77
+ sys.exit(1)
78
+
79
+ # Set sys.argv to simulate running the script directly
80
+ # This ensures the target script receives the correct arguments
81
+ sys.argv = [script_path] + script_args
82
+
83
+ # Run the target script in the same process using runpy
84
+ # This ensures instrumentation hooks are active in the script
85
+ try:
86
+ runpy.run_path(script_path, run_name="__main__")
87
+ except FileNotFoundError:
88
+ logger.error(f"Script not found: {script_path}")
89
+ sys.exit(1)
90
+ except Exception as e:
91
+ logger.error(f"Error running script {script_path}: {e}", exc_info=True)
92
+ sys.exit(1)