Skip to content

[HDX-5162] LLM observability dashboard, span chat view, and sessions - #2990

Open
wrn14897 wants to merge 4 commits into
mainfrom
warren/llm-observability
Open

[HDX-5162] LLM observability dashboard, span chat view, and sessions#2990
wrn14897 wants to merge 4 commits into
mainfrom
warren/llm-observability

Conversation

@wrn14897

@wrn14897 wrn14897 commented Aug 25, 2026

Copy link
Copy Markdown
Member

Linear

https://linear.app/clickhouse/issue/HDX-5162/llm-observability-span-chat-view-cost-tracking-sessions-and-llm

Why

Teams running LLM apps and coding agents (OpenAI/Anthropic SDKs, Vercel AI SDK, LangChain via OpenLLMetry/OpenInference, opencode, Claude Code) already send their telemetry to HyperDX — but the product had zero LLM awareness: no chat rendering, no token/cost concepts, no model analytics. This PR adds LLM observability at feature parity with dedicated tools (Langfuse dashboards, Datadog LLM Observability's operational insights) while staying HyperDX-native.

Approach: read-time, schema-agnostic

Unlike Langfuse (ingest-time workers + dedicated tables), everything here derives from span/log attribute maps at query time:

  • zero ingestion or schema changes — works retroactively on already-ingested data
  • works with Map and JSON-typed attribute columns via the source's eventAttributesExpression
  • all derived expressions are plain SQL, so they also compose with search, alerts, and custom dashboards

What's included

Normalization lib (packages/app/src/llm/lib) — pure, unit-tested TS:

  • Detects LLM spans and normalizes model, provider, usage (incl. cached + reasoning tokens), cost, session id, TTFT, tool names, finish reasons, and chat messages (roles, markdown, tool calls)
  • Four dialects: OTel GenAI semconv (attribute- and event-based), OpenLLMetry, OpenInference, Vercel AI SDK — plus real-world variants captured as fixtures from opencode and Claude Code telemetry (whole-string llm.input_messages, camelCase ai.usage.*, flat input_tokens/cost_usd keys, bracketed model ids like claude-opus-5[1m])

Span-level UX:

  • New LLM tab in the row side panel and trace span detail: normalized conversation with role badges, markdown, collapsible tool calls, and a usage/cost summary (params, TTFT, session id)
  • LLM section in the Overview tab; waterfall labels LLM spans with model · N tok

Cost estimation (lib/modelPrices.ts, lib/cost.ts):

  • Bundled price catalog adapted from Langfuse's MIT-licensed default price list (OpenAI/Anthropic/Google families, provider/Bedrock/Vertex id flavors)
  • An instrumentation-provided cost attribute (gen_ai.usage.cost, llm.cost.total, cost_usd) always wins; catalog is the fallback
  • SQL multiIf generator for dashboard aggregation

/llm preset dashboard (Overview | Sessions | Search), listed on the Dashboards page:

  • Overview: KPI tiles (calls, tokens, est. cost, avg cost/call, cache hit rate, error rate), calls + error trends, token split (uncached/cached input, output, reasoning), cost by model, cache-hit-rate + finish-reason trends (truncation/content-filter signal), models/services/users/error-message tables, latency heatmap, p95 by model, TTFT p50/p95, and tool analytics (calls by tool, per-tool error rate + p95)
  • Sessions: LLM activity grouped by the cross-dialect session id (gen_ai.conversation.idsession.idai.telemetry.metadata.sessionId) — the correlation surface for instrumentations that stamp session ids but don't propagate trace context. Row click opens a timeline drawer; each call expands into the chat view
  • Search: side-by-side LLM trace-span and log-event tables (row click opens the standard side panel with the LLM tab)
  • Top-bar scoping: trace source, correlated log source, session filter, where input, time picker — all charts honor them

Correctness & performance notes

  • Token/cost sums are gated on authoritative usage reporters (gen_ai.usage.* / llm.token_count.* / flat primary-reporter keys) so SDK wrapper spans (e.g. Vercel's ai.streamText around doStream) don't double count
  • Cache-hit-rate handles both conventions (OpenAI-style cached-⊆-input vs Anthropic-style exclusive reporting)
  • Session drawer fetches a lightweight scalar list and loads each span's attributes lazily on expand — agent SDKs stamp the full conversation history on every span, so the naive approach shipped ~48 MiB per session vs ~20 KiB now
  • Finish reasons are normalized across encodings (stop vs ["stop"])

Known limitations

  • Bundled prices go stale between releases (provided cost attributes always win); team-editable overrides are a natural follow-up
  • Apps that double-instrument every call (e.g. opencode emitting both OpenInference spans and Vercel AI spans) still double count in aggregates

Testing

  • 12 unit/component suites, 74 tests in src/llm/__tests__ (per-dialect fixtures lifted from real opencode/Claude Code telemetry, cost math, SQL expression generation, tab gating, lazy-loading regression guards)
  • Touched component suites (DBRowSidePanel, DBTracePanel, DBRowOverviewPanel, DBTraceWaterfallChart) all green; tsc --noEmit clean; eslint 0 errors
  • Chart SQL validated against live ClickHouse with real opencode + Claude Code telemetry (Map and JSON schema variants)

…s, and /llm dashboard

Adds read-time, schema-agnostic LLM observability on top of existing trace
and log data. No ingestion changes: everything derives from span/log
attribute maps at query time, so it works retroactively on already-ingested
telemetry.

- Normalization lib (packages/app/src/llm/lib): detects LLM spans and
  normalizes model, provider, token usage (incl. cached/reasoning), cost,
  session ids, TTFT, and chat messages across four instrumentation dialects:
  OTel GenAI semconv (attribute- and event-based), OpenLLMetry,
  OpenInference, and the Vercel AI SDK. Includes real-world variants
  observed from opencode and Claude Code telemetry (whole-string
  llm.input_messages, camelCase ai.usage.*, flat token/cost keys,
  bracketed model ids like claude-opus-5[1m]).
- Span side panel: an LLM tab renders the normalized conversation (roles,
  markdown, tool calls) with a usage/cost summary; the Overview tab gains an
  LLM section; trace waterfall labels LLM spans with model + token count.
- Cost estimation: bundled model price catalog (adapted from Langfuse's
  MIT-licensed price list) with regex matching for provider/Bedrock/Vertex
  id flavors; an instrumentation-provided cost attribute always wins.
- /llm preset dashboard (Overview | Sessions | Search):
  - Overview: KPI tiles (calls, tokens, est. cost, avg cost/call, cache hit
    rate, error rate), calls/error trends, token split
    (uncached/cached/output/reasoning), cost by model, finish reasons,
    models/services/users/error tables, latency heatmap, p95 by model,
    TTFT, and tool analytics.
  - Sessions: activity grouped by the cross-dialect session id
    (gen_ai.conversation.id, session.id, ai.telemetry.metadata.sessionId)
    with a drawer timeline; span attributes load lazily per expanded call
    since agent SDKs stamp full conversation history on every span
    (~48 MiB -> ~20 KiB list payload).
  - Search: side-by-side LLM trace span and log event tables, correlating
    signals for instrumentations that emit session ids without trace
    context.
- Aggregations gate token/cost sums on authoritative usage reporters so SDK
  wrapper spans don't double count.

Known limitations: bundled prices go stale between releases, and apps that
double-instrument (e.g. opencode emitting both OpenInference and Vercel AI
spans per call) still double count in sums.
@changeset-bot

changeset-bot Bot commented Aug 25, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c37f5fc

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

This PR includes changesets to release 3 packages
Name Type
@hyperdx/app 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 25, 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 25, 2026 7:39am
hyperdx-storybook Ready Ready Preview Aug 25, 2026 7:39am

Request Review

@github-actions github-actions Bot added the review/tier-4 Critical — deep review + domain expert sign-off label Aug 25, 2026
@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

🔴 Tier 4 — Critical

Touches authentication, tenancy data models, the public API or shipped database config — or substantially changes the query rendering engine, background tasks, the OTel pipeline, image build, or release CI.

Why this tier:

  • Large diff: 5182 production lines changed (threshold: 1000)

Review process: Deep review from a domain expert. Synchronous walkthrough may be required.
SLA: Schedule synchronous review within 2 business days.

Stats
  • Production files changed: 51
  • Production lines changed: 5182 (+ 1470 in test files, excluded from tier calculation)
  • Branch: warren/llm-observability
  • Author: wrn14897

To override this classification, remove the review/tier-4 label and apply a different review/tier-* label. Manual overrides are preserved on subsequent pushes.

@greptile-apps

greptile-apps Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds schema-agnostic LLM telemetry normalization, conversation rendering, cost estimation, session exploration, and a preset LLM dashboard.

  • Normalizes messages, usage, models, providers, sessions, tools, and costs across several instrumentation dialects.
  • Adds LLM-aware row and trace panels with chat, usage, and waterfall presentation.
  • Adds overview, session, and search dashboard surfaces backed by read-time ClickHouse expressions.

Confidence Score: 5/5

The PR appears safe to merge because no blocking failure remains.

No blocking failure remains.

Important Files Changed

Filename Overview
packages/app/src/llm/lib/extract.ts Coordinates dialect adapters to derive normalized LLM information from telemetry rows.
packages/app/src/llm/dashboard/LLMDashboardPage.tsx Defines the LLM dashboard's source selection, filters, tabs, and chart composition.
packages/app/src/llm/dashboard/LLMSessionPanel.tsx Queries session span summaries and expands individual entries through lazy-loaded detail views.
packages/app/src/llm/dashboard/SessionSpanDetail.tsx Loads span attributes using trace, span, and timestamp identity before rendering the conversation.
packages/app/src/llm/components/ChatMessageItem.tsx Renders normalized chat messages, Markdown content, and collapsible tool-call details.
packages/app/src/llm/lib/expressions.ts Generates schema-aware ClickHouse expressions used by LLM dashboard aggregations.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  Telemetry[Span and log attributes] --> Normalize[LLM dialect normalization]
  Normalize --> Conversation[Conversation and usage views]
  Normalize --> Expressions[Read-time SQL expressions]
  Expressions --> Overview[Overview charts]
  Expressions --> Sessions[Session timeline]
  Expressions --> Search[LLM search tables]
  Sessions --> Detail[Lazy-loaded span conversation]
Loading

Reviews (4): Last reviewed commit: "feat(app): mark the LLM dashboard as bet..." | Re-trigger Greptile

Comment thread packages/app/src/llm/dashboard/SessionSpanDetail.tsx Outdated
@@ -0,0 +1,316 @@
import { useCallback, useEffect, useState } from 'react';

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 Components exceed file-size limit

This new component is 316 lines, while LLMSessionPanel.tsx is also 302 lines. Both exceed the repository's 300-line maximum, increasing maintenance cost; split them into smaller focused components.

Context Used: AGENTS.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

Comment thread packages/app/src/llm/dashboard/AgentToolCharts.tsx
Comment on lines +77 to +81
!isLoading && (
<Text size="sm" c="dimmed">
No LLM messages found on this span. Prompt and completion capture
may be disabled in the instrumentation.
</Text>

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 Ad-hoc empty states added

This no-message branch and the no-session-results branch in LLMSessionPanel.tsx render plain Text elements instead of the required shared EmptyState, bypassing the repository's consistent empty-state presentation and behavior. Use @/components/EmptyState for both branches.

Context Used: AGENTS.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 25, 2026

Copy link
Copy Markdown
Contributor

E2E Test Results

All tests passed • 306 passed • 1 skipped • 1059s

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

Tests ran across 4 shards in parallel.

View full report →

@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Deep Review

No critical issues found. No P0/P1 defects survive re-grading — SQL is parameterized via SqlString.format (no injection), markdown chat rendering uses react-markdown v10 with no rehype-raw/dangerouslySetInnerHTML (no XSS), and the prior review's P1 "trace identity omitted from lookup" is already resolved: SessionSpanDetail.tsx:41-50 pins the point lookup to traceId + spanId + ts. The items below are recommended hardening for a large new feature.

🟡 P2 -- recommended

  • packages/app/src/llm/lib/attributeUtils.ts:107 -- keyPathsToArray accepts any non-negative integer as an array index, so an attribute key like gen_ai.prompt.4000000000.content makes items[index] ??= {} allocate a multi-billion-length sparse array that the trailing .filter then walks, freezing the tab that renders the span.
    • Fix: Reject or cap index against the current item count (or a small bound) before assigning into items.
  • packages/app/src/llm/lib/expressions.ts:56 -- PROVIDER_KEYS omits llm.system, which the client-side extractor at extract.ts:29 includes, so an llm.system-only span shows a provider badge in the span view but groups as empty in the dashboard's SQL provider breakdowns.
    • Fix: Add llm.system to the SQL PROVIDER_KEYS, ideally sharing one key list between extract.ts and expressions.ts.
    • maintainability, correctness
  • packages/app/src/llm/dashboard/SessionSelect.tsx:46 -- The session dropdown fetches up to 10,000 distinct sessions and filters them client-side in an unvirtualized Mantine Select, which degrades for active agent workloads with many sessions per window.
    • Fix: Lower the limit and push the search term into the SQL WHERE, or switch to a virtualized async-search combobox.
  • packages/app/src/llm/dashboard/LLMSessionPanel.tsx:117 -- Accordion expansion state is keyed by positional row index and never reset when sessionId changes, so an expanded item can point at a different span after a session switch or a spans refetch reorders the list.
    • Fix: Reset expandedItems on sessionId change and derive itemValue from traceId-spanId-ts instead of the array index.
  • packages/app/src/llm/dashboard/LLMDashboardPage.tsx:1 -- Three new files exceed the documented 300-line limit (AGENTS.md): LLMDashboardPage.tsx (353), lib/expressions.ts (380), and LLMSessionPanel.tsx (310); this prior-comment feedback remains unaddressed.
    • Fix: Split each into focused sub-modules under the 300-line ceiling.
    • project-standards, previous-comments
  • packages/app/src/llm/lib/adapters/vercelAi.ts:87 -- The Vercel ai.toolCall.* tool-execution branch that synthesizes assistant tool-call and tool-result messages has no fixture or test.
    • Fix: Add a tool-call-only fixture asserting the synthesized tool-call input message and tool-result output message.
  • packages/app/src/llm/lib/cost.ts:1 -- The normalized LLM cost/session logic exists only client-side with no MCP-tool equivalent, unlike other nav surfaces (dashboards, alerts), so an agent cannot reproduce the /llm view; likely acceptable for a beta feature but worth a follow-up.
    • Fix: Expose the extraction/pricing/session logic via a server-side MCP tool, or confirm UI-only is the intended interim scope.
🔵 P3 nitpicks (8)
  • packages/app/src/llm/lib/attributeUtils.ts:49 -- flattenContentToText routes already-string content through JSON coercion, so a chat message whose text is a JSON primitive ("null", "true", [1,2,3]) is transformed or silently dropped.
    • Fix: Only attempt JSON parsing for array/object shapes; render primitive strings verbatim.
  • packages/app/src/llm/lib/extract.ts:251 -- formatCostUsd strips all trailing zeros, so a cost that rounds to zero at six decimals renders as the malformed $0..
    • Fix: Guard the trailing-zero strip and fall back to a fixed form such as <$0.000001.
  • packages/app/src/llm/lib/cost.ts:142 -- Output, cached, and cache-write token expressions are multiplied by their rates unclamped, so a negative token string understates (or negates) cost aggregates; only uncached input tokens have a greatest(...,0) floor.
    • Fix: Clamp all token terms to >= 0 in both the SQL (greatest) and JS (Math.max) paths.
  • packages/app/src/llm/dashboard/OverviewCharts.tsx:67 -- Dashboard chart titles and column headers use Title Case (LLM Calls, Token Usage, Top Users) instead of the documented sentence-case convention, across OverviewCharts, TokenCostCharts, AgentToolCharts, AttributionCharts, EfficiencyCharts, LatencyCharts, and SessionsTab.
    • Fix: Convert new user-facing titles and column aliases to sentence case.
    • project-standards, previous-comments
  • packages/app/src/llm/dashboard/SessionSelect.tsx:51 -- The placeholderData and row-map callbacks are typed any while sibling chart files in the same PR type them as Record<string, unknown>.
    • Fix: Type the prev and d callbacks to match the sibling chart files.
    • project-standards, kieran-typescript
  • packages/app/src/llm/dashboard/LLMSessionPanel.tsx:298 -- Empty states render a plain Text element instead of the shared @/components/EmptyState, diverging from the app's consistent empty-state presentation (also LLMConversationPanel.tsx).
    • Fix: Use @/components/EmptyState for the no-results branches.
  • packages/app/src/llm/dashboard/TokenCostCharts.tsx:33 -- The buildLLMSearchUrl equality-condition snippet (SqlString.format('? = ?', ...)) is duplicated near-verbatim across five chart files.
    • Fix: Extract a shared buildEqualityCondition(fieldExpr, value) helper mirroring buildSessionCondition.
  • packages/app/src/llm/lib/cost.ts:54 -- The inclusive-vs-exclusive cache-token heuristic is reimplemented three times (cost.ts, extract.ts, expressions.ts SQL) with no shared source of truth, so a future correction risks a partial update.
    • Fix: Extract the token-reconciliation math into one pure function and document that the SQL mirrors it.

Reviewers (11): correctness, security, adversarial, performance, testing, maintainability, project-standards, kieran-typescript, frontend-races, previous-comments, agent-native.

Testing gaps:

  • OpenInference structured input.value.messages and llm.tools-only output fallback branches (openinference.ts) are untested.
  • expressions.ts/cost.ts SQL is asserted only as strings, never executed, so the cache heuristic, regex escaping, and stop vs ["stop"] finish-reason bucketing are unverified against ClickHouse.
  • No negative / NaN / huge-token case for computeCostUsd or the generated cost SQL.
  • No component/smoke test for LLMDashboardPage source-defaulting effects or chart-config wiring.
  • SessionSpanDetail's exact-timestamp equality lookup is untested against sources with computed or lower-precision timestamp expressions, where a round-trip mismatch would surface a misleading "No captured messages" state.

@wrn14897 wrn14897 changed the title feat(app): LLM observability dashboard, span chat view, and sessions [HDX-5162] LLM observability dashboard, span chat view, and sessions Aug 25, 2026
…ion lookup hardening

Post-review fixes for the LLM observability branch, driven by a live
comparison against opencode's self-reported session cost: the /llm
dashboard showed ~$22.72 for a session opencode itself priced at $9.16.

Cost accuracy (verified exact against opencode's cost_usd on 111 calls):

- Provided-cost election: apps that stamp their own per-call cost
  (cost_usd / llm.cost.total / gen_ai.usage.cost) are treated as the
  authoritative reporters, and all token/cost/call aggregations sum only
  those rows when any exist in scope (llmGatedSumExpr /
  llmGatedCountExpr, rendered as raw select aggregates). This dedupes
  dual-instrumented apps — opencode emits OpenInference spans (with
  cost) AND Vercel AI SDK spans (with gen_ai.usage.*) for every call, in
  separate traces, so no row-local gate could catch it.
- Cache-aware estimation: the SQL cost expression now prices uncached
  input, cache reads (discounted), cache writes (Anthropic's 1.25x
  premium, new catalog rate + attribute keys incl. OpenInference
  prompt_details.cache_write, Vercel inputTokenDetails.cacheWriteTokens,
  and flat cache_creation_tokens), and output separately, matching the
  TS-side computeCostUsd. The inclusive/exclusive input-token heuristic
  now accounts for writes, and totalTokens reports effective context.
- Query-size guard: the enriched cost expression embeds the price
  catalog per token term; the first live run exceeded ClickHouse's
  256 KiB max_query_size. Rates are now factored into per-term multiIfs
  and the whole expression is bound once per query as a WITH expression
  alias (LLM_COST_SQL_ALIAS), keeping dashboard queries at ~80 KiB.

Session drawer correctness (review feedback):

- The per-span attribute lookup now pins TraceId alongside SpanId +
  timestamp (span ids can be empty or collide across traces) and is
  bounded to the searched window for partition pruning.

Also trims the llm module's public surface to what external consumers
import (fixes 23 knip unused-export findings) and replaces the unused
zod chat-message schemas with plain interfaces. Adds the missing
changeset for the LLM observability feature.
The LLM observability branch added 6 eslint-disable comments, tripping the
app/eslint-disable ratchet (150 > baseline 144). Remove the escapes by
fixing the underlying patterns instead of suppressing them:

- Chat messages get a stable `id` assigned in extractConversation
  (conversations are immutable once extracted), so message lists key on
  data instead of array indexes.
- Session timeline rows carry their accordion `itemValue` in the row data
  built per fetch, replacing the index-derived key/value pair.
- The session filter now lives in the URL only: SessionSelectControlled
  becomes a plain value/onChange SessionSelect wired straight to the
  nuqs param, deleting the two deliberately-under-depped form<->URL sync
  effects (the drawer's "Filter dashboard" action writes the same param).
- The trace-source default adoption effect gets full dependencies — the
  select only offers usable trace sources, so a user selection always
  resolves to itself and the effect can never fight it.
Adds the standard beta badge (matching Service Map's nav badge) next to
the LLM breadcrumb, plus an info hover card explaining what to expect:
the dashboard is experimental, which instrumentations it understands,
that costs are catalog estimates unless the instrumentation reports its
own cost, and how dual-instrumented apps are counted.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review/tier-4 Critical — deep review + domain expert sign-off

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant