feat(tracing): add memory read-path spans - #1252
Conversation
Add framework-neutral Runtime tracing backed by ServerTracing. Trace memory search, reranking, experience recall, and context preparation. Cover span hierarchy, privacy, cancellation, and tracing failure isolation. Closes oceanbase#1241
| attributes={ | ||
| **attributes, | ||
| "powercontext.operation.name": name, | ||
| "powercontext.operation.unit": "stage", |
There was a problem hiding this comment.
This adds a fifth value for powercontext.operation.unit, but the docs that define the vocabulary aren't updated: the unit table in docs/{en,zh}/rfcs/0046_observability_foundations.md
and the span table in docs/{en,zh}/docs/how-to/trace-with-phoenix.md
.
There was a problem hiding this comment.
Get, it was an oversight on my part, and I will add it later.
| def policy_id(self) -> str: | ||
| return self._delegate.policy_id | ||
|
|
||
| @policy_id.setter |
There was a problem hiding this comment.
The policy_id setter has no callers — policy_id is only ever read, at service.py. I removed locally: uv run ty check stays green and tests/e2e/test_builtin_runtime.py + tests/e2e/test_observability.py still pass (8 passed). The getter is needed — _DynamicPolicyReranker covers the dynamic read — just not the setter.
What is the purpose of adding this setter?
There was a problem hiding this comment.
The setter was added because MemoryReranker declares policy_id as a read-write property, and the tracing decorator needs to fully proxy this interface.
It exists to forward future assignments made to the wrapper's policy_id to the underlying reranker; it is not required for the current business logic. Scenarios involving dynamic policy_id updates actually rely solely on the getter.
@Ethan-Xingyue @PsiACE Do you think it should be kept? Unless policy_id is explicitly defined as a read-only property, I would prefer to retain the setter functionality.
There was a problem hiding this comment.
policy_id is plain state, so please keep it as a regular attribute instead of proxying it through a property with a setter.
There was a problem hiding this comment.
Okay, I'll modify _TracingMemoryReranker to use standard properties and synchronize the latest value to the delegate after rerank completes.
| span.set_attributes({"powercontext.experience.search.result_count": len(experience_hits)}) | ||
|
|
||
| with self._runtime._stage( | ||
| "context.prepare", |
There was a problem hiding this comment.
Minor naming thought: this sits right next to the existing powercontext prepare_context span (app.py:1299), which covers the whole request, while this one wraps just the synchronous builder.build_result(). Might be worth something like context.assemble or context.build to make the difference obvious in a trace viewer?
There was a problem hiding this comment.
Get, I think it could be changed to context.build, as context.assemble might misleadingly imply the inclusion of the preceding Memory/Experience search, making the scope seem a bit too broad.
|
Just a thought, not a blocker: _context() (which awaits self._provider.get() and opens a DB session) and the wait on _lock(scope_id) both happen before the first stage span opens, so they don't show up in the trace. Since the goal here is locating latency in the read path, and test_same_scope_read_only_searches_do_not_serialize_reranking suggests lock contention is already on your radar, those might be interesting to cover eventually. |
|
Those are my thoughts so far; let's wait for @PsiACE's review before settling on the specifics. |
PsiACE
left a comment
There was a problem hiding this comment.
Please focus the tests on exported tracing behavior and real regressions: drop the dynamic policy_id, fake _RecordingTracing, and synthetic SystemExit cases, and cover the actual vector-search span tree plus the readiness probe's no-root-span behavior.
@Ethan-Xingyue I noticed a detail here: the default Given that context retrieval and lock waiting are runtime concerns spanning multiple stages—whereas the scope of this PR is limited to the memory read phase—I intend to keep the PR focused. We can add |
Which issue or RFC does this PR close?
Closes #1241
Rationale for this change
Memory read requests currently expose only top-level application spans, making it difficult to determine whether latency comes from retrieval, embeddings, reranking, Experience recall, or context assembly.
This change adds stage-level tracing while keeping OpenTelemetry owned by the Server layer. The Runtime remains framework-neutral, and tracing failures do not affect search behavior.
What changes are included in this PR?
RuntimeSpanandRuntimeTracingprotocols.BuiltinRuntime.ServerTracing.stage()using OpenTelemetryINTERNALspans.success,failure, andcancelledoutcomes while preserving original exceptions.memory.searchmemory.rerankexperience.searchcontext.preparepolicy_id.Validated span trees:
Are there any user-facing changes?
Operators with tracing enabled will see the additional internal spans and bounded attributes.
Search results and error behavior remain unchanged. This PR does not change public APIs, OpenAPI, CLI behavior, database schemas, persisted formats, or dependencies.
How was this change tested?
The tests also cover missing Memory, unconfigured Experience recall, empty results, reranker fallback, embedding and agent span nesting, sensitive-data exclusion, cancellation, and tracing failure isolation.
AI usage statement
OpenAI Codex (GPT-5.6-Sol) was used to help analyze the issue, assist with making changes, write tests.