From b0fed32b10aeca3fd7efee11e2c843fe3c179023 Mon Sep 17 00:00:00 2001 From: Nishchay Mahor Date: Fri, 29 May 2026 17:31:39 -0700 Subject: [PATCH 1/2] fix(qdrant): cover AsyncQdrantClient.query_points and query_batch_points The sync qdrant_client_methods.json already wraps query_points and query_batch_points on QdrantClient, but the async config is missing both, so async users get no spans for the modern query APIs even after #3500 quieted the original upload_records crash. Add the matching AsyncQdrantClient entries, plus two tests that exercise each method through the in-memory client and assert the span shows up with the expected attributes. --- .../qdrant/async_qdrant_client_methods.json | 10 +++ .../tests/test_qdrant_instrumentation.py | 78 ++++++++++++++++++- 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/packages/opentelemetry-instrumentation-qdrant/opentelemetry/instrumentation/qdrant/async_qdrant_client_methods.json b/packages/opentelemetry-instrumentation-qdrant/opentelemetry/instrumentation/qdrant/async_qdrant_client_methods.json index ec161a5ee6..28a4b3b20a 100644 --- a/packages/opentelemetry-instrumentation-qdrant/opentelemetry/instrumentation/qdrant/async_qdrant_client_methods.json +++ b/packages/opentelemetry-instrumentation-qdrant/opentelemetry/instrumentation/qdrant/async_qdrant_client_methods.json @@ -44,11 +44,21 @@ "method": "query", "span_name": "qdrant.query" }, + { + "object": "AsyncQdrantClient", + "method": "query_points", + "span_name": "qdrant.query_points" + }, { "object": "AsyncQdrantClient", "method": "query_batch", "span_name": "qdrant.query_batch" }, + { + "object": "AsyncQdrantClient", + "method": "query_batch_points", + "span_name": "qdrant.query_batch_points" + }, { "object": "AsyncQdrantClient", "method": "discover", diff --git a/packages/opentelemetry-instrumentation-qdrant/tests/test_qdrant_instrumentation.py b/packages/opentelemetry-instrumentation-qdrant/tests/test_qdrant_instrumentation.py index 6203b19a08..17f133ab66 100644 --- a/packages/opentelemetry-instrumentation-qdrant/tests/test_qdrant_instrumentation.py +++ b/packages/opentelemetry-instrumentation-qdrant/tests/test_qdrant_instrumentation.py @@ -1,5 +1,7 @@ +import asyncio + import pytest -from qdrant_client import QdrantClient, models +from qdrant_client import AsyncQdrantClient, QdrantClient, models from opentelemetry.semconv_ai import SpanAttributes @@ -122,3 +124,77 @@ def test_qdrant_search(exporter, qdrant): == COLLECTION_NAME ) assert span.attributes.get(SpanAttributes.QDRANT_SEARCH_BATCH_REQUESTS_COUNT) == 4 + + +# --- AsyncQdrantClient coverage for query_points / query_batch_points --- +# +# The sync client's query_points / query_batch_points are already wrapped, but +# AsyncQdrantClient was missing the equivalents in async_qdrant_client_methods.json, +# so async users got no spans for those calls. These tests pin the parity. + +async def _async_setup(client: AsyncQdrantClient) -> None: + # `AsyncQdrantClient.upload_collection` runs through a ThreadPoolExecutor + # internally and returns None instead of an awaitable, so set up the + # fixture data with `upsert` which is genuinely async. + await client.create_collection( + COLLECTION_NAME, + vectors_config=models.VectorParams( + size=EMBEDDING_DIM, distance=models.Distance.COSINE + ), + ) + await client.upsert( + COLLECTION_NAME, + points=[ + models.PointStruct(id=1, vector=[0.1] * EMBEDDING_DIM, payload={"name": "Paul"}), + models.PointStruct(id=2, vector=[0.2] * EMBEDDING_DIM, payload={"name": "John"}), + models.PointStruct(id=3, vector=[0.3] * EMBEDDING_DIM, payload={}), + ], + ) + + +def test_qdrant_async_query_points(exporter): + async def run() -> None: + client = AsyncQdrantClient(location=":memory:") + await _async_setup(client) + await client.query_points( + collection_name=COLLECTION_NAME, + query=[0.1] * EMBEDDING_DIM, + limit=1, + ) + + asyncio.run(run()) + + spans = exporter.get_finished_spans() + span = next(span for span in spans if span.name == "qdrant.query_points") + + assert ( + span.attributes.get(SpanAttributes.QDRANT_SEARCH_COLLECTION_NAME) + == COLLECTION_NAME + ) + assert span.attributes.get(SpanAttributes.VECTOR_DB_QUERY_TOP_K) == 1 + + +def test_qdrant_async_query_batch_points(exporter): + async def run() -> None: + client = AsyncQdrantClient(location=":memory:") + await _async_setup(client) + await client.query_batch_points( + collection_name=COLLECTION_NAME, + requests=[ + models.QueryRequest(query=[0.1] * EMBEDDING_DIM, limit=10), + models.QueryRequest(query=[0.2] * EMBEDDING_DIM, limit=5), + models.QueryRequest(query=[0.42] * EMBEDDING_DIM, limit=2), + models.QueryRequest(query=[0.21] * EMBEDDING_DIM, limit=1), + ], + ) + + asyncio.run(run()) + + spans = exporter.get_finished_spans() + span = next(span for span in spans if span.name == "qdrant.query_batch_points") + + assert ( + span.attributes.get(SpanAttributes.QDRANT_SEARCH_BATCH_COLLECTION_NAME) + == COLLECTION_NAME + ) + assert span.attributes.get(SpanAttributes.QDRANT_SEARCH_BATCH_REQUESTS_COUNT) == 4 From ace73d43b498491ec7b1758eaf2e5ad92b701181 Mon Sep 17 00:00:00 2001 From: Nishchay Mahor Date: Fri, 29 May 2026 17:39:27 -0700 Subject: [PATCH 2/2] test(qdrant): clear exporter at the start of async tests The session-scoped exporter is cleared via an autouse fixture between tests, but adding an explicit clear at the top of each async test makes the contract self-documenting and immune to future fixture changes. Addresses the coderabbitai review on #4202. --- .../tests/test_qdrant_instrumentation.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/opentelemetry-instrumentation-qdrant/tests/test_qdrant_instrumentation.py b/packages/opentelemetry-instrumentation-qdrant/tests/test_qdrant_instrumentation.py index 17f133ab66..debf028204 100644 --- a/packages/opentelemetry-instrumentation-qdrant/tests/test_qdrant_instrumentation.py +++ b/packages/opentelemetry-instrumentation-qdrant/tests/test_qdrant_instrumentation.py @@ -153,6 +153,8 @@ async def _async_setup(client: AsyncQdrantClient) -> None: def test_qdrant_async_query_points(exporter): + exporter.clear() + async def run() -> None: client = AsyncQdrantClient(location=":memory:") await _async_setup(client) @@ -175,6 +177,8 @@ async def run() -> None: def test_qdrant_async_query_batch_points(exporter): + exporter.clear() + async def run() -> None: client = AsyncQdrantClient(location=":memory:") await _async_setup(client)