genai-otel-instrument 0.1.1.dev0__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 genai-otel-instrument might be problematic. Click here for more details.

Files changed (44) hide show
  1. genai_otel/__init__.py +129 -0
  2. genai_otel/__version__.py +34 -0
  3. genai_otel/auto_instrument.py +413 -0
  4. genai_otel/cli.py +92 -0
  5. genai_otel/config.py +187 -0
  6. genai_otel/cost_calculator.py +276 -0
  7. genai_otel/exceptions.py +17 -0
  8. genai_otel/gpu_metrics.py +240 -0
  9. genai_otel/instrumentors/__init__.py +47 -0
  10. genai_otel/instrumentors/anthropic_instrumentor.py +134 -0
  11. genai_otel/instrumentors/anyscale_instrumentor.py +27 -0
  12. genai_otel/instrumentors/aws_bedrock_instrumentor.py +94 -0
  13. genai_otel/instrumentors/azure_openai_instrumentor.py +69 -0
  14. genai_otel/instrumentors/base.py +528 -0
  15. genai_otel/instrumentors/cohere_instrumentor.py +76 -0
  16. genai_otel/instrumentors/google_ai_instrumentor.py +87 -0
  17. genai_otel/instrumentors/groq_instrumentor.py +106 -0
  18. genai_otel/instrumentors/huggingface_instrumentor.py +97 -0
  19. genai_otel/instrumentors/langchain_instrumentor.py +75 -0
  20. genai_otel/instrumentors/llamaindex_instrumentor.py +36 -0
  21. genai_otel/instrumentors/mistralai_instrumentor.py +119 -0
  22. genai_otel/instrumentors/ollama_instrumentor.py +83 -0
  23. genai_otel/instrumentors/openai_instrumentor.py +241 -0
  24. genai_otel/instrumentors/replicate_instrumentor.py +42 -0
  25. genai_otel/instrumentors/togetherai_instrumentor.py +42 -0
  26. genai_otel/instrumentors/vertexai_instrumentor.py +42 -0
  27. genai_otel/llm_pricing.json +589 -0
  28. genai_otel/logging_config.py +45 -0
  29. genai_otel/mcp_instrumentors/__init__.py +14 -0
  30. genai_otel/mcp_instrumentors/api_instrumentor.py +144 -0
  31. genai_otel/mcp_instrumentors/base.py +105 -0
  32. genai_otel/mcp_instrumentors/database_instrumentor.py +336 -0
  33. genai_otel/mcp_instrumentors/kafka_instrumentor.py +31 -0
  34. genai_otel/mcp_instrumentors/manager.py +139 -0
  35. genai_otel/mcp_instrumentors/redis_instrumentor.py +31 -0
  36. genai_otel/mcp_instrumentors/vector_db_instrumentor.py +265 -0
  37. genai_otel/metrics.py +148 -0
  38. genai_otel/py.typed +2 -0
  39. genai_otel_instrument-0.1.1.dev0.dist-info/METADATA +463 -0
  40. genai_otel_instrument-0.1.1.dev0.dist-info/RECORD +44 -0
  41. genai_otel_instrument-0.1.1.dev0.dist-info/WHEEL +5 -0
  42. genai_otel_instrument-0.1.1.dev0.dist-info/entry_points.txt +2 -0
  43. genai_otel_instrument-0.1.1.dev0.dist-info/licenses/LICENSE +201 -0
  44. genai_otel_instrument-0.1.1.dev0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,241 @@
