Skip to content

feat(app): search across multiple log and trace sources at once - #2862

Closed
teeohhem wants to merge 3 commits into
mainfrom
tom/multi-source-search
Closed

feat(app): search across multiple log and trace sources at once#2862
teeohhem wants to merge 3 commits into
mainfrom
tom/multi-source-search

Conversation

@teeohhem

@teeohhem teeohhem commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Searching has meant picking exactly one source, so a question that spans sources (several log sources, or logs plus traces) takes multiple tabs and manual correlation. The search page's source selector can now expand into a multi-select (up to 3 log/trace sources): every selected source runs its own query pipeline, and the results interleave into one timestamp-ordered timeline with per-row source badges, normalized columns (Timestamp, Source, Service, Level, Message, and Duration when traces are included), a histogram stacked by source, and summed totals.

Schemas are reconciled by projecting each source's semantic expressions (timestampValueExpression, severityTextExpression/statusCodeExpression, bodyExpression/spanNameExpression, ...) under shared column aliases, and an add-column picker offers the union of the selected sources' columns (blank cells where a source lacks one). There is no UNION ALL: sources can live on different ClickHouse connections, each source keeps its per-table machinery (Lucene serializer, text-index detection, materialized-column rewrites, query settings), and a failing source degrades to a status chip instead of failing the whole search. A client-side k-way merge orders rows behind a "safe frontier" (the timestamp every source has covered), so the timeline never shows a gap a slower source could still fill, and "fetch more" advances only the lagging source.

The selection is shareable via a ?sources= URL param. Multi mode is Lucene-only for now; saved searches, alerts, sidebar filters, and delta/pattern modes remain single-source and are gated off with explanations. Single-source search is unchanged. Also fixes chSqlToAliasMap dropping NULL literal projections, which made row-WHERE clauses reference nonexistent columns.

Screenshots or video

tom-multi-source-search-20260811-145206-004402-multi-source-search-demo

Performance

image

Each source’s stream arrives already timestamp-sorted (its SQL ORDER BY), and the browser does a k-way merge: parse timestamps (cached per distinct raw value), filter to the “safe frontier,” one stable sort. Row volume is bounded — 200 rows/source/page, the table stops auto-fetching at 1,000 rendered rows, and live mode caps retention at 5 pages/source — so the merge sorts a few thousand rows at most, single-digit milliseconds against network round-trips of hundreds. Memory is a few MB worst case. Client-side merging is also what buys the properties we wanted: sources on different ClickHouse connections work, and one failing source degrades to a chip instead of killing the query. The trade-off it does have: total rows read from ClickHouse can exceed what’s displayed, because each source fetches its own LIMIT while the frontier holds some rows back — that’s the conservative-correctness cost, bounded by the same caps.

How to test on Vercel preview

Preview routes: /search

Steps:

  1. Open /search and wait for search result rows to appear.
  2. Click the "+" button next to the source selector (data-testid "add-search-source").
  3. Click the sources multi-select (data-testid "source-multi-selector") and select "Demo Traces" in the dropdown, then press Escape to close the dropdown.
  4. Wait a few seconds for the search to re-run automatically.
  5. Verify the results table shows a "Source" column with both "Demo Logs" and "Demo Traces" badges interleaved in one timestamp-descending list, and the histogram legend lists both sources.

The search page's source selector can expand into a multi-select (up to
5 log/trace sources). Each selected source runs its own query pipeline
(own connection, Lucene serializer, windowed pagination) with its SELECT
rewritten to canonical aliases; a client-side k-way merge interleaves
the streams by timestamp behind a "safe frontier" so the timeline never
shows a gap a lagging source could still fill. Results render in one
table with per-row source badges, a histogram stacked by source, summed
totals, and an add-column picker over the union of the sources' columns.

Multi mode is Lucene-only and URL-shareable (?sources=). Saved searches,
alerts, filters, and delta/pattern modes stay single-source and are
gated off with explanations; single-source behavior is unchanged.

Also fixes chSqlToAliasMap dropping NULL-literal projections, which
would have made row-WHERE clauses reference nonexistent columns.
@changeset-bot

changeset-bot Bot commented Aug 11, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 0ab39a2

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@hyperdx/app Minor
@hyperdx/common-utils Minor
@hyperdx/api Minor
@hyperdx/otel-collector Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Aug 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hyperdx-oss Ready Ready Preview Aug 11, 2026 3:17pm
hyperdx-storybook Ready Ready Preview Aug 11, 2026 3:17pm

