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.
- genai_otel/__init__.py +129 -0
- genai_otel/__version__.py +34 -0
- genai_otel/auto_instrument.py +413 -0
- genai_otel/cli.py +92 -0
- genai_otel/config.py +187 -0
- genai_otel/cost_calculator.py +276 -0
- genai_otel/exceptions.py +17 -0
- genai_otel/gpu_metrics.py +240 -0
- genai_otel/instrumentors/__init__.py +47 -0
- genai_otel/instrumentors/anthropic_instrumentor.py +134 -0
- genai_otel/instrumentors/anyscale_instrumentor.py +27 -0
- genai_otel/instrumentors/aws_bedrock_instrumentor.py +94 -0
- genai_otel/instrumentors/azure_openai_instrumentor.py +69 -0
- genai_otel/instrumentors/base.py +528 -0
- genai_otel/instrumentors/cohere_instrumentor.py +76 -0
- genai_otel/instrumentors/google_ai_instrumentor.py +87 -0
- genai_otel/instrumentors/groq_instrumentor.py +106 -0
- genai_otel/instrumentors/huggingface_instrumentor.py +97 -0
- genai_otel/instrumentors/langchain_instrumentor.py +75 -0
- genai_otel/instrumentors/llamaindex_instrumentor.py +36 -0
- genai_otel/instrumentors/mistralai_instrumentor.py +119 -0
- genai_otel/instrumentors/ollama_instrumentor.py +83 -0
- genai_otel/instrumentors/openai_instrumentor.py +241 -0
- genai_otel/instrumentors/replicate_instrumentor.py +42 -0
- genai_otel/instrumentors/togetherai_instrumentor.py +42 -0
- genai_otel/instrumentors/vertexai_instrumentor.py +42 -0
- genai_otel/llm_pricing.json +589 -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_instrument-0.1.1.dev0.dist-info/METADATA +463 -0
- genai_otel_instrument-0.1.1.dev0.dist-info/RECORD +44 -0
- genai_otel_instrument-0.1.1.dev0.dist-info/WHEEL +5 -0
- genai_otel_instrument-0.1.1.dev0.dist-info/entry_points.txt +2 -0
- genai_otel_instrument-0.1.1.dev0.dist-info/licenses/LICENSE +201 -0
- 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
|