Skip to content

Commit ab95ef6

Browse files
committed
Initial implementation and tests
1 parent e6869cd commit ab95ef6

18 files changed

+22013
-31
lines changed

instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/__init__.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,12 @@
5454
from opentelemetry.trace import get_tracer
5555

5656
from .instruments import Instruments
57-
from .patch import async_chat_completions_create, chat_completions_create
57+
from .patch import (
58+
async_chat_completions_create,
59+
async_embeddings_create,
60+
chat_completions_create,
61+
embeddings_create,
62+
)
5863

5964

6065
class OpenAIInstrumentor(BaseInstrumentor):
@@ -106,8 +111,27 @@ def _instrument(self, **kwargs):
106111
),
107112
)
108113

114+
# Add instrumentation for the embeddings API
115+
wrap_function_wrapper(
116+
module="openai.resources.embeddings",
117+
name="Embeddings.create",
118+
wrapper=embeddings_create(
119+
tracer, event_logger, instruments, is_content_enabled()
120+
),
121+
)
122+
123+
wrap_function_wrapper(
124+
module="openai.resources.embeddings",
125+
name="AsyncEmbeddings.create",
126+
wrapper=async_embeddings_create(
127+
tracer, event_logger, instruments, is_content_enabled()
128+
),
129+
)
130+
109131
def _uninstrument(self, **kwargs):
110132
import openai # pylint: disable=import-outside-toplevel
111133

112134
unwrap(openai.resources.chat.completions.Completions, "create")
113135
unwrap(openai.resources.chat.completions.AsyncCompletions, "create")
136+
unwrap(openai.resources.embeddings.Embeddings, "create")
137+
unwrap(openai.resources.embeddings.AsyncEmbeddings, "create")

instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/patch.py

Lines changed: 196 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ def traced_method(wrapped, instance, args, kwargs):
9191
result,
9292
span_attributes,
9393
error_type,
94+
GenAIAttributes.GenAiOperationNameValues.CHAT.value,
9495
)
9596

9697
return traced_method
@@ -149,6 +150,137 @@ async def traced_method(wrapped, instance, args, kwargs):
149150
result,
150151
span_attributes,
151152
error_type,
153+
GenAIAttributes.GenAiOperationNameValues.CHAT.value,
154+
)
155+
156+
return traced_method
157+
158+
159+
def embeddings_create(
160+
tracer: Tracer,
161+
event_logger: EventLogger,
162+
instruments: Instruments,
163+
capture_content: bool,
164+
):
165+
"""Wrap the `create` method of the `Embeddings` class to trace it."""
166+
167+
def traced_method(wrapped, instance, args, kwargs):
168+
span_attributes = {
169+
**get_llm_request_attributes(
170+
kwargs,
171+
instance,
172+
GenAIAttributes.GenAiOperationNameValues.EMBEDDINGS.value,
173+
)
174+
}
175+
176+
# Set embeddings dimensions if specified in the request
177+
if "dimensions" in kwargs and kwargs["dimensions"] is not None:
178+
span_attributes["gen_ai.embeddings.dimensions"] = kwargs[
179+
"dimensions"
180+
]
181+
182+
span_name = f"{span_attributes[GenAIAttributes.GEN_AI_OPERATION_NAME]} {span_attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL]}"
183+
with tracer.start_as_current_span(
184+
name=span_name,
185+
kind=SpanKind.CLIENT,
186+
attributes=span_attributes,
187+
end_on_exit=False,
188+
) as span:
189+
# Store the input for later use in the response attributes
190+
input_text = kwargs.get("input", "")
191+
192+
start = default_timer()
193+
result = None
194+
error_type = None
195+
try:
196+
result = wrapped(*args, **kwargs)
197+
198+
if span.is_recording():
199+
_set_embeddings_response_attributes(
200+
span, result, event_logger, capture_content, input_text
201+
)
202+
203+
span.end()
204+
return result
205+
206+
except Exception as error:
207+
error_type = type(error).__qualname__
208+
handle_span_exception(span, error)
209+
raise
210+
finally:
211+
duration = max((default_timer() - start), 0)
212+
_record_metrics(
213+
instruments,
214+
duration,
215+
result,
216+
span_attributes,
217+
error_type,
218+
GenAIAttributes.GenAiOperationNameValues.EMBEDDINGS.value,
219+
)
220+
221+
return traced_method
222+
223+
224+
def async_embeddings_create(
225+
tracer: Tracer,
226+
event_logger: EventLogger,
227+
instruments: Instruments,
228+
capture_content: bool,
229+
):
230+
"""Wrap the `create` method of the `AsyncEmbeddings` class to trace it."""
231+
232+
async def traced_method(wrapped, instance, args, kwargs):
233+
span_attributes = {
234+
**get_llm_request_attributes(
235+
kwargs,
236+
instance,
237+
GenAIAttributes.GenAiOperationNameValues.EMBEDDINGS.value,
238+
)
239+
}
240+
241+
# Set embeddings dimensions if specified in the request
242+
if "dimensions" in kwargs and kwargs["dimensions"] is not None:
243+
span_attributes["gen_ai.embeddings.dimensions"] = kwargs[
244+
"dimensions"
245+
]
246+
247+
span_name = f"{span_attributes[GenAIAttributes.GEN_AI_OPERATION_NAME]} {span_attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL]}"
248+
with tracer.start_as_current_span(
249+
name=span_name,
250+
kind=SpanKind.CLIENT,
251+
attributes=span_attributes,
252+
end_on_exit=False,
253+
) as span:
254+
# Store the input for later use in the response attributes
255+
input_text = kwargs.get("input", "")
256+
257+
start = default_timer()
258+
result = None
259+
error_type = None
260+
try:
261+
result = await wrapped(*args, **kwargs)
262+
263+
if span.is_recording():
264+
_set_embeddings_response_attributes(
265+
span, result, event_logger, capture_content, input_text
266+
)
267+
268+
span.end()
269+
return result
270+
271+
except Exception as error:
272+
error_type = type(error).__qualname__
273+
handle_span_exception(span, error)
274+
raise
275+
finally:
276+
duration = max((default_timer() - start), 0)
277+
_record_metrics(
278+
instruments,
279+
duration,
280+
result,
281+
span_attributes,
282+
error_type,
283+
GenAIAttributes.GenAiOperationNameValues.EMBEDDINGS.value,
152284
)
153285