Request Review

@greptile-apps

greptile-apps Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR adds concurrent search across multiple log and trace sources, normalizes and merges their rows, and renders combined counts and histograms.

  • Adds URL-backed multi-source selection and per-source query configuration.
  • Adds frontier-based client-side stream merging and pagination.
  • Adds normalized result columns, source badges, combined charts, and column selection.
  • Updates shared ClickHouse alias handling and search-chart configuration.

Confidence Score: 4/5

The PR is not yet safe to merge because source-defined ascending or non-timestamp-first ordering can still violate the merged timeline's descending-stream assumption.

Explicit source order expressions are passed through unchanged while the multi-source merge always applies descending frontier semantics, leaving the previously reported ordering failure reachable.

Files Needing Attention: packages/app/src/DBSearchPage.tsx and packages/app/src/components/MultiSourceRowTable.tsx

Important Files Changed

Filename Overview
packages/app/src/DBSearchPage.tsx Adds multi-source URL/form state, source-specific query specifications, and conditional multi-source result rendering.
packages/app/src/components/MultiSourceRowTable.tsx Orchestrates per-source paginated queries, merges normalized rows, and integrates row details and source status.
packages/app/src/utils/multiSourceMerge.ts Implements timestamp-based stream coverage, frontier calculation, row interleaving, and lagging-source selection.
packages/app/src/components/MultiSourceTimeChart.tsx Executes and combines per-source histogram and count queries for the multi-source presentation.
packages/common-utils/src/core/searchChartConfig.ts Adds canonical multi-source search projections while preserving each source's query semantics.
packages/common-utils/src/clickhouse/index.ts Preserves NULL literal projections in SQL alias recovery for downstream row predicates.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  URL[URL source selection] --> Config[Per-source chart configs]
  Config --> Q1[Source query 1]
  Config --> Q2[Source query 2]
  Config --> QN[Source query N]
  Q1 --> Merge[Safe-frontier merge]
  Q2 --> Merge
  QN --> Merge
  Merge --> Table[Normalized result timeline]
  Config --> Hist[Per-source histograms]
  Hist --> Chart[Stacked chart and summed count]
Loading

Reviews (3): Last reviewed commit: "feat(app): cap multi-source search at 3 ..." | Re-trigger Greptile

Comment thread packages/app/src/DBSearchPage.tsx
{streams.map((stream, i) => {
if (stream.spec == null) return null;
const name = stream.spec.source.name;
return (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Multi-source components exceed size limit

This new 472-line component combines query orchestration, pagination, merge state, side-panel state, and rendering; MultiSourceTimeChart.tsx is also 320 lines. Split these concerns into smaller modules to satisfy the repository's 300-line component limit and make the behavior independently testable.

Context Used: CLAUDE.md (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code Fix in Conductor Fix in Cursor Fix in Codex

@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

E2E Test Results

All tests passed • 276 passed • 1 skipped • 906s

Status Count
✅ Passed 276
❌ Failed 0
⚠️ Flaky 1
⏭️ Skipped 1

Tests ran across 4 shards in parallel.

View full report →

Three components were each hand-unrolling slot0..slot4 hook calls to keep
a constant hook count across a variable source selection. Rules of hooks
allow calling a use*-named function parameter from a custom hook, so one
generic useMultiSourceSlots(items, useSlot, opts) now owns the unrolling
and the MAX_SEARCH_SOURCES pin; consumers are a single call. Slot hooks
return memoized values so the returned array is dependency-list safe.
Query cost scales linearly with the selection (~3 ClickHouse queries per
source per refresh, re-fired every live-tail tick, plus one-time
metadata), and past 3 sources the interleaved timeline stops being
legible. 3 covers the common "app logs + infra logs + traces" case.
@teeohhem

Copy link
Copy Markdown
Contributor Author

Superseded by #2869, which contains these commits and additionally makes the N-ary pipeline the single search path (one source is just N=1), removing the separate single-source results table from the search page. Leaving this open for reference; suggest closing once #2869 is reviewed.

@teeohhem

Copy link
Copy Markdown
Contributor Author

Superseded by the re-split stack: #2881#2882#2883#2884#2885#2886. Same end state (the final branch's production code is byte-identical to what this stack produced), but sliced into reviewable pieces with the throwaway intermediate components removed.

@teeohhem teeohhem closed this Aug 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant