Skip to content

Commit a78672e

Browse files
HYBIM-513 Push down attributes to Weaviate (#247)
1 parent 460a978 commit a78672e

2 files changed

Lines changed: 80 additions & 7 deletions

File tree

instrumentation-genai/opentelemetry-instrumentation-weaviate/src/opentelemetry/instrumentation/weaviate/wrapper.py

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,13 @@
4040
parse_url_to_host_port,
4141
)
4242

43+
# GenAI context attributes to propagate from parent agent/workflow spans
44+
_GENAI_PROPAGATE_ATTRS = (
45+
"gen_ai.agent.name",
46+
"gen_ai.agent.id",
47+
"gen_ai.workflow.name",
48+
)
49+
4350

4451
# Context variable for passing connection info within operation call stacks
4552
_connection_host_context: ContextVar[Optional[str]] = ContextVar(
@@ -163,6 +170,10 @@ def _create_db_span(
163170
wrapped, instance, args, kwargs, module_name, function_name
164171
)
165172

173+
# Capture parent span before entering the new span so we can propagate
174+
# gen_ai agent/workflow attributes set by LangChain, CrewAI, LlamaIndex, etc.
175+
parent_span = trace.get_current_span()
176+
166177
with self.tracer.start_as_current_span(name, kind=SpanKind.CLIENT) as span:
167178
span.set_attribute(DbAttributes.DB_SYSTEM_NAME, "weaviate")
168179

@@ -176,6 +187,8 @@ def _create_db_span(
176187
if connection_port is not None:
177188
span.set_attribute(ServerAttributes.SERVER_PORT, connection_port)
178189

190+
self._propagate_genai_context(span, parent_span)
191+
179192
# Record operation duration as a metric
180193
start_time = time.perf_counter()
181194
try:
@@ -196,14 +209,9 @@ def _create_db_span(
196209
if connection_port is not None:
197210
metric_attributes[ServerAttributes.SERVER_PORT] = connection_port
198211

199-
# Record the duration metric with span context to link metric to trace
200-
try:
201-
context = trace.set_span_in_context(span)
202-
except (TypeError, ValueError, AttributeError):
203-
context = None
204-
212+
# Record the duration metric (automatically linked to current span context)
205213
self.duration_histogram.record(
206-
duration_ms, attributes=metric_attributes, context=context
214+
duration_ms, attributes=metric_attributes
207215
)
208216

209217
# Extract documents from similarity search operations
@@ -247,6 +255,29 @@ def _create_db_span(
247255

248256
return return_value
249257

258+
def _propagate_genai_context(self, span: Any, parent_span: Any) -> None:
259+
"""Copy gen_ai agent/workflow attributes from the parent span into the Weaviate span.
260+
261+
This allows Weaviate spans to inherit context (e.g. gen_ai.agent.name,
262+
gen_ai.workflow.name) set by upstream instrumentations such as LangChain,
263+
CrewAI, or LlamaIndex, without taking a dependency on splunk-otel-util-genai.
264+
"""
265+
try:
266+
if parent_span is None or not parent_span.is_recording():
267+
return
268+
parent_attrs = getattr(parent_span, "_attributes", None) or getattr(
269+
parent_span, "attributes", None
270+
)
271+
if not parent_attrs:
272+
return
273+
for attr in _GENAI_PROPAGATE_ATTRS:
274+
val = parent_attrs.get(attr)
275+
if val is not None:
276+
span.set_attribute(attr, val)
277+
except Exception as e:
278+
if Config.exception_logger:
279+
Config.exception_logger(e)
280+
250281
def _is_similarity_search(self) -> bool:
251282
"""Check if the operation is a similarity search or retrieval operation that returns documents."""
252283
function_name = self.wrap_properties.get("function", "")

instrumentation-genai/opentelemetry-instrumentation-weaviate/tests/test_span_attributes.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,48 @@ def mock_graphql_func(*args, **kwargs):
524524
assert event.name == "weaviate.document"
525525

526526

527+
class TestGenAIContextPropagation:
528+
"""Test that gen_ai agent/workflow attributes are propagated from parent spans."""
529+
530+
def test_propagates_agent_and_workflow_attrs_from_parent(
531+
self, tracer_provider, span_exporter
532+
):
533+
"""Test that gen_ai.agent.name, gen_ai.agent.id, and gen_ai.workflow.name
534+
are copied from an active parent span into the Weaviate span."""
535+
trace.set_tracer_provider(tracer_provider)
536+
tracer = tracer_provider.get_tracer(__name__)
537+
mock_histogram = MagicMock()
538+
539+
wrapper = _WeaviateOperationWrapper(
540+
tracer=tracer,
541+
duration_histogram=mock_histogram,
542+
wrap_properties={
543+
"module": "weaviate.collections.queries.fetch_objects",
544+
"function": "fetch_objects",
545+
"span_name": "collections.query.fetch_objects",
546+
},
547+
)
548+
549+
def mock_op(*args, **kwargs):
550+
return MagicMock(objects=[])
551+
552+
with tracer.start_as_current_span("invoke_agent my_agent") as parent_span:
553+
parent_span.set_attribute("gen_ai.agent.name", "my_agent")
554+
parent_span.set_attribute("gen_ai.agent.id", "agent-123")
555+
parent_span.set_attribute("gen_ai.workflow.name", "my_workflow")
556+
557+
wrapper(mock_op, None, (), {})
558+
559+
spans = span_exporter.get_finished_spans()
560+
weaviate_span = next((s for s in spans if "fetch_objects" in s.name), None)
561+
assert weaviate_span is not None
562+
563+
attrs = dict(weaviate_span.attributes or {})
564+
assert attrs.get("gen_ai.agent.name") == "my_agent"
565+
assert attrs.get("gen_ai.agent.id") == "agent-123"
566+
assert attrs.get("gen_ai.workflow.name") == "my_workflow"
567+
568+
527569
@pytest.mark.integration
528570
@pytest.mark.skipif(
529571
not WEAVIATE_AVAILABLE or WEAVIATE_VERSION < 4,

0 commit comments

Comments
 (0)