154286
return traced_method
@@ -160,15 +292,22 @@ def _record_metrics(
160292
result,
161293
span_attributes: dict,
162294
error_type: Optional[str],
295+
operation_name: str,
163296
):
297+
"""Generalized function to record metrics for both chat and embeddings operations."""
164298
common_attributes = {
165-
GenAIAttributes.GEN_AI_OPERATION_NAME: GenAIAttributes.GenAiOperationNameValues.CHAT.value,
299+
GenAIAttributes.GEN_AI_OPERATION_NAME: operation_name,
166300
GenAIAttributes.GEN_AI_SYSTEM: GenAIAttributes.GenAiSystemValues.OPENAI.value,
167301
GenAIAttributes.GEN_AI_REQUEST_MODEL: span_attributes[
168302
GenAIAttributes.GEN_AI_REQUEST_MODEL
169303
],
170304
}
171305

306+
if "gen_ai.embeddings.dimensions" in span_attributes:
307+
common_attributes["gen_ai.embeddings.dimensions"] = span_attributes[
308+
"gen_ai.embeddings.dimensions"
309+
]
310+
172311
if error_type:
173312
common_attributes["error.type"] = error_type
174313

@@ -210,13 +349,18 @@ def _record_metrics(
210349
attributes=input_attributes,
211350
)
212351

