Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
255 changes: 106 additions & 149 deletions packages/app/src/components/DBInfraPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,22 +32,22 @@ import { TableSourceForm } from '@/components/Sources/SourceForm';
import { IS_LOCAL_MODE } from '@/config';
import { useSource } from '@/source';

import {
K8S_CPU_PERCENTAGE_NUMBER_FORMAT,
K8S_FILESYSTEM_NUMBER_FORMAT,
K8S_MEM_NUMBER_FORMAT,
} from '../ChartUtils';

import { DBTimeChart } from './DBTimeChart';
import {
getActiveInfraCorrelations,
InfraChartSpec,
} from './infraCorrelations';
import { KubeTimeline } from './KubeComponents';

const InfraSubpanelGroup = ({
charts,
fieldPrefix,
metricSource,
timestamp,
title,
where,
}: {
charts: readonly InfraChartSpec[];
fieldPrefix: string;
metricSource: TMetricSource;
timestamp: any;
Expand Down Expand Up @@ -115,96 +115,38 @@ const InfraSubpanelGroup = ({
</Group>
</Group>
<SimpleGrid mt="md" cols={cols}>
<Card data-testid="cpu-usage-card">
<Card.Section py={8} px={8} h={height}>
<DBTimeChart
title="CPU Usage (%)"
config={convertV1ChartConfigToV2(
{
dateRange,
granularity,
seriesReturnType: 'column',
series: [
{
type: 'time',
where,
groupBy: [],
aggFn: 'avg',
field: `${fieldPrefix}cpu.utilization - Gauge`,
table: 'metrics',
numberFormat: K8S_CPU_PERCENTAGE_NUMBER_FORMAT,
},
],
},
{
metric: metricSource,
},
)}
showDisplaySwitcher={false}
logReferenceTimestamp={timestamp / 1000}
/>
</Card.Section>
</Card>
<Card data-testid="memory-usage-card">
<Card.Section py={8} px={8} h={height}>
<DBTimeChart
title="Memory Used"
config={convertV1ChartConfigToV2(
{
dateRange,
granularity,
seriesReturnType: 'column',
series: [
{
type: 'time',
where,
groupBy: [],
aggFn: 'avg',
field: `${fieldPrefix}memory.usage - Gauge`,
table: 'metrics',
numberFormat: K8S_MEM_NUMBER_FORMAT,
},
],
},
{
metric: metricSource,
},
)}
showDisplaySwitcher={false}
logReferenceTimestamp={timestamp / 1000}
/>
</Card.Section>
</Card>
<Card data-testid="disk-usage-card">
<Card.Section py={8} px={8} h={height}>
<DBTimeChart
title="Disk Available"
config={convertV1ChartConfigToV2(
{
dateRange,
granularity,
seriesReturnType: 'column',
series: [
{
type: 'time',
where,
groupBy: [],
aggFn: 'avg',
field: `${fieldPrefix}filesystem.available - Gauge`,
table: 'metrics',
numberFormat: K8S_FILESYSTEM_NUMBER_FORMAT,
},
],
},
{
metric: metricSource,
},
)}
showDisplaySwitcher={false}
logReferenceTimestamp={timestamp / 1000}
/>
</Card.Section>
</Card>
{charts.map(chart => (
<Card key={chart.cardTestId} data-testid={chart.cardTestId}>
<Card.Section py={8} px={8} h={height}>
<DBTimeChart
title={chart.title}
config={convertV1ChartConfigToV2(
{
dateRange,
granularity,
seriesReturnType: 'column',
series: [
{
type: 'time',
where,
groupBy: [],
aggFn: 'avg',
field: `${fieldPrefix}${chart.field} - Gauge`,
table: 'metrics',
numberFormat: chart.numberFormat,
},
],
},
{
metric: metricSource,
},
)}
showDisplaySwitcher={false}
logReferenceTimestamp={timestamp / 1000}
/>
</Card.Section>
</Card>
))}
</SimpleGrid>
</div>
);
Expand All @@ -229,8 +171,11 @@ export default ({
kinds: [SourceKind.Metric],
});

const podUid = rowData?.__hdx_resource_attributes['k8s.pod.uid'];
const nodeName = rowData?.__hdx_resource_attributes['k8s.node.name'];
const resourceAttributes = rowData?.__hdx_resource_attributes;
const activeCorrelations = useMemo(
() => getActiveInfraCorrelations(resourceAttributes),
[resourceAttributes],
);

const timestamp = new Date(rowData?.__hdx_timestamp).getTime();

Expand Down Expand Up @@ -269,57 +214,69 @@ export default ({
)}
</>
)}
{podUid && (
<div>
{metricSource && (
<InfraSubpanelGroup
title="Pod"
where={`${metricSource.resourceAttributesExpression}.k8s.pod.uid:"${podUid}"`}
fieldPrefix="k8s.pod."
timestamp={timestamp}
metricSource={metricSource}
/>
)}
{source && source.kind === SourceKind.Log && (
<Card p="md" mt="xl">
<Card.Section p="md" py="xs">
Pod Timeline
</Card.Section>
<Card.Section>
<ScrollArea
viewportProps={{
style: { maxHeight: 280 },
}}
>
<Box p="md" py="sm">
<KubeTimeline
logSource={source}
q={`\`k8s.pod.uid\`:"${podUid}"`}
dateRange={[
sub(new Date(timestamp), { days: 1 }),
add(new Date(timestamp), { days: 1 }),
]}
anchorEvent={{
label: <div className="text-brand">This Event</div>,
timestamp: new Date(timestamp).toISOString(),
}}
/>
</Box>
</ScrollArea>
</Card.Section>
</Card>
)}
</div>
)}
{nodeName && metricSource && (
<InfraSubpanelGroup
metricSource={metricSource}
title="Node"
where={`${metricSource.resourceAttributesExpression}.k8s.node.name:"${nodeName}"`}
fieldPrefix="k8s.node."
timestamp={timestamp}
/>
)}
{activeCorrelations.map(correlation => {
const value = resourceAttributes?.[correlation.correlateAttribute];
// Truthiness guard, mirroring the previous Pod/Node render blocks
// (which gated on the attribute value with `&&`); the tab gate uses
// != null. detect and correlate are the same attribute for the
// built-in k8s descriptors, so this stays byte-identical. A future
// descriptor that splits the two decides here how an empty correlate
// value should render.
if (!value) {
return null;
}
Comment on lines +218 to +227

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 The render guard uses falsy semantics (!value) while getActiveInfraCorrelations (the gate) uses != null. They should match. For the current Kubernetes descriptors where detectAttribute === correlateAttribute and the value is always a non-empty string, this is harmless — but the declared purpose of separating detectAttribute from correlateAttribute is to let a future descriptor detect on one attribute and correlate on another. If that future correlateAttribute holds an empty string, 0, or false, the gate would pass (the Infrastructure tab would appear) while the render guard would silently skip every group, leaving the panel empty.

Suggested change
const value = resourceAttributes?.[correlation.correlateAttribute];
if (!value) {
return null;
}
const value = resourceAttributes?.[correlation.correlateAttribute];
if (value == null) {
return null;
}

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

const showTimeline =
correlation.timeline != null && source.kind === SourceKind.Log;
// Skip rendering an empty container when neither the metric group nor
// the timeline has anything to show (e.g. no metric source configured
// on a non-Log source).
if (!metricSource && !showTimeline) {
return null;
}
return (
<div key={correlation.title}>
{metricSource && (
<InfraSubpanelGroup
title={correlation.title}
where={`${metricSource.resourceAttributesExpression}.${correlation.correlateAttribute}:"${value}"`}
fieldPrefix={correlation.fieldPrefix}
charts={correlation.charts}
timestamp={timestamp}
metricSource={metricSource}
/>
)}
{correlation.timeline && source.kind === SourceKind.Log && (
<Card p="md" mt="xl">
<Card.Section p="md" py="xs">
{correlation.title} Timeline
</Card.Section>
<Card.Section>
<ScrollArea
viewportProps={{
style: { maxHeight: 280 },
}}
>
<Box p="md" py="sm">
<KubeTimeline
logSource={source}
q={`\`${correlation.timeline.queryAttribute}\`:"${resourceAttributes?.[correlation.timeline.queryAttribute]}"`}
dateRange={[
sub(new Date(timestamp), { days: 1 }),
add(new Date(timestamp), { days: 1 }),
]}
anchorEvent={{
label: <div className="text-brand">This Event</div>,
timestamp: new Date(timestamp).toISOString(),
}}
/>
</Box>
</ScrollArea>
</Card.Section>
</Card>
)}
</div>
);
})}
</Stack>
);
};
15 changes: 8 additions & 7 deletions packages/app/src/components/DBRowDataPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { getDisplayedTimestampValueExpression, getEventBody } from '@/source';
import { getSelectExpressionsForHighlightedAttributes } from '@/utils/highlightedAttributes';

