mirror of
https://github.com/hyperdxio/hyperdx
synced 2026-04-21 13:37:15 +00:00
feat: Log Side Panel Host Metrics (#164)
Co-authored-by: Mike Shi <mike@deploysentinel.com>
This commit is contained in:
parent
8e44260f59
commit
af70f7d2b1
5 changed files with 254 additions and 8 deletions
5
.changeset/dirty-ads-exist.md
Normal file
5
.changeset/dirty-ads-exist.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'@hyperdx/app': patch
|
||||
---
|
||||
|
||||
Link Infrastructure Metrics with Events
|
||||
|
|
@ -13,8 +13,8 @@ export const SORT_ORDER = [
|
|||
{ value: 'asc' as const, label: 'Ascending' },
|
||||
{ value: 'desc' as const, label: 'Descending' },
|
||||
];
|
||||
import { NumberFormat } from './types';
|
||||
export type SortOrder = (typeof SORT_ORDER)[number]['value'];
|
||||
import type { NumberFormat } from './types';
|
||||
|
||||
export const TABLES = [
|
||||
{ value: 'logs' as const, label: 'Logs / Spans' },
|
||||
|
|
@ -759,3 +759,20 @@ export function timeBucketByGranularity(
|
|||
|
||||
return buckets;
|
||||
}
|
||||
|
||||
export const K8S_CPU_PERCENTAGE_NUMBER_FORMAT: NumberFormat = {
|
||||
output: 'percent',
|
||||
mantissa: 0,
|
||||
};
|
||||
|
||||
export const K8S_FILESYSTEM_NUMBER_FORMAT: NumberFormat = {
|
||||
output: 'byte',
|
||||
};
|
||||
|
||||
export const K8S_MEM_NUMBER_FORMAT: NumberFormat = {
|
||||
output: 'byte',
|
||||
};
|
||||
|
||||
export const K8S_NETWORK_NUMBER_FORMAT: NumberFormat = {
|
||||
output: 'byte',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ const MemoChart = memo(function MemoChart({
|
|||
groupKeys,
|
||||
alertThreshold,
|
||||
alertThresholdType,
|
||||
logReferenceTimestamp,
|
||||
displayType = 'line',
|
||||
numberFormat,
|
||||
}: {
|
||||
|
|
@ -67,6 +68,7 @@ const MemoChart = memo(function MemoChart({
|
|||
alertThresholdType?: 'above' | 'below';
|
||||
displayType?: 'stacked_bar' | 'line';
|
||||
numberFormat?: NumberFormat;
|
||||
logReferenceTimestamp?: number;
|
||||
}) {
|
||||
const ChartComponent = displayType === 'stacked_bar' ? BarChart : LineChart;
|
||||
|
||||
|
|
@ -208,6 +210,14 @@ const MemoChart = memo(function MemoChart({
|
|||
{isClickActive != null ? (
|
||||
<ReferenceLine x={isClickActive.activeLabel} stroke="#ccc" />
|
||||
) : null}
|
||||
{logReferenceTimestamp != null ? (
|
||||
<ReferenceLine
|
||||
x={logReferenceTimestamp}
|
||||
stroke="#ff5d5b"
|
||||
strokeDasharray="3 3"
|
||||
label="Event"
|
||||
/>
|
||||
) : null}
|
||||
</ChartComponent>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
|
|
@ -249,6 +259,7 @@ const HDXLineChart = memo(
|
|||
onSettled,
|
||||
alertThreshold,
|
||||
alertThresholdType,
|
||||
logReferenceTimestamp,
|
||||
}: {
|
||||
config: {
|
||||
table: string;
|
||||
|
|
@ -263,6 +274,7 @@ const HDXLineChart = memo(
|
|||
onSettled?: () => void;
|
||||
alertThreshold?: number;
|
||||
alertThresholdType?: 'above' | 'below';
|
||||
logReferenceTimestamp?: number;
|
||||
}) => {
|
||||
const { data, isError, isLoading } =
|
||||
table === 'logs'
|
||||
|
|
@ -517,6 +529,7 @@ const HDXLineChart = memo(
|
|||
alertThresholdType={alertThresholdType}
|
||||
displayType={displayType}
|
||||
numberFormat={numberFormat}
|
||||
logReferenceTimestamp={logReferenceTimestamp}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import cx from 'classnames';
|
||||
import { add, format } from 'date-fns';
|
||||
import { add, format, sub } from 'date-fns';
|
||||
import Fuse from 'fuse.js';
|
||||
import get from 'lodash/get';
|
||||
import isPlainObject from 'lodash/isPlainObject';
|
||||
|
|
@ -30,7 +30,15 @@ import {
|
|||
import HyperJson, { GetLineActions, LineAction } from './components/HyperJson';
|
||||
import { Table } from './components/Table';
|
||||
import api from './api';
|
||||
import {
|
||||
K8S_CPU_PERCENTAGE_NUMBER_FORMAT,
|
||||
K8S_FILESYSTEM_NUMBER_FORMAT,
|
||||
K8S_MEM_NUMBER_FORMAT,
|
||||
K8S_NETWORK_NUMBER_FORMAT,
|
||||
} from './ChartUtils';
|
||||
import { K8S_METRICS_ENABLED } from './config';
|
||||
import { CurlGenerator } from './curlGenerator';
|
||||
import HDXLineChart from './HDXLineChart';
|
||||
import LogLevel from './LogLevel';
|
||||
import {
|
||||
breadcrumbColumns,
|
||||
|
|
@ -2276,6 +2284,190 @@ const ExceptionSubpanel = ({
|
|||
</div>
|
||||
);
|
||||
};
|
||||
import { Card, SimpleGrid, Stack } from '@mantine/core';
|
||||
|
||||
import { convertDateRangeToGranularityString, Granularity } from './ChartUtils';
|
||||
|
||||
const MetricsSubpanelGroup = ({
|
||||
timestamp,
|
||||
where,
|
||||
fieldPrefix,
|
||||
title,
|
||||
}: {
|
||||
timestamp: any;
|
||||
where: string;
|
||||
fieldPrefix: string;
|
||||
title: string;
|
||||
}) => {
|
||||
const [range, setRange] = useState<'30m' | '1h' | '1d'>('30m');
|
||||
const [size, setSize] = useState<'sm' | 'md' | 'lg'>('sm');
|
||||
|
||||
const dateRange = useMemo<[Date, Date]>(() => {
|
||||
const duration = {
|
||||
'30m': { minutes: 15 },
|
||||
'1h': { minutes: 30 },
|
||||
'1d': { hours: 12 },
|
||||
}[range];
|
||||
return [
|
||||
sub(new Date(timestamp), duration),
|
||||
add(new Date(timestamp), duration),
|
||||
];
|
||||
}, [timestamp, range]);
|
||||
|
||||
const { cols, height } = useMemo(() => {
|
||||
switch (size) {
|
||||
case 'sm':
|
||||
return { cols: 3, height: 200 };
|
||||
case 'md':
|
||||
return { cols: 2, height: 250 };
|
||||
case 'lg':
|
||||
return { cols: 1, height: 320 };
|
||||
}
|
||||
}, [size]);
|
||||
|
||||
const granularity = useMemo<Granularity>(() => {
|
||||
return convertDateRangeToGranularityString(dateRange, 60);
|
||||
}, [dateRange]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Group position="apart" align="center">
|
||||
<Group align="center">
|
||||
<h4 className="text-slate-300 fs-6 m-0">{title}</h4>
|
||||
<SegmentedControl
|
||||
bg="dark.7"
|
||||
color="dark.5"
|
||||
size="xs"
|
||||
data={[
|
||||
{ label: '30m', value: '30m' },
|
||||
{ label: '1h', value: '1h' },
|
||||
{ label: '1d', value: '1d' },
|
||||
]}
|
||||
value={range}
|
||||
onChange={value => setRange(value as any)}
|
||||
/>
|
||||
</Group>
|
||||
<Group align="center">
|
||||
<SegmentedControl
|
||||
bg="dark.7"
|
||||
color="dark.5"
|
||||
size="xs"
|
||||
data={[
|
||||
{ label: 'SM', value: 'sm' },
|
||||
{ label: 'MD', value: 'md' },
|
||||
{ label: 'LG', value: 'lg' },
|
||||
]}
|
||||
value={size}
|
||||
onChange={value => setSize(value as any)}
|
||||
/>
|
||||
</Group>
|
||||
</Group>
|
||||
<SimpleGrid mt="md" cols={cols}>
|
||||
<Card p="md">
|
||||
<Card.Section p="md" py="xs" withBorder>
|
||||
CPU Usage (%)
|
||||
</Card.Section>
|
||||
<Card.Section py={8} px={4} h={height}>
|
||||
<HDXLineChart
|
||||
config={{
|
||||
dateRange,
|
||||
granularity,
|
||||
where,
|
||||
groupBy: '',
|
||||
aggFn: 'avg',
|
||||
field: `${fieldPrefix}cpu.utilization - Gauge`,
|
||||
table: 'metrics',
|
||||
numberFormat: K8S_CPU_PERCENTAGE_NUMBER_FORMAT,
|
||||
}}
|
||||
logReferenceTimestamp={timestamp / 1000}
|
||||
/>
|
||||
</Card.Section>
|
||||
</Card>
|
||||
<Card p="md">
|
||||
<Card.Section p="md" py="xs" withBorder>
|
||||
Memory Used
|
||||
</Card.Section>
|
||||
<Card.Section py={8} px={4} h={height}>
|
||||
<HDXLineChart
|
||||
config={{
|
||||
dateRange,
|
||||
granularity,
|
||||
where,
|
||||
groupBy: '',
|
||||
aggFn: 'avg',
|
||||
field: `${fieldPrefix}memory.usage - Gauge`,
|
||||
table: 'metrics',
|
||||
numberFormat: K8S_MEM_NUMBER_FORMAT,
|
||||
}}
|
||||
logReferenceTimestamp={timestamp / 1000}
|
||||
/>
|
||||
</Card.Section>
|
||||
</Card>
|
||||
<Card p="md">
|
||||
<Card.Section p="md" py="xs" withBorder>
|
||||
Disk Available
|
||||
</Card.Section>
|
||||
<Card.Section py={8} px={4} h={height}>
|
||||
<HDXLineChart
|
||||
config={{
|
||||
dateRange,
|
||||
granularity,
|
||||
where,
|
||||
groupBy: '',
|
||||
aggFn: 'avg',
|
||||
field: `${fieldPrefix}filesystem.available - Gauge`,
|
||||
table: 'metrics',
|
||||
numberFormat: K8S_FILESYSTEM_NUMBER_FORMAT,
|
||||
}}
|
||||
logReferenceTimestamp={timestamp / 1000}
|
||||
/>
|
||||
</Card.Section>
|
||||
</Card>
|
||||
</SimpleGrid>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const MetricsSubpanel = ({ logData }: { logData?: any }) => {
|
||||
const podUid = useMemo(() => {
|
||||
return logData?.['string.values']?.[
|
||||
logData?.['string.names']?.indexOf('k8s.pod.uid')
|
||||
];
|
||||
}, [logData]);
|
||||
|
||||
const nodeName = useMemo(() => {
|
||||
return logData?.['string.values']?.[
|
||||
logData?.['string.names']?.indexOf('k8s.node.name')
|
||||
];
|
||||
}, [logData]);
|
||||
|
||||
const timestamp = new Date(logData?.timestamp).getTime();
|
||||
|
||||
return (
|
||||
<Stack my="md" spacing={40}>
|
||||
{podUid && (
|
||||
<MetricsSubpanelGroup
|
||||
title="Pod Metrics"
|
||||
where={`k8s.pod.uid:"${podUid}"`}
|
||||
fieldPrefix="k8s.pod."
|
||||
timestamp={timestamp}
|
||||
/>
|
||||
)}
|
||||
{nodeName && (
|
||||
<MetricsSubpanelGroup
|
||||
title="Node Metrics"
|
||||
where={`k8s.node.name:"${nodeName}"`}
|
||||
fieldPrefix="k8s.node."
|
||||
timestamp={timestamp}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
const checkKeyExistsInLogData = (key: string, logData: any) => {
|
||||
return logData?.['string.values']?.[logData?.['string.names']?.indexOf(key)];
|
||||
};
|
||||
|
||||
export default function LogSidePanel({
|
||||
logId,
|
||||
|
|
@ -2383,12 +2575,8 @@ export default function LogSidePanel({
|
|||
|
||||
// TODO: use rum_session_id instead ?
|
||||
const rumSessionId: string | undefined =
|
||||
logData?.['string.values']?.[
|
||||
logData?.['string.names']?.indexOf('rum.sessionId')
|
||||
] ??
|
||||
logData?.['string.values']?.[
|
||||
logData?.['string.names']?.indexOf('process.tag.rum.sessionId')
|
||||
] ??
|
||||
checkKeyExistsInLogData('rum.sessionId', logData) ??
|
||||
checkKeyExistsInLogData('process.tag.rum.sessionId', logData) ??
|
||||
sessionId;
|
||||
|
||||
const { width } = useWindowSize();
|
||||
|
|
@ -2396,6 +2584,13 @@ export default function LogSidePanel({
|
|||
|
||||
const drawerZIndex = contextZIndex + 1;
|
||||
|
||||
const hasK8sContext = useMemo(() => {
|
||||
return (
|
||||
checkKeyExistsInLogData('k8s.pod.uid', logData) != null ||
|
||||
checkKeyExistsInLogData('k8s.node.name', logData) != null
|
||||
);
|
||||
}, [logData]);
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
enableOverlay
|
||||
|
|
@ -2459,6 +2654,14 @@ export default function LogSidePanel({
|
|||
},
|
||||
] as const)
|
||||
: []),
|
||||
...(K8S_METRICS_ENABLED && hasK8sContext
|
||||
? ([
|
||||
{
|
||||
text: 'Metrics',
|
||||
value: 'metrics',
|
||||
},
|
||||
] as const)
|
||||
: []),
|
||||
]}
|
||||
activeItem={displayedTab}
|
||||
onClick={(v: any) => setTab(v)}
|
||||
|
|
@ -2553,6 +2756,13 @@ export default function LogSidePanel({
|
|||
)}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Metrics */}
|
||||
{displayedTab === 'metrics' ? (
|
||||
<div className="px-4 overflow-auto">
|
||||
<MetricsSubpanel logData={logData} />
|
||||
</div>
|
||||
) : null}
|
||||
</ErrorBoundary>
|
||||
<LogSidePanelKbdShortcuts />
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -12,3 +12,4 @@ export const IS_OSS = process.env.NEXT_PUBLIC_IS_OSS ?? 'true' === 'true';
|
|||
|
||||
// Features in development
|
||||
export const METRIC_ALERTS_ENABLED = process.env.NODE_ENV === 'development';
|
||||
export const K8S_METRICS_ENABLED = process.env.NODE_ENV === 'development';
|
||||
|
|
|
|||
Loading…
Reference in a new issue