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.
- genai_otel/__init__.py +132 -0
- genai_otel/__version__.py +34 -0
- genai_otel/auto_instrument.py +602 -0
- genai_otel/cli.py +92 -0
- genai_otel/config.py +333 -0
- genai_otel/cost_calculator.py +467 -0
- genai_otel/cost_enriching_exporter.py +207 -0
- genai_otel/cost_enrichment_processor.py +174 -0
- genai_otel/evaluation/__init__.py +76 -0
- genai_otel/evaluation/bias_detector.py +364 -0
- genai_otel/evaluation/config.py +261 -0
- genai_otel/evaluation/hallucination_detector.py +525 -0
- genai_otel/evaluation/pii_detector.py +356 -0
- genai_otel/evaluation/prompt_injection_detector.py +262 -0
- genai_otel/evaluation/restricted_topics_detector.py +316 -0
- genai_otel/evaluation/span_processor.py +962 -0
- genai_otel/evaluation/toxicity_detector.py +406 -0
- genai_otel/exceptions.py +17 -0
- genai_otel/gpu_metrics.py +516 -0
- genai_otel/instrumentors/__init__.py +71 -0
- genai_otel/instrumentors/anthropic_instrumentor.py +134 -0
- genai_otel/instrumentors/anyscale_instrumentor.py +27 -0
- genai_otel/instrumentors/autogen_instrumentor.py +394 -0
- genai_otel/instrumentors/aws_bedrock_instrumentor.py +94 -0
- genai_otel/instrumentors/azure_openai_instrumentor.py +69 -0
- genai_otel/instrumentors/base.py +919 -0
- genai_otel/instrumentors/bedrock_agents_instrumentor.py +398 -0
- genai_otel/instrumentors/cohere_instrumentor.py +140 -0
- genai_otel/instrumentors/crewai_instrumentor.py +311 -0
- genai_otel/instrumentors/dspy_instrumentor.py +661 -0
- genai_otel/instrumentors/google_ai_instrumentor.py +310 -0
- genai_otel/instrumentors/groq_instrumentor.py +106 -0
- genai_otel/instrumentors/guardrails_ai_instrumentor.py +510 -0
- genai_otel/instrumentors/haystack_instrumentor.py +503 -0
- genai_otel/instrumentors/huggingface_instrumentor.py +399 -0
- genai_otel/instrumentors/hyperbolic_instrumentor.py +236 -0
- genai_otel/instrumentors/instructor_instrumentor.py +425 -0
- genai_otel/instrumentors/langchain_instrumentor.py +340 -0
- genai_otel/instrumentors/langgraph_instrumentor.py +328 -0
- genai_otel/instrumentors/llamaindex_instrumentor.py +36 -0
- genai_otel/instrumentors/mistralai_instrumentor.py +315 -0
- genai_otel/instrumentors/ollama_instrumentor.py +197 -0
- genai_otel/instrumentors/ollama_server_metrics_poller.py +336 -0
- genai_otel/instrumentors/openai_agents_instrumentor.py +291 -0
- genai_otel/instrumentors/openai_instrumentor.py +260 -0
- genai_otel/instrumentors/pydantic_ai_instrumentor.py +362 -0
- genai_otel/instrumentors/replicate_instrumentor.py +87 -0
- genai_otel/instrumentors/sambanova_instrumentor.py +196 -0
- genai_otel/instrumentors/togetherai_instrumentor.py +146 -0
- genai_otel/instrumentors/vertexai_instrumentor.py +106 -0
- genai_otel/llm_pricing.json +1676 -0
- genai_otel/logging_config.py +45 -0
- genai_otel/mcp_instrumentors/__init__.py +14 -0
- genai_otel/mcp_instrumentors/api_instrumentor.py +144 -0
- genai_otel/mcp_instrumentors/base.py +105 -0
- genai_otel/mcp_instrumentors/database_instrumentor.py +336 -0
- genai_otel/mcp_instrumentors/kafka_instrumentor.py +31 -0
- genai_otel/mcp_instrumentors/manager.py +139 -0
- genai_otel/mcp_instrumentors/redis_instrumentor.py +31 -0
- genai_otel/mcp_instrumentors/vector_db_instrumentor.py +265 -0
- genai_otel/metrics.py +148 -0
- genai_otel/py.typed +2 -0
- genai_otel/server_metrics.py +197 -0
- genai_otel_instrument-0.1.24.dist-info/METADATA +1404 -0
- genai_otel_instrument-0.1.24.dist-info/RECORD +69 -0
- genai_otel_instrument-0.1.24.dist-info/WHEEL +5 -0
- genai_otel_instrument-0.1.24.dist-info/entry_points.txt +2 -0
- genai_otel_instrument-0.1.24.dist-info/licenses/LICENSE +680 -0
- 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)
|