Repository navigation
feat: Add a trace logs tab to the event side panel #3127
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a036a05
0c59d60
f7740e8
c5dfd79
101d633
f934f6e
690f14e
edb4569
d98d95e
4974611
c33c378
e23298c
a954f6c
56d5c19
3d07907
9eefc58
748fd88
d27e06a
2e4d987
e6cc62b
aafd57d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| --- | ||
| '@hyperdx/app': minor | ||
| --- | ||
|
|
||
| Add a "Trace logs" tab to the event side panel, listing the trace's logs as a flat chronological table over the same time window the waterfall uses. It appears on any row that carries a trace id and resolves a log source — a span (via the trace source's correlated log source) or a log (its own source). Previously the only route to a trace's logs was hunting for the green rows interleaved in the waterfall, which a log-heavy trace buries. | ||
|
|
||
| The tab lists the trace unfiltered; "Open in search" hands the same query — same source, same trace, same window — to the search page for anything narrower. Picking a log opens it in the panel: a breadcrumb hop from a span, or a row change from another log. Sorting is ascending, which inside a trace is execution order. |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -80,6 +80,7 @@ import { | |||||
| import LogLevel from './LogLevel'; | ||||||
| import SidePanelBreadcrumbs, { BreadcrumbItem } from './SidePanelBreadcrumbs'; | ||||||
| import { SpanLinkData } from './SpanLinksSubpanel'; | ||||||
| import TraceLogsPanel from './TraceLogsPanel'; | ||||||
| import { ViewTraceCalloutButton } from './ViewTraceCalloutButton'; | ||||||
|
|
||||||
| import styles from '@/../styles/LogSidePanel.module.scss'; | ||||||
|
|
@@ -549,6 +550,14 @@ export const DBRowSidePanelInner = ({ | |||||
| ? source.traceSourceId | ||||||
| : undefined; | ||||||
|
|
||||||
| const traceLogSourceId = isTraceSource(source) | ||||||
| ? childSourceId | ||||||
| : isLogSource(source) | ||||||
| ? source.id | ||||||
| : undefined; | ||||||
| const enableTraceLogs = !!traceId && !!traceLogSourceId; | ||||||
| const isOwnTraceLogSource = traceLogSourceId === source.id; | ||||||
|
|
||||||
| const enableServiceMap = traceId && traceSourceId; | ||||||
|
|
||||||
| const { data: traceSourceData } = useSource({ id: traceSourceId }); | ||||||
|
|
@@ -611,6 +620,35 @@ export const DBRowSidePanelInner = ({ | |||||
| [traceSourceData, handleSourceStackPush, mainContent], | ||||||
| ); | ||||||
|
|
||||||
| const handleTraceLogNavigate = useCallback( | ||||||
| (rowId: string, aliasWith: WithClause[], label: string) => { | ||||||
| if (traceLogSourceId == null) { | ||||||
| return; | ||||||
| } | ||||||
| // The row on screen is one of the logs listed, so this is a row change. | ||||||
| if (isOwnTraceLogSource) { | ||||||
| handleNavigateToRow(rowId, aliasWith, label, SourceKind.Log); | ||||||
| return; | ||||||
| } | ||||||
| handleSourceStackPush({ | ||||||
| sourceId: traceLogSourceId, | ||||||
| rowId, | ||||||
| aliasWith, | ||||||
| label, | ||||||
| sourceKind: SourceKind.Log, | ||||||
| // Every log in the trace is within seconds of the origin span. | ||||||
| focusTimestamp: rowFocusTimestamp, | ||||||
|
MikeShi42 marked this conversation as resolved.
Resolved
|
||||||
| }); | ||||||
| }, | ||||||
| [ | ||||||
| traceLogSourceId, | ||||||
| isOwnTraceLogSource, | ||||||
| handleNavigateToRow, | ||||||
| handleSourceStackPush, | ||||||
| rowFocusTimestamp, | ||||||
| ], | ||||||
| ); | ||||||
|
|
||||||
| // "Open trace" on a span link: the link carries only the linked span's | ||||||
| // TraceId + SpanId, so resolve it against the current trace source. | ||||||
| const handleOpenLinkedTrace = useCallback( | ||||||
|
|
@@ -786,6 +824,7 @@ export const DBRowSidePanelInner = ({ | |||||
| if (hasOverviewPanel && !sourceIsTrace) tabs.push(Tab.Overview); | ||||||
| if (!sourceIsTrace) tabs.push(Tab.Parsed); | ||||||
| if (sourceIsTrace) tabs.push(Tab.Trace); | ||||||
| if (enableTraceLogs) tabs.push(Tab.Logs); | ||||||
| if (enableServiceMap) tabs.push(Tab.ServiceMap); | ||||||
| tabs.push(Tab.Context); | ||||||
| if (rumSessionId != null) tabs.push(Tab.Replay); | ||||||
|
|
@@ -794,6 +833,7 @@ export const DBRowSidePanelInner = ({ | |||||
| }, [ | ||||||
| hasOverviewPanel, | ||||||
| sourceIsTrace, | ||||||
| enableTraceLogs, | ||||||
| enableServiceMap, | ||||||
| rumSessionId, | ||||||
| hasK8sContext, | ||||||
|
|
@@ -1020,6 +1060,14 @@ export const DBRowSidePanelInner = ({ | |||||
| }, | ||||||
| ] | ||||||
| : []), | ||||||
| ...(enableTraceLogs | ||||||
| ? [ | ||||||
| { | ||||||
| text: 'Trace Logs', | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The new user-facing label
Suggested change
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!
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔵 minor — Tab label "Trace Logs" is Title Case, against the sentence-case convention The repository conventions require sentence case for every user-facing string, explicitly including tab items. Use Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond |
||||||
| value: Tab.Logs, | ||||||
| }, | ||||||
| ] | ||||||
| : []), | ||||||
| ...(enableServiceMap | ||||||
| ? [ | ||||||
| { | ||||||
|
|
@@ -1097,6 +1145,26 @@ export const DBRowSidePanelInner = ({ | |||||
| </Box> | ||||||
| </ErrorBoundary> | ||||||
| )} | ||||||
| {displayedTab === Tab.Logs && traceLogSourceId && traceId && ( | ||||||
| <ErrorBoundary | ||||||
| onError={err => { | ||||||
| console.error(err); | ||||||
| }} | ||||||
| fallbackRender={() => ( | ||||||
| <div className="text-danger px-2 py-1 m-2 fs-7 font-monospace bg-danger-transparent p-4"> | ||||||
| An error occurred while rendering this event. | ||||||
| </div> | ||||||
| )} | ||||||
| > | ||||||
| <TraceLogsPanel | ||||||
| data-testid="side-panel-tab-logs" | ||||||
| logSourceId={traceLogSourceId} | ||||||
| traceId={traceId} | ||||||
| dateRange={oneHourRange} | ||||||
|
greptile-apps[bot] marked this conversation as resolved.
Resolved
|
||||||
| onNavigateToLog={handleTraceLogNavigate} | ||||||
| /> | ||||||
| </ErrorBoundary> | ||||||
| )} | ||||||
| {displayedTab === Tab.ServiceMap && enableServiceMap && ( | ||||||
| <ErrorBoundary | ||||||
| onError={err => { | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,175 @@ | ||
| import { use, useCallback, useMemo } from 'react'; | ||
| import { useRouter } from 'next/router'; | ||
| import { buildSearchChartConfig } from '@hyperdx/common-utils/dist/core/searchChartConfig'; | ||
| import { | ||
| BuilderChartConfigWithDateRange, | ||
| SourceKind, | ||
| } from '@hyperdx/common-utils/dist/types'; | ||
| import { Button, Flex, Group } from '@mantine/core'; | ||
| import { IconExternalLink, IconLogs } from '@tabler/icons-react'; | ||
|
|
||
| import EmptyState from '@/components/EmptyState'; | ||
| import { RowWhereResult, WithClause } from '@/hooks/useRowWhere'; | ||
| import { getEventBody, useSource } from '@/source'; | ||
| import { buildDirectTraceWhereClause } from '@/utils/directTrace'; | ||
| import { parseAsSortingStateString } from '@/utils/queryParsers'; | ||
|
|
||
| import { RowSidePanelContext } from './DBRowSidePanel'; | ||
| import { DBSqlRowTable } from './DBRowTable'; | ||
|
|
||
| export default function TraceLogsPanel({ | ||
| logSourceId, | ||
| traceId, | ||
| dateRange, | ||
| onNavigateToLog, | ||
| 'data-testid': dataTestId, | ||
| }: { | ||
| logSourceId: string; | ||
| traceId: string; | ||
| dateRange: [Date, Date]; | ||
| onNavigateToLog: ( | ||
| rowId: string, | ||
| aliasWith: WithClause[], | ||
| label: string, | ||
| ) => void; | ||
| 'data-testid'?: string; | ||
| }) { | ||
| const { data: logSource, isLoading } = useSource({ | ||
| id: logSourceId, | ||
| kinds: [SourceKind.Log], | ||
| }); | ||
|
|
||
| // `min(1)` on the schema accepts a space, which would render as a bare `=`. | ||
| const traceIdExpression = logSource?.traceIdExpression?.trim(); | ||
|
|
||
| const traceWhere = useMemo( | ||
| () => | ||
| traceIdExpression | ||
| ? buildDirectTraceWhereClause(traceIdExpression, traceId) | ||
| : undefined, | ||
| [traceIdExpression, traceId], | ||
| ); | ||
|
|
||
| // Inside a trace, chronological order is execution order. | ||
| const orderBy = logSource | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔵 minor —
Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond |
||
| ? `${logSource.timestampValueExpression} ASC` | ||
| : undefined; | ||
|
|
||
| const config = useMemo((): BuilderChartConfigWithDateRange | undefined => { | ||
| if (logSource == null || traceWhere == null) { | ||
| return undefined; | ||
| } | ||
| return { | ||
| // Carries the source's tableFilterExpression, sample weighting and id. | ||
| ...buildSearchChartConfig(logSource, { | ||
| where: traceWhere, | ||
| whereLanguage: 'sql', | ||
| orderBy, | ||
| }), | ||
| limit: { limit: 200 }, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔵 minor — Hardcoded
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔵 minor — Hardcoded
Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond |
||
| dateRange, | ||
|
MikeShi42 marked this conversation as resolved.
Resolved
|
||
| }; | ||
| }, [logSource, traceWhere, orderBy, dateRange]); | ||
|
|
||
| const handleRowDetailsClick = useCallback( | ||
|
MikeShi42 marked this conversation as resolved.
Resolved
|
||
| (rowWhere: RowWhereResult, row: Record<string, unknown>) => { | ||
| // The table selects the source's expressions, not normalized aliases. | ||
| const body = logSource ? row[getEventBody(logSource) ?? ''] : undefined; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔵 minor — Breadcrumb label looks the row up by SELECT expression, not by the returned column name
Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond |
||
| onNavigateToLog( | ||
| rowWhere.where, | ||
| rowWhere.aliasWith, | ||
| typeof body === 'string' && body.length > 0 ? body : 'Log', | ||
| ); | ||
| }, | ||
| [logSource, onNavigateToLog], | ||
| ); | ||
|
|
||
| const parentContext = use(RowSidePanelContext); | ||
| const router = useRouter(); | ||
|
|
||
| // These rows aren't the searched table's: its cell filter would write a log | ||
| // column into that source's filters, and its column removal maps by index. | ||
| const rowSidePanelContextValue = useMemo( | ||
|
MikeShi42 marked this conversation as resolved.
Resolved
MikeShi42 marked this conversation as resolved.
Resolved
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔵 minor — Hand-rolls deriveRowSidePanelContextForSource and leaves generateSearchUrl bound to the span's source Build the value as
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔵 minor — Row-context narrowing is hand-rolled next to the existing
Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond |
||
| () => ({ | ||
| ...parentContext, | ||
| onPropertyAddClick: undefined, | ||
| displayedColumns: undefined, | ||
| toggleColumn: undefined, | ||
| }), | ||
| [parentContext], | ||
| ); | ||
|
greptile-apps[bot] marked this conversation as resolved.
Resolved
|
||
|
|
||
| if (isLoading) { | ||
| return null; | ||
| } | ||
|
|
||
| if (config == null || logSource == null || traceWhere == null) { | ||
| return ( | ||
| <EmptyState | ||
| icon={<IconLogs size={24} />} | ||
| title="Correlated logs unavailable" | ||
| description="This trace's log source is missing, or has no trace ID expression configured. Check the source's settings." | ||
| variant="card" | ||
| m="sm" | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| const parsedSort = parseAsSortingStateString.parse(orderBy ?? ''); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔵 minor — initialSortBy id is timestampValueExpression, which is not one of the table's columns on the default log source Derive the sort id from the column the table actually renders —
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔵 minor —
Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond |
||
| const sortBy = parsedSort ? [parsedSort] : undefined; | ||
|
|
||
| // Not `generateSearchUrl`: it stamps the search page's time range and filter | ||
| // pills, landing the reader on a different row set. `where` and `select` are | ||
| // read by `parseAsStringEncoded`, which decodes one level past the query | ||
| // string's own; `select` also seeds the page's SELECT input, which is blank | ||
| // without it. | ||
| // `router.basePath` rather than a root-relative path: a hard navigation isn't | ||
| // prefixed for the /clickstack build the way `next/link` would be. | ||
| const searchUrl = `${router.basePath}/search?${new URLSearchParams({ | ||
| source: logSource.id, | ||
| select: encodeURIComponent(logSource.defaultTableSelectExpression), | ||
| where: encodeURIComponent(traceWhere), | ||
| whereLanguage: 'sql', | ||
| from: dateRange[0].getTime().toString(), | ||
| to: dateRange[1].getTime().toString(), | ||
| isLive: 'false', | ||
| }).toString()}`; | ||
|
|
||
| return ( | ||
| <RowSidePanelContext value={rowSidePanelContextValue}> | ||
| <Flex | ||
| direction="column" | ||
| mih={0} | ||
| px="md" | ||
| style={{ flexGrow: 1 }} | ||
| data-testid={dataTestId} | ||
| > | ||
| <Group justify="flex-end" py="xs"> | ||
| <Button | ||
| variant="link" | ||
| size="xs" | ||
| component="a" | ||
| href={searchUrl} | ||
|
MikeShi42 marked this conversation as resolved.
Resolved
|
||
| rightSection={<IconExternalLink size={14} />} | ||
| data-testid="trace-logs-open-in-search" | ||
| > | ||
| Open in search | ||
| </Button> | ||
| </Group> | ||
| <div style={{ height: '100%', overflow: 'auto' }}> | ||
| <DBSqlRowTable | ||
|
MikeShi42 marked this conversation as resolved.
Resolved
MikeShi42 marked this conversation as resolved.
Resolved
|
||
| sourceId={logSource.id} | ||
| config={config} | ||
| queryKeyPrefix="trace-logs" | ||
|
MikeShi42 marked this conversation as resolved.
Resolved
|
||
| tableId="trace-logs" | ||
| initialSortBy={sortBy} | ||
| isLive={false} | ||
| showExpandButton={false} | ||
| onRowDetailsClick={handleRowDetailsClick} | ||
| onChildModalOpen={parentContext.setChildModalOpen} | ||
| /> | ||
| </div> | ||
| </Flex> | ||
| </RowSidePanelContext> | ||
| ); | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.