Skip to content

Commit b176358

Browse files
123liuzimingclaude
andcommitted
fix(cognee): P0-1/2/3 — entry wrapper path, attribute migration, new_span patch
P0-1: ENTRY wrapper targeted ``cognee.api.v1.remember.remember`` etc., but in cognee v1.2.1 ``cognee.api.v1.remember`` is also re-exported as a function into the v1 package namespace, so the dotted-path wrap is ambiguous. Switch to wrapping the top-level ``cognee.{add,cognify,search, recall,remember}`` attributes — these are the canonical user-facing entry points re-exported by ``cognee/__init__.py``. P0-2: ``install_attribute_migration_patch`` monkey-patched ``cognee.modules.observability.new_span`` to wrap each returned span's ``set_attribute``. The wrap never actually ran (cognee re-imports ``new_span`` lazily in some code paths) AND broke the cognee exception path (``CollectionNotFoundError`` fallback was swallowed, triggering ``RuntimeError: generator didn't stop after throw()``). Delete the patch and move the ``_wrap_span_set_attribute`` call into ``CogneeAttributeSpanProcessor.on_start`` — the SDK Span is still mutable at ``on_start`` and the wrap is visible to all subsequent ``set_attribute`` calls in the span body. Also pre-inject ``gen_ai.task.name`` from the span name for ``cognee.pipeline.task.*`` so the attribute is present even when Cognee never sets ``cognee.pipeline.task_name``. P0-3: same as P0-2 — the broken ``_patched_new_span`` is removed. No ``try/except/finally`` re-raise variant is kept because the simpler on_start hook is correct and lower-risk. Tests updated: conftest now exposes ``cognee.add`` etc. at the top level (matching v1.2.1 re-exports); test_entry_wrapper calls ``cognee.add`` instead of ``cognee.api.v1.add.add``. All 46 tests pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 5b60003 commit b176358

5 files changed

Lines changed: 74 additions & 89 deletions

File tree

instrumentation-loongsuite/loongsuite-instrumentation-cognee/src/opentelemetry/instrumentation/cognee/__init__.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@
5656
)
5757
from opentelemetry.instrumentation.cognee.internal._span_processor import (
5858
CogneeAttributeSpanProcessor,
59-
install_attribute_migration_patch,
6059
)
6160
from opentelemetry.instrumentation.cognee.internal._step_wrapper import (
6261
install_step_wrapper,
@@ -142,17 +141,16 @@ def _instrument(self, **kwargs: Any) -> None:
142141
_enable_cognee_tracing(tracer_provider)
143142

144143
# Step 2: attach SpanProcessor for cognee.* span normalization.
144+
# The processor rewrites span name/kind/operation in on_start and
145+
# wraps span.set_attribute so cognee.* attributes set during the span
146+
# body are mirrored to gen_ai.* on the same span.
145147
self._cognee_processor = CogneeAttributeSpanProcessor()
146148
if hasattr(tracer_provider, "add_span_processor"):
147149
try:
148150
tracer_provider.add_span_processor(self._cognee_processor)
149151
except Exception as e:
150152
logger.debug("add_span_processor(CogneeAttributeSpanProcessor) failed: %s", e)
151153

152-
# Step 2b: wrap cognee.modules.observability.new_span so cognee.* attrs
153-
# set during the span body are mirrored to gen_ai.* attrs on the same span.
154-
install_attribute_migration_patch()
155-
156154
# Step 3: install wrappers (each in its own try/except — single failure
157155
# does not break the rest).
158156
install_entry_wrappers(telemetry_handler)

instrumentation-loongsuite/loongsuite-instrumentation-cognee/src/opentelemetry/instrumentation/cognee/internal/_entry_wrapper.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,18 @@
2727

2828

2929
# module_path, attr_name, operation_name (used for input extraction)
30+
# Cognee v1.2.1 re-exports ``add``/``cognify``/``search``/``recall``/``remember``
31+
# as top-level attributes of the ``cognee`` package (see ``cognee/__init__.py``).
32+
# ``cognee.api.v1.remember`` is also a function (re-exported into the v1 package
33+
# namespace by ``cognee/api/v1/__init__.py``), so wrapping at the v1 module
34+
# path would try to import a function as a module. Wrap at the top-level
35+
# ``cognee`` package instead — this is the canonical user-facing entry point.
3036
_ENTRY_TARGETS: tuple[tuple[str, str, str], ...] = (
31-
("cognee.api.v1.add", "add", "add"),
32-
("cognee.api.v1.cognify", "cognify", "cognify"),
33-
("cognee.api.v1.search", "search", "search"),
34-
("cognee.api.v1.recall", "recall", "recall"),
35-
("cognee.api.v1.remember", "remember", "remember"),
37+
("cognee", "add", "add"),
38+
("cognee", "cognify", "cognify"),
39+
("cognee", "search", "search"),
40+
("cognee", "recall", "recall"),
41+
("cognee", "remember", "remember"),
3642
)
3743

3844

instrumentation-loongsuite/loongsuite-instrumentation-cognee/src/opentelemetry/instrumentation/cognee/internal/_span_processor.py

Lines changed: 38 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,16 @@
99
Cognee-specific attributes (``cognee.search.query``, ``cognee.pipeline.task_name`` …)
1010
are set by Cognee's own code during the span body, after ``on_start`` runs.
1111
Because the SDK Span becomes read-only once ``end()`` is called, we cannot
12-
migrate them in ``on_end``. Instead, ``install_attribute_migration_patch``
13-
wraps ``cognee.modules.observability.new_span`` so the span returned to
14-
Cognee's caller has its ``set_attribute`` method intercepted — when Cognee
15-
sets a ``cognee.*`` key, we ALSO set the corresponding ``gen_ai.*`` key on
16-
the same span.
12+
migrate them in ``on_end``. Instead, ``on_start`` wraps the span's
13+
``set_attribute`` method so that when Cognee calls
14+
``span.set_attribute("cognee.search.query", value)`` we ALSO set the matching
15+
``gen_ai.*`` key on the same span. The wrapping is idempotent.
16+
17+
This replaces the previous ``install_attribute_migration_patch`` which patched
18+
``cognee.modules.observability.new_span`` — that patch broke Cognee's
19+
exception path (``CollectionNotFoundError`` fallback was swallowed, triggering
20+
``RuntimeError: generator didn't stop after throw()``) and never actually ran
21+
because cognee re-imports ``new_span`` lazily in some code paths.
1722
"""
1823

1924
from __future__ import annotations
@@ -52,8 +57,8 @@
5257
)
5358

5459
# Cognee attribute -> gen-ai attribute migration table.
55-
# Applied by the set_attribute interceptor installed via
56-
# ``install_attribute_migration_patch``.
60+
# Applied by the ``set_attribute`` interceptor installed on each Cognee span
61+
# in ``CogneeAttributeSpanProcessor.on_start``.
5762
_MIGRATION_MAP: dict[str, str] = {
5863
COGNEE_SEARCH_QUERY: "gen_ai.retrieval.query.text",
5964
COGNEE_PIPELINE_TASK_NAME: "gen_ai.task.name",
@@ -77,14 +82,23 @@ class CogneeAttributeSpanProcessor(SpanProcessor):
7782
Uses ``on_start`` because the SDK Span becomes immutable after ``end()``.
7883
The name is known at start time (passed to ``start_span``), so prefix-based
7984
rewriting works.
85+
86+
In ``on_start`` we also wrap the span's ``set_attribute`` method so that
87+
when Cognee code later calls ``span.set_attribute("cognee.search.query", v)``
88+
we mirror the write to ``gen_ai.retrieval.query.text`` on the same span.
89+
This is the only reliable hook point because the SDK Span is still mutable
90+
at ``on_start`` and the wrap is visible to all subsequent ``set_attribute``
91+
calls in the span body.
8092
"""
8193

8294
def on_start(self, span: Span, parent_context: Optional[Context] = None) -> None:
8395
try:
8496
name = span.name or ""
97+
matched = False
8598
for prefix, kind, op, new_name in _PREFIX_RULES:
8699
if not name.startswith(prefix):
87100
continue
101+
matched = True
88102
try:
89103
span.set_attribute("gen_ai.span.kind", kind)
90104
span.set_attribute("gen_ai.operation.name", op)
@@ -96,16 +110,30 @@ def on_start(self, span: Span, parent_context: Optional[Context] = None) -> None
96110
except Exception as e: # pragma: no cover - defensive
97111
logger.debug("update_name failed: %s", e)
98112
elif prefix == "cognee.pipeline.task.":
113+
task_name = name[len(prefix) :]
99114
try:
100-
span.update_name(f"task {name[len(prefix):]}")
115+
span.update_name(f"task {task_name}")
116+
# Pre-inject gen_ai.task.name from span name so the
117+
# attribute is present even if Cognee never sets
118+
# cognee.pipeline.task_name (e.g. for early-exit paths).
119+
span.set_attribute("gen_ai.task.name", task_name)
101120
except Exception as e: # pragma: no cover - defensive
102121
logger.debug("update_name(task) failed: %s", e)
103122
elif prefix == "cognee.retrieval.":
104123
try:
105-
span.update_name(f"retrieval {name[len('cognee.retrieval.') :]}")
124+
span.update_name(
125+
f"retrieval {name[len('cognee.retrieval.') :]}"
126+
)
106127
except Exception as e: # pragma: no cover - defensive
107128
logger.debug("update_name(retrieval) failed: %s", e)
108-
return
129+
break
130+
# Wrap set_attribute for any Cognee-native span so cognee.* attrs
131+
# set during the span body are mirrored to gen_ai.* on the same
132+
# span. We do this regardless of whether the prefix matched,
133+
# because Cognee may create spans whose names we don't recognize
134+
# yet still set cognee.* attributes worth migrating.
135+
if matched or name.startswith("cognee."):
136+
_wrap_span_set_attribute(span)
109137
except Exception as e: # pragma: no cover - defensive
110138
logger.debug("CogneeAttributeSpanProcessor.on_start failed: %s", e)
111139

@@ -120,55 +148,6 @@ def force_flush(self, timeout_millis: int = 30000) -> bool:
120148
return True
121149

122150

123-
def install_attribute_migration_patch() -> None:
124-
"""Patch ``cognee.modules.observability.new_span`` to migrate cognee.* attrs.
125-
126-
When Cognee calls ``span.set_attribute("cognee.search.query", value)``, we
127-
also call ``span.set_attribute("gen_ai.retrieval.query.text", value)`` on
128-
the same span. This runs before ``end()`` so the SDK Span is still mutable.
129-
"""
130-
try:
131-
import cognee.modules.observability as cognee_obs # type: ignore
132-
except ImportError:
133-
logger.debug(
134-
"cognee.modules.observability not importable — attribute migration patch skipped"
135-
)
136-
return
137-
138-
original = getattr(cognee_obs, "new_span", None)
139-
if original is None or getattr(original, "_cognee_genai_patched", False):
140-
return
141-
142-
try:
143-
import contextlib
144-
145-
@contextlib.contextmanager
146-
def _patched_new_span(name: str):
147-
ctx = original(name) if original else _noop_ctx()
148-
try:
149-
with ctx as span:
150-
if span is not None and not isinstance(span, _NullSpanType):
151-
_wrap_span_set_attribute(span)
152-
yield span
153-
except Exception:
154-
# Fall back to yielding a null span if the original blows up.
155-
yield cognee_obs._NullSpan()
156-
157-
def _noop_ctx():
158-
import contextlib
159-
160-
@contextlib.contextmanager
161-
def _cm():
162-
yield None
163-
164-
return _cm()
165-
166-
_patched_new_span._cognee_genai_patched = True # type: ignore[attr-defined]
167-
cognee_obs.new_span = _patched_new_span
168-
except Exception as e:
169-
logger.debug("install_attribute_migration_patch failed: %s", e)
170-
171-
172151
def _wrap_span_set_attribute(span: Any) -> None:
173152
"""Wrap a single span's ``set_attribute`` to mirror cognee.* → gen_ai.*.
174153
@@ -199,14 +178,3 @@ def _set_attribute(key, value):
199178
span._cognee_genai_set_attr_wrapped = True # type: ignore[attr-defined]
200179
except (AttributeError, TypeError) as e:
201180
logger.debug("could not wrap span.set_attribute: %s", e)
202-
203-
204-
# Late import to avoid hard dependency on cognee at module load.
205-
try:
206-
from cognee.modules.observability import (
207-
_NullSpan as _NullSpanType, # type: ignore
208-
)
209-
except ImportError: # pragma: no cover - cognee not installed
210-
211-
class _NullSpanType: # type: ignore[no-redef]
212-
pass

instrumentation-loongsuite/loongsuite-instrumentation-cognee/tests/conftest.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,15 @@ def _ensure_fake_module(dotted_path: str, attrs: dict) -> types.ModuleType:
128128

129129
@pytest.fixture
130130
def fake_cognee_v1_api():
131-
"""Install fake ``cognee.api.v1.{add,cognify,search,recall,remember}`` modules."""
131+
"""Install fake ``cognee.{add,cognify,search,recall,remember}`` entry points.
132+
133+
The instrumentor wraps the top-level ``cognee`` package attributes (not the
134+
deep ``cognee.api.v1.add`` module path) because in v1.2.1
135+
``cognee.api.v1.remember`` is re-exported as a function into the v1
136+
package namespace, which makes the dotted-path wrap ambiguous. We still
137+
install the deep module hierarchy so callers that import it directly
138+
keep working.
139+
"""
132140
async def add(data, *args, **kwargs):
133141
return {"added": data}
134142

@@ -144,14 +152,20 @@ async def recall(query_text, *args, **kwargs):
144152
async def remember(data, *args, **kwargs):
145153
return {"remembered": data}
146154

147-
_ensure_fake_module("cognee", {})
155+
cognee_root = _ensure_fake_module("cognee", {})
148156
_ensure_fake_module("cognee.api", {})
149157
_ensure_fake_module("cognee.api.v1", {})
150158
_ensure_fake_module("cognee.api.v1.add", {"add": add})
151159
_ensure_fake_module("cognee.api.v1.cognify", {"cognify": cognify})
152160
_ensure_fake_module("cognee.api.v1.search", {"search": search})
153161
_ensure_fake_module("cognee.api.v1.recall", {"recall": recall})
154162
_ensure_fake_module("cognee.api.v1.remember", {"remember": remember})
163+
# Top-level re-exports — this is what the instrumentor wraps.
164+
cognee_root.add = add
165+
cognee_root.cognify = cognify
166+
cognee_root.search = search
167+
cognee_root.recall = recall
168+
cognee_root.remember = remember
155169
yield
156170

157171

instrumentation-loongsuite/loongsuite-instrumentation-cognee/tests/test_entry_wrapper.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,11 @@ def instrumented(fake_cognee_v1_api, monkeypatch):
5959
@pytest.mark.asyncio
6060
async def test_entry_add_creates_entry_span(instrumented):
6161
instrumentor, exporter, provider = instrumented
62-
# We need to call the wrapped cognee.add via the instrumented module.
63-
import cognee.api.v1.add as add_mod
62+
# The instrumentor wraps the top-level ``cognee.add`` attribute (v1.2.1
63+
# re-exports the V1 API functions at the package root).
64+
import cognee as cognee_root
6465

65-
result = await add_mod.add("hello world")
66+
result = await cognee_root.add("hello world")
6667
assert result == {"added": "hello world"}
6768

6869
spans = exporter.get_finished_spans()
@@ -78,11 +79,9 @@ async def test_entry_add_creates_entry_span(instrumented):
7879
@pytest.mark.asyncio
7980
async def test_entry_search_propagates_session_id(instrumented):
8081
instrumentor, exporter, _ = instrumented
81-
import cognee.api.v1.search as search_mod
82+
import cognee as cognee_root
8283

83-
await search_mod.search(
84-
"what is cognee", session_id="sess-123"
85-
)
84+
await cognee_root.search("what is cognee", session_id="sess-123")
8685

8786
spans = exporter.get_finished_spans()
8887
entry_spans = [

0 commit comments

Comments
 (0)