Fix/events search descendants only#373
Conversation
… to descendants Events search used to return every span in any matching trace — siblings, parents, and uncles included — so a query like `kind=="server" AND sdk_type=="JsExpress"` would also pull in pg.query, pg-pool.connect, and internal spans that failed the predicate. Two fixes: 1. Default to exact match. New `with_children` flag (POST body field on /api/v1/events/query, query param on GET /api/v1/events) opts back into trace context. CLI exposes `--with-children`. UI handler still passes True to preserve the LogExplorer waterfall. 2. When expansion is on, return only descendants of matched rows. The broad SQL fetch in selectChildSpansAndLogs is preserved for performance (one indexed scan on the partitioned otel_logs_and_spans), and a new keepDescendantsOf BFS trims the candidate set to actual sub-trees of the predicate hits. The function previously returned the whole trace despite its name. Caught a latent bug along the way: selectChildSpansAndLogs built its column-index map from getProcessedColumns output, which retains "<expr> as <alias>" strings — so lookups for "latency_breakdown" / "parent_id" silently returned Nothing. Piped through listToColNames so the alias-only keys match.
Code ReviewOverviewThis PR fixes a correctness bug in trace-tree expansion: previously The logic is sound and the test coverage is solid. A few issues worth addressing: Issues1. Unnecessary
|
|
Code review posted below (body split to avoid shell quoting issues) |
|
Code Review - issues found in keepDescendantsOf (LogQueries.hs): 1) Drop Data.Map.Strict import - HM.HashMap already imported gives O(1) avg vs O(log n). 2) O(n^2) list appends in BFS - rest <> newSids and acc <> newKids are O(n) each step; concat/reverse pattern fixes it. 3) maybe True should be maybe False in newKids filter - rows without span_id pass through but never enter visited so they cannot be de-duplicated. 4) V.toList seedSpanIds computed twice in bfs call. Positives: design rationale comment is exemplary, default exact-match behaviour is correct, sanity-check in tests is good, apiLogExpandH update is minimal. |
- Drop Data.Map.Strict; reuse the already-imported HashMap (HM is O(1) avg, and avoids a new import surface for one helper). - Fix latent bug: rows missing a span_id (latency_breakdown) used to pass the visited-set filter unconditionally via `maybe True`, leaking unidentifiable rows into descendants. Flip to `maybe False` — drop them, since we can't dedup or descend from them. - Switch BFS to prepend-then-reverse to avoid O(n²) `acc <> newKids` / `rest <> newSids` chains that materialise quadratic spines under lazy evaluation. - Cache `V.toList seedSpanIds` (used twice). - Use `<$ guard` for the CLI flag — matches the surrounding style for Maybe-shaped param entries. - Clarify the `count` vs `logsData length` assertion in the LogSpec trace-tree test.
Code ReviewOverview: Fixes the regression where search returned every span sharing a trace_id with a predicate hit (siblings, parents, uncles). Now the API/CLI default is exact-match only; Bug / Correctness
bfs visited' (newSids <> rest) (newKids <> acc)
-- ^^^^^^^^^^^^^^^^ prepend = stack = DFS
Code Succinctness / Style
In this codebase -- current
pid' r = lookupVecTextByKey r colIdxMap "parent_id"
-- suggested
parentId r = lookupVecTextByKey r colIdxMap "parent_id"
seedSpanIds = V.mapMaybe (\v -> lookupVecTextByKey v colIdxMap "latency_breakdown") requestVecsThis expression is not duplicated in the diff itself, but it mirrors the same pattern used three lines above for Over-commenting (project style: comment the why, not the what) Several blocks explain implementation mechanics that are legible from the code:
Test CoverageNear-identical fixture setup duplicated across two test files
Sanity-check assertion in Minor
Overall: the fix is correct, the in-memory descendant walk is well-reasoned (the "why not recursive CTE" Haddock is worth keeping), and the regression tests cover the exact failure mode. Address the |
The existing 'should paginate through multiple pages using cursor' test
fetches page 1 and asserts hasMore=True, but never actually fetches
page 2 — so a broken cursor would silently pass. It also ingests 1000
messages all at frozenTime, so any ordering bug across the cursor
boundary would be hidden by ties.
New test walks pages 1 → 2 → 3 from 5 rows ingested at distinct
timestamps with `| limit 2`, asserting:
* each page returns the exact expected slice (idx [1,2], [3,4], [5])
* the -0.001s cursor nudge actually skips the boundary row (no
overlap between consecutive pages)
* hasMore flips False on the last page
Identifies rows by querying `attributes.page.idx` from the DB after
extracting `latency_breakdown` (= context___span_id) from each result
row — LogResult doesn't surface attributes directly.
Code Review — Fix/events search descendants onlyOverviewThis PR fixes a significant correctness bug: the events search API was returning every span in a matching trace (siblings, uncles, unrelated roots), not just predicate hits and their descendants. The fix adds an in-memory BFS/DFS filter after fetching candidate spans via a single broad SQL scan, plus a new The approach is sound and well-tested. A few points worth addressing: Bug:
|
Code ReviewOverview: Fixes the search-returns-whole-trace bug by introducing a seed-based BFS filter ( CorrectnessThe BFS logic is correct: builds a One subtle consequence: Succinctness / packages we already have
HM.findWithDefault [] s childrenByParentUsed elsewhere in the codebase (
listToIndexHashMap list = HM.fromList [(x, i) | (x, i) <- zip list [0 ..]]The list comprehension destructures listToIndexHashMap = HM.fromList . flip zip [0..]Worth fixing in Utils while you're touching this module. Comments / verbosityThe inline block inside API surfaceThe The TestsSolid. The ingest-then-sanity-check pattern in the tree expansion test (asserting the persisted row names before running the search) is good defensive testing. The cursor pagination test is well-structured with clear per-page expectations. Both tests make good use of real DB rather than mocks, consistent with project convention. Minor: SummaryNo correctness bugs found. Actionable improvements in order of priority:
|
- Rename bfs → go (the queue prepended new nodes, making it DFS in practice; correctness is unaffected but the name was misleading). - HM.findWithDefault [] over fromMaybe [] (HM.lookup ...) for the parent→children lookup, matching the library's intended API. - Drop two over-explanatory inline comments inside keepDescendantsOf (the maybe False rationale and the prepend-build-then-reverse note); Haddock already covers what's load-bearing. - Document in Haddock that seeds are not included in the output — callers already hold them in requestVecs. - Note on the V.null seedSpanIds early return: kept to skip the SQL round-trip; comment why so the redundancy with keepDescendantsOf's guard reads as intentional.
Code Review — Fix/events search descendants onlySummaryFixes a long-standing correctness bug: the events search was returning every span in a matched trace (siblings, parents, uncles) rather than just predicate hits. The fix adds a Correctness
go visited' (newSids <> rest) (newKids <> acc)
-- ^^ prepend children = DFS order for the work queue
-- but newKids <> acc + reverse acc at the end produces an unpredictable output order
childrenByParent = HM.fromListWith (<>) [(p, [r]) | r <- rows, Just p <- [pid' r]]
Silent failure risk — hardcoded column aliases
-- LogQueries.hs
sid r = lookupVecTextByKey r colIdxMap "latency_breakdown"
pid' r = lookupVecTextByKey r colIdxMap "parent_id"
-- Log.hs (two call sites)
seedSpanIds = V.mapMaybe (\v -> lookupVecTextByKey v colIdxMap "latency_breakdown") requestVecsIf the API/type nits
The Signature growth in buildLogResult :: (DB es, Time.Time :> es) => Bool -> Projects.ProjectId -> UTCTime -> Maybe Text -> Maybe Text -> Maybe Text -> [Text] -> (...) -> Eff es LogResultThe function now has 9 positional arguments, the first four of which are the same type ( PerformanceThe strategy comment in the haddock is good. One additional note: the 2000-row TestsThe regression tests are well-structured:
Minor gap: there's no explicit test for the Misc style
|
Closes #
How to test
Checklist