fix: bugs with showing non otel spans (ex. clickhouse opentelemetry span logs) (#789)

<img width="1645" alt="image" src="https://github.com/user-attachments/assets/2f7eb93f-9648-4c98-8bfd-a2d0f65be9d5" />

fixing a few bugs that prevented us from properly rendering trace view for `system.opentelemetry_span_log`

fix HDX-1676

Co-authored-by: Warren <5959690+wrn14897@users.noreply.github.com>
This commit is contained in:
Mike Shi 2025-05-07 09:08:21 -07:00 committed by GitHub
parent 1674ab8672
commit 931d7387d9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 72 additions and 20 deletions

View file

@ -0,0 +1,7 @@
---
"@hyperdx/common-utils": patch
"@hyperdx/api": patch
"@hyperdx/app": patch
---
fix: bugs with showing non otel spans (ex. clickhouse opentelemetry span logs)

View file

@ -63,6 +63,7 @@ export const Source = mongoose.model<ISource>(
spanKindExpression: String,
statusCodeExpression: String,
statusMessageExpression: String,
spanEventsValueExpression: String,
metricTables: {
type: {

View file

@ -82,10 +82,10 @@ export function useRowData({
},
]
: []),
...(source.kind === SourceKind.Trace
...(source.kind === SourceKind.Trace && source.spanEventsValueExpression
? [
{
valueExpression: `Events.Attributes[indexOf(Events.Name, 'exception')]`,
valueExpression: `${source.spanEventsValueExpression}.Attributes[indexOf(${source.spanEventsValueExpression}.Name, 'exception')]`,
alias: '__hdx_events_exception_attributes',
},
]

View file

@ -94,9 +94,21 @@ export default function DBRowSidePanel({
Infrastructure = 'infrastructure',
}
const hasOverviewPanel = useMemo(() => {
if (
source.resourceAttributesExpression ||
source.eventAttributesExpression
) {
return true;
}
return false;
}, [source.eventAttributesExpression, source.resourceAttributesExpression]);
const defaultTab = hasOverviewPanel ? Tab.Overview : Tab.Parsed;
const [queryTab, setQueryTab] = useQueryState(
'tab',
parseAsStringEnum<Tab>(Object.values(Tab)).withDefault(Tab.Overview),
parseAsStringEnum<Tab>(Object.values(Tab)).withDefault(defaultTab),
);
const initialWidth = 80;
@ -114,7 +126,7 @@ export default function DBRowSidePanel({
// Keep track of sub-drawers so we can disable closing this root drawer
const [subDrawerOpen, setSubDrawerOpen] = useState(false);
const [stateTab, setStateTab] = useState<Tab>(Tab.Overview);
const [stateTab, setStateTab] = useState<Tab>(defaultTab);
// Nested panels can't share the query param or else they'll conflict, so we'll use local state for nested panels
// We'll need to handle this properly eventually...
const tab = isNestedPanel ? stateTab : queryTab;
@ -211,15 +223,20 @@ export default function DBRowSidePanel({
});
const hasK8sContext = useMemo(() => {
if (!source?.resourceAttributesExpression || !normalizedRow) {
try {
if (!source?.resourceAttributesExpression || !normalizedRow) {
return false;
}
return (
normalizedRow[source.resourceAttributesExpression]?.['k8s.pod.uid'] !=
null ||
normalizedRow[source.resourceAttributesExpression]?.['k8s.node.name'] !=
null
);
} catch (e) {
console.error(e);
return false;
}
return (
normalizedRow[source.resourceAttributesExpression]['k8s.pod.uid'] !=
null ||
normalizedRow[source.resourceAttributesExpression]['k8s.node.name'] !=
null
);
}, [source, normalizedRow]);
return (
@ -264,10 +281,14 @@ export default function DBRowSidePanel({
<TabBar
className="fs-8 mt-2"
items={[
{
text: 'Overview',
value: Tab.Overview,
},
...(hasOverviewPanel
? [
{
text: 'Overview',
value: Tab.Overview,
},
]
: []),
{
text: 'Column Values',
value: Tab.Parsed,

View file

@ -93,7 +93,7 @@ function FormRow({
...(!helpText ? { opacity: 0, pointerEvents: 'none' } : {}),
}}
>
<Tooltip label={helpText} color="dark" c="white">
<Tooltip label={helpText} color="dark" c="white" multiline maw={600}>
<i className="bi bi-question-circle cursor-pointer" />
</Tooltip>
</Text>
@ -413,7 +413,10 @@ export function TraceTableModelForm({
rules={{ required: 'Table is required' }}
/>
</FormRow>
<FormRow label={'Timestamp Column'}>
<FormRow
label={'Timestamp Column'}
helpText="DateTime column or expression defines the start of the span"
>
<SQLInlineEditorControlled
tableConnections={{
databaseName,
@ -619,6 +622,21 @@ export function TraceTableModelForm({
placeholder="SpanAttributes"
/>
</FormRow>
<FormRow
label={'Span Events Expression'}
helpText="Expression to extract span events. Used to capture events associated with spans. Expected to be Nested ( Timestamp DateTime64(9), Name LowCardinality(String), Attributes Map(LowCardinality(String), String)"
>
<SQLInlineEditorControlled
tableConnections={{
databaseName,
tableName,
connectionId,
}}
control={control}
name="spanEventsValueExpression"
placeholder="Events"
/>
</FormRow>
<FormRow
label={'Implicit Column Expression'}
helpText="Column used for full text search if no property is specified in a Lucene-based search. Typically the message body of a log."

View file

@ -246,6 +246,9 @@ export async function inferTableSourceConfig({
'StatusMessage',
]);
// Check if SpanEvents column is available
const hasSpanEvents = columns.some(col => col.name === 'Events');
const timestampColumns = filterColumnMetaByType(columns, [JSDataType.Date]);
const primaryKeyTimestampColumn = timestampColumns?.find(c =>
keys.find(
@ -299,17 +302,18 @@ export async function inferTableSourceConfig({
traceIdExpression: 'TraceId',
statusCodeExpression: 'StatusCode',
statusMessageExpression: 'StatusMessage',
...(hasSpanEvents ? { spanEventsValueExpression: 'Events' } : {}),
}
: {}),
};
}
export function getDurationMsExpression(source: TSource) {
return `${source.durationExpression}/1e${(source.durationPrecision ?? 9) - 3}`;
return `(${source.durationExpression})/1e${(source.durationPrecision ?? 9) - 3}`;
}
export function getDurationSecondsExpression(source: TSource) {
return `${source.durationExpression}/1e${source.durationPrecision ?? 9}`;
return `(${source.durationExpression})/1e${source.durationPrecision ?? 9}`;
}
const ReqMetricTableColumns = {

View file

@ -251,7 +251,7 @@ const fastifySQL = ({
return parser.sqlify(ast);
} catch (e) {
console.error('[renderWhereExpression]feat: Failed to parse SQL AST', e);
console.debug('[renderWhereExpression]feat: Failed to parse SQL AST', e);
return rawSQL;
}
};

View file

@ -510,6 +510,7 @@ export const SourceSchema = z.object({
durationPrecision: z.number().min(0).max(9).optional(),
parentSpanIdExpression: z.string().optional(),
spanNameExpression: z.string().optional(),
spanEventsValueExpression: z.string().optional(),
spanKindExpression: z.string().optional(),
statusCodeExpression: z.string().optional(),