213-
completion_attributes = {
352+
token_count = (
353+
result.usage.total_tokens
354+
if operation_name
355+
== GenAIAttributes.GenAiOperationNameValues.EMBEDDINGS.value
356+
else result.usage.completion_tokens
357+
)
358+
attributes = {
214359
**common_attributes,
215360
GenAIAttributes.GEN_AI_TOKEN_TYPE: GenAIAttributes.GenAiTokenTypeValues.COMPLETION.value,
216361
}
217362
instruments.token_usage_histogram.record(
218-
result.usage.completion_tokens,
219-
attributes=completion_attributes,
363+
token_count, attributes=attributes
220364
)
221365

222366

@@ -262,6 +406,54 @@ def _set_response_attributes(
262406
)
263407

264408

409+
def _set_embeddings_response_attributes(
410+
span,
411+
result,
412+
event_logger: EventLogger,
413+
capture_content: bool,
414+
input_text: str,
415+
):
416+
"""Set attributes on the span based on the embeddings response."""
417+
# Set the model name if available
418+
set_span_attribute(
419+
span, GenAIAttributes.GEN_AI_RESPONSE_MODEL, result.model
420+
)
421+
422+
# Set embeddings dimensions if we can determine it from the response
423+
if getattr(result, "data", None) and len(result.data) > 0:
424+
first_embedding = result.data[0]
425+
if getattr(first_embedding, "embedding", None):
426+
set_span_attribute(
427+
span,
428+
"gen_ai.embeddings.dimensions",
429+
len(first_embedding.embedding),
430+
)
431+
432+
# Get the usage
433+
if getattr(result, "usage", None):
434+
set_span_attribute(
435+
span,
436+
GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS,
437+
result.usage.prompt_tokens,
438+
)
439+
set_span_attribute(
440+
span,
441+
GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS,
442+
result.usage.total_tokens,
443+
)
444+
445+
# Emit event for embeddings if content capture is enabled
446+
if capture_content:
447+
input_event = Event(
448+
name="gen_ai.embeddings",
449+
attributes={
450+
GenAIAttributes.GEN_AI_SYSTEM: GenAIAttributes.GenAiSystemValues.OPENAI.value
451+
},
452+
body={"content": input_text, "role": "user"},
453+
)
454+
event_logger.emit(input_event)
455+
456+
265457
class ToolCallBuffer:
266458
def __init__(self, index, tool_call_id, function_name):
267459
self.index = index

instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/utils.py

Lines changed: 53 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -192,38 +192,65 @@ def get_llm_request_attributes(
192192
GenAIAttributes.GEN_AI_OPERATION_NAME: operation_name,
193193
GenAIAttributes.GEN_AI_SYSTEM: GenAIAttributes.GenAiSystemValues.OPENAI.value,
194194
GenAIAttributes.GEN_AI_REQUEST_MODEL: kwargs.get("model"),
195-
GenAIAttributes.GEN_AI_REQUEST_TEMPERATURE: kwargs.get("temperature"),
196-
GenAIAttributes.GEN_AI_REQUEST_TOP_P: kwargs.get("p")
197-
or kwargs.get("top_p"),
198-
GenAIAttributes.GEN_AI_REQUEST_MAX_TOKENS: kwargs.get("max_tokens"),
199-
GenAIAttributes.GEN_AI_REQUEST_PRESENCE_PENALTY: kwargs.get(
200-
"presence_penalty"
201-
),
202-
GenAIAttributes.GEN_AI_REQUEST_FREQUENCY_PENALTY: kwargs.get(
203-
"frequency_penalty"
204-
),
205-
GenAIAttributes.GEN_AI_OPENAI_REQUEST_SEED: kwargs.get("seed"),
206195
}
207196

208-
if (response_format := kwargs.get("response_format")) is not None:
209-
# response_format may be string or object with a string in the `type` key
210-
if isinstance(response_format, Mapping):
211-
if (
212-
response_format_type := response_format.get("type")
213-
) is not None:
197+
# Add chat-specific attributes only for chat operations
198+
if operation_name == GenAIAttributes.GenAiOperationNameValues.CHAT.value:
199+
attributes.update(
200+
{
201+
GenAIAttributes.GEN_AI_REQUEST_TEMPERATURE: kwargs.get(
202+
"temperature"
203+
),
204+
GenAIAttributes.GEN_AI_REQUEST_TOP_P: kwargs.get("p")
205+
or kwargs.get("top_p"),
206+
GenAIAttributes.GEN_AI_REQUEST_MAX_TOKENS: kwargs.get(
207+
"max_tokens"
208+
),
209+
GenAIAttributes.GEN_AI_REQUEST_PRESENCE_PENALTY: kwargs.get(
210+
"presence_penalty"
211+
),
212+
GenAIAttributes.GEN_AI_REQUEST_FREQUENCY_PENALTY: kwargs.get(
213+
"frequency_penalty"
214+
),
215+
GenAIAttributes.GEN_AI_OPENAI_REQUEST_SEED: kwargs.get("seed"),
216+
}
217+
)
218+
219+
if (response_format := kwargs.get("response_format")) is not None:
220+
# response_format may be string or object with a string in the `type` key
221+
if isinstance(response_format, Mapping):
222+
if (
223+
response_format_type := response_format.get("type")
224+
) is not None:
225+
attributes[
226+
GenAIAttributes.GEN_AI_OPENAI_REQUEST_RESPONSE_FORMAT
227+
] = response_format_type
228+
else:
214229
attributes[
215230
GenAIAttributes.GEN_AI_OPENAI_REQUEST_RESPONSE_FORMAT
216-
] = response_format_type
217-
else:
218-
attributes[
219-
GenAIAttributes.GEN_AI_OPENAI_REQUEST_RESPONSE_FORMAT
220-
] = response_format
231+
] = response_format
232+
233+
service_tier = kwargs.get("service_tier")
234+
attributes[GenAIAttributes.GEN_AI_OPENAI_RESPONSE_SERVICE_TIER] = (
235+
service_tier if service_tier != "auto" else None
236+
)
237+
238+
# Add embeddings-specific attributes
239+
elif (
240+
operation_name
241+
== GenAIAttributes.GenAiOperationNameValues.EMBEDDINGS.value
242+
):
243+
# Add embedding dimensions if specified
244+
if "dimensions" in kwargs and kwargs["dimensions"] is not None:
245+
attributes["gen_ai.embeddings.dimensions"] = kwargs["dimensions"]
246+
247+
# Add encoding format if specified
248+
if "encoding_format" in kwargs:
249+
attributes["gen_ai.embeddings.encoding_format"] = kwargs[
250+
"encoding_format"
251+
]
221252

222253
set_server_address_and_port(client_instance, attributes)
223-
service_tier = kwargs.get("service_tier")
224-
attributes[GenAIAttributes.GEN_AI_OPENAI_RESPONSE_SERVICE_TIER] = (
225-
service_tier if service_tier != "auto" else None
226-
)
227254

228255
# filter out None values
229256
return {k: v for k, v in attributes.items() if v is not None}

0 commit comments

Comments
 (0)