From ce819828f4c6f91bdc57dd34a77a3edb224c3461 Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Wed, 24 Sep 2025 19:13:35 +0000 Subject: [PATCH] Rename UploadHook -> CompletionHook --- docs/instrumentation-genai/util.rst | 2 +- util/opentelemetry-util-genai/CHANGELOG.md | 11 +- util/opentelemetry-util-genai/pyproject.toml | 4 +- .../util/genai/_fsspec_upload/__init__.py | 17 +-- .../{fsspec_hook.py => completion_hook.py} | 14 +-- .../{upload_hook.py => completion_hook.py} | 52 ++++----- .../util/genai/environment_variables.py | 6 +- .../tests/test_completion_hook.py | 101 ++++++++++++++++++ .../tests/test_fsspec_upload.py | 52 ++++----- .../tests/test_upload_hook.py | 99 ----------------- 10 files changed, 186 insertions(+), 172 deletions(-) rename util/opentelemetry-util-genai/src/opentelemetry/util/genai/_fsspec_upload/{fsspec_hook.py => completion_hook.py} (94%) rename util/opentelemetry-util-genai/src/opentelemetry/util/genai/{upload_hook.py => completion_hook.py} (65%) create mode 100644 util/opentelemetry-util-genai/tests/test_completion_hook.py delete mode 100644 util/opentelemetry-util-genai/tests/test_upload_hook.py diff --git a/docs/instrumentation-genai/util.rst b/docs/instrumentation-genai/util.rst index 2ea0852e3c..a2b1635099 100644 --- a/docs/instrumentation-genai/util.rst +++ b/docs/instrumentation-genai/util.rst @@ -21,7 +21,7 @@ OpenTelemetry Python - GenAI Util :undoc-members: :show-inheritance: -.. automodule:: opentelemetry.util.genai.upload_hook +.. automodule:: opentelemetry.util.genai.completion_hook :members: :undoc-members: :show-inheritance: diff --git a/util/opentelemetry-util-genai/CHANGELOG.md b/util/opentelemetry-util-genai/CHANGELOG.md index bfd4c4daab..d4100425ff 100644 --- a/util/opentelemetry-util-genai/CHANGELOG.md +++ b/util/opentelemetry-util-genai/CHANGELOG.md @@ -7,12 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased -- Add upload hook to genai utils to implement semconv v1.37. +- Add completion hook to genai utils to implement semconv v1.37. - The hook uses [`fsspec`](https://filesystem-spec.readthedocs.io/en/latest/) to support - various pluggable backends. + Includes a hook implementation using + [`fsspec`](https://filesystem-spec.readthedocs.io/en/latest/) to support uploading to various + pluggable backends. + + ([#3780](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3780)) ([#3752](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3752)) - ([#3759](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3752)) + ([#3759](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3759)) ([#3763](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3763)) - Add a utility to parse the `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` environment variable. Add `gen_ai_latest_experimental` as a new value to the Sem Conv stability flag ([#3716](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3716)). diff --git a/util/opentelemetry-util-genai/pyproject.toml b/util/opentelemetry-util-genai/pyproject.toml index 9e371c1a1d..2c21acd9a7 100644 --- a/util/opentelemetry-util-genai/pyproject.toml +++ b/util/opentelemetry-util-genai/pyproject.toml @@ -30,8 +30,8 @@ dependencies = [ "opentelemetry-api>=1.31.0", ] -[project.entry-points.opentelemetry_genai_upload_hook] -fsspec = "opentelemetry.util.genai._fsspec_upload:fsspec_upload_hook" +[project.entry-points.opentelemetry_genai_completion_hook] +fsspec_upload = "opentelemetry.util.genai._fsspec_upload:fsspec_completion_upload_hook" [project.optional-dependencies] test = ["pytest>=7.0.0"] diff --git a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/_fsspec_upload/__init__.py b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/_fsspec_upload/__init__.py index 210dba3dcd..2dd571caf8 100644 --- a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/_fsspec_upload/__init__.py +++ b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/_fsspec_upload/__init__.py @@ -16,24 +16,27 @@ from os import environ +from opentelemetry.util.genai.completion_hook import ( + CompletionHook, + _NoOpCompletionHook, +) from opentelemetry.util.genai.environment_variables import ( OTEL_INSTRUMENTATION_GENAI_UPLOAD_BASE_PATH, ) -from opentelemetry.util.genai.upload_hook import UploadHook, _NoOpUploadHook -def fsspec_upload_hook() -> UploadHook: +def fsspec_completion_upload_hook() -> CompletionHook: # If fsspec is not installed the hook will be a no-op. try: # pylint: disable=import-outside-toplevel - from opentelemetry.util.genai._fsspec_upload.fsspec_hook import ( - FsspecUploadHook, + from opentelemetry.util.genai._fsspec_upload.completion_hook import ( + FsspecUploadCompletionHook, ) except ImportError: - return _NoOpUploadHook() + return _NoOpCompletionHook() base_path = environ.get(OTEL_INSTRUMENTATION_GENAI_UPLOAD_BASE_PATH) if not base_path: - return _NoOpUploadHook() + return _NoOpCompletionHook() - return FsspecUploadHook(base_path=base_path) + return FsspecUploadCompletionHook(base_path=base_path) diff --git a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/_fsspec_upload/fsspec_hook.py b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/_fsspec_upload/completion_hook.py similarity index 94% rename from util/opentelemetry-util-genai/src/opentelemetry/util/genai/_fsspec_upload/fsspec_hook.py rename to util/opentelemetry-util-genai/src/opentelemetry/util/genai/_fsspec_upload/completion_hook.py index d2ea9f2435..56d7b0dcd6 100644 --- a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/_fsspec_upload/fsspec_hook.py +++ b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/_fsspec_upload/completion_hook.py @@ -34,7 +34,7 @@ from opentelemetry.semconv._incubating.attributes import gen_ai_attributes from opentelemetry.trace import Span from opentelemetry.util.genai import types -from opentelemetry.util.genai.upload_hook import UploadHook +from opentelemetry.util.genai.completion_hook import CompletionHook GEN_AI_INPUT_MESSAGES_REF: Final = ( gen_ai_attributes.GEN_AI_INPUT_MESSAGES + "_ref" @@ -75,12 +75,12 @@ def fsspec_open(urlpath: str, mode: Literal["w"]) -> TextIO: return cast(TextIO, fsspec.open(urlpath, mode)) # pyright: ignore[reportUnknownMemberType] -class FsspecUploadHook(UploadHook): - """An upload hook using ``fsspec`` to upload to external storage +class FsspecUploadCompletionHook(CompletionHook): + """An completion hook using ``fsspec`` to upload to external storage This function can be used as the - :func:`~opentelemetry.util.genai.upload_hook.load_upload_hook` implementation by - setting :envvar:`OTEL_INSTRUMENTATION_GENAI_UPLOAD_HOOK` to ``fsspec``. + :func:`~opentelemetry.util.genai.completion_hook.load_completion_hook` implementation by + setting :envvar:`OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK` to ``fsspec_upload``. :envvar:`OTEL_INSTRUMENTATION_GENAI_UPLOAD_BASE_PATH` must be configured to specify the base path for uploads. @@ -128,7 +128,7 @@ def done(future: Future[None]) -> None: fut.add_done_callback(done) except RuntimeError: _logger.info( - "attempting to upload file after FsspecUploadHook.shutdown() was already called" + "attempting to upload file after FsspecUploadCompletionHook.shutdown() was already called" ) self._semaphore.release() @@ -161,7 +161,7 @@ def _do_upload( cls=Base64JsonEncoder, ) - def upload( + def on_completion( self, *, inputs: list[types.InputMessage], diff --git a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/upload_hook.py b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/completion_hook.py similarity index 65% rename from util/opentelemetry-util-genai/src/opentelemetry/util/genai/upload_hook.py rename to util/opentelemetry-util-genai/src/opentelemetry/util/genai/completion_hook.py index 9180b98eb8..76d199ce84 100644 --- a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/upload_hook.py +++ b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/completion_hook.py @@ -12,13 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""This module defines the generic hooks for GenAI content uploading +"""This module defines the generic hooks for GenAI content completion The hooks are specified as part of semconv in `Uploading content to external storage `__. -This module defines the `UploadHook` type that custom implementations should implement, and a -`load_upload_hook` function to load it from an entry point. +This module defines the `CompletionHook` type that custom implementations should implement, and a +`load_completion_hook` function to load it from an entry point. """ from __future__ import annotations @@ -34,18 +34,18 @@ ) from opentelemetry.util.genai import types from opentelemetry.util.genai.environment_variables import ( - OTEL_INSTRUMENTATION_GENAI_UPLOAD_HOOK, + OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK, ) _logger = logging.getLogger(__name__) @runtime_checkable -class UploadHook(Protocol): - """A hook to upload GenAI content to an external storage. +class CompletionHook(Protocol): + """A hook to be called on completion of a GenAI operation. This is the interface for a hook that can be - used to upload GenAI content to an external storage. The hook is a + used to capture GenAI content on completion. The hook is a callable that takes the inputs, outputs, and system instruction of a GenAI interaction, as well as the span and log record associated with it. @@ -66,7 +66,7 @@ class UploadHook(Protocol): interaction. """ - def upload( + def on_completion( self, *, inputs: list[types.InputMessage], @@ -77,43 +77,47 @@ def upload( ) -> None: ... -class _NoOpUploadHook(UploadHook): - def upload(self, **kwargs: Any) -> None: +class _NoOpCompletionHook(CompletionHook): + def on_completion(self, **kwargs: Any) -> None: return None -def load_upload_hook() -> UploadHook: - """Load the upload hook from entry point or return a noop implementation +def load_completion_hook() -> CompletionHook: + """Load the completion hook from entry point or return a noop implementation - This function loads an upload hook from the entry point group - ``opentelemetry_genai_upload_hook`` with name coming from - :envvar:`OTEL_INSTRUMENTATION_GENAI_UPLOAD_HOOK`. If one can't be found, returns a no-op + This function loads an completion hook from the entry point group + ``opentelemetry_genai_completion_hook`` with name coming from + :envvar:`OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK`. If one can't be found, returns a no-op implementation. """ - hook_name = environ.get(OTEL_INSTRUMENTATION_GENAI_UPLOAD_HOOK, None) + hook_name = environ.get(OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK, None) if not hook_name: - return _NoOpUploadHook() + return _NoOpCompletionHook() - for entry_point in entry_points(group="opentelemetry_genai_upload_hook"): # pyright: ignore[reportUnknownVariableType] + for entry_point in entry_points( # pyright: ignore[reportUnknownVariableType] + group="opentelemetry_genai_completion_hook" + ): name = cast(str, entry_point.name) # pyright: ignore[reportUnknownMemberType] try: if hook_name != name: continue hook = entry_point.load()() # pyright: ignore[reportUnknownVariableType, reportUnknownMemberType] - if not isinstance(hook, UploadHook): - _logger.debug("%s is not a valid UploadHook. Using noop", name) + if not isinstance(hook, CompletionHook): + _logger.debug( + "%s is not a valid CompletionHook. Using noop", name + ) continue - _logger.debug("Using UploadHook %s", name) + _logger.debug("Using CompletionHook %s", name) return hook except Exception: # pylint: disable=broad-except _logger.exception( - "UploadHook %s configuration failed. Using noop", name + "CompletionHook %s configuration failed. Using noop", name ) - return _NoOpUploadHook() + return _NoOpCompletionHook() -__all__ = ["UploadHook", "load_upload_hook"] +__all__ = ["CompletionHook", "load_completion_hook"] diff --git a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/environment_variables.py b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/environment_variables.py index 69c4419ae3..0ff089d82a 100644 --- a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/environment_variables.py +++ b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/environment_variables.py @@ -16,11 +16,11 @@ "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT" ) -OTEL_INSTRUMENTATION_GENAI_UPLOAD_HOOK = ( - "OTEL_INSTRUMENTATION_GENAI_UPLOAD_HOOK" +OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK = ( + "OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK" ) """ -.. envvar:: OTEL_INSTRUMENTATION_GENAI_UPLOAD_HOOK +.. envvar:: OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK """ OTEL_INSTRUMENTATION_GENAI_UPLOAD_BASE_PATH = ( diff --git a/util/opentelemetry-util-genai/tests/test_completion_hook.py b/util/opentelemetry-util-genai/tests/test_completion_hook.py new file mode 100644 index 0000000000..619441b2ae --- /dev/null +++ b/util/opentelemetry-util-genai/tests/test_completion_hook.py @@ -0,0 +1,101 @@ +# 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. + +import logging +from dataclasses import dataclass +from typing import Any, Callable +from unittest import TestCase +from unittest.mock import Mock, patch + +from opentelemetry.util.genai.completion_hook import ( + CompletionHook, + _NoOpCompletionHook, + load_completion_hook, +) +from opentelemetry.util.genai.environment_variables import ( + OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK, +) + + +class FakeCompletionHook(CompletionHook): + def on_completion(self, **kwargs: Any): + pass + + +class InvalidCompletionHook: + pass + + +@dataclass +class FakeEntryPoint: + name: str + load: Callable[[], type[CompletionHook]] + + +class TestCompletionHook(TestCase): + @patch.dict("os.environ", {}) + def test_load_completion_hook_noop(self): + self.assertIsInstance(load_completion_hook(), _NoOpCompletionHook) + + @patch( + "opentelemetry.util.genai.completion_hook.entry_points", + ) + @patch.dict( + "os.environ", {OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK: "my-hook"} + ) + def test_load_completion_hook_custom(self, mock_entry_points: Mock): + mock_entry_points.return_value = [ + FakeEntryPoint("my-hook", lambda: FakeCompletionHook) + ] + + self.assertIsInstance(load_completion_hook(), FakeCompletionHook) + + @patch("opentelemetry.util.genai.completion_hook.entry_points") + @patch.dict( + "os.environ", {OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK: "my-hook"} + ) + def test_load_completion_hook_invalid(self, mock_entry_points: Mock): + mock_entry_points.return_value = [ + FakeEntryPoint("my-hook", lambda: InvalidCompletionHook) + ] + + with self.assertLogs(level=logging.DEBUG) as logs: + self.assertIsInstance(load_completion_hook(), _NoOpCompletionHook) + self.assertEqual(len(logs.output), 1) + self.assertIn( + "is not a valid CompletionHook. Using noop", logs.output[0] + ) + + @patch("opentelemetry.util.genai.completion_hook.entry_points") + @patch.dict( + "os.environ", {OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK: "my-hook"} + ) + def test_load_completion_hook_error(self, mock_entry_points: Mock): + def load(): + raise RuntimeError("error") + + mock_entry_points.return_value = [FakeEntryPoint("my-hook", load)] + + self.assertIsInstance(load_completion_hook(), _NoOpCompletionHook) + + @patch("opentelemetry.util.genai.completion_hook.entry_points") + @patch.dict( + "os.environ", {OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK: "my-hook"} + ) + def test_load_completion_hook_not_found(self, mock_entry_points: Mock): + mock_entry_points.return_value = [ + FakeEntryPoint("other-hook", lambda: FakeCompletionHook) + ] + + self.assertIsInstance(load_completion_hook(), _NoOpCompletionHook) diff --git a/util/opentelemetry-util-genai/tests/test_fsspec_upload.py b/util/opentelemetry-util-genai/tests/test_fsspec_upload.py index 2cf65e40ba..96c76d8458 100644 --- a/util/opentelemetry-util-genai/tests/test_fsspec_upload.py +++ b/util/opentelemetry-util-genai/tests/test_fsspec_upload.py @@ -29,12 +29,12 @@ from opentelemetry._logs import LogRecord from opentelemetry.test.test_base import TestBase from opentelemetry.util.genai import types -from opentelemetry.util.genai._fsspec_upload.fsspec_hook import ( - FsspecUploadHook, +from opentelemetry.util.genai._fsspec_upload.completion_hook import ( + FsspecUploadCompletionHook, ) -from opentelemetry.util.genai.upload_hook import ( - _NoOpUploadHook, - load_upload_hook, +from opentelemetry.util.genai.completion_hook import ( + _NoOpCompletionHook, + load_completion_hook, ) # Use MemoryFileSystem for testing @@ -45,14 +45,16 @@ @patch.dict( "os.environ", { - "OTEL_INSTRUMENTATION_GENAI_UPLOAD_HOOK": "fsspec", + "OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK": "fsspec_upload", "OTEL_INSTRUMENTATION_GENAI_UPLOAD_BASE_PATH": BASE_PATH, }, clear=True, ) class TestFsspecEntryPoint(TestCase): def test_fsspec_entry_point(self): - self.assertIsInstance(load_upload_hook(), FsspecUploadHook) + self.assertIsInstance( + load_completion_hook(), FsspecUploadCompletionHook + ) def test_fsspec_entry_point_no_fsspec(self): """Tests that the a no-op uploader is used when fsspec is not installed""" @@ -62,10 +64,10 @@ def test_fsspec_entry_point_no_fsspec(self): # Simulate fsspec imports failing with patch.dict( sys.modules, - {"opentelemetry.util.genai._fsspec_upload.fsspec_hook": None}, + {"opentelemetry.util.genai._fsspec_upload.completion_hook": None}, ): importlib.reload(_fsspec_upload) - self.assertIsInstance(load_upload_hook(), _NoOpUploadHook) + self.assertIsInstance(load_completion_hook(), _NoOpCompletionHook) MAXSIZE = 5 @@ -95,15 +97,15 @@ def _increment_mock_call(self, /, *args, **kwargs): super()._increment_mock_call(*args, **kwargs) -class TestFsspecUploadHook(TestCase): +class TestFsspecUploadCompletionHook(TestCase): def setUp(self): self._fsspec_patcher = patch( - "opentelemetry.util.genai._fsspec_upload.fsspec_hook.fsspec" + "opentelemetry.util.genai._fsspec_upload.completion_hook.fsspec" ) self.mock_fsspec = self._fsspec_patcher.start() self.mock_fsspec.open = ThreadSafeMagicMock() - self.hook = FsspecUploadHook( + self.hook = FsspecUploadCompletionHook( base_path=BASE_PATH, max_size=MAXSIZE, ) @@ -130,7 +132,7 @@ def test_shutdown_no_items(self): self.hook.shutdown() def test_upload_then_shutdown(self): - self.hook.upload( + self.hook.on_completion( inputs=FAKE_INPUTS, outputs=FAKE_OUTPUTS, system_instruction=FAKE_SYSTEM_INSTRUCTION, @@ -148,7 +150,7 @@ def test_upload_blocked(self): with self.block_upload(): # fill the queue for _ in range(MAXSIZE): - self.hook.upload( + self.hook.on_completion( inputs=FAKE_INPUTS, outputs=FAKE_OUTPUTS, system_instruction=FAKE_SYSTEM_INSTRUCTION, @@ -161,7 +163,7 @@ def test_upload_blocked(self): ) with self.assertLogs(level=logging.WARNING) as logs: - self.hook.upload( + self.hook.on_completion( inputs=FAKE_INPUTS, outputs=FAKE_OUTPUTS, system_instruction=FAKE_SYSTEM_INSTRUCTION, @@ -173,7 +175,7 @@ def test_upload_blocked(self): def test_shutdown_timeout(self): with self.block_upload(): - self.hook.upload( + self.hook.on_completion( inputs=FAKE_INPUTS, outputs=FAKE_OUTPUTS, system_instruction=FAKE_SYSTEM_INSTRUCTION, @@ -186,7 +188,7 @@ def test_failed_upload_logs(self): self.mock_fsspec.open.side_effect = RuntimeError("failed to upload") with self.assertLogs(level=logging.ERROR) as logs: - self.hook.upload( + self.hook.on_completion( inputs=FAKE_INPUTS, outputs=FAKE_OUTPUTS, system_instruction=FAKE_SYSTEM_INSTRUCTION, @@ -198,21 +200,21 @@ def test_failed_upload_logs(self): def test_upload_after_shutdown_logs(self): self.hook.shutdown() with self.assertLogs(level=logging.INFO) as logs: - self.hook.upload( + self.hook.on_completion( inputs=FAKE_INPUTS, outputs=FAKE_OUTPUTS, system_instruction=FAKE_SYSTEM_INSTRUCTION, ) self.assertEqual(len(logs.output), 3) self.assertIn( - "attempting to upload file after FsspecUploadHook.shutdown() was already called", + "attempting to upload file after FsspecUploadCompletionHook.shutdown() was already called", logs.output[0], ) class FsspecUploaderTest(TestCase): def test_upload(self): - FsspecUploadHook._do_upload( + FsspecUploadCompletionHook._do_upload( "memory://my_path", lambda: [asdict(fake_input) for fake_input in FAKE_INPUTS], ) @@ -224,10 +226,10 @@ def test_upload(self): ) -class TestFsspecUploadHookIntegration(TestBase): +class TestFsspecUploadCompletionHookIntegration(TestBase): def setUp(self): super().setUp() - self.hook = FsspecUploadHook(base_path=BASE_PATH) + self.hook = FsspecUploadCompletionHook(base_path=BASE_PATH) def tearDown(self): super().tearDown() @@ -242,7 +244,7 @@ def test_upload_completions(self): log_record = LogRecord() with tracer.start_as_current_span("chat mymodel") as span: - self.hook.upload( + self.hook.on_completion( inputs=FAKE_INPUTS, outputs=FAKE_OUTPUTS, system_instruction=FAKE_SYSTEM_INSTRUCTION, @@ -282,7 +284,7 @@ def test_upload_completions(self): def test_stamps_empty_log(self): log_record = LogRecord() - self.hook.upload( + self.hook.on_completion( inputs=FAKE_INPUTS, outputs=FAKE_OUTPUTS, system_instruction=FAKE_SYSTEM_INSTRUCTION, @@ -296,7 +298,7 @@ def test_stamps_empty_log(self): def test_upload_bytes(self) -> None: log_record = LogRecord() - self.hook.upload( + self.hook.on_completion( inputs=[ types.InputMessage( role="user", diff --git a/util/opentelemetry-util-genai/tests/test_upload_hook.py b/util/opentelemetry-util-genai/tests/test_upload_hook.py deleted file mode 100644 index 93731bce95..0000000000 --- a/util/opentelemetry-util-genai/tests/test_upload_hook.py +++ /dev/null @@ -1,99 +0,0 @@ -# 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. - -import logging -from dataclasses import dataclass -from typing import Any, Callable -from unittest import TestCase -from unittest.mock import Mock, patch - -from opentelemetry.util.genai.environment_variables import ( - OTEL_INSTRUMENTATION_GENAI_UPLOAD_HOOK, -) -from opentelemetry.util.genai.upload_hook import ( - UploadHook, - _NoOpUploadHook, - load_upload_hook, -) - - -class FakeUploadHook(UploadHook): - def upload(self, **kwargs: Any): - pass - - -class InvalidUploadHook: - pass - - -@dataclass -class FakeEntryPoint: - name: str - load: Callable[[], type[UploadHook]] - - -class TestUploadHook(TestCase): - @patch.dict("os.environ", {}) - def test_load_upload_hook_noop(self): - self.assertIsInstance(load_upload_hook(), _NoOpUploadHook) - - @patch( - "opentelemetry.util.genai.upload_hook.entry_points", - ) - @patch.dict( - "os.environ", {OTEL_INSTRUMENTATION_GENAI_UPLOAD_HOOK: "my-hook"} - ) - def test_load_upload_hook_custom(self, mock_entry_points: Mock): - mock_entry_points.return_value = [ - FakeEntryPoint("my-hook", lambda: FakeUploadHook) - ] - - self.assertIsInstance(load_upload_hook(), FakeUploadHook) - - @patch("opentelemetry.util.genai.upload_hook.entry_points") - @patch.dict( - "os.environ", {OTEL_INSTRUMENTATION_GENAI_UPLOAD_HOOK: "my-hook"} - ) - def test_load_upload_hook_invalid(self, mock_entry_points: Mock): - mock_entry_points.return_value = [ - FakeEntryPoint("my-hook", lambda: InvalidUploadHook) - ] - - with self.assertLogs(level=logging.DEBUG) as logs: - self.assertIsInstance(load_upload_hook(), _NoOpUploadHook) - self.assertEqual(len(logs.output), 1) - self.assertIn("is not a valid UploadHook. Using noop", logs.output[0]) - - @patch("opentelemetry.util.genai.upload_hook.entry_points") - @patch.dict( - "os.environ", {OTEL_INSTRUMENTATION_GENAI_UPLOAD_HOOK: "my-hook"} - ) - def test_load_upload_hook_error(self, mock_entry_points: Mock): - def load(): - raise RuntimeError("error") - - mock_entry_points.return_value = [FakeEntryPoint("my-hook", load)] - - self.assertIsInstance(load_upload_hook(), _NoOpUploadHook) - - @patch("opentelemetry.util.genai.upload_hook.entry_points") - @patch.dict( - "os.environ", {OTEL_INSTRUMENTATION_GENAI_UPLOAD_HOOK: "my-hook"} - ) - def test_load_upload_hook_not_found(self, mock_entry_points: Mock): - mock_entry_points.return_value = [ - FakeEntryPoint("other-hook", lambda: FakeUploadHook) - ] - - self.assertIsInstance(load_upload_hook(), _NoOpUploadHook)