diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8adab9a..968251d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,7 +38,6 @@ jobs: test: runs-on: ubuntu-latest env: - py38: "3.8" py39: "3.9" py310: "3.10" py311: "3.11" @@ -51,7 +50,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: [py38, py39, py310, py311, py312, py313] + python-version: [py39, py310, py311, py312, py313] openai-version: [baseline, latest] steps: - uses: actions/checkout@v5 @@ -60,10 +59,10 @@ jobs: with: python-version: ${{ env[matrix.python-version] }} architecture: "x64" - - if: ${{ env[matrix.python-version] == '3.8' || env[matrix.python-version] == '3.9' }} + - if: ${{ env[matrix.python-version] == '3.9' }} run: pip install -r dev-requirements-3.9.txt working-directory: ${{ env.working_dir }} - - if: ${{ env[matrix.python-version] != '3.8' && env[matrix.python-version] != '3.9' }} + - if: ${{ env[matrix.python-version] != '3.9' }} run: pip install -r dev-requirements.txt working-directory: ${{ env.working_dir }} - if: ${{ env[matrix.openai-version] }} diff --git a/instrumentation/elastic-opentelemetry-instrumentation-openai/dev-requirements-3.9.txt b/instrumentation/elastic-opentelemetry-instrumentation-openai/dev-requirements-3.9.txt index 0ca02dd..e6bd33c 100644 --- a/instrumentation/elastic-opentelemetry-instrumentation-openai/dev-requirements-3.9.txt +++ b/instrumentation/elastic-opentelemetry-instrumentation-openai/dev-requirements-3.9.txt @@ -1,38 +1,36 @@ # -# This file is autogenerated by pip-compile with Python 3.8 +# This file is autogenerated by pip-compile with Python 3.9 # by the following command: # # pip-compile --extra=dev --output-file=dev-requirements-3.9.txt --strip-extras pyproject.toml # annotated-types==0.7.0 # via pydantic -anyio==4.5.2 +anyio==4.10.0 # via # httpx # openai -asgiref==3.8.1 +asgiref==3.9.1 # via opentelemetry-test-utils -build==1.2.2.post1 +backports-asyncio-runner==1.2.0 + # via pytest-asyncio +build==1.3.0 # via pip-tools -certifi==2025.1.31 +certifi==2025.8.3 # via # httpcore # httpx click==8.1.8 # via pip-tools -deprecated==1.2.18 - # via - # opentelemetry-api - # opentelemetry-semantic-conventions distro==1.9.0 # via openai -exceptiongroup==1.2.2 +exceptiongroup==1.3.0 # via # anyio # pytest -h11==0.14.0 +h11==0.16.0 # via httpcore -httpcore==1.0.7 +httpcore==1.0.9 # via httpx httpx==0.27.2 # via openai @@ -41,65 +39,67 @@ idna==3.10 # anyio # httpx # yarl -importlib-metadata==8.5.0 +importlib-metadata==8.7.0 # via # build # opentelemetry-api -iniconfig==2.0.0 +iniconfig==2.1.0 # via pytest -jiter==0.9.0 +jiter==0.11.0 # via openai -multidict==6.1.0 +multidict==6.6.4 # via yarl -numpy==1.24.4 +numpy==2.0.2 # via elastic-opentelemetry-instrumentation-openai (pyproject.toml) -openai==1.66.5 +openai==1.108.0 # via elastic-opentelemetry-instrumentation-openai (pyproject.toml) -opentelemetry-api==1.31.0 +opentelemetry-api==1.37.0 # via # elastic-opentelemetry-instrumentation-openai (pyproject.toml) # opentelemetry-instrumentation # opentelemetry-sdk # opentelemetry-semantic-conventions # opentelemetry-test-utils -opentelemetry-instrumentation==0.52b0 +opentelemetry-instrumentation==0.58b0 # via elastic-opentelemetry-instrumentation-openai (pyproject.toml) -opentelemetry-sdk==1.31.0 +opentelemetry-sdk==1.37.0 # via opentelemetry-test-utils -opentelemetry-semantic-conventions==0.52b0 +opentelemetry-semantic-conventions==0.58b0 # via # elastic-opentelemetry-instrumentation-openai (pyproject.toml) # opentelemetry-instrumentation # opentelemetry-sdk -opentelemetry-test-utils==0.52b0 +opentelemetry-test-utils==0.58b0 # via elastic-opentelemetry-instrumentation-openai (pyproject.toml) -packaging==24.2 +packaging==25.0 # via # build # opentelemetry-instrumentation # pytest -pip-tools==7.4.1 +pip-tools==7.5.0 # via elastic-opentelemetry-instrumentation-openai (pyproject.toml) -pluggy==1.5.0 +pluggy==1.6.0 # via pytest -propcache==0.2.0 +propcache==0.3.2 # via yarl -pydantic==2.10.6 +pydantic==2.11.9 # via # elastic-opentelemetry-instrumentation-openai (pyproject.toml) # openai -pydantic-core==2.27.2 +pydantic-core==2.33.2 # via pydantic +pygments==2.19.2 + # via pytest pyproject-hooks==1.2.0 # via # build # pip-tools -pytest==8.3.5 +pytest==8.4.2 # via # elastic-opentelemetry-instrumentation-openai (pyproject.toml) # pytest-asyncio # pytest-vcr -pytest-asyncio==0.24.0 +pytest-asyncio==1.2.0 # via elastic-opentelemetry-instrumentation-openai (pyproject.toml) pytest-vcr==1.0.2 # via elastic-opentelemetry-instrumentation-openai (pyproject.toml) @@ -116,33 +116,38 @@ tomli==2.2.1 # pytest tqdm==4.67.1 # via openai -typing-extensions==4.12.2 +typing-extensions==4.15.0 # via - # annotated-types # anyio # asgiref + # exceptiongroup # multidict # openai + # opentelemetry-api # opentelemetry-sdk + # opentelemetry-semantic-conventions # pydantic # pydantic-core + # pytest-asyncio + # typing-inspection +typing-inspection==0.4.1 + # via pydantic urllib3==1.26.20 # via vcrpy -vcrpy==6.0.2 +vcrpy==7.0.0 # via # elastic-opentelemetry-instrumentation-openai (pyproject.toml) # pytest-vcr wheel==0.45.1 # via pip-tools -wrapt==1.17.2 +wrapt==1.17.3 # via - # deprecated # elastic-opentelemetry-instrumentation-openai (pyproject.toml) # opentelemetry-instrumentation # vcrpy -yarl==1.15.2 +yarl==1.20.1 # via vcrpy -zipp==3.20.2 +zipp==3.23.0 # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: diff --git a/instrumentation/elastic-opentelemetry-instrumentation-openai/dev-requirements.txt b/instrumentation/elastic-opentelemetry-instrumentation-openai/dev-requirements.txt index 38630c2..d19b4ec 100644 --- a/instrumentation/elastic-opentelemetry-instrumentation-openai/dev-requirements.txt +++ b/instrumentation/elastic-opentelemetry-instrumentation-openai/dev-requirements.txt @@ -6,33 +6,31 @@ # annotated-types==0.7.0 # via pydantic -anyio==4.9.0 +anyio==4.10.0 # via # httpx # openai -asgiref==3.8.1 +asgiref==3.9.1 # via opentelemetry-test-utils -build==1.2.2.post1 +backports-asyncio-runner==1.2.0 ; python_version < '3.11' + # via pytest-asyncio +build==1.3.0 # via pip-tools -certifi==2025.1.31 +certifi==2025.8.3 # via # httpcore # httpx -click==8.1.8 +click==8.3.0 # via pip-tools -deprecated==1.2.18 - # via - # opentelemetry-api - # opentelemetry-semantic-conventions distro==1.9.0 # via openai -exceptiongroup==1.2.2 +exceptiongroup==1.3.0 # via # anyio # pytest -h11==0.14.0 +h11==0.16.0 # via httpcore -httpcore==1.0.7 +httpcore==1.0.9 # via httpx httpx==0.27.2 # via openai @@ -41,63 +39,65 @@ idna==3.10 # anyio # httpx # yarl -importlib-metadata==8.6.1 +importlib-metadata==8.7.0 # via opentelemetry-api -iniconfig==2.0.0 +iniconfig==2.1.0 # via pytest -jiter==0.9.0 +jiter==0.11.0 # via openai -multidict==6.2.0 +multidict==6.6.4 # via yarl -numpy==2.2.4 +numpy==2.2.6 # via elastic-opentelemetry-instrumentation-openai (pyproject.toml) -openai==1.66.5 +openai==1.108.0 # via elastic-opentelemetry-instrumentation-openai (pyproject.toml) -opentelemetry-api==1.31.0 +opentelemetry-api==1.37.0 # via # elastic-opentelemetry-instrumentation-openai (pyproject.toml) # opentelemetry-instrumentation # opentelemetry-sdk # opentelemetry-semantic-conventions # opentelemetry-test-utils -opentelemetry-instrumentation==0.52b0 +opentelemetry-instrumentation==0.58b0 # via elastic-opentelemetry-instrumentation-openai (pyproject.toml) -opentelemetry-sdk==1.31.0 +opentelemetry-sdk==1.37.0 # via opentelemetry-test-utils -opentelemetry-semantic-conventions==0.52b0 +opentelemetry-semantic-conventions==0.58b0 # via # elastic-opentelemetry-instrumentation-openai (pyproject.toml) # opentelemetry-instrumentation # opentelemetry-sdk -opentelemetry-test-utils==0.52b0 +opentelemetry-test-utils==0.58b0 # via elastic-opentelemetry-instrumentation-openai (pyproject.toml) -packaging==24.2 +packaging==25.0 # via # build # opentelemetry-instrumentation # pytest -pip-tools==7.4.1 +pip-tools==7.5.0 # via elastic-opentelemetry-instrumentation-openai (pyproject.toml) -pluggy==1.5.0 +pluggy==1.6.0 # via pytest -propcache==0.3.0 +propcache==0.3.2 # via yarl -pydantic==2.10.6 +pydantic==2.11.9 # via # elastic-opentelemetry-instrumentation-openai (pyproject.toml) # openai -pydantic-core==2.27.2 +pydantic-core==2.33.2 # via pydantic +pygments==2.19.2 + # via pytest pyproject-hooks==1.2.0 # via # build # pip-tools -pytest==8.3.5 +pytest==8.4.2 # via # elastic-opentelemetry-instrumentation-openai (pyproject.toml) # pytest-asyncio # pytest-vcr -pytest-asyncio==0.25.3 +pytest-asyncio==1.2.0 # via elastic-opentelemetry-instrumentation-openai (pyproject.toml) pytest-vcr==1.0.2 # via elastic-opentelemetry-instrumentation-openai (pyproject.toml) @@ -114,16 +114,23 @@ tomli==2.2.1 # pytest tqdm==4.67.1 # via openai -typing-extensions==4.12.2 +typing-extensions==4.15.0 # via # anyio # asgiref + # exceptiongroup # multidict # openai + # opentelemetry-api # opentelemetry-sdk + # opentelemetry-semantic-conventions # pydantic # pydantic-core -urllib3==2.3.0 + # pytest-asyncio + # typing-inspection +typing-inspection==0.4.1 + # via pydantic +urllib3==2.5.0 # via vcrpy vcrpy==7.0.0 # via @@ -131,15 +138,14 @@ vcrpy==7.0.0 # pytest-vcr wheel==0.45.1 # via pip-tools -wrapt==1.17.2 +wrapt==1.17.3 # via - # deprecated # elastic-opentelemetry-instrumentation-openai (pyproject.toml) # opentelemetry-instrumentation # vcrpy -yarl==1.18.3 +yarl==1.20.1 # via vcrpy -zipp==3.21.0 +zipp==3.23.0 # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: diff --git a/instrumentation/elastic-opentelemetry-instrumentation-openai/pyproject.toml b/instrumentation/elastic-opentelemetry-instrumentation-openai/pyproject.toml index 31d7525..bf658a4 100644 --- a/instrumentation/elastic-opentelemetry-instrumentation-openai/pyproject.toml +++ b/instrumentation/elastic-opentelemetry-instrumentation-openai/pyproject.toml @@ -9,7 +9,7 @@ maintainers = [ {name = "Riccardo Magliocchetti", email = "riccardo.magliocchetti@elastic.co"}, ] license = {file = "LICENSE"} -requires-python = ">=3.8" +requires-python = ">=3.9" classifiers = [ "Development Status :: 3 - Alpha", "Intended Audience :: Developers", @@ -18,7 +18,6 @@ classifiers = [ "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", @@ -26,10 +25,10 @@ classifiers = [ "Programming Language :: Python :: 3.13", ] dependencies = [ - # 1.31.0 is required for proper histogram bucket advisory support - "opentelemetry-api ~= 1.31", - "opentelemetry-instrumentation ~= 0.52b0", - "opentelemetry-semantic-conventions ~= 0.52b0", + # 1.37 is required for proper emit of LogRecord + "opentelemetry-api ~= 1.37", + "opentelemetry-instrumentation ~= 0.58b0", + "opentelemetry-semantic-conventions ~= 0.58b0", "wrapt >= 1.0.0, < 2.0.0", ] diff --git a/instrumentation/elastic-opentelemetry-instrumentation-openai/src/opentelemetry/instrumentation/openai/__init__.py b/instrumentation/elastic-opentelemetry-instrumentation-openai/src/opentelemetry/instrumentation/openai/__init__.py index 4020097..c458660 100644 --- a/instrumentation/elastic-opentelemetry-instrumentation-openai/src/opentelemetry/instrumentation/openai/__init__.py +++ b/instrumentation/elastic-opentelemetry-instrumentation-openai/src/opentelemetry/instrumentation/openai/__init__.py @@ -19,7 +19,7 @@ from timeit import default_timer from typing import Collection -from opentelemetry._events import get_event_logger +from opentelemetry._logs import get_logger from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.openai.environment_variables import ( OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, @@ -33,8 +33,8 @@ _is_raw_response, _record_operation_duration_metric, _record_token_usage_metrics, - _send_log_events_from_choices, - _send_log_events_from_messages, + _send_logs_from_choices, + _send_logs_from_messages, _span_name_from_attributes, ) from opentelemetry.instrumentation.openai.metrics import ( @@ -73,7 +73,7 @@ def _instrument(self, **kwargs): **kwargs: Optional arguments ``tracer_provider``: a TracerProvider, defaults to global ``meter_provider``: a MeterProvider, defaults to global - ``event_logger_provider``: a EventLoggerProvider, defaults to global + ``logger_provider``: a LoggerProvider, defaults to global ``capture_message_content``: to enable content capturing, defaults to False """ capture_message_content = "true" if kwargs.get("capture_message_content") else "false" @@ -96,8 +96,8 @@ def _instrument(self, **kwargs): meter_provider, schema_url=Schemas.V1_31_0.value, ) - event_logger_provider = kwargs.get("event_logger_provider") - self.event_logger = get_event_logger(__name__, event_logger_provider) + logger_provider = kwargs.get("logger_provider") + self.logger = get_logger(__name__, logger_provider) self.token_usage_metric = self.meter.create_histogram( name=GEN_AI_CLIENT_TOKEN_USAGE, @@ -117,7 +117,7 @@ def _instrument(self, **kwargs): def _patch(self, module): version = tuple([int(x) for x in getattr(getattr(module, "version"), "VERSION").split(".")]) - self.beta_chat_available = version >= (1, 40, 0) + self.beta_chat_available = version >= (1, 40, 0) and version < (1, 93, 0) wrap_function_wrapper( "openai.resources.chat.completions", "Completions.create", @@ -178,8 +178,8 @@ def _chat_completion_wrapper(self, wrapped, instance, args, kwargs): end_on_exit=False, ) as span: messages = kwargs.get("messages", []) - _send_log_events_from_messages( - self.event_logger, + _send_logs_from_messages( + self.logger, messages=messages, attributes=event_attributes, capture_message_content=self.capture_message_content, @@ -203,7 +203,7 @@ def _chat_completion_wrapper(self, wrapped, instance, args, kwargs): span_attributes=span_attributes, capture_message_content=self.capture_message_content, event_attributes=event_attributes, - event_logger=self.event_logger, + logger=self.logger, start_time=start_time, token_usage_metric=self.token_usage_metric, operation_duration_metric=self.operation_duration_metric, @@ -226,8 +226,8 @@ def _chat_completion_wrapper(self, wrapped, instance, args, kwargs): _record_token_usage_metrics(self.token_usage_metric, metrics_attributes, result.usage) _record_operation_duration_metric(self.operation_duration_metric, metrics_attributes, start_time) - _send_log_events_from_choices( - self.event_logger, + _send_logs_from_choices( + self.logger, choices=result.choices, attributes=event_attributes, capture_message_content=self.capture_message_content, @@ -252,8 +252,8 @@ async def _async_chat_completion_wrapper(self, wrapped, instance, args, kwargs): end_on_exit=False, ) as span: messages = kwargs.get("messages", []) - _send_log_events_from_messages( - self.event_logger, + _send_logs_from_messages( + self.logger, messages=messages, attributes=event_attributes, capture_message_content=self.capture_message_content, @@ -277,7 +277,7 @@ async def _async_chat_completion_wrapper(self, wrapped, instance, args, kwargs): span_attributes=span_attributes, capture_message_content=self.capture_message_content, event_attributes=event_attributes, - event_logger=self.event_logger, + logger=self.logger, start_time=start_time, token_usage_metric=self.token_usage_metric, operation_duration_metric=self.operation_duration_metric, @@ -300,8 +300,8 @@ async def _async_chat_completion_wrapper(self, wrapped, instance, args, kwargs): _record_token_usage_metrics(self.token_usage_metric, metrics_attributes, result.usage) _record_operation_duration_metric(self.operation_duration_metric, metrics_attributes, start_time) - _send_log_events_from_choices( - self.event_logger, + _send_logs_from_choices( + self.logger, choices=result.choices, attributes=event_attributes, capture_message_content=self.capture_message_content, diff --git a/instrumentation/elastic-opentelemetry-instrumentation-openai/src/opentelemetry/instrumentation/openai/helpers.py b/instrumentation/elastic-opentelemetry-instrumentation-openai/src/opentelemetry/instrumentation/openai/helpers.py index f497481..0764df4 100644 --- a/instrumentation/elastic-opentelemetry-instrumentation-openai/src/opentelemetry/instrumentation/openai/helpers.py +++ b/instrumentation/elastic-opentelemetry-instrumentation-openai/src/opentelemetry/instrumentation/openai/helpers.py @@ -18,7 +18,7 @@ from timeit import default_timer from typing import TYPE_CHECKING, Optional -from opentelemetry._events import Event, EventLogger +from opentelemetry._logs import Logger, LogRecord from opentelemetry.semconv._incubating.attributes.gen_ai_attributes import ( GEN_AI_OPENAI_REQUEST_SERVICE_TIER, GEN_AI_OPENAI_RESPONSE_SERVICE_TIER, @@ -266,7 +266,7 @@ def _key_or_property(obj, name): return getattr(obj, name) -def _serialize_tool_calls_for_event(tool_calls): +def _serialize_tool_calls_for_log(tool_calls): return [ { "id": _key_or_property(tool_call, "id"), @@ -280,9 +280,7 @@ def _serialize_tool_calls_for_event(tool_calls): ] -def _send_log_events_from_messages( - event_logger: EventLogger, messages, attributes: Attributes, capture_message_content: bool -): +def _send_logs_from_messages(logger: Logger, messages, attributes: Attributes, capture_message_content: bool): for message in messages: body = {} if capture_message_content: @@ -292,48 +290,61 @@ def _send_log_events_from_messages( if message["role"] == "system" or message["role"] == "developer": if message["role"] == "developer": body["role"] = message["role"] - event = Event(name=EVENT_GEN_AI_SYSTEM_MESSAGE, body=body, attributes=attributes) - event_logger.emit(event) + # keep compat on the exported attributes with Event + log = LogRecord( + event_name=EVENT_GEN_AI_SYSTEM_MESSAGE, + body=body, + attributes={**attributes, "event.name": EVENT_GEN_AI_SYSTEM_MESSAGE}, + ) + logger.emit(log) elif message["role"] == "user": - event = Event(name=EVENT_GEN_AI_USER_MESSAGE, body=body, attributes=attributes) - event_logger.emit(event) + # keep compat on the exported attributes with Event + log = LogRecord( + event_name=EVENT_GEN_AI_USER_MESSAGE, + body=body, + attributes={**attributes, "event.name": EVENT_GEN_AI_USER_MESSAGE}, + ) + logger.emit(log) elif message["role"] == "assistant": - tool_calls = _serialize_tool_calls_for_event(message.get("tool_calls", [])) + tool_calls = _serialize_tool_calls_for_log(message.get("tool_calls", [])) if tool_calls: body["tool_calls"] = tool_calls - event = Event( - name=EVENT_GEN_AI_ASSISTANT_MESSAGE, + # keep compat on the exported attributes with Event + log = LogRecord( + event_name=EVENT_GEN_AI_ASSISTANT_MESSAGE, body=body, - attributes=attributes, + attributes={**attributes, "event.name": EVENT_GEN_AI_ASSISTANT_MESSAGE}, ) - event_logger.emit(event) + logger.emit(log) elif message["role"] == "tool": body["id"] = message["tool_call_id"] - event = Event( - name=EVENT_GEN_AI_TOOL_MESSAGE, + # keep compat on the exported attributes with Event + log = LogRecord( + event_name=EVENT_GEN_AI_TOOL_MESSAGE, body=body, - attributes=attributes, + attributes={**attributes, "event.name": EVENT_GEN_AI_TOOL_MESSAGE}, ) - event_logger.emit(event) + logger.emit(log) -def _send_log_events_from_choices( - event_logger: EventLogger, choices, attributes: Attributes, capture_message_content: bool -): +def _send_logs_from_choices(logger: Logger, choices, attributes: Attributes, capture_message_content: bool): for choice in choices: - tool_calls = _serialize_tool_calls_for_event(choice.message.tool_calls or []) + tool_calls = _serialize_tool_calls_for_log(choice.message.tool_calls or []) body = {"finish_reason": choice.finish_reason, "index": choice.index, "message": {}} if tool_calls: body["message"]["tool_calls"] = tool_calls if capture_message_content and choice.message.content: body["message"]["content"] = choice.message.content - event = Event(name=EVENT_GEN_AI_CHOICE, body=body, attributes=attributes) - event_logger.emit(event) + # keep compat on the exported attributes with Event + log = LogRecord( + event_name=EVENT_GEN_AI_CHOICE, body=body, attributes={**attributes, "event.name": EVENT_GEN_AI_CHOICE} + ) + logger.emit(log) -def _send_log_events_from_stream_choices( - event_logger: EventLogger, choices, span: Span, attributes: Attributes, capture_message_content: bool +def _send_logs_from_stream_choices( + logger: Logger, choices, span: Span, attributes: Attributes, capture_message_content: bool ): body = {} message = {} @@ -370,15 +381,16 @@ def _send_log_events_from_stream_choices( } # StreamWrapper is consumed after start_as_current_span exits, so capture the current span ctx = span.get_span_context() - event = Event( - name=EVENT_GEN_AI_CHOICE, + # keep compat on the exported attributes with Event + log = LogRecord( + event_name=EVENT_GEN_AI_CHOICE, body=body, - attributes=attributes, + attributes={**attributes, "event.name": EVENT_GEN_AI_CHOICE}, trace_id=ctx.trace_id, span_id=ctx.span_id, trace_flags=ctx.trace_flags, ) - event_logger.emit(event) + logger.emit(log) def _is_raw_response(response): diff --git a/instrumentation/elastic-opentelemetry-instrumentation-openai/src/opentelemetry/instrumentation/openai/wrappers.py b/instrumentation/elastic-opentelemetry-instrumentation-openai/src/opentelemetry/instrumentation/openai/wrappers.py index 22d431d..031beb6 100644 --- a/instrumentation/elastic-opentelemetry-instrumentation-openai/src/opentelemetry/instrumentation/openai/wrappers.py +++ b/instrumentation/elastic-opentelemetry-instrumentation-openai/src/opentelemetry/instrumentation/openai/wrappers.py @@ -16,12 +16,12 @@ import logging -from opentelemetry._events import EventLogger +from opentelemetry._logs import Logger from opentelemetry.instrumentation.openai.helpers import ( _get_attributes_from_response, _record_operation_duration_metric, _record_token_usage_metrics, - _send_log_events_from_stream_choices, + _send_logs_from_stream_choices, ) from opentelemetry.metrics import Histogram from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE @@ -43,7 +43,7 @@ def __init__( span_attributes: Attributes, capture_message_content: bool, event_attributes: Attributes, - event_logger: EventLogger, + logger: Logger, start_time: float, token_usage_metric: Histogram, operation_duration_metric: Histogram, @@ -55,7 +55,7 @@ def __init__( self.span_attributes = span_attributes self.capture_message_content = capture_message_content self.event_attributes = event_attributes - self.event_logger = event_logger + self.logger = logger self.token_usage_metric = token_usage_metric self.operation_duration_metric = operation_duration_metric self.start_time = start_time @@ -92,8 +92,8 @@ def end(self, exc=None): if self.usage: _record_token_usage_metrics(self.token_usage_metric, metrics_attributes, self.usage) - _send_log_events_from_stream_choices( - self.event_logger, + _send_logs_from_stream_choices( + self.logger, choices=self.choices, span=self.span, attributes=self.event_attributes, @@ -161,7 +161,7 @@ def parse(self): span_attributes=self.span_attributes, capture_message_content=self.capture_message_content, event_attributes=self.event_attributes, - event_logger=self.event_logger, + logger=self.logger, start_time=self.start_time, token_usage_metric=self.token_usage_metric, operation_duration_metric=self.operation_duration_metric, diff --git a/instrumentation/elastic-opentelemetry-instrumentation-openai/tests/conftest.py b/instrumentation/elastic-opentelemetry-instrumentation-openai/tests/conftest.py index d8320d3..bffdc8c 100644 --- a/instrumentation/elastic-opentelemetry-instrumentation-openai/tests/conftest.py +++ b/instrumentation/elastic-opentelemetry-instrumentation-openai/tests/conftest.py @@ -23,7 +23,6 @@ import yaml from openai._base_client import BaseClient from opentelemetry import metrics, trace -from opentelemetry._events import set_event_logger_provider from opentelemetry._logs import set_logger_provider from opentelemetry.instrumentation.openai import OpenAIInstrumentor from opentelemetry.instrumentation.openai.metrics import ( @@ -31,7 +30,6 @@ _GEN_AI_CLIENT_TOKEN_USAGE_BUCKETS, ) from opentelemetry.metrics import Histogram -from opentelemetry.sdk._events import EventLoggerProvider from opentelemetry.sdk._logs import LoggerProvider from opentelemetry.sdk._logs.export import ( InMemoryLogExporter, @@ -83,9 +81,6 @@ def logs_exporter(): logger_provider.add_log_record_processor(SimpleLogRecordProcessor(exporter)) - event_logger_provider = EventLoggerProvider(logger_provider=logger_provider) - set_event_logger_provider(event_logger_provider) - return exporter diff --git a/instrumentation/elastic-opentelemetry-instrumentation-openai/tests/test_beta_chat_completions.py b/instrumentation/elastic-opentelemetry-instrumentation-openai/tests/test_beta_chat_completions.py index 6bdbc62..f9991ba 100644 --- a/instrumentation/elastic-opentelemetry-instrumentation-openai/tests/test_beta_chat_completions.py +++ b/instrumentation/elastic-opentelemetry-instrumentation-openai/tests/test_beta_chat_completions.py @@ -24,7 +24,6 @@ import openai import pytest -from opentelemetry._events import Event from opentelemetry._logs import LogRecord from opentelemetry.instrumentation.openai import OpenAIInstrumentor from opentelemetry.semconv._incubating.attributes.gen_ai_attributes import ( @@ -67,9 +66,10 @@ TEST_CHAT_MODEL = "gpt-4o-mini" TEST_CHAT_RESPONSE_MODEL = "gpt-4o-mini-2024-07-18" TEST_CHAT_INPUT = "Answer in up to 3 words: Which ocean contains Bouvet Island?" +HAS_BETA_CHAT_COMPLETIONS = (1, 40, 0) <= OPENAI_VERSION < (1, 93, 0) -@pytest.mark.skipif(OPENAI_VERSION < (1, 40, 0), reason="beta completions added in 1.40.0") +@pytest.mark.skipif(not HAS_BETA_CHAT_COMPLETIONS, reason="beta completions added in 1.40.0, removed in 1.93.0") @pytest.mark.vcr() def test_chat(default_openai_env, trace_exporter, metrics_reader, logs_exporter): client = openai.OpenAI() @@ -134,7 +134,7 @@ def test_chat(default_openai_env, trace_exporter, metrics_reader, logs_exporter) ) -@pytest.mark.skipif(OPENAI_VERSION < (1, 40, 0), reason="beta completions added in 1.40.0") +@pytest.mark.skipif(not HAS_BETA_CHAT_COMPLETIONS, reason="beta completions added in 1.40.0, removed in 1.93.0") @pytest.mark.vcr() def test_chat_with_developer_role_message(default_openai_env, trace_exporter, metrics_reader, logs_exporter): client = openai.OpenAI() @@ -207,7 +207,7 @@ def test_chat_with_developer_role_message(default_openai_env, trace_exporter, me ) -@pytest.mark.skipif(OPENAI_VERSION < (1, 40, 0), reason="beta completions added in 1.40.0") +@pytest.mark.skipif(not HAS_BETA_CHAT_COMPLETIONS, reason="beta completions added in 1.40.0, removed in 1.93.0") @pytest.mark.vcr() def test_chat_all_the_client_options(default_openai_env, trace_exporter, metrics_reader, logs_exporter): client = openai.OpenAI() @@ -296,7 +296,7 @@ def test_chat_all_the_client_options(default_openai_env, trace_exporter, metrics ) -@pytest.mark.skipif(OPENAI_VERSION < (1, 40, 0), reason="beta completions added in 1.40.0") +@pytest.mark.skipif(not HAS_BETA_CHAT_COMPLETIONS, reason="beta completions added in 1.40.0, removed in 1.93.0") @pytest.mark.vcr() def test_chat_multiple_choices_with_capture_message_content( default_openai_env, trace_exporter, metrics_reader, logs_exporter @@ -371,7 +371,7 @@ def test_chat_multiple_choices_with_capture_message_content( ) -@pytest.mark.skipif(OPENAI_VERSION < (1, 40, 0), reason="beta completions added in 1.40.0") +@pytest.mark.skipif(not HAS_BETA_CHAT_COMPLETIONS, reason="beta completions added in 1.40.0, removed in 1.93.0") @pytest.mark.vcr() def test_chat_function_calling_with_tools(default_openai_env, trace_exporter, metrics_reader, logs_exporter): client = openai.OpenAI() @@ -474,7 +474,7 @@ def test_chat_function_calling_with_tools(default_openai_env, trace_exporter, me ) -@pytest.mark.skipif(OPENAI_VERSION < (1, 40, 0), reason="beta completions added in 1.40.0") +@pytest.mark.skipif(not HAS_BETA_CHAT_COMPLETIONS, reason="beta completions added in 1.40.0, removed in 1.93.0") @pytest.mark.vcr() def test_chat_tools_with_capture_message_content(default_openai_env, trace_exporter, logs_exporter, metrics_reader): # Redo the instrumentation dance to be affected by the environment variable @@ -585,7 +585,7 @@ def test_chat_tools_with_capture_message_content(default_openai_env, trace_expor ) -@pytest.mark.skipif(OPENAI_VERSION < (1, 40, 0), reason="beta completions added in 1.40.0") +@pytest.mark.skipif(not HAS_BETA_CHAT_COMPLETIONS, reason="beta completions added in 1.40.0, removed in 1.93.0") @pytest.mark.integration def test_chat_tools_with_capture_message_content_integration(trace_exporter, logs_exporter, metrics_reader): client = get_integration_client() @@ -696,7 +696,7 @@ def test_chat_tools_with_capture_message_content_integration(trace_exporter, log ) -@pytest.mark.skipif(OPENAI_VERSION < (1, 40, 0), reason="beta completions added in 1.40.0") +@pytest.mark.skipif(not HAS_BETA_CHAT_COMPLETIONS, reason="beta completions added in 1.40.0, removed in 1.93.0") def test_chat_connection_error(default_openai_env, trace_exporter, metrics_reader, logs_exporter): client = openai.Client(base_url="http://localhost:9999/v5", api_key="not-read", max_retries=1) messages = [ @@ -747,7 +747,7 @@ def test_chat_connection_error(default_openai_env, trace_exporter, metrics_reade ) -@pytest.mark.skipif(OPENAI_VERSION < (1, 40, 0), reason="beta completions added in 1.40.0") +@pytest.mark.skipif(not HAS_BETA_CHAT_COMPLETIONS, reason="beta completions added in 1.40.0, removed in 1.93.0") @pytest.mark.integration def test_chat_with_capture_message_content_integration(trace_exporter, logs_exporter, metrics_reader): model = os.getenv("TEST_CHAT_MODEL", TEST_CHAT_MODEL) @@ -823,7 +823,7 @@ def test_chat_with_capture_message_content_integration(trace_exporter, logs_expo ) -@pytest.mark.skipif(OPENAI_VERSION < (1, 40, 0), reason="beta completions added in 1.40.0") +@pytest.mark.skipif(not HAS_BETA_CHAT_COMPLETIONS, reason="beta completions added in 1.40.0, removed in 1.93.0") @pytest.mark.vcr() def test_chat_with_capture_message_content(default_openai_env, trace_exporter, logs_exporter, metrics_reader): client = openai.OpenAI() @@ -894,7 +894,7 @@ def test_chat_with_capture_message_content(default_openai_env, trace_exporter, l ) -@pytest.mark.skipif(OPENAI_VERSION < (1, 40, 0), reason="beta completions added in 1.40.0") +@pytest.mark.skipif(not HAS_BETA_CHAT_COMPLETIONS, reason="beta completions added in 1.40.0, removed in 1.93.0") @pytest.mark.vcr() def test_chat_tools_with_followup_and_capture_message_content( default_openai_env, trace_exporter, metrics_reader, logs_exporter @@ -1066,7 +1066,7 @@ def test_chat_tools_with_followup_and_capture_message_content( ) -@pytest.mark.skipif(OPENAI_VERSION < (1, 40, 0), reason="beta completions added in 1.40.0") +@pytest.mark.skipif(not HAS_BETA_CHAT_COMPLETIONS, reason="beta completions added in 1.40.0, removed in 1.93.0") @pytest.mark.asyncio @pytest.mark.vcr() async def test_chat_async(default_openai_env, trace_exporter, metrics_reader, logs_exporter): @@ -1132,7 +1132,7 @@ async def test_chat_async(default_openai_env, trace_exporter, metrics_reader, lo ) -@pytest.mark.skipif(OPENAI_VERSION < (1, 40, 0), reason="beta completions added in 1.40.0") +@pytest.mark.skipif(not HAS_BETA_CHAT_COMPLETIONS, reason="beta completions added in 1.40.0, removed in 1.93.0") @pytest.mark.asyncio @pytest.mark.vcr() async def test_chat_async_with_capture_message_content( @@ -1206,7 +1206,7 @@ async def test_chat_async_with_capture_message_content( ) -@pytest.mark.skipif(OPENAI_VERSION < (1, 40, 0), reason="beta completions added in 1.40.0") +@pytest.mark.skipif(not HAS_BETA_CHAT_COMPLETIONS, reason="beta completions added in 1.40.0, removed in 1.93.0") @pytest.mark.integration @pytest.mark.asyncio async def test_chat_async_with_capture_message_content_integration(trace_exporter, logs_exporter, metrics_reader): @@ -1280,7 +1280,7 @@ async def test_chat_async_with_capture_message_content_integration(trace_exporte ) -@pytest.mark.skipif(OPENAI_VERSION < (1, 40, 0), reason="beta completions added in 1.40.0") +@pytest.mark.skipif(not HAS_BETA_CHAT_COMPLETIONS, reason="beta completions added in 1.40.0, removed in 1.93.0") @pytest.mark.vcr() @pytest.mark.asyncio async def test_chat_async_tools_with_capture_message_content( @@ -1394,7 +1394,7 @@ async def test_chat_async_tools_with_capture_message_content( ) -@pytest.mark.skipif(OPENAI_VERSION < (1, 40, 0), reason="beta completions added in 1.40.0") +@pytest.mark.skipif(not HAS_BETA_CHAT_COMPLETIONS, reason="beta completions added in 1.40.0, removed in 1.93.0") @pytest.mark.vcr() def test_chat_without_model_parameter(default_openai_env, trace_exporter, metrics_reader): client = openai.OpenAI() @@ -1440,7 +1440,7 @@ def test_chat_without_model_parameter(default_openai_env, trace_exporter, metric ) -@pytest.mark.skipif(OPENAI_VERSION < (1, 40, 0), reason="beta completions added in 1.40.0") +@pytest.mark.skipif(not HAS_BETA_CHAT_COMPLETIONS, reason="beta completions added in 1.40.0, removed in 1.93.0") @pytest.mark.vcr() def test_chat_with_model_not_found(default_openai_env, trace_exporter, metrics_reader): client = openai.OpenAI() @@ -1486,7 +1486,7 @@ def test_chat_with_model_not_found(default_openai_env, trace_exporter, metrics_r ) -@pytest.mark.skipif(OPENAI_VERSION < (1, 40, 0), reason="beta completions added in 1.40.0") +@pytest.mark.skipif(not HAS_BETA_CHAT_COMPLETIONS, reason="beta completions added in 1.40.0, removed in 1.93.0") @pytest.mark.vcr() def test_chat_exported_schema_version(default_openai_env, trace_exporter, metrics_reader): client = openai.OpenAI() @@ -1512,7 +1512,7 @@ def test_chat_exported_schema_version(default_openai_env, trace_exporter, metric assert scope_metrics.schema_url == "https://opentelemetry.io/schemas/1.31.0" -@pytest.mark.skipif(OPENAI_VERSION < (1, 40, 0), reason="beta completions added in 1.40.0") +@pytest.mark.skipif(not HAS_BETA_CHAT_COMPLETIONS, reason="beta completions added in 1.40.0, removed in 1.93.0") @pytest.mark.vcr() def test_parse_response_format_json_object_with_capture_message_content( default_openai_env, trace_exporter, metrics_reader, logs_exporter @@ -1589,7 +1589,7 @@ class Reason(BaseModel): reason: str -@pytest.mark.skipif(OPENAI_VERSION < (1, 40, 0), reason="beta completions added in 1.40.0") +@pytest.mark.skipif(not HAS_BETA_CHAT_COMPLETIONS, reason="beta completions added in 1.40.0, removed in 1.93.0") @pytest.mark.vcr() def test_parse_response_format_structured_output_with_capture_message_content( default_openai_env, trace_exporter, metrics_reader, logs_exporter @@ -1686,10 +1686,10 @@ def assert_tool_call_log_record(log_record: LogRecord, expected_tool_calls: List assert_tool_calls(message["tool_calls"], expected_tool_calls) -def assert_tool_call_event(event: Event, expected_tool_calls: List[ToolCall]): - assert event.name == "gen_ai.content.completion" +def assert_tool_call_log(log: LogRecord, expected_tool_calls: List[ToolCall]): + assert log.event_name == "gen_ai.content.completion" # The 'gen_ai.completion' attribute is a JSON string, so parse it first. - gen_ai_completions = json.loads(event.attributes["gen_ai.completion"]) + gen_ai_completions = json.loads(log.attributes["gen_ai.completion"]) gen_ai_completion = gen_ai_completions[0] assert gen_ai_completion["role"] == "assistant" diff --git a/pyproject.toml b/pyproject.toml index eaa3d7e..43243b7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [tool.ruff] -target-version = "py38" +target-version = "py39" line-length = 120 [lint.isort]