import { DBRowJsonViewer } from './DBRowJsonViewer';
import { getActiveInfraCorrelations } from './infraCorrelations';

export enum ROW_DATA_ALIASES {
TIMESTAMP = '__hdx_timestamp',
Expand Down Expand Up @@ -190,9 +191,12 @@ export function useRowData({
};
}

// Detects whether a normalized row carries Kubernetes resource attributes, used
// to conditionally surface the Infrastructure tab/panel. Requires the source to
// expose resource attributes; returns false (rather than throwing) on any gap.
// Detects whether a normalized row carries resource attributes that match a
// built-in infrastructure correlation (Kubernetes Pod or Node today), used to
// conditionally surface the Infrastructure tab/panel. Delegates to the same
// descriptor list the panel renders from, so the gate and the render never
// drift apart. Requires the source to expose resource attributes; returns
// false (rather than throwing) on any gap.
export function rowHasK8sContext(
source: TSource | null | undefined,
normalizedRow: Record<string, any> | null | undefined,
Expand All @@ -208,10 +212,7 @@ export function rowHasK8sContext(
}

const resourceAttrs = normalizedRow[ROW_DATA_ALIASES.RESOURCE_ATTRIBUTES];
return (
resourceAttrs?.['k8s.pod.uid'] != null ||
resourceAttrs?.['k8s.node.name'] != null
);
return getActiveInfraCorrelations(resourceAttrs).length > 0;
} catch (e) {
console.error(e);
return false;
Expand Down
Loading
Loading