Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions util/opentelemetry-util-genai/.changelog/891.fixed
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Log a warning instead of falling back silently to a no-op hook when `OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK` names a hook that cannot be loaded.
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,8 @@ def load_completion_hook() -> CompletionHook:

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.
:envvar:`OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK`. If the configured name cannot be
resolved to a valid hook, a warning is logged and a no-op implementation is returned.

The returned hook wraps the user-provided implementation so any exception
raised by ``on_completion`` is logged and swallowed.
Expand All @@ -124,17 +124,19 @@ def load_completion_hook() -> CompletionHook:
if not hook_name:
return _NoOpCompletionHook()

hook_names: list[str] = []
for entry_point in entry_points(
group="opentelemetry_genai_completion_hook"
):
name = entry_point.name
hook_names.append(name)
try:
if hook_name != name:
continue

hook = entry_point.load()()
if not isinstance(hook, CompletionHook):
_logger.debug(
_logger.warning(
"%s is not a valid CompletionHook. Using noop", name
)
continue
Expand All @@ -147,6 +149,13 @@ def load_completion_hook() -> CompletionHook:
"CompletionHook %s configuration failed. Using noop", name
)

if hook_name not in hook_names:
_logger.warning(
"%s is not a valid option for %s. Must be one of %s. Using noop.",
hook_name,
OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK,
", ".join(hook_names) or "none",
)
return _NoOpCompletionHook()


Expand Down
38 changes: 38 additions & 0 deletions util/opentelemetry-util-genai/tests/test_completion_hook.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,44 @@ def test_load_completion_hook_not_found(self, mock_entry_points: Mock):

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_warns(
self, mock_entry_points: Mock
):
mock_entry_points.return_value = [
FakeEntryPoint("other-hook", lambda: FakeCompletionHook)
]

with self.assertLogs(level=logging.WARNING) as logs:
self.assertIsInstance(load_completion_hook(), _NoOpCompletionHook)

self.assertEqual(len(logs.records), 1)
self.assertEqual(logs.records[0].levelno, logging.WARNING)
self.assertIn("my-hook", logs.records[0].getMessage())
self.assertIn("other-hook", logs.records[0].getMessage())

@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_warns(self, mock_entry_points: Mock):
mock_entry_points.return_value = [
FakeEntryPoint("my-hook", lambda: InvalidCompletionHook)
]

with self.assertLogs(level=logging.WARNING) as logs:
self.assertIsInstance(load_completion_hook(), _NoOpCompletionHook)

self.assertEqual(len(logs.records), 1)
self.assertEqual(logs.records[0].levelno, logging.WARNING)
self.assertIn(
"is not a valid CompletionHook. Using noop",
logs.records[0].getMessage(),
)


class TestSafeCompletionHook(TestCase):
def test_passes_arguments_to_wrapped_hook(self):
Expand Down
Loading