diff --git a/util/opentelemetry-util-genai/CHANGELOG.md b/util/opentelemetry-util-genai/CHANGELOG.md index f64092a69..44716b83c 100644 --- a/util/opentelemetry-util-genai/CHANGELOG.md +++ b/util/opentelemetry-util-genai/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +- Avoid import-time warnings when optional audio dependencies for PCM16-to-WAV conversion are not installed. + ## Version 0.3b0 (2026-02-20) - Add `gen_ai.tool_definitions` to completion hook ([#4181](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4181)) diff --git a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/_multimodal_upload/pre_uploader.py b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/_multimodal_upload/pre_uploader.py index 7bb18d3a2..133e9beb9 100644 --- a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/_multimodal_upload/pre_uploader.py +++ b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/_multimodal_upload/pre_uploader.py @@ -78,12 +78,6 @@ _logger = logging.getLogger(__name__) -# Log warning if audio libraries are not available -if not _audio_libs_available: - _logger.warning( - "numpy or soundfile not available, PCM16 to WAV conversion will be skipped" - ) - # Supported modality types for pre-upload (derived from Modality type) _SUPPORTED_MODALITIES = get_args(Modality) @@ -591,9 +585,6 @@ def _convert_pcm16_to_wav( Byte data in WAV format, None if conversion fails """ if not _audio_libs_available or np is None or sf is None: - _logger.warning( - "Cannot convert PCM16 to WAV: numpy or soundfile not available" - ) return None try: diff --git a/util/opentelemetry-util-genai/tests/_multimodal_upload/test_pre_uploader_audio.py b/util/opentelemetry-util-genai/tests/_multimodal_upload/test_pre_uploader_audio.py index 85dc9b632..385ba6836 100644 --- a/util/opentelemetry-util-genai/tests/_multimodal_upload/test_pre_uploader_audio.py +++ b/util/opentelemetry-util-genai/tests/_multimodal_upload/test_pre_uploader_audio.py @@ -18,6 +18,10 @@ and audio format conversion (e.g., PCM16 to WAV) """ +import os +import subprocess +import sys +import textwrap from pathlib import Path from unittest.mock import patch @@ -65,6 +69,55 @@ def _read_audio_file(filename: str) -> bytes: with open(filepath, "rb") as file_obj: return file_obj.read() + @staticmethod + def test_import_without_audio_libs_does_not_write_to_standard_streams(): + """Missing optional audio libs should not emit import-time output.""" + project_root = Path(__file__).parents[4] + util_genai_src = Path(__file__).parents[2] / "src" + instrumentation_src = ( + project_root / "opentelemetry-instrumentation" / "src" + ) + env = os.environ.copy() + pythonpath_parts = [ + str(util_genai_src), + str(instrumentation_src), + env.get("PYTHONPATH", ""), + ] + env["PYTHONPATH"] = os.pathsep.join( + part for part in pythonpath_parts if part + ) + + script = textwrap.dedent( + """ + import importlib.abc + import logging + import sys + + class BlockAudioLibs(importlib.abc.MetaPathFinder): + def find_spec(self, fullname, path=None, target=None): + if fullname == "numpy" or fullname.startswith("numpy."): + raise ImportError(fullname) + if fullname == "soundfile" or fullname.startswith("soundfile."): + raise ImportError(fullname) + return None + + sys.meta_path.insert(0, BlockAudioLibs()) + logging.basicConfig(level=logging.WARNING, stream=sys.stdout) + import opentelemetry.util.genai._multimodal_upload.pre_uploader # noqa: F401 + """ + ) + + completed = subprocess.run( + [sys.executable, "-c", script], + env=env, + check=True, + capture_output=True, + text=True, + ) + + assert completed.stdout == "" + assert completed.stderr == "" + # ========== Edge Case Tests ========== @staticmethod @@ -174,6 +227,53 @@ def test_pcm16_to_wav_conversion(pre_uploader, pcm_mime_type): # If library unavailable, should keep original format assert uploads[0].content_type == pcm_mime_type + @staticmethod + def test_pcm16_conversion_missing_audio_libs_logs_single_warning( + caplog, + ): + """Missing optional audio libs should only log the actual conversion skip.""" + with ( + patch( + "opentelemetry.util.genai._multimodal_upload.pre_uploader._audio_libs_available", + False, + ), + patch( + "opentelemetry.util.genai._multimodal_upload.pre_uploader.np", + None, + ), + patch( + "opentelemetry.util.genai._multimodal_upload.pre_uploader.sf", + None, + ), + ): + pre_uploader = MultimodalPreUploader(base_path="/tmp/test_upload") + part = Blob( + content=b"\x00\x01" * 1000, + mime_type="audio/pcm16", + modality="audio", + ) + input_messages = [InputMessage(role="user", parts=[part])] + + with caplog.at_level( + "WARNING", + logger=( + "opentelemetry.util.genai._multimodal_upload.pre_uploader" + ), + ): + uploads = pre_uploader.pre_upload( + span_context=None, + start_time_utc_nano=1000000000000000000, + input_messages=input_messages, + output_messages=None, + ) + + assert len(uploads) == 1 + assert uploads[0].content_type == "audio/pcm16" + warning_messages = [record.getMessage() for record in caplog.records] + assert warning_messages == [ + "Failed to convert PCM16 to WAV, using original format" + ] + @staticmethod def test_pcm16_conversion_disabled_by_default(): """Test PCM16 conversion stays disabled when env var is unset"""