mirror of
https://github.com/hyperdxio/hyperdx
synced 2026-04-21 13:37:15 +00:00
Revisit the bug fix for https://github.com/hyperdxio/hyperdx/pull/1614. The alias map should be used in useRowWhere hook Ref: HDX-3196 Example: For select ``` Timestamp,ServiceName,SeverityText,Body AS b, concat(b, 'blabla') ``` The generated query from useRowWhere is ``` WITH (Body) AS b SELECT *, Timestamp AS "__hdx_timestamp", Body AS "__hdx_body", TraceId AS "__hdx_trace_id", SpanId AS "__hdx_span_id", SeverityText AS "__hdx_severity_text", ServiceName AS "__hdx_service_name", ResourceAttributes AS "__hdx_resource_attributes", LogAttributes AS "__hdx_event_attributes" FROM DEFAULT.otel_logs WHERE ( Timestamp = parseDateTime64BestEffort('2026-01-20T06:11:00.170000000Z', 9) AND ServiceName = 'hdx-oss-dev-api' AND SeverityText = 'info' AND Body = 'Received alert metric [saved_search source]' AND concat(b, 'blabla') = 'Received alert metric [saved_search source]blabla' AND TimestampTime = parseDateTime64BestEffort('2026-01-20T06:11:00Z', 9) ) LIMIT 1 ```
339 lines
10 KiB
TypeScript
339 lines
10 KiB
TypeScript
import { useCallback, useContext, useMemo } from 'react';
|
|
import isString from 'lodash/isString';
|
|
import pickBy from 'lodash/pickBy';
|
|
import { SourceKind, TSource } from '@hyperdx/common-utils/dist/types';
|
|
import { Accordion, Box, Flex, Text } from '@mantine/core';
|
|
|
|
import { WithClause } from '@/hooks/useRowWhere';
|
|
import { getEventBody } from '@/source';
|
|
import { getHighlightedAttributesFromData } from '@/utils/highlightedAttributes';
|
|
|
|
import { getJSONColumnNames, useRowData } from './DBRowDataPanel';
|
|
import { DBRowJsonViewer } from './DBRowJsonViewer';
|
|
import { RowSidePanelContext } from './DBRowSidePanel';
|
|
import DBRowSidePanelHeader from './DBRowSidePanelHeader';
|
|
import EventTag from './EventTag';
|
|
import { ExceptionSubpanel } from './ExceptionSubpanel';
|
|
import { NetworkPropertySubpanel } from './NetworkPropertyPanel';
|
|
import { SpanEventsSubpanel } from './SpanEventsSubpanel';
|
|
|
|
const EMPTY_OBJ = {};
|
|
export function RowOverviewPanel({
|
|
source,
|
|
rowId,
|
|
aliasWith,
|
|
hideHeader = false,
|
|
'data-testid': dataTestId,
|
|
}: {
|
|
source: TSource;
|
|
rowId: string | undefined | null;
|
|
aliasWith?: WithClause[];
|
|
hideHeader?: boolean;
|
|
'data-testid'?: string;
|
|
}) {
|
|
const { data } = useRowData({ source, rowId, aliasWith });
|
|
const { onPropertyAddClick, generateSearchUrl } =
|
|
useContext(RowSidePanelContext);
|
|
|
|
const highlightedAttributeValues = useMemo(() => {
|
|
const attributeExpressions =
|
|
source.kind === SourceKind.Trace || source.kind === SourceKind.Log
|
|
? (source.highlightedRowAttributeExpressions ?? [])
|
|
: [];
|
|
|
|
return data
|
|
? getHighlightedAttributesFromData(
|
|
source,
|
|
attributeExpressions,
|
|
data.data || [],
|
|
data.meta || [],
|
|
)
|
|
: [];
|
|
}, [source, data]);
|
|
|
|
const jsonColumns = getJSONColumnNames(data?.meta);
|
|
|
|
const eventAttributesExpr = source.eventAttributesExpression;
|
|
|
|
const firstRow = useMemo(() => {
|
|
const firstRow = { ...(data?.data?.[0] ?? {}) };
|
|
if (!firstRow) {
|
|
return null;
|
|
}
|
|
return firstRow;
|
|
}, [data]);
|
|
|
|
// TODO: Use source config to select these in SQL, but we'll just
|
|
// assume OTel column names for now
|
|
const topLevelAttributeKeys = [
|
|
'ServiceName',
|
|
'SpanName',
|
|
'Duration',
|
|
'SeverityText',
|
|
'StatusCode',
|
|
'StatusMessage',
|
|
'SpanKind',
|
|
'TraceId',
|
|
'SpanId',
|
|
'ParentSpanId',
|
|
'ScopeName',
|
|
'ScopeVersion',
|
|
];
|
|
const topLevelAttributes = pickBy(firstRow, (value, key) => {
|
|
if (value === '') {
|
|
return false;
|
|
}
|
|
if (topLevelAttributeKeys.includes(key)) {
|
|
return true;
|
|
}
|
|
return false;
|
|
});
|
|
|
|
const resourceAttributes = firstRow?.__hdx_resource_attributes ?? EMPTY_OBJ;
|
|
const flattenedEventAttributes =
|
|
firstRow?.__hdx_event_attributes ?? EMPTY_OBJ;
|
|
|
|
const dataAttributes = useMemo(
|
|
() =>
|
|
eventAttributesExpr &&
|
|
firstRow?.[eventAttributesExpr] &&
|
|
Object.keys(firstRow[eventAttributesExpr]).length > 0
|
|
? { [eventAttributesExpr]: firstRow[eventAttributesExpr] }
|
|
: {},
|
|
[eventAttributesExpr, firstRow],
|
|
);
|
|
|
|
const _generateSearchUrl = useCallback(
|
|
(query?: string, queryLanguage?: 'sql' | 'lucene') => {
|
|
return (
|
|
generateSearchUrl?.({
|
|
where: query,
|
|
whereLanguage: queryLanguage,
|
|
}) ?? '/'
|
|
);
|
|
},
|
|
[generateSearchUrl],
|
|
);
|
|
|
|
const isHttpRequest = useMemo(() => {
|
|
const attributes =
|
|
eventAttributesExpr && dataAttributes?.[eventAttributesExpr];
|
|
return attributes?.['http.url'] != null;
|
|
}, [dataAttributes, eventAttributesExpr]);
|
|
|
|
const filteredEventAttributes = useMemo(() => {
|
|
if (!eventAttributesExpr) return dataAttributes;
|
|
|
|
const attributes = dataAttributes?.[eventAttributesExpr];
|
|
return isHttpRequest && attributes
|
|
? {
|
|
[eventAttributesExpr]: pickBy(
|
|
attributes,
|
|
(_, key) => !key.startsWith('http.'),
|
|
),
|
|
}
|
|
: dataAttributes;
|
|
}, [dataAttributes, isHttpRequest, eventAttributesExpr]);
|
|
|
|
const exceptionValues = useMemo(() => {
|
|
const parsedEvents =
|
|
firstRow?.__hdx_events_exception_attributes ?? EMPTY_OBJ;
|
|
const stacktrace =
|
|
parsedEvents?.['exception.stacktrace'] ||
|
|
parsedEvents?.['exception.parsed_stacktrace'];
|
|
|
|
let parsedStacktrace = stacktrace ?? '[]';
|
|
try {
|
|
parsedStacktrace = JSON.parse(stacktrace);
|
|
} catch (e) {
|
|
// do nothing
|
|
}
|
|
|
|
return [
|
|
{
|
|
stacktrace: parsedStacktrace,
|
|
type: parsedEvents?.['exception.type'],
|
|
value:
|
|
typeof parsedEvents?.['exception.message'] !== 'string'
|
|
? JSON.stringify(parsedEvents?.['exception.message'])
|
|
: parsedEvents?.['exception.message'],
|
|
mechanism: parsedEvents?.['exception.mechanism'],
|
|
},
|
|
];
|
|
}, [firstRow]);
|
|
|
|
const hasException = useMemo(() => {
|
|
return (
|
|
Object.keys(firstRow?.__hdx_events_exception_attributes ?? {}).length > 0
|
|
);
|
|
}, [firstRow?.__hdx_events_exception_attributes]);
|
|
|
|
const hasSpanEvents = useMemo(() => {
|
|
return (
|
|
Array.isArray(firstRow?.__hdx_span_events) &&
|
|
firstRow?.__hdx_span_events.length > 0
|
|
);
|
|
}, [firstRow?.__hdx_span_events]);
|
|
|
|
const mainContentColumn = getEventBody(source);
|
|
const mainContent = isString(firstRow?.['__hdx_body'])
|
|
? firstRow['__hdx_body']
|
|
: firstRow?.['__hdx_body'] !== undefined
|
|
? JSON.stringify(firstRow['__hdx_body'])
|
|
: undefined;
|
|
|
|
return (
|
|
<div className="flex-grow-1 overflow-auto" data-testid={dataTestId}>
|
|
{!hideHeader && (
|
|
<Box px="sm" pt="md">
|
|
<DBRowSidePanelHeader
|
|
attributes={highlightedAttributeValues}
|
|
date={new Date(firstRow?.__hdx_timestamp ?? 0)}
|
|
mainContent={mainContent}
|
|
mainContentHeader={mainContentColumn}
|
|
severityText={firstRow?.__hdx_severity_text}
|
|
/>
|
|
</Box>
|
|
)}
|
|
<Accordion
|
|
mt="sm"
|
|
defaultValue={[
|
|
'exception',
|
|
'spanEvents',
|
|
'network',
|
|
'resourceAttributes',
|
|
'eventAttributes',
|
|
'topLevelAttributes',
|
|
]}
|
|
multiple
|
|
variant="noPadding"
|
|
>
|
|
{isHttpRequest && (
|
|
<Accordion.Item value="network">
|
|
<Accordion.Control>
|
|
<Text size="sm" ps="md">
|
|
HTTP Request
|
|
</Text>
|
|
</Accordion.Control>
|
|
<Accordion.Panel>
|
|
<Box px="md">
|
|
<NetworkPropertySubpanel
|
|
eventAttributes={flattenedEventAttributes}
|
|
/>
|
|
</Box>
|
|
</Accordion.Panel>
|
|
</Accordion.Item>
|
|
)}
|
|
|
|
{hasException && (
|
|
<Accordion.Item value="exception">
|
|
<Accordion.Control>
|
|
<Text size="sm" ps="md">
|
|
Exception
|
|
</Text>
|
|
</Accordion.Control>
|
|
<Accordion.Panel>
|
|
<Box px="md">
|
|
<ExceptionSubpanel
|
|
exceptionValues={exceptionValues}
|
|
breadcrumbs={[]}
|
|
logData={{
|
|
timestamp: firstRow?.__hdx_timestamp,
|
|
}}
|
|
/>
|
|
</Box>
|
|
</Accordion.Panel>
|
|
</Accordion.Item>
|
|
)}
|
|
|
|
{hasSpanEvents && (
|
|
<Accordion.Item value="spanEvents">
|
|
<Accordion.Control>
|
|
<Text size="sm" ps="md">
|
|
Span Events
|
|
</Text>
|
|
</Accordion.Control>
|
|
<Accordion.Panel>
|
|
<Box px="md">
|
|
<SpanEventsSubpanel spanEvents={firstRow?.__hdx_span_events} />
|
|
</Box>
|
|
</Accordion.Panel>
|
|
</Accordion.Item>
|
|
)}
|
|
|
|
{Object.keys(topLevelAttributes).length > 0 && (
|
|
<Accordion.Item value="topLevelAttributes">
|
|
<Accordion.Control>
|
|
<Text size="sm" ps="md">
|
|
Top Level Attributes
|
|
</Text>
|
|
</Accordion.Control>
|
|
<Accordion.Panel>
|
|
<Box px="md">
|
|
<DBRowJsonViewer
|
|
data={topLevelAttributes}
|
|
jsonColumns={jsonColumns}
|
|
/>
|
|
</Box>
|
|
</Accordion.Panel>
|
|
</Accordion.Item>
|
|
)}
|
|
|
|
<Accordion.Item value="eventAttributes">
|
|
<Accordion.Control>
|
|
<Text size="sm" ps="md">
|
|
{source.kind === 'log' ? 'Log' : 'Span'} Attributes
|
|
</Text>
|
|
</Accordion.Control>
|
|
<Accordion.Panel>
|
|
<Box px="md">
|
|
<DBRowJsonViewer
|
|
data={filteredEventAttributes}
|
|
jsonColumns={jsonColumns}
|
|
/>
|
|
</Box>
|
|
</Accordion.Panel>
|
|
</Accordion.Item>
|
|
|
|
<Accordion.Item value="resourceAttributes">
|
|
<Accordion.Control>
|
|
<Text size="sm" ps="md">
|
|
Resource Attributes
|
|
</Text>
|
|
</Accordion.Control>
|
|
<Accordion.Panel>
|
|
<Flex wrap="wrap" gap="2px" mx="md" mb="lg">
|
|
{Object.entries(resourceAttributes).map(([key, value]) => (
|
|
<EventTag
|
|
{...(onPropertyAddClick
|
|
? {
|
|
onPropertyAddClick,
|
|
sqlExpression:
|
|
source.resourceAttributesExpression &&
|
|
jsonColumns?.includes(
|
|
source.resourceAttributesExpression,
|
|
)
|
|
? // If resource attributes is a JSON column, we need to cast the key to a string so we can run where X in Y queries
|
|
`toString(${source.resourceAttributesExpression}.${key})`
|
|
: `${source.resourceAttributesExpression}['${key}']`,
|
|
}
|
|
: {
|
|
onPropertyAddClick: undefined,
|
|
sqlExpression: undefined,
|
|
})}
|
|
generateSearchUrl={
|
|
generateSearchUrl ? _generateSearchUrl : undefined
|
|
}
|
|
displayedKey={key}
|
|
name={`${source.resourceAttributesExpression}.${key}`}
|
|
value={value as string}
|
|
key={key}
|
|
/>
|
|
))}
|
|
</Flex>
|
|
</Accordion.Panel>
|
|
</Accordion.Item>
|
|
</Accordion>
|
|
</div>
|
|
);
|
|
}
|