diff --git a/packages/opentelemetry-instrumentation-qdrant/opentelemetry/instrumentation/qdrant/__init__.py b/packages/opentelemetry-instrumentation-qdrant/opentelemetry/instrumentation/qdrant/__init__.py index b4e146f31e..7d46a3220d 100644 --- a/packages/opentelemetry-instrumentation-qdrant/opentelemetry/instrumentation/qdrant/__init__.py +++ b/packages/opentelemetry-instrumentation-qdrant/opentelemetry/instrumentation/qdrant/__init__.py @@ -2,6 +2,7 @@ import json import logging +from inspect import getattr_static from pathlib import Path from opentelemetry.instrumentation.qdrant.config import Config from opentelemetry.instrumentation.qdrant.wrapper import _wrap @@ -33,6 +34,11 @@ WRAPPED_METHODS = QDRANT_CLIENT_METHODS + ASYNC_QDRANT_CLIENT_METHODS MODULE = "qdrant_client" +_MISSING = object() + + +def _has_static_attribute(obj, name): + return getattr_static(obj, name, _MISSING) is not _MISSING class QdrantInstrumentor(BaseInstrumentor): @@ -52,7 +58,7 @@ def _instrument(self, **kwargs): wrap_object = wrapped_method.get("object") wrap_method = wrapped_method.get("method") obj = getattr(qdrant_client, wrap_object, None) - if obj and hasattr(obj, wrap_method): + if obj and _has_static_attribute(obj, wrap_method): wrap_function_wrapper( MODULE, f"{wrap_object}.{wrap_method}", @@ -64,5 +70,5 @@ def _uninstrument(self, **kwargs): wrap_object = wrapped_method.get("object") wrap_method = wrapped_method.get("method") obj = getattr(qdrant_client, wrap_object, None) - if obj and hasattr(obj, wrap_method): + if obj and _has_static_attribute(obj, wrap_method): unwrap(f"{MODULE}.{wrap_object}", wrap_method) diff --git a/packages/opentelemetry-instrumentation-qdrant/tests/test_missing_methods.py b/packages/opentelemetry-instrumentation-qdrant/tests/test_missing_methods.py new file mode 100644 index 0000000000..1ba0171765 --- /dev/null +++ b/packages/opentelemetry-instrumentation-qdrant/tests/test_missing_methods.py @@ -0,0 +1,75 @@ +from types import SimpleNamespace + +from opentelemetry.instrumentation import qdrant as qdrant_module + + +class _DynamicMethodMeta(type): + def __getattr__(cls, name): + if name == "upload_records": + return lambda *args, **kwargs: None + raise AttributeError(name) + + +class _FakeQdrantClient(metaclass=_DynamicMethodMeta): + def upload_points(self): + return None + + def upload_collection(self): + return None + + +class _FakeAsyncQdrantClient(_FakeQdrantClient): + pass + + +def test_static_attribute_check_ignores_dynamic_class_lookup(): + assert hasattr(_FakeQdrantClient, "upload_records") + assert not qdrant_module._has_static_attribute(_FakeQdrantClient, "upload_records") + + +def _patch_fake_clients(monkeypatch): + monkeypatch.setattr( + qdrant_module, + "qdrant_client", + SimpleNamespace( + QdrantClient=_FakeQdrantClient, + AsyncQdrantClient=_FakeAsyncQdrantClient, + ), + ) + + +def test_instrumentor_skips_methods_missing_from_client_class(monkeypatch): + wrapped_paths = [] + + _patch_fake_clients(monkeypatch) + monkeypatch.setattr(qdrant_module, "get_tracer", lambda *args, **kwargs: object()) + monkeypatch.setattr( + qdrant_module, + "wrap_function_wrapper", + lambda module, path, wrapper: wrapped_paths.append(path), + ) + + qdrant_module.QdrantInstrumentor()._instrument() + + assert "QdrantClient.upload_points" in wrapped_paths + assert "AsyncQdrantClient.upload_points" in wrapped_paths + assert "QdrantClient.upload_records" not in wrapped_paths + assert "AsyncQdrantClient.upload_records" not in wrapped_paths + + +def test_uninstrumentor_skips_methods_missing_from_client_class(monkeypatch): + unwrapped_methods = [] + + _patch_fake_clients(monkeypatch) + monkeypatch.setattr( + qdrant_module, + "unwrap", + lambda module_path, method: unwrapped_methods.append((module_path, method)), + ) + + qdrant_module.QdrantInstrumentor()._uninstrument() + + assert ("qdrant_client.QdrantClient", "upload_points") in unwrapped_methods + assert ("qdrant_client.AsyncQdrantClient", "upload_points") in unwrapped_methods + assert ("qdrant_client.QdrantClient", "upload_records") not in unwrapped_methods + assert ("qdrant_client.AsyncQdrantClient", "upload_records") not in unwrapped_methods