Skip to content

Commit

Permalink
Add common gen AI utils into opentelemetry-instrumentation
Browse files Browse the repository at this point in the history
  • Loading branch information
aabmass committed Jan 15, 2025
1 parent 07c97ea commit c325839
Show file tree
Hide file tree
Showing 6 changed files with 153 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@
from wrapt import wrap_function_wrapper

from opentelemetry._events import get_event_logger
from opentelemetry.instrumentation.genai_utils import is_content_enabled
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
from opentelemetry.instrumentation.openai_v2.package import _instruments
from opentelemetry.instrumentation.openai_v2.utils import is_content_enabled
from opentelemetry.instrumentation.utils import unwrap
from opentelemetry.semconv.schemas import Schemas
from opentelemetry.trace import get_tracer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,18 @@
from openai import Stream

from opentelemetry._events import Event, EventLogger
from opentelemetry.instrumentation.genai_utils import (
get_span_name,
handle_span_exception,
)
from opentelemetry.semconv._incubating.attributes import (
gen_ai_attributes as GenAIAttributes,
)
from opentelemetry.trace import Span, SpanKind, Tracer

from .utils import (
choice_to_event,
get_llm_request_attributes,
handle_span_exception,
get_genai_request_attributes,
is_streaming,
message_to_event,
set_span_attribute,
Expand All @@ -39,9 +42,9 @@ def chat_completions_create(
"""Wrap the `create` method of the `ChatCompletion` class to trace it."""

def traced_method(wrapped, instance, args, kwargs):
span_attributes = {**get_llm_request_attributes(kwargs, instance)}
span_attributes = {**get_genai_request_attributes(kwargs, instance)}

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

async def traced_method(wrapped, instance, args, kwargs):
span_attributes = {**get_llm_request_attributes(kwargs, instance)}
span_attributes = {**get_genai_request_attributes(kwargs, instance)}

span_name = f"{span_attributes[GenAIAttributes.GEN_AI_OPERATION_NAME]} {span_attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL]}"
with tracer.start_as_current_span(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from os import environ
from typing import Mapping, Optional, Union
from urllib.parse import urlparse

Expand All @@ -26,22 +25,6 @@
from opentelemetry.semconv._incubating.attributes import (
server_attributes as ServerAttributes,
)
from opentelemetry.semconv.attributes import (
error_attributes as ErrorAttributes,
)
from opentelemetry.trace.status import Status, StatusCode

OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT = (
"OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"
)


def is_content_enabled() -> bool:
capture_content = environ.get(
OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, "false"
)

return capture_content.lower() == "true"


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


def get_llm_request_attributes(
def get_genai_request_attributes(
kwargs,
client_instance,
operation_name=GenAIAttributes.GenAiOperationNameValues.CHAT.value,
Expand Down Expand Up @@ -227,12 +210,3 @@ def get_llm_request_attributes(

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


def handle_span_exception(span, error):
span.set_status(Status(StatusCode.ERROR, str(error)))
if span.is_recording():
span.set_attribute(
ErrorAttributes.ERROR_TYPE, type(error).__qualname__
)
span.end()
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
import yaml
from openai import AsyncOpenAI, OpenAI

from opentelemetry.instrumentation.openai_v2 import OpenAIInstrumentor
from opentelemetry.instrumentation.openai_v2.utils import (
from opentelemetry.instrumentation.genai_utils import (
OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT,
)
from opentelemetry.instrumentation.openai_v2 import OpenAIInstrumentor
from opentelemetry.sdk._events import EventLoggerProvider
from opentelemetry.sdk._logs import LoggerProvider
from opentelemetry.sdk._logs.export import (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from os import environ

from opentelemetry.semconv._incubating.attributes import (
gen_ai_attributes as GenAIAttributes,
)
from opentelemetry.semconv.attributes import (
error_attributes as ErrorAttributes,
)
from opentelemetry.trace.status import Status, StatusCode

OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT = (
"OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"
)


def is_content_enabled() -> bool:
capture_content = environ.get(
OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, "false"
)

return capture_content.lower() == "true"


def get_span_name(span_attributes):
name = span_attributes.get(GenAIAttributes.GEN_AI_OPERATION_NAME, "")
model = span_attributes.get(GenAIAttributes.GEN_AI_REQUEST_MODEL, "")
return f"{name} {model}"


def handle_span_exception(span, error):
span.set_status(Status(StatusCode.ERROR, str(error)))
if span.is_recording():
span.set_attribute(
ErrorAttributes.ERROR_TYPE, type(error).__qualname__
)
span.end()
91 changes: 91 additions & 0 deletions opentelemetry-instrumentation/tests/test_genai_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from unittest.mock import patch

from opentelemetry.instrumentation.genai_utils import (
OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT,
get_span_name,
handle_span_exception,
is_content_enabled,
)
from opentelemetry.sdk.trace import ReadableSpan
from opentelemetry.test.test_base import TestBase
from opentelemetry.trace.status import StatusCode


class MyTestException(Exception):
pass


class TestGenaiUtils(TestBase):
@patch.dict(
"os.environ",
{},
)
def test_is_content_enabled_default(self):
self.assertFalse(is_content_enabled())

def test_is_content_enabled_true(self):
for env_value in "true", "TRUE", "True", "tRue":
with patch.dict(
"os.environ",
{
OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: env_value
},
):
self.assertTrue(is_content_enabled())

def test_is_content_enabled_false(self):
for env_value in "false", "FALSE", "False", "fAlse":
with patch.dict(
"os.environ",
{
OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: env_value
},
):
self.assertFalse(is_content_enabled())

def test_get_span_name(self):
span_attributes = {
"gen_ai.operation.name": "chat",
"gen_ai.request.model": "mymodel",
}
self.assertEqual(get_span_name(span_attributes), "chat mymodel")

span_attributes = {
"gen_ai.operation.name": "chat",
}
self.assertEqual(get_span_name(span_attributes), "chat ")

span_attributes = {
"gen_ai.request.model": "mymodel",
}
self.assertEqual(get_span_name(span_attributes), " mymodel")

span_attributes = {}
self.assertEqual(get_span_name(span_attributes), " ")

def test_handle_span_exception(self):
tracer = self.tracer_provider.get_tracer("test_handle_span_exception")
with tracer.start_as_current_span("foo") as span:
handle_span_exception(span, MyTestException())

self.assertEqual(len(self.get_finished_spans()), 1)
finished_span: ReadableSpan = self.get_finished_spans()[0]
self.assertEqual(finished_span.name, "foo")
self.assertIs(finished_span.status.status_code, StatusCode.ERROR)
self.assertEqual(
finished_span.attributes["error.type"], "MyTestException"
)

0 comments on commit c325839

Please sign in to comment.