diff --git a/.changeset/dirty-ads-exist.md b/.changeset/dirty-ads-exist.md new file mode 100644 index 00000000..4879fd5a --- /dev/null +++ b/.changeset/dirty-ads-exist.md @@ -0,0 +1,5 @@ +--- +'@hyperdx/app': patch +--- + +Link Infrastructure Metrics with Events diff --git a/packages/app/src/ChartUtils.tsx b/packages/app/src/ChartUtils.tsx index 34ba90d8..1f52986f 100644 --- a/packages/app/src/ChartUtils.tsx +++ b/packages/app/src/ChartUtils.tsx @@ -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', +}; diff --git a/packages/app/src/HDXLineChart.tsx b/packages/app/src/HDXLineChart.tsx index 1e42617c..6d9d67aa 100644 --- a/packages/app/src/HDXLineChart.tsx +++ b/packages/app/src/HDXLineChart.tsx @@ -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 ? ( ) : null} + {logReferenceTimestamp != null ? ( + + ) : null} ); @@ -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} /> diff --git a/packages/app/src/LogSidePanel.tsx b/packages/app/src/LogSidePanel.tsx index 1c70d5b0..2214da81 100644 --- a/packages/app/src/LogSidePanel.tsx +++ b/packages/app/src/LogSidePanel.tsx @@ -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 = ({ ); }; +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(() => { + return convertDateRangeToGranularityString(dateRange, 60); + }, [dateRange]); + + return ( +
+ + +

{title}

+ setRange(value as any)} + /> +
+ + setSize(value as any)} + /> + +
+ + + + CPU Usage (%) + + + + + + + + Memory Used + + + + + + + + Disk Available + + + + + + +
+ ); +}; + +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 ( + + {podUid && ( + + )} + {nodeName && ( + + )} + + ); +}; + +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 ( setTab(v)} @@ -2553,6 +2756,13 @@ export default function LogSidePanel({ )} ) : null} + + {/* Metrics */} + {displayedTab === 'metrics' ? ( +
+ +
+ ) : null} diff --git a/packages/app/src/config.ts b/packages/app/src/config.ts index b718a6e1..3831acbf 100644 --- a/packages/app/src/config.ts +++ b/packages/app/src/config.ts @@ -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';