Skip to content

Commit d04edad

Browse files
committed
Refactoring Openai instrumentation
1 parent e601f6d commit d04edad

File tree

7 files changed

+195
-252
lines changed

7 files changed

+195
-252
lines changed

Diff for: instrumentation/opentelemetry-instrumentation-openai/pyproject.toml

+2-2
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ classifiers = [
2626
]
2727
dependencies = [
2828
"opentelemetry-api ~= 1.12",
29-
"opentelemetry-instrumentation == 0.47b0",
30-
"tiktoken>=0.1.1",
29+
"opentelemetry-instrumentation == 0.48b0.dev",
30+
"opentelemetry-semantic-conventions == 0.48b0.dev",
3131
"pydantic>=1.8"
3232

3333
]

Diff for: instrumentation/opentelemetry-instrumentation-openai/src/opentelemetry/instrumentation/openai/__init__.py

+5-8
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
4747
from opentelemetry.instrumentation.openai.package import _instruments
4848
from opentelemetry.trace import get_tracer
49-
from wrapt import wrap_function_wrapper
49+
from wrapt import wrap_function_wrapper as _W
5050
from .patch import chat_completions_create
5151

5252

@@ -59,13 +59,10 @@ def _instrument(self, **kwargs):
5959
"""Enable OpenAI instrumentation."""
6060
tracer_provider = kwargs.get("tracer_provider")
6161
tracer = get_tracer(__name__, "", tracer_provider)
62-
version = importlib.metadata.version("openai")
63-
wrap_function_wrapper(
64-
"openai.resources.chat.completions",
65-
"Completions.create",
66-
chat_completions_create(
67-
"openai.chat.completions.create", version, tracer
68-
),
62+
_W(
63+
module="openai.resources.chat.completions",
64+
name="Completions.create",
65+
wrapper=chat_completions_create(tracer),
6966
)
7067

7168
def _uninstrument(self, **kwargs):

Diff for: instrumentation/opentelemetry-instrumentation-openai/src/opentelemetry/instrumentation/openai/patch.py

+54-181
Original file line numberDiff line numberDiff line change
@@ -12,54 +12,38 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
import json
16-
from typing import Optional, Union
15+
1716
from opentelemetry import trace
1817
from opentelemetry.trace import SpanKind, Span
1918
from opentelemetry.trace.status import Status, StatusCode
2019
from opentelemetry.trace.propagation import set_span_in_context
21-
from openai import NOT_GIVEN
22-
from .span_attributes import LLMSpanAttributes, SpanAttributes
2320

24-
from .utils import silently_fail, extract_content
21+
from .span_attributes import LLMSpanAttributes, SpanAttributes
22+
from opentelemetry.semconv._incubating.attributes import (
23+
gen_ai_attributes as GenAIAttributes,
24+
)
25+
from .utils import (
26+
silently_fail,
27+
extract_content,
28+
get_llm_request_attributes,
29+
is_streaming,
30+
set_span_attribute,
31+
set_event_completion,
32+
extract_tools_prompt,
33+
)
2534
from opentelemetry.trace import Tracer
2635

2736

28-
def chat_completions_create(original_method, version, tracer: Tracer):
37+
def chat_completions_create(tracer: Tracer):
2938
"""Wrap the `create` method of the `ChatCompletion` class to trace it."""
3039

3140
def traced_method(wrapped, instance, args, kwargs):
41+
3242
llm_prompts = []
43+
3344
for item in kwargs.get("messages", []):
34-
tools = get_tool_calls(item)
35-
if tools is not None:
36-
tool_calls = []
37-
for tool_call in tools:
38-
tool_call_dict = {
39-
"id": tool_call.id if hasattr(tool_call, "id") else "",
40-
"type": (
41-
tool_call.type
42-
if hasattr(tool_call, "type")
43-
else ""
44-
),
45-
}
46-
if hasattr(tool_call, "function"):
47-
tool_call_dict["function"] = {
48-
"name": (
49-
tool_call.function.name
50-
if hasattr(tool_call.function, "name")
51-
else ""
52-
),
53-
"arguments": (
54-
tool_call.function.arguments
55-
if hasattr(tool_call.function, "arguments")
56-
else ""
57-
),
58-
}
59-
tool_calls.append(tool_call_dict)
60-
llm_prompts.append(tool_calls)
61-
else:
62-
llm_prompts.append(item)
45+
tools_prompt = extract_tools_prompt(item)
46+
llm_prompts.append(tools_prompt if tools_prompt else item)
6347

6448
span_attributes = {
6549
**get_llm_request_attributes(kwargs, prompts=llm_prompts),
@@ -74,7 +58,7 @@ def traced_method(wrapped, instance, args, kwargs):
7458
kind=SpanKind.CLIENT,
7559
context=set_span_in_context(trace.get_current_span()),
7660
)
77-
_set_input_attributes(span, kwargs, attributes)
61+
_set_input_attributes(span, attributes)
7862

7963
try:
8064
result = wrapped(*args, **kwargs)
@@ -86,52 +70,31 @@ def traced_method(wrapped, instance, args, kwargs):
8670
tool_calls=kwargs.get("tools") is not None,
8771
)
8872
else:
89-
_set_response_attributes(span, kwargs, result)
73+
_set_response_attributes(span, result)
9074
span.end()
9175
return result
9276

