Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import inspect
import logging
import traceback
from opentelemetry.instrumentation.lancedb.config import Config
Expand Down Expand Up @@ -26,3 +27,28 @@ def wrapper(*args, **kwargs):
Config.exception_logger(e)

return wrapper


def get_call_arguments(wrapped, args, kwargs):
"""
Returns the arguments of a wrapped call as a single name -> value dict.

SDK methods are commonly called positionally (for example
``table.add(data)``), so reading ``kwargs`` alone would
silently drop those arguments. Extra keyword arguments collected by a
``**kwargs`` parameter are flattened into the result. Falls back to
``kwargs`` if the signature cannot be bound.
"""
try:
signature = inspect.signature(wrapped)
bound = signature.bind_partial(*args, **kwargs)
call_args = {}
for name, value in bound.arguments.items():
kind = signature.parameters[name].kind
if kind is inspect.Parameter.VAR_KEYWORD:
call_args.update(value)
elif kind is not inspect.Parameter.VAR_POSITIONAL:
call_args[name] = value
return call_args
except Exception:
return dict(kwargs)
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from opentelemetry.instrumentation.lancedb.utils import dont_throw
from opentelemetry.instrumentation.lancedb.utils import dont_throw, get_call_arguments
from opentelemetry.semconv.trace import SpanAttributes
from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE
from opentelemetry.trace.status import Status, StatusCode
Expand Down Expand Up @@ -42,12 +42,13 @@ def _wrap(tracer, to_wrap, wrapped, instance, args, kwargs):
span.set_attribute(SpanAttributes.DB_SYSTEM, "lancedb")
span.set_attribute(SpanAttributes.DB_OPERATION, to_wrap.get("method"))

call_args = get_call_arguments(wrapped, args, kwargs)
if to_wrap.get("method") == "add":
_set_add_attributes(span, kwargs)
_set_add_attributes(span, call_args)
elif to_wrap.get("method") == "search":
_set_search_attributes(span, kwargs)
_set_search_attributes(span, call_args)
elif to_wrap.get("method") == "delete":
_set_delete_attributes(span, kwargs)
_set_delete_attributes(span, call_args)

try:
return_value = wrapped(*args, **kwargs)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""Regression tests: span attributes must be captured for positional arguments.

LanceDB documents ``table.add(data)``, ``table.search(query)`` and
``table.delete(where)`` as positional calls, so the instrumentation must not
rely on ``kwargs`` alone. These tests use a local, temporary LanceDB.
"""

import lancedb
import pytest
from opentelemetry.semconv_ai import SpanAttributes

ROWS = [
{"vector": [1.3, 1.4], "item": "fizz", "price": 100.0},
{"vector": [9.5, 56.2], "item": "buzz", "price": 200.0},
]


@pytest.fixture
def table(tmp_path):
db = lancedb.connect(str(tmp_path))
return db.create_table("positional_table", data=ROWS)


def _span(exporter, name):
return next(s for s in exporter.get_finished_spans() if s.name == name)


def test_add_positional_args(exporter, table):
exporter.clear()
table.add(ROWS)

span = _span(exporter, "lancedb.add")
assert span.attributes.get(SpanAttributes.MILVUS_INSERT_DATA_COUNT) == 2


def test_search_positional_args(exporter, table):
exporter.clear()
table.search([100, 100]).limit(2).to_pandas()

span = _span(exporter, "lancedb.search")
assert span.attributes.get(SpanAttributes.MILVUS_SEARCH_FILTER) == "[100, 100]"


def test_delete_positional_args(exporter, table):
exporter.clear()
table.delete("item = 'fizz'")

span = _span(exporter, "lancedb.delete")
assert span.attributes.get(SpanAttributes.CHROMADB_DELETE_WHERE) == "item = 'fizz'"