mirror of
https://github.com/hyperdxio/hyperdx
synced 2026-04-21 13:37:15 +00:00
feat: introduce k8s preset dashboard - Pt2 (#719)
Ref: HDX-1543 + HDX-1545 - fix: k8s dashboard uptime metrics - fix: warning k8s event body - feat: add side panel to warning events table - perf: opt pods/nodes/namespaces logs table performance <img width="1227" alt="Screenshot 2025-03-27 at 6 28 15 PM" src="https://github.com/user-attachments/assets/f222ca14-68f4-413c-9867-6f5a98aad025" /> <img width="488" alt="image" src="https://github.com/user-attachments/assets/e3076ec7-95c6-4e4b-a961-e7779d0a88ea" />
This commit is contained in:
parent
a6fd5e3535
commit
decd622fdf
8 changed files with 334 additions and 74 deletions
5
.changeset/twenty-trains-turn.md
Normal file
5
.changeset/twenty-trains-turn.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@hyperdx/app": patch
|
||||
---
|
||||
|
||||
fix: k8s dashboard uptime metrics + warning k8s event body
|
||||
|
|
@ -933,7 +933,10 @@ export function formatResponseForTimeChart({
|
|||
}
|
||||
|
||||
// Define a mapping from app AggFn to common-utils AggregateFunction
|
||||
export const mapV1AggFnToV2 = (aggFn: AggFn): AggFnV2 => {
|
||||
export const mapV1AggFnToV2 = (aggFn?: AggFn): AggFnV2 | undefined => {
|
||||
if (aggFn == null) {
|
||||
return aggFn;
|
||||
}
|
||||
// Map rate-based aggregations to their base aggregation
|
||||
if (aggFn.endsWith('_rate')) {
|
||||
return mapV1AggFnToV2(aggFn.replace('_rate', '') as AggFn);
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ import {
|
|||
import { TimePicker } from '@/components/TimePicker';
|
||||
|
||||
import { ConnectionSelectControlled } from './components/ConnectionSelect';
|
||||
import DBRowSidePanel from './components/DBRowSidePanel';
|
||||
import { DBSqlRowTable } from './components/DBRowTable';
|
||||
import { DBTimeChart } from './components/DBTimeChart';
|
||||
import { FormatPodStatus } from './components/KubeComponents';
|
||||
|
|
@ -56,7 +57,7 @@ import NamespaceDetailsSidePanel from './NamespaceDetailsSidePanel';
|
|||
import NodeDetailsSidePanel from './NodeDetailsSidePanel';
|
||||
import PodDetailsSidePanel from './PodDetailsSidePanel';
|
||||
import HdxSearchInput from './SearchInput';
|
||||
import { getEventBody, useSources } from './source';
|
||||
import { getEventBody, useSource, useSources } from './source';
|
||||
import { parseTimeQuery, useTimeQuery } from './timeQuery';
|
||||
import { KubePhase } from './types';
|
||||
import { formatNumber, formatUptime } from './utils';
|
||||
|
|
@ -189,7 +190,7 @@ export const InfraPodsStatusTable = ({
|
|||
table: 'metrics',
|
||||
field: 'k8s.pod.uptime - Sum',
|
||||
type: 'table',
|
||||
aggFn: 'sum',
|
||||
aggFn: undefined,
|
||||
where,
|
||||
groupBy,
|
||||
...(sortState.column === 'uptime' && {
|
||||
|
|
@ -259,7 +260,7 @@ export const InfraPodsStatusTable = ({
|
|||
row["arrayElement(ResourceAttributes, 'k8s.namespace.name')"],
|
||||
node: row["arrayElement(ResourceAttributes, 'k8s.node.name')"],
|
||||
restarts: row['last_value(k8s.container.restarts)'],
|
||||
uptime: row['sum(k8s.pod.uptime)'],
|
||||
uptime: row['undefined(k8s.pod.uptime)'],
|
||||
cpuAvg: row['avg(k8s.pod.cpu.utilization)'],
|
||||
cpuLimitUtilization: row['avg(k8s.pod.cpu_limit_utilization)'],
|
||||
memAvg: row['avg(k8s.pod.memory.usage)'],
|
||||
|
|
@ -477,7 +478,7 @@ const NodesTable = ({
|
|||
table: 'metrics',
|
||||
field: 'k8s.node.uptime - Sum',
|
||||
type: 'table',
|
||||
aggFn: 'avg',
|
||||
aggFn: undefined,
|
||||
where,
|
||||
groupBy,
|
||||
},
|
||||
|
|
@ -508,7 +509,7 @@ const NodesTable = ({
|
|||
cpuAvg: row['avg(k8s.node.cpu.utilization)'],
|
||||
memAvg: row['avg(k8s.node.memory.usage)'],
|
||||
ready: row['avg(k8s.node.condition_ready)'],
|
||||
uptime: row['avg(k8s.node.uptime)'],
|
||||
uptime: row['undefined(k8s.node.uptime)'],
|
||||
};
|
||||
});
|
||||
}, [data]);
|
||||
|
|
@ -866,6 +867,25 @@ function KubernetesDashboardPage() {
|
|||
},
|
||||
[_searchQuery, setSearchQuery],
|
||||
);
|
||||
|
||||
// Row details side panel
|
||||
const [rowId, setRowId] = useQueryState('rowWhere');
|
||||
const [rowSource, setRowSource] = useQueryState('rowSource');
|
||||
const { data: rowSidePanelSource } = useSource({ id: rowSource || '' });
|
||||
|
||||
const handleSidePanelClose = React.useCallback(() => {
|
||||
setRowId(null);
|
||||
setRowSource(null);
|
||||
}, [setRowId, setRowSource]);
|
||||
|
||||
const handleRowExpandClick = React.useCallback(
|
||||
(rowWhere: string) => {
|
||||
setRowId(rowWhere);
|
||||
setRowSource(logSource?.id ?? null);
|
||||
},
|
||||
[logSource?.id, setRowId, setRowSource],
|
||||
);
|
||||
|
||||
return (
|
||||
<Box p="sm">
|
||||
<OnboardingModal requireSource={false} />
|
||||
|
|
@ -887,6 +907,13 @@ function KubernetesDashboardPage() {
|
|||
logSource={logSource}
|
||||
/>
|
||||
)}
|
||||
{rowId && rowSidePanelSource && (
|
||||
<DBRowSidePanel
|
||||
source={rowSidePanelSource}
|
||||
rowId={rowId}
|
||||
onClose={handleSidePanelClose}
|
||||
/>
|
||||
)}
|
||||
<Group justify="space-between">
|
||||
<Group>
|
||||
<Text c="gray.4" size="xl">
|
||||
|
|
@ -1074,7 +1101,7 @@ function KubernetesDashboardPage() {
|
|||
alias: 'Name',
|
||||
},
|
||||
{
|
||||
valueExpression: `${getEventBody(logSource)}`,
|
||||
valueExpression: `JSONExtractString(${getEventBody(logSource)}, 'object', 'note')`,
|
||||
alias: 'Message',
|
||||
},
|
||||
],
|
||||
|
|
@ -1088,8 +1115,8 @@ function KubernetesDashboardPage() {
|
|||
limit: { limit: 200, offset: 0 },
|
||||
dateRange,
|
||||
}}
|
||||
onRowExpandClick={() => {}}
|
||||
highlightedLineId={undefined}
|
||||
onRowExpandClick={handleRowExpandClick}
|
||||
highlightedLineId={rowId ?? undefined}
|
||||
isLive={false}
|
||||
queryKeyPrefix="k8s-dashboard-events"
|
||||
onScroll={() => {}}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import * as React from 'react';
|
|||
import Link from 'next/link';
|
||||
import Drawer from 'react-modern-drawer';
|
||||
import { StringParam, useQueryParam, withDefault } from 'use-query-params';
|
||||
import { tcFromSource } from '@hyperdx/common-utils/dist/metadata';
|
||||
import { TSource } from '@hyperdx/common-utils/dist/types';
|
||||
import {
|
||||
Anchor,
|
||||
|
|
@ -30,6 +31,8 @@ import { getEventBody } from '@/source';
|
|||
import { parseTimeQuery, useTimeQuery } from '@/timeQuery';
|
||||
import { useZIndex, ZIndexContext } from '@/zIndex';
|
||||
|
||||
import { useGetKeyValues, useTableMetadata } from './hooks/useMetadata';
|
||||
|
||||
import styles from '../styles/LogSidePanel.module.scss';
|
||||
|
||||
const CHART_HEIGHT = 300;
|
||||
|
|
@ -226,8 +229,8 @@ export default function NamespaceDetailsSidePanel({
|
|||
metricSource,
|
||||
logSource,
|
||||
}: {
|
||||
metricSource?: TSource;
|
||||
logSource?: TSource;
|
||||
metricSource: TSource;
|
||||
logSource: TSource;
|
||||
}) {
|
||||
const [namespaceName, setNamespaceName] = useQueryParam(
|
||||
'namespaceName',
|
||||
|
|
@ -240,9 +243,9 @@ export default function NamespaceDetailsSidePanel({
|
|||
const contextZIndex = useZIndex();
|
||||
const drawerZIndex = contextZIndex + 10;
|
||||
|
||||
const where = React.useMemo(() => {
|
||||
const metricsWhere = React.useMemo(() => {
|
||||
return `${metricSource?.resourceAttributesExpression}.k8s.namespace.name:"${namespaceName}"`;
|
||||
}, [namespaceName]);
|
||||
}, [namespaceName, metricSource]);
|
||||
|
||||
const { searchedTimeRange: dateRange } = useTimeQuery({
|
||||
defaultValue: 'Past 1h',
|
||||
|
|
@ -252,6 +255,72 @@ export default function NamespaceDetailsSidePanel({
|
|||
],
|
||||
});
|
||||
|
||||
const { data: logsTableMetadata } = useTableMetadata(tcFromSource(logSource));
|
||||
|
||||
let doesPrimaryOrSortingKeysContainServiceExpression = false;
|
||||
|
||||
if (
|
||||
logSource?.serviceNameExpression &&
|
||||
(logsTableMetadata?.primary_key || logsTableMetadata?.sorting_key)
|
||||
) {
|
||||
if (
|
||||
logsTableMetadata.primary_key &&
|
||||
logsTableMetadata.primary_key.includes(logSource.serviceNameExpression)
|
||||
) {
|
||||
doesPrimaryOrSortingKeysContainServiceExpression = true;
|
||||
} else if (
|
||||
logsTableMetadata.sorting_key &&
|
||||
logsTableMetadata.sorting_key.includes(logSource.serviceNameExpression)
|
||||
) {
|
||||
doesPrimaryOrSortingKeysContainServiceExpression = true;
|
||||
}
|
||||
}
|
||||
|
||||
const { data: logServiceNames } = useGetKeyValues(
|
||||
{
|
||||
chartConfig: {
|
||||
from: logSource.from,
|
||||
where: `${logSource?.resourceAttributesExpression}.k8s.namespace.name:"${namespaceName}"`,
|
||||
whereLanguage: 'lucene',
|
||||
select: '',
|
||||
timestampValueExpression: logSource.timestampValueExpression ?? '',
|
||||
connection: logSource.connection,
|
||||
dateRange,
|
||||
},
|
||||
keys: [logSource.serviceNameExpression ?? ''],
|
||||
limit: 10,
|
||||
disableRowLimit: false,
|
||||
},
|
||||
{
|
||||
enabled:
|
||||
!!namespaceName &&
|
||||
!!logSource.serviceNameExpression &&
|
||||
doesPrimaryOrSortingKeysContainServiceExpression,
|
||||
},
|
||||
);
|
||||
|
||||
// HACK: craft where clause for logs given the ServiceName is part of the primary key
|
||||
const logsWhere = React.useMemo(() => {
|
||||
const _where = `${logSource?.resourceAttributesExpression}.k8s.namespace.name:"${namespaceName}"`;
|
||||
if (
|
||||
logServiceNames &&
|
||||
logServiceNames[0].value.length > 0 &&
|
||||
doesPrimaryOrSortingKeysContainServiceExpression
|
||||
) {
|
||||
const _svs: string[] = logServiceNames[0].value;
|
||||
const _key = logServiceNames[0].key;
|
||||
return `(${_svs
|
||||
.map(sv => `${_key}:"${sv}"`)
|
||||
.join(' OR ')}) AND ${_where}`;
|
||||
}
|
||||
return _where;
|
||||
}, [
|
||||
namespaceName,
|
||||
logSource,
|
||||
doesPrimaryOrSortingKeysContainServiceExpression,
|
||||
logServiceNames,
|
||||
]);
|
||||
|
||||
const handleClose = React.useCallback(() => {
|
||||
setNamespaceName(undefined);
|
||||
}, [setNamespaceName]);
|
||||
|
|
@ -303,7 +372,7 @@ export default function NamespaceDetailsSidePanel({
|
|||
{
|
||||
type: 'time',
|
||||
groupBy: ['k8s.pod.name'],
|
||||
where,
|
||||
where: metricsWhere,
|
||||
table: 'metrics',
|
||||
aggFn: 'avg',
|
||||
field: 'k8s.pod.cpu.utilization - Gauge',
|
||||
|
|
@ -338,7 +407,7 @@ export default function NamespaceDetailsSidePanel({
|
|||
{
|
||||
type: 'time',
|
||||
groupBy: ['k8s.pod.name'],
|
||||
where,
|
||||
where: metricsWhere,
|
||||
table: 'metrics',
|
||||
aggFn: 'avg',
|
||||
field: 'k8s.pod.memory.usage - Gauge',
|
||||
|
|
@ -359,14 +428,14 @@ export default function NamespaceDetailsSidePanel({
|
|||
<InfraPodsStatusTable
|
||||
dateRange={dateRange}
|
||||
metricSource={metricSource}
|
||||
where={where}
|
||||
where={metricsWhere}
|
||||
/>
|
||||
)}
|
||||
</Grid.Col>
|
||||
<Grid.Col span={12}>
|
||||
{logSource && (
|
||||
<NamespaceLogs
|
||||
where={where}
|
||||
where={logsWhere}
|
||||
dateRange={dateRange}
|
||||
logSource={logSource}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import * as React from 'react';
|
|||
import Link from 'next/link';
|
||||
import Drawer from 'react-modern-drawer';
|
||||
import { StringParam, useQueryParam, withDefault } from 'use-query-params';
|
||||
import { tcFromSource } from '@hyperdx/common-utils/dist/metadata';
|
||||
import {
|
||||
SearchConditionLanguage,
|
||||
TSource,
|
||||
|
|
@ -18,7 +19,6 @@ import {
|
|||
Text,
|
||||
} from '@mantine/core';
|
||||
|
||||
import api from '@/api';
|
||||
import {
|
||||
convertDateRangeToGranularityString,
|
||||
convertV1ChartConfigToV2,
|
||||
|
|
@ -34,6 +34,9 @@ import { parseTimeQuery, useTimeQuery } from '@/timeQuery';
|
|||
import { formatUptime } from '@/utils';
|
||||
import { useZIndex, ZIndexContext } from '@/zIndex';
|
||||
|
||||
import { useQueriedChartConfig } from './hooks/useChartConfig';
|
||||
import { useGetKeyValues, useTableMetadata } from './hooks/useMetadata';
|
||||
|
||||
import styles from '../styles/LogSidePanel.module.scss';
|
||||
|
||||
const CHART_HEIGHT = 300;
|
||||
|
|
@ -58,44 +61,55 @@ const PodDetailsProperty = React.memo(
|
|||
const NodeDetails = ({
|
||||
name,
|
||||
dateRange,
|
||||
metricSource,
|
||||
}: {
|
||||
name: string;
|
||||
dateRange: [Date, Date];
|
||||
metricSource: TSource;
|
||||
}) => {
|
||||
const where = `k8s.node.name:"${name}"`;
|
||||
const where = `${metricSource.resourceAttributesExpression}.k8s.node.name:"${name}"`;
|
||||
const groupBy = ['k8s.node.name'];
|
||||
|
||||
const { data } = api.useMultiSeriesChart({
|
||||
series: [
|
||||
const { data, isError, isLoading } = useQueriedChartConfig(
|
||||
convertV1ChartConfigToV2(
|
||||
{
|
||||
table: 'metrics',
|
||||
field: 'k8s.node.condition_ready - Gauge',
|
||||
type: 'table',
|
||||
aggFn: 'last_value',
|
||||
where,
|
||||
groupBy,
|
||||
series: [
|
||||
{
|
||||
table: 'metrics',
|
||||
field: 'k8s.node.condition_ready - Gauge',
|
||||
type: 'table',
|
||||
aggFn: 'last_value',
|
||||
where,
|
||||
groupBy,
|
||||
},
|
||||
{
|
||||
table: 'metrics',
|
||||
field: 'k8s.node.uptime - Sum',
|
||||
type: 'table',
|
||||
aggFn: undefined,
|
||||
where,
|
||||
groupBy,
|
||||
},
|
||||
],
|
||||
dateRange,
|
||||
seriesReturnType: 'column',
|
||||
},
|
||||
{
|
||||
table: 'metrics',
|
||||
field: 'k8s.node.uptime - Sum',
|
||||
type: 'table',
|
||||
aggFn: 'last_value',
|
||||
where,
|
||||
groupBy,
|
||||
metric: metricSource,
|
||||
},
|
||||
],
|
||||
endDate: dateRange[1] ?? new Date(),
|
||||
startDate: dateRange[0] ?? new Date(),
|
||||
seriesReturnType: 'column',
|
||||
});
|
||||
),
|
||||
);
|
||||
|
||||
const properties = React.useMemo(() => {
|
||||
const series: Record<string, any> = data?.data?.[0] || {};
|
||||
if (!data) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
ready: series['series_0.data'],
|
||||
uptime: series['series_1.data'],
|
||||
ready: data.data?.[0]?.['last_value(k8s.node.condition_ready)'],
|
||||
uptime: data.data?.[0]?.['undefined(k8s.node.uptime)'],
|
||||
};
|
||||
}, [data?.data]);
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<Grid.Col span={12}>
|
||||
|
|
@ -234,8 +248,8 @@ export default function NodeDetailsSidePanel({
|
|||
metricSource,
|
||||
logSource,
|
||||
}: {
|
||||
metricSource?: TSource;
|
||||
logSource?: TSource;
|
||||
metricSource: TSource;
|
||||
logSource: TSource;
|
||||
}) {
|
||||
const [nodeName, setNodeName] = useQueryParam(
|
||||
'nodeName',
|
||||
|
|
@ -248,7 +262,7 @@ export default function NodeDetailsSidePanel({
|
|||
const contextZIndex = useZIndex();
|
||||
const drawerZIndex = contextZIndex + 10;
|
||||
|
||||
const where = React.useMemo(() => {
|
||||
const metricsWhere = React.useMemo(() => {
|
||||
return `${metricSource?.resourceAttributesExpression}.k8s.node.name:"${nodeName}"`;
|
||||
}, [nodeName, metricSource]);
|
||||
|
||||
|
|
@ -260,6 +274,72 @@ export default function NodeDetailsSidePanel({
|
|||
],
|
||||
});
|
||||
|
||||
const { data: logsTableMetadata } = useTableMetadata(tcFromSource(logSource));
|
||||
|
||||
let doesPrimaryOrSortingKeysContainServiceExpression = false;
|
||||
|
||||
if (
|
||||
logSource?.serviceNameExpression &&
|
||||
(logsTableMetadata?.primary_key || logsTableMetadata?.sorting_key)
|
||||
) {
|
||||
if (
|
||||
logsTableMetadata.primary_key &&
|
||||
logsTableMetadata.primary_key.includes(logSource.serviceNameExpression)
|
||||
) {
|
||||
doesPrimaryOrSortingKeysContainServiceExpression = true;
|
||||
} else if (
|
||||
logsTableMetadata.sorting_key &&
|
||||
logsTableMetadata.sorting_key.includes(logSource.serviceNameExpression)
|
||||
) {
|
||||
doesPrimaryOrSortingKeysContainServiceExpression = true;
|
||||
}
|
||||
}
|
||||
|
||||
const { data: logServiceNames } = useGetKeyValues(
|
||||
{
|
||||
chartConfig: {
|
||||
from: logSource.from,
|
||||
where: `${logSource?.resourceAttributesExpression}.k8s.node.name:"${nodeName}"`,
|
||||
whereLanguage: 'lucene',
|
||||
select: '',
|
||||
timestampValueExpression: logSource.timestampValueExpression ?? '',
|
||||
connection: logSource.connection,
|
||||
dateRange,
|
||||
},
|
||||
keys: [logSource.serviceNameExpression ?? ''],
|
||||
limit: 10,
|
||||
disableRowLimit: false,
|
||||
},
|
||||
{
|
||||
enabled:
|
||||
!!nodeName &&
|
||||
!!logSource.serviceNameExpression &&
|
||||
doesPrimaryOrSortingKeysContainServiceExpression,
|
||||
},
|
||||
);
|
||||
|
||||
// HACK: craft where clause for logs given the ServiceName is part of the primary key
|
||||
const logsWhere = React.useMemo(() => {
|
||||
const _where = `${logSource?.resourceAttributesExpression}.k8s.node.name:"${nodeName}"`;
|
||||
if (
|
||||
logServiceNames &&
|
||||
logServiceNames[0].value.length > 0 &&
|
||||
doesPrimaryOrSortingKeysContainServiceExpression
|
||||
) {
|
||||
const _svs: string[] = logServiceNames[0].value;
|
||||
const _key = logServiceNames[0].key;
|
||||
return `(${_svs
|
||||
.map(sv => `${_key}:"${sv}"`)
|
||||
.join(' OR ')}) AND ${_where}`;
|
||||
}
|
||||
return _where;
|
||||
}, [
|
||||
nodeName,
|
||||
logSource,
|
||||
doesPrimaryOrSortingKeysContainServiceExpression,
|
||||
logServiceNames,
|
||||
]);
|
||||
|
||||
const handleClose = React.useCallback(() => {
|
||||
setNodeName(undefined);
|
||||
}, [setNodeName]);
|
||||
|
|
@ -287,7 +367,11 @@ export default function NodeDetailsSidePanel({
|
|||
/>
|
||||
<DrawerBody>
|
||||
<Grid>
|
||||
<NodeDetails name={nodeName} dateRange={dateRange} />
|
||||
<NodeDetails
|
||||
name={nodeName}
|
||||
dateRange={dateRange}
|
||||
metricSource={metricSource}
|
||||
/>
|
||||
<Grid.Col span={6}>
|
||||
<Card p="md">
|
||||
<Card.Section p="md" py="xs" withBorder>
|
||||
|
|
@ -307,7 +391,7 @@ export default function NodeDetailsSidePanel({
|
|||
{
|
||||
type: 'time',
|
||||
groupBy: ['k8s.pod.name'],
|
||||
where,
|
||||
where: metricsWhere,
|
||||
table: 'metrics',
|
||||
aggFn: 'avg',
|
||||
field: 'k8s.pod.cpu.utilization - Gauge',
|
||||
|
|
@ -342,7 +426,7 @@ export default function NodeDetailsSidePanel({
|
|||
{
|
||||
type: 'time',
|
||||
groupBy: ['k8s.pod.name'],
|
||||
where,
|
||||
where: metricsWhere,
|
||||
table: 'metrics',
|
||||
aggFn: 'avg',
|
||||
field: 'k8s.pod.memory.usage - Gauge',
|
||||
|
|
@ -363,14 +447,14 @@ export default function NodeDetailsSidePanel({
|
|||
<InfraPodsStatusTable
|
||||
metricSource={metricSource}
|
||||
dateRange={dateRange}
|
||||
where={where}
|
||||
where={metricsWhere}
|
||||
/>
|
||||
)}
|
||||
</Grid.Col>
|
||||
<Grid.Col span={12}>
|
||||
{logSource && (
|
||||
<NodeLogs
|
||||
where={where}
|
||||
where={logsWhere}
|
||||
dateRange={dateRange}
|
||||
logSource={logSource}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import * as React from 'react';
|
|||
import Link from 'next/link';
|
||||
import Drawer from 'react-modern-drawer';
|
||||
import { StringParam, useQueryParam, withDefault } from 'use-query-params';
|
||||
import { tcFromSource } from '@hyperdx/common-utils/dist/metadata';
|
||||
import { TSource } from '@hyperdx/common-utils/dist/types';
|
||||
import {
|
||||
Anchor,
|
||||
|
|
@ -27,6 +28,7 @@ import { KubeTimeline, useV2LogBatch } from '@/components/KubeComponents';
|
|||
import { parseTimeQuery, useTimeQuery } from '@/timeQuery';
|
||||
import { useZIndex, ZIndexContext } from '@/zIndex';
|
||||
|
||||
import { useGetKeyValues, useTableMetadata } from './hooks/useMetadata';
|
||||
import { getEventBody } from './source';
|
||||
|
||||
import styles from '../styles/LogSidePanel.module.scss';
|
||||
|
|
@ -51,15 +53,15 @@ const PodDetailsProperty = React.memo(
|
|||
);
|
||||
|
||||
const PodDetails = ({
|
||||
podName,
|
||||
dateRange,
|
||||
logSource,
|
||||
podName,
|
||||
}: {
|
||||
podName: string;
|
||||
dateRange: [Date, Date];
|
||||
logSource: TSource;
|
||||
podName: string;
|
||||
}) => {
|
||||
const { data } = useV2LogBatch<{
|
||||
const { data: logsData } = useV2LogBatch<{
|
||||
'k8s.node.name': string;
|
||||
'k8s.pod.name': string;
|
||||
'k8s.pod.uid': string;
|
||||
|
|
@ -96,11 +98,11 @@ const PodDetails = ({
|
|||
],
|
||||
});
|
||||
|
||||
if (data?.data?.[0] == null) {
|
||||
if (logsData?.data?.[0] == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const properties = data.data[0] ?? {};
|
||||
const properties = logsData.data[0] ?? {};
|
||||
|
||||
// If all properties are empty, don't show the panel
|
||||
if (Object.values(properties).every(v => !v)) {
|
||||
|
|
@ -243,7 +245,7 @@ export default function PodDetailsSidePanel({
|
|||
const contextZIndex = useZIndex();
|
||||
const drawerZIndex = contextZIndex + 10 + (isNested ? 100 : 0);
|
||||
|
||||
const where = React.useMemo(() => {
|
||||
const metricsWhere = React.useMemo(() => {
|
||||
return `${metricSource?.resourceAttributesExpression}.k8s.pod.name:"${podName}"`;
|
||||
}, [podName, metricSource]);
|
||||
|
||||
|
|
@ -255,6 +257,72 @@ export default function PodDetailsSidePanel({
|
|||
],
|
||||
});
|
||||
|
||||
const { data: logsTableMetadata } = useTableMetadata(tcFromSource(logSource));
|
||||
|
||||
let doesPrimaryOrSortingKeysContainServiceExpression = false;
|
||||
|
||||
if (
|
||||
logSource?.serviceNameExpression &&
|
||||
(logsTableMetadata?.primary_key || logsTableMetadata?.sorting_key)
|
||||
) {
|
||||
if (
|
||||
logsTableMetadata.primary_key &&
|
||||
logsTableMetadata.primary_key.includes(logSource.serviceNameExpression)
|
||||
) {
|
||||
doesPrimaryOrSortingKeysContainServiceExpression = true;
|
||||
} else if (
|
||||
logsTableMetadata.sorting_key &&
|
||||
logsTableMetadata.sorting_key.includes(logSource.serviceNameExpression)
|
||||
) {
|
||||
doesPrimaryOrSortingKeysContainServiceExpression = true;
|
||||
}
|
||||
}
|
||||
|
||||
const { data: logServiceNames } = useGetKeyValues(
|
||||
{
|
||||
chartConfig: {
|
||||
from: logSource.from,
|
||||
where: `${logSource?.resourceAttributesExpression}.k8s.pod.name:"${podName}"`,
|
||||
whereLanguage: 'lucene',
|
||||
select: '',
|
||||
timestampValueExpression: logSource.timestampValueExpression ?? '',
|
||||
connection: logSource.connection,
|
||||
dateRange,
|
||||
},
|
||||
keys: [logSource.serviceNameExpression ?? ''],
|
||||
limit: 10,
|
||||
disableRowLimit: false,
|
||||
},
|
||||
{
|
||||
enabled:
|
||||
!!podName &&
|
||||
!!logSource.serviceNameExpression &&
|
||||
doesPrimaryOrSortingKeysContainServiceExpression,
|
||||
},
|
||||
);
|
||||
|
||||
// HACK: craft where clause for logs given the ServiceName is part of the primary key
|
||||
const logsWhere = React.useMemo(() => {
|
||||
const _where = `${logSource?.resourceAttributesExpression}.k8s.pod.name:"${podName}"`;
|
||||
if (
|
||||
logServiceNames &&
|
||||
logServiceNames[0].value.length > 0 &&
|
||||
doesPrimaryOrSortingKeysContainServiceExpression
|
||||
) {
|
||||
const _svs: string[] = logServiceNames[0].value;
|
||||
const _key = logServiceNames[0].key;
|
||||
return `(${_svs
|
||||
.map(sv => `${_key}:"${sv}"`)
|
||||
.join(' OR ')}) AND ${_where}`;
|
||||
}
|
||||
return _where;
|
||||
}, [
|
||||
nodeName,
|
||||
logSource,
|
||||
doesPrimaryOrSortingKeysContainServiceExpression,
|
||||
logServiceNames,
|
||||
]);
|
||||
|
||||
const handleClose = React.useCallback(() => {
|
||||
setPodName(undefined);
|
||||
}, [setPodName]);
|
||||
|
|
@ -283,9 +351,9 @@ export default function PodDetailsSidePanel({
|
|||
<DrawerBody>
|
||||
<Grid>
|
||||
<PodDetails
|
||||
podName={podName}
|
||||
dateRange={dateRange}
|
||||
logSource={logSource}
|
||||
podName={podName}
|
||||
/>
|
||||
<Grid.Col span={6}>
|
||||
<Card p="md">
|
||||
|
|
@ -306,7 +374,7 @@ export default function PodDetailsSidePanel({
|
|||
{
|
||||
type: 'time',
|
||||
groupBy: ['k8s.pod.name'],
|
||||
where,
|
||||
where: metricsWhere,
|
||||
table: 'metrics',
|
||||
aggFn: 'avg',
|
||||
field: 'k8s.pod.cpu.utilization - Gauge',
|
||||
|
|
@ -342,7 +410,7 @@ export default function PodDetailsSidePanel({
|
|||
{
|
||||
type: 'time',
|
||||
groupBy: ['k8s.pod.name'],
|
||||
where,
|
||||
where: metricsWhere,
|
||||
table: 'metrics',
|
||||
aggFn: 'avg',
|
||||
field: 'k8s.pod.memory.usage - Gauge',
|
||||
|
|
@ -384,7 +452,7 @@ export default function PodDetailsSidePanel({
|
|||
<Grid.Col span={12}>
|
||||
<PodLogs
|
||||
logSource={logSource}
|
||||
where={where}
|
||||
where={logsWhere}
|
||||
dateRange={dateRange}
|
||||
/>
|
||||
</Grid.Col>
|
||||
|
|
|
|||
|
|
@ -95,19 +95,22 @@ export function useTableMetadata(
|
|||
});
|
||||
}
|
||||
|
||||
export function useGetKeyValues({
|
||||
chartConfig,
|
||||
keys,
|
||||
limit,
|
||||
disableRowLimit,
|
||||
}: {
|
||||
chartConfig: ChartConfigWithDateRange;
|
||||
keys: string[];
|
||||
limit?: number;
|
||||
disableRowLimit?: boolean;
|
||||
}) {
|
||||
export function useGetKeyValues(
|
||||
{
|
||||
chartConfig,
|
||||
keys,
|
||||
limit,
|
||||
disableRowLimit,
|
||||
}: {
|
||||
chartConfig: ChartConfigWithDateRange;
|
||||
keys: string[];
|
||||
limit?: number;
|
||||
disableRowLimit?: boolean;
|
||||
},
|
||||
options?: Omit<UseQueryOptions<any, Error>, 'queryKey'>,
|
||||
) {
|
||||
const metadata = getMetadata();
|
||||
return useQuery({
|
||||
return useQuery<{ key: string; value: string[] }[]>({
|
||||
queryKey: ['useMetadata.useGetKeyValues', { chartConfig, keys }],
|
||||
queryFn: async () => {
|
||||
return metadata.getKeyValues({
|
||||
|
|
@ -120,6 +123,7 @@ export function useGetKeyValues({
|
|||
staleTime: 1000 * 60 * 5, // Cache every 5 min
|
||||
enabled: !!keys.length,
|
||||
placeholderData: keepPreviousData,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -158,7 +158,7 @@ export type TimeChartSeries = {
|
|||
displayName?: string;
|
||||
table: SourceTable;
|
||||
type: 'time';
|
||||
aggFn: AggFn; // TODO: Type
|
||||
aggFn?: AggFn; // TODO: Type
|
||||
field?: string | undefined;
|
||||
where: string;
|
||||
groupBy: string[];
|
||||
|
|
@ -177,7 +177,7 @@ export type TableChartSeries = {
|
|||
displayName?: string;
|
||||
type: 'table';
|
||||
table: SourceTable;
|
||||
aggFn: AggFn;
|
||||
aggFn?: AggFn;
|
||||
field?: string | undefined;
|
||||
where: string;
|
||||
groupBy: string[];
|
||||
|
|
|
|||
Loading…
Reference in a new issue