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,311 @@
|
|
|
1
|
+
"""OpenTelemetry instrumentor for the CrewAI framework.
|
|
2
|
+
|
|
3
|
+
This instrumentor automatically traces crew execution, agents, tasks, and
|
|
4
|
+
collaborative workflows using the CrewAI multi-agent framework.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Any, Dict, Optional
|
|
10
|
+
|
|
11
|
+
from ..config import OTelConfig
|
|
12
|
+
from .base import BaseInstrumentor
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CrewAIInstrumentor(BaseInstrumentor):
|
|
18
|
+
"""Instrumentor for CrewAI multi-agent framework"""
|
|
19
|
+
|
|
20
|
+
def __init__(self):
|
|
21
|
+
"""Initialize the instrumentor."""
|
|
22
|
+
super().__init__()
|
|
23
|
+
self._crewai_available = False
|
|
24
|
+
self._check_availability()
|
|
25
|
+
|
|
26
|
+
def _check_availability(self):
|
|
27
|
+
"""Check if CrewAI library is available."""
|
|
28
|
+
try:
|
|
29
|
+
import crewai
|
|
30
|
+
|
|
31
|
+
self._crewai_available = True
|
|
32
|
+
logger.debug("CrewAI library detected and available for instrumentation")
|
|
33
|
+
except ImportError:
|
|
34
|
+
logger.debug("CrewAI library not installed, instrumentation will be skipped")
|
|
35
|
+
self._crewai_available = False
|
|
36
|
+
|
|
37
|
+
def instrument(self, config: OTelConfig):
|
|
38
|
+
"""Instrument CrewAI framework if available.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
config (OTelConfig): The OpenTelemetry configuration object.
|
|
42
|
+
"""
|
|
43
|
+
if not self._crewai_available:
|
|
44
|
+
logger.debug("Skipping CrewAI instrumentation - library not available")
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
self.config = config
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
import crewai
|
|
51
|
+
import wrapt
|
|
52
|
+
|
|
53
|
+
# Instrument Crew.kickoff() method (main execution entry point)
|
|
54
|
+
if hasattr(crewai, "Crew"):
|
|
55
|
+
if hasattr(crewai.Crew, "kickoff"):
|
|
56
|
+
original_kickoff = crewai.Crew.kickoff
|
|
57
|
+
crewai.Crew.kickoff = wrapt.FunctionWrapper(
|
|
58
|
+
original_kickoff, self._wrap_crew_kickoff
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
self._instrumented = True
|
|
62
|
+
logger.info("CrewAI instrumentation enabled")
|
|
63
|
+
|
|
64
|
+
except Exception as e:
|
|
65
|
+
logger.error("Failed to instrument CrewAI: %s", e, exc_info=True)
|
|
66
|
+
if config.fail_on_error:
|
|
67
|
+
raise
|
|
68
|
+
|
|
69
|
+
def _wrap_crew_kickoff(self, wrapped, instance, args, kwargs):
|
|
70
|
+
"""Wrap Crew.kickoff() method with span.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
wrapped: The original method.
|
|
74
|
+
instance: The Crew instance.
|
|
75
|
+
args: Positional arguments.
|
|
76
|
+
kwargs: Keyword arguments.
|
|
77
|
+
"""
|
|
78
|
+
return self.create_span_wrapper(
|
|
79
|
+
span_name="crewai.crew.execution",
|
|
80
|
+
extract_attributes=self._extract_crew_attributes,
|
|
81
|
+
)(wrapped)(instance, *args, **kwargs)
|
|
82
|
+
|
|
83
|
+
def _extract_crew_attributes(self, instance: Any, args: Any, kwargs: Any) -> Dict[str, Any]:
|
|
84
|
+
"""Extract attributes from Crew.kickoff() call.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
instance: The Crew instance.
|
|
88
|
+
args: Positional arguments.
|
|
89
|
+
kwargs: Keyword arguments.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
Dict[str, Any]: Dictionary of attributes to set on the span.
|
|
93
|
+
"""
|
|
94
|
+
attrs = {}
|
|
95
|
+
|
|
96
|
+
# Core attributes
|
|
97
|
+
attrs["gen_ai.system"] = "crewai"
|
|
98
|
+
attrs["gen_ai.operation.name"] = "crew.execution"
|
|
99
|
+
|
|
100
|
+
# Extract crew ID if available
|
|
101
|
+
if hasattr(instance, "id"):
|
|
102
|
+
attrs["crewai.crew.id"] = str(instance.id)
|
|
103
|
+
|
|
104
|
+
# Extract crew name if available
|
|
105
|
+
if hasattr(instance, "name"):
|
|
106
|
+
attrs["crewai.crew.name"] = instance.name
|
|
107
|
+
|
|
108
|
+
# Extract process type (sequential, hierarchical, etc.)
|
|
109
|
+
if hasattr(instance, "process"):
|
|
110
|
+
process = instance.process
|
|
111
|
+
# Process might be an enum or string
|
|
112
|
+
process_type = str(process).split(".")[-1] if hasattr(process, "name") else str(process)
|
|
113
|
+
attrs["crewai.process.type"] = process_type
|
|
114
|
+
|
|
115
|
+
# Extract agents
|
|
116
|
+
if hasattr(instance, "agents") and instance.agents:
|
|
117
|
+
try:
|
|
118
|
+
agent_count = len(instance.agents)
|
|
119
|
+
attrs["crewai.agent_count"] = agent_count
|
|
120
|
+
|
|
121
|
+
# Extract agent roles
|
|
122
|
+
agent_roles = []
|
|
123
|
+
agent_goals = []
|
|
124
|
+
for agent in instance.agents:
|
|
125
|
+
if hasattr(agent, "role"):
|
|
126
|
+
agent_roles.append(str(agent.role)[:100]) # Truncate long roles
|
|
127
|
+
if hasattr(agent, "goal"):
|
|
128
|
+
agent_goals.append(str(agent.goal)[:100]) # Truncate long goals
|
|
129
|
+
|
|
130
|
+
if agent_roles:
|
|
131
|
+
attrs["crewai.agent.roles"] = agent_roles
|
|
132
|
+
if agent_goals:
|
|
133
|
+
attrs["crewai.agent.goals"] = agent_goals
|
|
134
|
+
|
|
135
|
+
# Extract tools from agents
|
|
136
|
+
all_tools = []
|
|
137
|
+
for agent in instance.agents:
|
|
138
|
+
if hasattr(agent, "tools") and agent.tools:
|
|
139
|
+
for tool in agent.tools:
|
|
140
|
+
tool_name = str(getattr(tool, "name", type(tool).__name__))
|
|
141
|
+
if tool_name and tool_name not in all_tools:
|
|
142
|
+
all_tools.append(tool_name)
|
|
143
|
+
|
|
144
|
+
if all_tools:
|
|
145
|
+
attrs["crewai.tools"] = all_tools[:10] # Limit to 10 tools
|
|
146
|
+
attrs["crewai.tool_count"] = len(all_tools)
|
|
147
|
+
|
|
148
|
+
except Exception as e:
|
|
149
|
+
logger.debug("Failed to extract agent information: %s", e)
|
|
150
|
+
|
|
151
|
+
# Extract tasks
|
|
152
|
+
if hasattr(instance, "tasks") and instance.tasks:
|
|
153
|
+
try:
|
|
154
|
+
task_count = len(instance.tasks)
|
|
155
|
+
attrs["crewai.task_count"] = task_count
|
|
156
|
+
|
|
157
|
+
# Extract task descriptions (truncated)
|
|
158
|
+
task_descriptions = []
|
|
159
|
+
for task in instance.tasks:
|
|
160
|
+
if hasattr(task, "description"):
|
|
161
|
+
desc = str(task.description)[:100] # Truncate
|
|
162
|
+
task_descriptions.append(desc)
|
|
163
|
+
|
|
164
|
+
if task_descriptions:
|
|
165
|
+
attrs["crewai.task.descriptions"] = task_descriptions
|
|
166
|
+
|
|
167
|
+
except Exception as e:
|
|
168
|
+
logger.debug("Failed to extract task information: %s", e)
|
|
169
|
+
|
|
170
|
+
# Extract verbose setting
|
|
171
|
+
if hasattr(instance, "verbose"):
|
|
172
|
+
attrs["crewai.verbose"] = instance.verbose
|
|
173
|
+
|
|
174
|
+
# Extract inputs passed to kickoff (first positional arg or 'inputs' kwarg)
|
|
175
|
+
inputs = None
|
|
176
|
+
if len(args) > 0:
|
|
177
|
+
inputs = args[0]
|
|
178
|
+
elif "inputs" in kwargs:
|
|
179
|
+
inputs = kwargs["inputs"]
|
|
180
|
+
|
|
181
|
+
if inputs:
|
|
182
|
+
try:
|
|
183
|
+
if isinstance(inputs, dict):
|
|
184
|
+
# Store input keys and truncated values
|
|
185
|
+
input_keys = list(inputs.keys())
|
|
186
|
+
attrs["crewai.inputs.keys"] = input_keys
|
|
187
|
+
|
|
188
|
+
# Store input values (truncated)
|
|
189
|
+
for key, value in list(inputs.items())[:5]: # Limit to 5 inputs
|
|
190
|
+
value_str = str(value)[:200] # Truncate long values
|
|
191
|
+
attrs[f"crewai.inputs.{key}"] = value_str
|
|
192
|
+
else:
|
|
193
|
+
# Non-dict inputs
|
|
194
|
+
attrs["crewai.inputs"] = str(inputs)[:200]
|
|
195
|
+
except Exception as e:
|
|
196
|
+
logger.debug("Failed to extract inputs: %s", e)
|
|
197
|
+
|
|
198
|
+
# Extract manager agent for hierarchical process
|
|
199
|
+
if hasattr(instance, "manager_agent") and instance.manager_agent:
|
|
200
|
+
try:
|
|
201
|
+
if hasattr(instance.manager_agent, "role"):
|
|
202
|
+
attrs["crewai.manager.role"] = str(instance.manager_agent.role)[:100]
|
|
203
|
+
except Exception as e:
|
|
204
|
+
logger.debug("Failed to extract manager agent: %s", e)
|
|
205
|
+
|
|
206
|
+
return attrs
|
|
207
|
+
|
|
208
|
+
def _extract_usage(self, result) -> Optional[Dict[str, int]]:
|
|
209
|
+
"""Extract token usage from crew execution result.
|
|
210
|
+
|
|
211
|
+
Note: CrewAI doesn't directly expose token usage in the result.
|
|
212
|
+
Token usage is captured by underlying LLM provider instrumentors.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
result: The crew execution result.
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
Optional[Dict[str, int]]: Dictionary with token counts or None.
|
|
219
|
+
"""
|
|
220
|
+
# CrewAI doesn't directly expose usage in the result
|
|
221
|
+
# Token usage is captured by LLM provider instrumentors (OpenAI, Anthropic, etc.)
|
|
222
|
+
# We could try to aggregate if CrewAI provides usage info in the future
|
|
223
|
+
if hasattr(result, "token_usage"):
|
|
224
|
+
try:
|
|
225
|
+
usage = result.token_usage
|
|
226
|
+
return {
|
|
227
|
+
"prompt_tokens": getattr(usage, "prompt_tokens", 0),
|
|
228
|
+
"completion_tokens": getattr(usage, "completion_tokens", 0),
|
|
229
|
+
"total_tokens": getattr(usage, "total_tokens", 0),
|
|
230
|
+
}
|
|
231
|
+
except Exception as e:
|
|
232
|
+
logger.debug("Failed to extract token usage: %s", e)
|
|
233
|
+
return None
|
|
234
|
+
|
|
235
|
+
def _extract_response_attributes(self, result) -> Dict[str, Any]:
|
|
236
|
+
"""Extract response attributes from crew execution result.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
result: The crew execution result.
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
Dict[str, Any]: Dictionary of response attributes.
|
|
243
|
+
"""
|
|
244
|
+
attrs = {}
|
|
245
|
+
|
|
246
|
+
# CrewAI result can be a string or a CrewOutput object
|
|
247
|
+
try:
|
|
248
|
+
# If result is a string (common case)
|
|
249
|
+
if isinstance(result, str):
|
|
250
|
+
output = result[:500] # Truncate to avoid span size issues
|
|
251
|
+
attrs["crewai.output"] = output
|
|
252
|
+
attrs["crewai.output_length"] = len(result)
|
|
253
|
+
|
|
254
|
+
# If result is a CrewOutput object
|
|
255
|
+
elif hasattr(result, "raw"):
|
|
256
|
+
output = str(result.raw)[:500]
|
|
257
|
+
attrs["crewai.output"] = output
|
|
258
|
+
attrs["crewai.output_length"] = len(str(result.raw))
|
|
259
|
+
|
|
260
|
+
# If result has tasks_output attribute (list of task results)
|
|
261
|
+
if hasattr(result, "tasks_output"):
|
|
262
|
+
try:
|
|
263
|
+
tasks_output = result.tasks_output
|
|
264
|
+
if tasks_output:
|
|
265
|
+
attrs["crewai.tasks_completed"] = len(tasks_output)
|
|
266
|
+
|
|
267
|
+
# Extract output from each task
|
|
268
|
+
task_outputs = []
|
|
269
|
+
for idx, task_output in enumerate(tasks_output[:5]): # Limit to 5 tasks
|
|
270
|
+
if hasattr(task_output, "raw"):
|
|
271
|
+
task_result = str(task_output.raw)[:200] # Truncate
|
|
272
|
+
task_outputs.append(task_result)
|
|
273
|
+
|
|
274
|
+
if task_outputs:
|
|
275
|
+
attrs["crewai.task_outputs"] = task_outputs
|
|
276
|
+
except Exception as e:
|
|
277
|
+
logger.debug("Failed to extract tasks_output: %s", e)
|
|
278
|
+
|
|
279
|
+
# If result has json attribute
|
|
280
|
+
if hasattr(result, "json"):
|
|
281
|
+
try:
|
|
282
|
+
attrs["crewai.output.json"] = str(result.json)[:500]
|
|
283
|
+
except Exception as e:
|
|
284
|
+
logger.debug("Failed to extract JSON output: %s", e)
|
|
285
|
+
|
|
286
|
+
# If result has pydantic attribute
|
|
287
|
+
if hasattr(result, "pydantic"):
|
|
288
|
+
try:
|
|
289
|
+
attrs["crewai.output.pydantic"] = str(result.pydantic)[:500]
|
|
290
|
+
except Exception as e:
|
|
291
|
+
logger.debug("Failed to extract Pydantic output: %s", e)
|
|
292
|
+
|
|
293
|
+
except Exception as e:
|
|
294
|
+
logger.debug("Failed to extract response attributes: %s", e)
|
|
295
|
+
|
|
296
|
+
return attrs
|
|
297
|
+
|
|
298
|
+
def _extract_finish_reason(self, result) -> Optional[str]:
|
|
299
|
+
"""Extract finish reason from crew execution result.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
result: The crew execution result.
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
Optional[str]: The finish reason string or None if not available.
|
|
306
|
+
"""
|
|
307
|
+
# CrewAI doesn't typically provide a finish_reason
|
|
308
|
+
# We could infer completion status
|
|
309
|
+
if result:
|
|
310
|
+
return "completed"
|
|
311
|
+
return None
|