9377
except Exception as error:
9478
span.set_status(Status(StatusCode.ERROR, str(error)))
79+
span.set_attribute("error.type", error.__class__.__name__)
9580
span.end()
9681
raise
9782

9883
return traced_method
9984

10085

101-
def get_tool_calls(item):
102-
if isinstance(item, dict):
103-
return item.get("tool_calls")
104-
else:
105-
return getattr(item, "tool_calls", None)
106-
107-
10886
@silently_fail
109-
def _set_input_attributes(span, kwargs, attributes: LLMSpanAttributes):
110-
tools = []
111-
112-
if (
113-
kwargs.get("functions") is not None
114-
and kwargs.get("functions") != NOT_GIVEN
115-
):
116-
for function in kwargs.get("functions"):
117-
tools.append(
118-
json.dumps({"type": "function", "function": function})
119-
)
120-
121-
if kwargs.get("tools") is not None and kwargs.get("tools") != NOT_GIVEN:
122-
tools.append(json.dumps(kwargs.get("tools")))
123-
124-
if tools:
125-
set_span_attribute(span, SpanAttributes.LLM_TOOLS, json.dumps(tools))
126-
87+
def _set_input_attributes(span, attributes: LLMSpanAttributes):
12788
for field, value in attributes.model_dump(by_alias=True).items():
12889
set_span_attribute(span, field, value)
12990

13091