1
+ """OpenTelemetry instrumentor for the OpenAI Python SDK.
2
+
3
+ This instrumentor automatically traces chat completion calls made using the
4
+ OpenAI SDK, capturing relevant attributes such as the model name, message count,
5
+ and token usage.
6
+ """
7
+
8
+ import json
9
+ import logging
10
+ from typing import Any, Dict, Optional
11
+
12
+ from ..config import OTelConfig
13
+ from .base import BaseInstrumentor
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class OpenAIInstrumentor(BaseInstrumentor):
19
+ """Instrumentor for OpenAI SDK"""
20
+
21
+ def __init__(self):
22
+ """Initialize the instrumentor."""
23
+ super().__init__()
24
+ self._openai_available = False
25
+ self._check_availability()
26
+
27
+ def _check_availability(self):
28
+ """Check if OpenAI library is available."""
29
+ try:
30
+ import openai
31
+
32
+ self._openai_available = True
33
+ logger.debug("OpenAI library detected and available for instrumentation")
34
+ except ImportError:
35
+ logger.debug("OpenAI library not installed, instrumentation will be skipped")
36
+ self._openai_available = False
37
+
38
+ def instrument(self, config: OTelConfig):
39
+ """Instrument OpenAI SDK if available.
40
+
41
+ Args:
42
+ config (OTelConfig): The OpenTelemetry configuration object.
43
+ """
44
+ if not self._openai_available:
45
+ logger.debug("Skipping OpenAI instrumentation - library not available")
46
+ return
47
+
48
+ self.config = config
49
+
50
+ try:
51
+ import openai
52
+ import wrapt
53
+
54
+ # Instrument OpenAI client initialization
55
+ if hasattr(openai, "OpenAI"):
56
+ original_init = openai.OpenAI.__init__
57
+
58
+ def wrapped_init(wrapped, instance, args, kwargs):
59
+ result = wrapped(*args, **kwargs)
60
+ self._instrument_client(instance)
61
+ return result
62
+
63
+ openai.OpenAI.__init__ = wrapt.FunctionWrapper(original_init, wrapped_init)
64
+ self._instrumented = True
65
+ logger.info("OpenAI instrumentation enabled")
66
+
67
+ except Exception as e:
68
+ logger.error("Failed to instrument OpenAI: %s", e, exc_info=True)
69
+ if config.fail_on_error:
70
+ raise
71
+
72
+ def _instrument_client(self, client):
73
+ """Instrument OpenAI client methods.
74
+
75
+ Args:
76
+ client: The OpenAI client instance to instrument.
77
+ """
78
+ if (
79
+ hasattr(client, "chat")
80
+ and hasattr(client.chat, "completions")
81
+ and hasattr(client.chat.completions, "create")
82
+ ):
83
+ original_create = client.chat.completions.create
84
+ instrumented_create_method = self.create_span_wrapper(
85
+ span_name="openai.chat.completion",
86
+ extract_attributes=self._extract_openai_attributes,
87
+ )(original_create)
88
+ client.chat.completions.create = instrumented_create_method
89
+
90
+ def _extract_openai_attributes(self, instance: Any, args: Any, kwargs: Any) -> Dict[str, Any]:
91
+ """Extract attributes from OpenAI API call.
92
+
93
+ Args:
94
+ instance: The client instance.
95
+ args: Positional arguments.
96
+ kwargs: Keyword arguments.
97
+
98
+ Returns:
99
+ Dict[str, Any]: Dictionary of attributes to set on the span.
100
+ """
101
+ attrs = {}
102
+ model = kwargs.get("model", "unknown")
103
+ messages = kwargs.get("messages", [])
104
+
105
+ # Core attributes
106
+ attrs["gen_ai.system"] = "openai"
107
+ attrs["gen_ai.request.model"] = model
108
+ attrs["gen_ai.operation.name"] = "chat" # NEW: operation name
109
+ attrs["gen_ai.request.message_count"] = len(messages)
110
+
111
+ # Request parameters (NEW)
112
+ if "temperature" in kwargs:
113
+ attrs["gen_ai.request.temperature"] = kwargs["temperature"]
114
+ if "top_p" in kwargs:
115
+ attrs["gen_ai.request.top_p"] = kwargs["top_p"]
116
+ if "max_tokens" in kwargs:
117
+ attrs["gen_ai.request.max_tokens"] = kwargs["max_tokens"]
118
+ if "frequency_penalty" in kwargs:
119
+ attrs["gen_ai.request.frequency_penalty"] = kwargs["frequency_penalty"]
120
+ if "presence_penalty" in kwargs:
121
+ attrs["gen_ai.request.presence_penalty"] = kwargs["presence_penalty"]
122
+
123
+ # Tool/function definitions (Phase 3.1)
124
+ if "tools" in kwargs:
125
+ try:
126
+ attrs["llm.tools"] = json.dumps(kwargs["tools"])
127
+ except (TypeError, ValueError) as e:
128
+ logger.debug("Failed to serialize tools: %s", e)
129
+
130
+ if messages:
131
+ # Only capture first 200 chars to avoid sensitive data and span size issues
132
+ first_message = str(messages[0])[:200]
133
+ attrs["gen_ai.request.first_message"] = first_message
134
+
135
+ return attrs
136
+
137
+ def _extract_usage(self, result) -> Optional[Dict[str, int]]:
138
+ """Extract token usage from OpenAI response.
139
+
140
+ Args:
141
+ result: The API response object.
142
+
143
+ Returns:
144
+ Optional[Dict[str, int]]: Dictionary with token counts or None.
145
+ """
146
+ if hasattr(result, "usage") and result.usage:
147
+ usage = result.usage
148
+ usage_dict = {
149
+ "prompt_tokens": getattr(usage, "prompt_tokens", 0),
150
+ "completion_tokens": getattr(usage, "completion_tokens", 0),
151
+ "total_tokens": getattr(usage, "total_tokens", 0),
152
+ }
153
+
154
+ # Extract reasoning tokens for o1 models (Phase 3.2)
155
+ if hasattr(usage, "completion_tokens_details") and usage.completion_tokens_details:
156
+ details = usage.completion_tokens_details
157
+ usage_dict["completion_tokens_details"] = {
158
+ "reasoning_tokens": getattr(details, "reasoning_tokens", 0)
159
+ }
160
+
161
+ return usage_dict
162
+ return None
163
+
164
+ def _extract_response_attributes(self, result) -> Dict[str, Any]:
165
+ """Extract response attributes from OpenAI response.
166
+
167
+ Args:
168
+ result: The API response object.
169
+
170
+ Returns:
171
+ Dict[str, Any]: Dictionary of response attributes.
172
+ """
173
+ attrs = {}
174
+
175
+ # Response ID
176
+ if hasattr(result, "id"):
177
+ attrs["gen_ai.response.id"] = result.id
178
+
179
+ # Response model (actual model used, may differ from request)
180
+ if hasattr(result, "model"):
181
+ attrs["gen_ai.response.model"] = result.model
182
+
183
+ # Finish reasons
184
+ if hasattr(result, "choices") and result.choices:
185
+ finish_reasons = [
186
+ choice.finish_reason
187
+ for choice in result.choices
188
+ if hasattr(choice, "finish_reason")
189
+ ]
190
+ if finish_reasons:
191
+ attrs["gen_ai.response.finish_reasons"] = finish_reasons
192
+
193
+ # Tool calls extraction (Phase 3.1)
194
+ for choice_idx, choice in enumerate(result.choices):
195
+ message = getattr(choice, "message", None)
196
+ if message and hasattr(message, "tool_calls") and message.tool_calls:
197
+ for tc_idx, tool_call in enumerate(message.tool_calls):
198
+ prefix = f"llm.output_messages.{choice_idx}.message.tool_calls.{tc_idx}"
199
+ if hasattr(tool_call, "id"):
200
+ attrs[f"{prefix}.tool_call.id"] = tool_call.id
201
+ if hasattr(tool_call, "function"):
202
+ if hasattr(tool_call.function, "name"):
203
+ attrs[f"{prefix}.tool_call.function.name"] = tool_call.function.name
204
+ if hasattr(tool_call.function, "arguments"):
205
+ attrs[f"{prefix}.tool_call.function.arguments"] = (
206
+ tool_call.function.arguments
207
+ )
208
+
209
+ return attrs
210
+
211
+ def _add_content_events(self, span, result, request_kwargs: dict):
212
+ """Add prompt and completion content as span events.
213
+
214
+ Args:
215
+ span: The OpenTelemetry span.
216
+ result: The API response object.
217
+ request_kwargs: The original request kwargs.
218
+ """
219
+ # Add prompt content events
220
+ messages = request_kwargs.get("messages", [])
221
+ for idx, message in enumerate(messages):
222
+ if isinstance(message, dict):
223
+ role = message.get("role", "unknown")
224
+ content = message.get("content", "")
225
+ span.add_event(
226
+ f"gen_ai.prompt.{idx}",
227
+ attributes={"gen_ai.prompt.role": role, "gen_ai.prompt.content": str(content)},
228
+ )
229
+
230
+ # Add completion content events
231
+ if hasattr(result, "choices") and result.choices:
232
+ for idx, choice in enumerate(result.choices):
233
+ if hasattr(choice, "message") and hasattr(choice.message, "content"):
234
+ content = choice.message.content
235
+ span.add_event(
236
+ f"gen_ai.completion.{idx}",
237
+ attributes={
238
+ "gen_ai.completion.role": "assistant",
239
+ "gen_ai.completion.content": str(content),
240
+ },
241
+ )
@@ -0,0 +1,42 @@
1
+ """OpenTelemetry instrumentor for the Replicate API client.
2
+
3
+ This instrumentor automatically traces calls to Replicate models, capturing
4
+ relevant attributes such as the model name.
5
+ """
6
+
7
+ from typing import Dict, Optional
8
+
9
+ from ..config import OTelConfig
10
+ from .base import BaseInstrumentor
11
+
12
+
13
+ class ReplicateInstrumentor(BaseInstrumentor):
14
+ """Instrumentor for Replicate"""
15
+
16
+ def instrument(self, config: OTelConfig):
17
+ self.config = config
18
+ try:
19
+ import replicate
20
+
21
+ original_run = replicate.run
22
+
23
+ def wrapped_run(*args, **kwargs):
24
+ with self.tracer.start_as_current_span("replicate.run") as span:
25
+ model = args[0] if args else "unknown"
26
+
27
+ span.set_attribute("gen_ai.system", "replicate")
28
+ span.set_attribute("gen_ai.request.model", model)
29
+
30
+ if self.request_counter:
31
+ self.request_counter.add(1, {"model": model, "provider": "replicate"})
32
+
33
+ result = original_run(*args, **kwargs)
34
+ return result
35
+
36
+ replicate.run = wrapped_run
37
+
38
+ except ImportError:
39
+ pass
40
+
41
+ def _extract_usage(self, result) -> Optional[Dict[str, int]]:
42
+ return None
@@ -0,0 +1,42 @@
1
+ """OpenTelemetry instrumentor for the Together AI SDK.
2
+
3
+ This instrumentor automatically traces completion calls to Together AI models,
4
+ capturing relevant attributes such as the model name.
5
+ """
6
+
7
+ from typing import Dict, Optional
8
+
9
+ from ..config import OTelConfig
10
+ from .base import BaseInstrumentor
11
+
12
+
13
+ class TogetherAIInstrumentor(BaseInstrumentor):
14
+ """Instrumentor for Together AI"""
15
+
16
+ def instrument(self, config: OTelConfig):
17
+ self.config = config
18
+ try:
19
+ import together
20
+
21
+ original_complete = together.Complete.create
22
+
23
+ def wrapped_complete(*args, **kwargs):
24
+ with self.tracer.start_as_current_span("together.complete") as span:
25
+ model = kwargs.get("model", "unknown")
26
+
27
+ span.set_attribute("gen_ai.system", "together")
28
+ span.set_attribute("gen_ai.request.model", model)
29
+
30
+ if self.request_counter:
31
+ self.request_counter.add(1, {"model": model, "provider": "together"})
32
+
33
+ result = original_complete(*args, **kwargs)
34
+ return result
35
+
36
+ together.Complete.create = wrapped_complete
37
+
38
+ except ImportError:
39
+ pass
40
+
41
+ def _extract_usage(self, result) -> Optional[Dict[str, int]]:
42
+ return None
@@ -0,0 +1,42 @@
1
+ """OpenTelemetry instrumentor for Google Vertex AI SDK.
2
+
3
+ This instrumentor automatically traces content generation calls to Vertex AI models,
4
+ capturing relevant attributes such as the model name.
5
+ """
6
+
7
+ from typing import Dict, Optional
8
+
9
+ from ..config import OTelConfig
10
+ from .base import BaseInstrumentor
11
+
12
+
13
+ class VertexAIInstrumentor(BaseInstrumentor):
14
+ """Instrumentor for Google Vertex AI"""
15
+
16
+ def instrument(self, config: OTelConfig):
17
+ self.config = config
18
+ try:
19
+ from vertexai.preview.generative_models import GenerativeModel
20
+
21
+ original_generate = GenerativeModel.generate_content
22
+
23
+ def wrapped_generate(instance, *args, **kwargs):
24
+ with self.tracer.start_as_current_span("vertexai.generate_content") as span:
25
+ model_name = getattr(instance, "_model_name", "unknown")
26
+
27
+ span.set_attribute("gen_ai.system", "vertexai")
28
+ span.set_attribute("gen_ai.request.model", model_name)
29
+
30
+ if self.request_counter:
31
+ self.request_counter.add(1, {"model": model_name, "provider": "vertexai"})
32
+
33
+ result = original_generate(instance, *args, **kwargs)
34
+ return result
35
+
36
+ GenerativeModel.generate_content = wrapped_generate
37
+
38
+ except ImportError:
39
+ pass
40
+
41
+ def _extract_usage(self, result) -> Optional[Dict[str, int]]:
42
+ return None