Skip to content

Commit 5f8e223

Browse files
committed
Add common gen AI utils into opentelemetry-instrumentation
1 parent 5219242 commit 5f8e223

File tree

6 files changed

+153
-35
lines changed

6 files changed

+153
-35
lines changed

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,9 @@
4545
from wrapt import wrap_function_wrapper
4646

4747
from opentelemetry._events import get_event_logger
48+
from opentelemetry.instrumentation.genai_utils import is_content_enabled
4849
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
4950
from opentelemetry.instrumentation.openai_v2.package import _instruments
50-
from opentelemetry.instrumentation.openai_v2.utils import is_content_enabled
5151
from opentelemetry.instrumentation.utils import unwrap
5252
from opentelemetry.semconv.schemas import Schemas
5353
from opentelemetry.trace import get_tracer

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

+8-5
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,18 @@
1818
from openai import Stream
1919

2020
from opentelemetry._events import Event, EventLogger
21+
from opentelemetry.instrumentation.genai_utils import (
22+
get_span_name,
23+
handle_span_exception,
24+
)
2125
from opentelemetry.semconv._incubating.attributes import (
2226
gen_ai_attributes as GenAIAttributes,
2327
)
2428
from opentelemetry.trace import Span, SpanKind, Tracer
2529

2630
from .utils import (
2731
choice_to_event,
28-
get_llm_request_attributes,
29-
handle_span_exception,
32+
get_genai_request_attributes,
3033
is_streaming,
3134
message_to_event,
3235
set_span_attribute,
@@ -39,9 +42,9 @@ def chat_completions_create(
3942
"""Wrap the `create` method of the `ChatCompletion` class to trace it."""
4043

4144
def traced_method(wrapped, instance, args, kwargs):
42-
span_attributes = {**get_llm_request_attributes(kwargs, instance)}
45+
span_attributes = {**get_genai_request_attributes(kwargs, instance)}
4346

44-
span_name = f"{span_attributes[GenAIAttributes.GEN_AI_OPERATION_NAME]} {span_attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL]}"
47+
span_name = get_span_name(span_attributes)
4548
with tracer.start_as_current_span(
4649
name=span_name,
4750
kind=SpanKind.CLIENT,
@@ -81,7 +84,7 @@ def async_chat_completions_create(
8184
"""Wrap the `create` method of the `AsyncChatCompletion` class to trace it."""
8285

8386
async def traced_method(wrapped, instance, args, kwargs):
84-
span_attributes = {**get_llm_request_attributes(kwargs, instance)}
87+
span_attributes = {**get_genai_request_attributes(kwargs, instance)}
8588

8689
span_name = f"{span_attributes[GenAIAttributes.GEN_AI_OPERATION_NAME]} {span_attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL]}"
8790
with tracer.start_as_current_span(

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

+1-27
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
from os import environ
1615
from typing import Mapping, Optional, Union
1716
from urllib.parse import urlparse
1817

@@ -26,22 +25,6 @@
2625
from opentelemetry.semconv._incubating.attributes import (
2726
server_attributes as ServerAttributes,
2827
)
29-
from opentelemetry.semconv.attributes import (
30-
error_attributes as ErrorAttributes,
31-
)
32-
from opentelemetry.trace.status import Status, StatusCode
33-
34-
OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT = (
35-
"OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"
36-
)
37-
38-
39-
def is_content_enabled() -> bool:
40-
capture_content = environ.get(
41-
OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, "false"
42-
)
43-
44-
return capture_content.lower() == "true"
4528

4629

4730
def extract_tool_calls(item, capture_content):
@@ -183,7 +166,7 @@ def non_numerical_value_is_set(value: Optional[Union[bool, str]]):
183166
return bool(value) and value != NOT_GIVEN
184167

185168

186-
def get_llm_request_attributes(
169+
def get_genai_request_attributes(
187170
kwargs,
188171
client_instance,
189172
operation_name=GenAIAttributes.GenAiOperationNameValues.CHAT.value,
@@ -227,12 +210,3 @@ def get_llm_request_attributes(
227210

228211
# filter out None values
229212
return {k: v for k, v in attributes.items() if v is not None}
230-
231-
232-
def handle_span_exception(span, error):
233-
span.set_status(Status(StatusCode.ERROR, str(error)))
234-
if span.is_recording():
235-
span.set_attribute(
236-
ErrorAttributes.ERROR_TYPE, type(error).__qualname__
237-
)
238-
span.end()

Diff for: instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/conftest.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@
77
import yaml
88
from openai import AsyncOpenAI, OpenAI
99

10-
from opentelemetry.instrumentation.openai_v2 import OpenAIInstrumentor
11-
from opentelemetry.instrumentation.openai_v2.utils import (
10+
from opentelemetry.instrumentation.genai_utils import (
1211
OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT,
1312
)
13+
from opentelemetry.instrumentation.openai_v2 import OpenAIInstrumentor
1414
from opentelemetry.sdk._events import EventLoggerProvider
1515
from opentelemetry.sdk._logs import LoggerProvider
1616
from opentelemetry.sdk._logs.export import (
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from os import environ
16+
17+
from opentelemetry.semconv._incubating.attributes import (
18+
gen_ai_attributes as GenAIAttributes,
19+
)
20+
from opentelemetry.semconv.attributes import (
21+
error_attributes as ErrorAttributes,
22+
)
23+
from opentelemetry.trace.status import Status, StatusCode
24+
25+
OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT = (
26+
"OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"
27+
)
28+
29+
30+
def is_content_enabled() -> bool:
31+
capture_content = environ.get(
32+
OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, "false"
33+
)
34+
35+
return capture_content.lower() == "true"
36+
37+
38+
def get_span_name(span_attributes):
39+
name = span_attributes.get(GenAIAttributes.GEN_AI_OPERATION_NAME, "")
40+
model = span_attributes.get(GenAIAttributes.GEN_AI_REQUEST_MODEL, "")
41+
return f"{name} {model}"
42+
43+
44+
def handle_span_exception(span, error):
45+
span.set_status(Status(StatusCode.ERROR, str(error)))
46+
if span.is_recording():
47+
span.set_attribute(
48+
ErrorAttributes.ERROR_TYPE, type(error).__qualname__
49+
)
50+
span.end()
+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from unittest.mock import patch
16+
17+
from opentelemetry.instrumentation.genai_utils import (
18+
OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT,
19+
get_span_name,
20+
handle_span_exception,
21+
is_content_enabled,
22+
)
23+
from opentelemetry.sdk.trace import ReadableSpan
24+
from opentelemetry.test.test_base import TestBase
25+
from opentelemetry.trace.status import StatusCode
26+
27+
28+
class MyTestException(Exception):
29+
pass
30+
31+
32+
class TestGenaiUtils(TestBase):
33+
@patch.dict(
34+
"os.environ",
35+
{},
36+
)
37+
def test_is_content_enabled_default(self):
38+
self.assertFalse(is_content_enabled())
39+
40+
def test_is_content_enabled_true(self):
41+
for env_value in "true", "TRUE", "True", "tRue":
42+
with patch.dict(
43+
"os.environ",
44+
{
45+
OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: env_value
46+
},
47+
):
48+
self.assertTrue(is_content_enabled())
49+
50+
def test_is_content_enabled_false(self):
51+
for env_value in "false", "FALSE", "False", "fAlse":
52+
with patch.dict(
53+
"os.environ",
54+
{
55+
OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: env_value
56+
},
57+
):
58+
self.assertFalse(is_content_enabled())
59+
60+
def test_get_span_name(self):
61+
span_attributes = {
62+
"gen_ai.operation.name": "chat",
63+
"gen_ai.request.model": "mymodel",
64+
}
65+
self.assertEqual(get_span_name(span_attributes), "chat mymodel")
66+
67+
span_attributes = {
68+
"gen_ai.operation.name": "chat",
69+
}
70+
self.assertEqual(get_span_name(span_attributes), "chat ")
71+
72+
span_attributes = {
73+
"gen_ai.request.model": "mymodel",
74+
}
75+
self.assertEqual(get_span_name(span_attributes), " mymodel")
76+
77+
span_attributes = {}
78+
self.assertEqual(get_span_name(span_attributes), " ")
79+
80+
def test_handle_span_exception(self):
81+
tracer = self.tracer_provider.get_tracer("test_handle_span_exception")
82+
with tracer.start_as_current_span("foo") as span:
83+
handle_span_exception(span, MyTestException())
84+
85+
self.assertEqual(len(self.get_finished_spans()), 1)
86+
finished_span: ReadableSpan = self.get_finished_spans()[0]
87+
self.assertEqual(finished_span.name, "foo")
88+
self.assertIs(finished_span.status.status_code, StatusCode.ERROR)
89+
self.assertEqual(
90+
finished_span.attributes["error.type"], "MyTestException"
91+
)

0 commit comments

Comments
 (0)