13192
@silently_fail
132-
def _set_response_attributes(span, kwargs, result):
133-
set_span_attribute(span, SpanAttributes.LLM_RESPONSE_MODEL, result.model)
134-
if hasattr(result, "choices") and result.choices is not None:
93+
def _set_response_attributes(span, result):
94+
set_span_attribute(
95+
span, GenAIAttributes.GEN_AI_RESPONSE_MODEL, result.model
96+
)
97+
if getattr(result, "choices", None):
13598
responses = [
13699
{
137100
"role": (
@@ -154,120 +117,30 @@ def _set_response_attributes(span, kwargs, result):
154117
]
155118
set_event_completion(span, responses)
156119

157-
if (
158-
hasattr(result, "system_fingerprint")
159-
and result.system_fingerprint is not None
160-
and result.system_fingerprint != NOT_GIVEN
161-
):
120+
if getattr(result, "system_fingerprint", None):
162121
set_span_attribute(
163122
span,
164123
SpanAttributes.LLM_SYSTEM_FINGERPRINT,
165124
result.system_fingerprint,
166125
)
167-
# Get the usage
168-
if hasattr(result, "usage") and result.usage is not None:
169-
usage = result.usage
170-
if usage is not None:
171-
set_span_attribute(
172-
span,
173-
SpanAttributes.LLM_USAGE_PROMPT_TOKENS,
174-
result.usage.prompt_tokens,
175-
)
176-
set_span_attribute(
177-
span,
178-
SpanAttributes.LLM_USAGE_COMPLETION_TOKENS,
179-
result.usage.completion_tokens,
180-
)
181-
set_span_attribute(
182-
span,
183-
SpanAttributes.LLM_USAGE_TOTAL_TOKENS,
184-
result.usage.total_tokens,
185-
)
186-
187-
188-
def set_event_prompt(span: Span, prompt):
189-
span.add_event(
190-
name=SpanAttributes.LLM_CONTENT_PROMPT,
191-
attributes={
192-
SpanAttributes.LLM_PROMPTS: prompt,
193-
},
194-
)
195126

196-
197-
def set_span_attributes(span: Span, attributes: dict):
198-
for field, value in attributes.model_dump(by_alias=True).items():
199-
set_span_attribute(span, field, value)
200-
201-
202-
def set_event_completion(span: Span, result_content):
203-
span.add_event(
204-
name=SpanAttributes.LLM_CONTENT_COMPLETION,
205-
attributes={
206-
SpanAttributes.LLM_COMPLETIONS: json.dumps(result_content),
207-
},
208-
)
209-
210-
211-
def set_span_attribute(span: Span, name, value):
212-
if value is not None:
213-
if value != "" or value != NOT_GIVEN:
214-
if name == SpanAttributes.LLM_PROMPTS:
215-
set_event_prompt(span, value)
216-
else:
217-
span.set_attribute(name, value)
218-
return
219-
220-
221-
def is_streaming(kwargs):
222-
return non_numerical_value_is_set(kwargs.get("stream"))
223-
224-
225-
def non_numerical_value_is_set(value: Optional[Union[bool, str]]):
226-
return bool(value) and value != NOT_GIVEN
227-
228-
229-
def get_llm_request_attributes(
230-
kwargs, prompts=None, model=None, operation_name="chat"
231-
):
232-
233-
user = kwargs.get("user")
234-
if prompts is None:
235-
prompts = (
236-
[{"role": user or "user", "content": kwargs.get("prompt")}]
237-
if "prompt" in kwargs
238-
else None
127+
# Get the usage
128+
if getattr(result, "usage", None):
129+
set_span_attribute(
130+
span,
131+
GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS,
132+
result.usage.prompt_tokens,
133+
)
134+
set_span_attribute(
135+
span,
136+
GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS,
137+
result.usage.completion_tokens,
138+
)
139+
set_span_attribute(
140+
span,
141+
"gen_ai.usage.total_tokens",
142+
result.usage.total_tokens,
239143
)
240-
top_k = (
241-
kwargs.get("n")
242-
or kwargs.get("k")
243-
or kwargs.get("top_k")
244-
or kwargs.get("top_n")
245-
)
246-
247-
top_p = kwargs.get("p") or kwargs.get("top_p")
248-
tools = kwargs.get("tools")
249-
return {
250-
SpanAttributes.LLM_OPERATION_NAME: operation_name,
251-
SpanAttributes.LLM_REQUEST_MODEL: model or kwargs.get("model"),
252-
SpanAttributes.LLM_IS_STREAMING: kwargs.get("stream"),
253-
SpanAttributes.LLM_REQUEST_TEMPERATURE: kwargs.get("temperature"),
254-
SpanAttributes.LLM_TOP_K: top_k,
255-
SpanAttributes.LLM_PROMPTS: json.dumps(prompts) if prompts else None,
256-
SpanAttributes.LLM_USER: user,
257-
SpanAttributes.LLM_REQUEST_TOP_P: top_p,
258-
SpanAttributes.LLM_REQUEST_MAX_TOKENS: kwargs.get("max_tokens"),
259-
SpanAttributes.LLM_SYSTEM_FINGERPRINT: kwargs.get(
260-
"system_fingerprint"
261-
),
262-
SpanAttributes.LLM_PRESENCE_PENALTY: kwargs.get("presence_penalty"),
263-
SpanAttributes.LLM_FREQUENCY_PENALTY: kwargs.get("frequency_penalty"),
264-
SpanAttributes.LLM_REQUEST_SEED: kwargs.get("seed"),
265-
SpanAttributes.LLM_TOOLS: json.dumps(tools) if tools else None,
266-
SpanAttributes.LLM_TOOL_CHOICE: kwargs.get("tool_choice"),
267-
SpanAttributes.LLM_REQUEST_LOGPROPS: kwargs.get("logprobs"),
268-
SpanAttributes.LLM_REQUEST_LOGITBIAS: kwargs.get("logit_bias"),
269-
SpanAttributes.LLM_REQUEST_TOP_LOGPROPS: kwargs.get("top_logprobs"),
270-
}
271144

272145

273146
class StreamWrapper:
@@ -277,7 +150,7 @@ def __init__(
277150
self,
278151
stream,
279152
span,
280-
prompt_tokens=None,
153+
prompt_tokens=0,
281154
function_call=False,
282155
tool_calls=False,
283156
):
@@ -299,17 +172,17 @@ def cleanup(self):
299172
if self._span_started:
300173
set_span_attribute(
301174
self.span,
302-
SpanAttributes.LLM_USAGE_PROMPT_TOKENS,
175+
GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS,
303176
self.prompt_tokens,
304177
)
305178
set_span_attribute(
306179
self.span,
307-
SpanAttributes.LLM_USAGE_COMPLETION_TOKENS,
180+
GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS,
308181
self.completion_tokens,
309182
)
310183
set_span_attribute(
311184
self.span,
312-
SpanAttributes.LLM_USAGE_TOTAL_TOKENS,
185+
"gen_ai.usage.total_tokens",
313186
self.prompt_tokens + self.completion_tokens,
314187
)
315188
set_event_completion(
@@ -346,14 +219,14 @@ def __next__(self):
346219
raise
347220

348221
def process_chunk(self, chunk):
349-
if hasattr(chunk, "model") and chunk.model is not None:
222+
if getattr(chunk, "model", None):
350223
set_span_attribute(
351224
self.span,
352-
SpanAttributes.LLM_RESPONSE_MODEL,
225+
GenAIAttributes.GEN_AI_RESPONSE_MODEL,
353226
chunk.model,
354227
)
355228

356-
if hasattr(chunk, "choices") and chunk.choices is not None:
229+
if getattr(chunk, "choices", None):
357230
content = []
358231
if not self.function_call and not self.tool_calls:
359232
for choice in chunk.choices:
@@ -383,12 +256,12 @@ def process_chunk(self, chunk):
383256
if content:
384257
self.result_content.append(content[0])
385258

386-
if hasattr(chunk, "text"):
259+
if getattr(chunk, "text", None):
387260
content = [chunk.text]
388261

389262
if content:
390263
self.result_content.append(content[0])
391264

392-
if getattr(chunk, "usage"):
265+
if getattr(chunk, "usage", None):
393266
self.completion_tokens = chunk.usage.completion_tokens
394267
self.prompt_tokens = chunk.usage.prompt_tokens

0 commit comments

Comments
 (0)