Skip to content

Commit

Permalink
feat: added new tab for infra metrics in logs detailed page (#5771)
Browse files Browse the repository at this point in the history
* feat: added new tab for infra metrics in logs detailed page

* feat: added yaxis unit for the charts

* chore: cleanup query_range params

* fix: clusterName, podName variables not working

* feat: added skeleton for each charts in infra metrics tab

* change card height to 300px

* fix: updated the test cases

* feat: added new sub-tabs node and pod for infra metrics tab

* feat: added new components for node and pod metrics

* feat: added card titles for host metrics and handled empty state

* fix: updated the constant for host name

* feat: added vertical dotted line to all panels and updated y axis units for all panels

* feat: removed other panel types other than graph from host metrics query payload

* fix: updated the query payload for node metrics

* feat: moved the label of vertical dotted line to top

* feat: added console statement to check query payload

* fix: added pod name instead of node name in pod query payload

* fix: added key as pod name instead of node name in file system usage

* fix: updated query payload for file system usage in pod metrics and removed label from dotted line

* fix: updated the y axis units for network io

* fix: custom date time issue while plotting the graph

* feat: compare end time and current time update the end time accordingly

* feat: added the start and end time in query payloads

* refactor: removed the comments and unused variables

* chore: added a todo to make common component for sub-tabs

* fix: addressed review comments

---------

Co-authored-by: Ankit Nayan <ankit@signoz.io>
  • Loading branch information
rahulkeswani101 and ankitnayan authored Sep 20, 2024
1 parent f3c01a5 commit c5b5bfe
Show file tree
Hide file tree
Showing 25 changed files with 3,697 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,20 @@ beforeAll(() => {
matchMedia();
});

jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});

jest.mock('react-dnd', () => ({
useDrop: jest.fn().mockImplementation(() => [jest.fn(), jest.fn(), jest.fn()]),
useDrag: jest.fn().mockImplementation(() => [jest.fn(), jest.fn(), jest.fn()]),
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/components/LogDetail/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@ export const VIEW_TYPES = {
OVERVIEW: 'OVERVIEW',
JSON: 'JSON',
CONTEXT: 'CONTEXT',
INFRAMETRICS: 'INFRAMETRICS',
} as const;

export type VIEWS = typeof VIEW_TYPES[keyof typeof VIEW_TYPES];

export const RESOURCE_KEYS = {
CLUSTER_NAME: 'k8s.cluster.name',
POD_NAME: 'k8s.pod.name',
NODE_NAME: 'k8s.node.name',
HOST_NAME: 'host.name',
} as const;
24 changes: 23 additions & 1 deletion frontend/src/components/LogDetail/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import cx from 'classnames';
import { LogType } from 'components/Logs/LogStateIndicator/LogStateIndicator';
import { LOCALSTORAGE } from 'constants/localStorage';
import ContextView from 'container/LogDetailedView/ContextView/ContextView';
import InfraMetrics from 'container/LogDetailedView/InfraMetrics/InfraMetrics';
import JSONView from 'container/LogDetailedView/JsonView';
import Overview from 'container/LogDetailedView/Overview';
import {
Expand All @@ -22,6 +23,7 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useNotifications } from 'hooks/useNotifications';
import {
BarChart2,
Braces,
Copy,
Filter,
Expand All @@ -36,7 +38,7 @@ import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource, StringOperators } from 'types/common/queryBuilder';
import { FORBID_DOM_PURIFY_TAGS } from 'utils/app';

import { VIEW_TYPES, VIEWS } from './constants';
import { RESOURCE_KEYS, VIEW_TYPES, VIEWS } from './constants';
import { LogDetailProps } from './LogDetail.interfaces';
import QueryBuilderSearchWrapper from './QueryBuilderSearchWrapper';

Expand Down Expand Up @@ -192,6 +194,17 @@ function LogDetail({
Context
</div>
</Radio.Button>
<Radio.Button
className={
selectedView === VIEW_TYPES.INFRAMETRICS ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.INFRAMETRICS}
>
<div className="view-title">
<BarChart2 size={14} />
Metrics
</div>
</Radio.Button>
</Radio.Group>

{selectedView === VIEW_TYPES.JSON && (
Expand Down Expand Up @@ -246,6 +259,15 @@ function LogDetail({
isEdit={isEdit}
/>
)}
{selectedView === VIEW_TYPES.INFRAMETRICS && (
<InfraMetrics
clusterName={log.resources_string?.[RESOURCE_KEYS.CLUSTER_NAME] || ''}
podName={log.resources_string?.[RESOURCE_KEYS.POD_NAME] || ''}
nodeName={log.resources_string?.[RESOURCE_KEYS.NODE_NAME] || ''}
hostName={log.resources_string?.[RESOURCE_KEYS.HOST_NAME] || ''}
logLineTimestamp={log.timestamp.toString()}
/>
)}
</Drawer>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
.empty-container {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}

.infra-metrics-container {
.views-tabs {
margin-bottom: 1rem;
}
}

.infra-metrics-card {
margin: 1rem 0;
height: 300px;
padding: 10px;

.ant-card-body {
padding: 0;
}

.chart-container {
width: 100%;
height: 100%;
}

.no-data-container {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import './InfraMetrics.styles.scss';

import { Empty, Radio } from 'antd';
import { RadioChangeEvent } from 'antd/lib';
import { History, Table } from 'lucide-react';
import { useState } from 'react';

import { VIEW_TYPES } from './constants';
import NodeMetrics from './NodeMetrics';
import PodMetrics from './PodMetrics';

interface MetricsDataProps {
podName: string;
nodeName: string;
hostName: string;
clusterName: string;
logLineTimestamp: string;
}

function InfraMetrics({
podName,
nodeName,
hostName,
clusterName,
logLineTimestamp,
}: MetricsDataProps): JSX.Element {
const [selectedView, setSelectedView] = useState<string>(() =>
podName ? VIEW_TYPES.POD : VIEW_TYPES.NODE,
);

const handleModeChange = (e: RadioChangeEvent): void => {
setSelectedView(e.target.value);
};

if (!podName && !nodeName && !hostName) {
return (
<div className="empty-container">
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description="No data available. Please select a valid log line containing a pod, node, or host attributes to view metrics."
/>
</div>
);
}

return (
<div className="infra-metrics-container">
<Radio.Group
className="views-tabs"
onChange={handleModeChange}
value={selectedView}
>
<Radio.Button
className={selectedView === VIEW_TYPES.NODE ? 'selected_view tab' : 'tab'}
value={VIEW_TYPES.NODE}
>
<div className="view-title">
<Table size={14} />
Node
</div>
</Radio.Button>
{podName && (
<Radio.Button
className={selectedView === VIEW_TYPES.POD ? 'selected_view tab' : 'tab'}
value={VIEW_TYPES.POD}
>
<div className="view-title">
<History size={14} />
Pod
</div>
</Radio.Button>
)}
</Radio.Group>
{/* TODO(Rahul): Make a common config driven component for this and other infra metrics components */}
{selectedView === VIEW_TYPES.NODE && (
<NodeMetrics
nodeName={nodeName}
clusterName={clusterName}
hostName={hostName}
logLineTimestamp={logLineTimestamp}
/>
)}
{selectedView === VIEW_TYPES.POD && podName && (
<PodMetrics
podName={podName}
clusterName={clusterName}
logLineTimestamp={logLineTimestamp}
/>
)}
</div>
);
}

export default InfraMetrics;
140 changes: 140 additions & 0 deletions frontend/src/container/LogDetailedView/InfraMetrics/NodeMetrics.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { Card, Col, Row, Skeleton, Typography } from 'antd';
import cx from 'classnames';
import Uplot from 'components/Uplot';
import { ENTITY_VERSION_V4 } from 'constants/app';
import dayjs from 'dayjs';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
import { useMemo, useRef } from 'react';
import { useQueries, UseQueryResult } from 'react-query';
import { SuccessResponse } from 'types/api';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';

import {
getHostQueryPayload,
getNodeQueryPayload,
hostWidgetInfo,
nodeWidgetInfo,
} from './constants';

function NodeMetrics({
nodeName,
clusterName,
hostName,
logLineTimestamp,
}: {
nodeName: string;
clusterName: string;
hostName: string;
logLineTimestamp: string;
}): JSX.Element {
const { start, end, verticalLineTimestamp } = useMemo(() => {
const logTimestamp = dayjs(logLineTimestamp);
const now = dayjs();
const startTime = logTimestamp.subtract(3, 'hour');

const endTime = logTimestamp.add(3, 'hour').isBefore(now)
? logTimestamp.add(3, 'hour')
: now;

return {
start: startTime.unix(),
end: endTime.unix(),
verticalLineTimestamp: logTimestamp.unix(),
};
}, [logLineTimestamp]);

const queryPayloads = useMemo(() => {
if (nodeName) {
return getNodeQueryPayload(clusterName, nodeName, start, end);
}
return getHostQueryPayload(hostName, start, end);
}, [nodeName, hostName, clusterName, start, end]);

const widgetInfo = nodeName ? nodeWidgetInfo : hostWidgetInfo;
const queries = useQueries(
queryPayloads.map((payload) => ({
queryKey: ['metrics', payload, ENTITY_VERSION_V4, 'NODE'],
queryFn: (): Promise<SuccessResponse<MetricRangePayloadProps>> =>
GetMetricQueryRange(payload, ENTITY_VERSION_V4),
enabled: !!payload,
})),
);

const isDarkMode = useIsDarkMode();
const graphRef = useRef<HTMLDivElement>(null);
const dimensions = useResizeObserver(graphRef);

const chartData = useMemo(
() => queries.map(({ data }) => getUPlotChartData(data?.payload)),
[queries],
);

const options = useMemo(
() =>
queries.map(({ data }, idx) =>
getUPlotChartOptions({
apiResponse: data?.payload,
isDarkMode,
dimensions,
yAxisUnit: widgetInfo[idx].yAxisUnit,
softMax: null,
softMin: null,
minTimeScale: start,
maxTimeScale: end,
verticalLineTimestamp,
}),
),
[
queries,
isDarkMode,
dimensions,
widgetInfo,
start,
verticalLineTimestamp,
end,
],
);

const renderCardContent = (
query: UseQueryResult<SuccessResponse<MetricRangePayloadProps>, unknown>,
idx: number,
): JSX.Element => {
if (query.isLoading) {
return <Skeleton />;
}

if (query.error) {
const errorMessage =
(query.error as Error)?.message || 'Something went wrong';
return <div>{errorMessage}</div>;
}
return (
<div
className={cx('chart-container', {
'no-data-container':
!query.isLoading && !query?.data?.payload?.data?.result?.length,
})}
>
<Uplot options={options[idx]} data={chartData[idx]} />
</div>
);
};
return (
<Row gutter={24}>
{queries.map((query, idx) => (
<Col span={12} key={widgetInfo[idx].title}>
<Typography.Text>{widgetInfo[idx].title}</Typography.Text>
<Card bordered className="infra-metrics-card" ref={graphRef}>
{renderCardContent(query, idx)}
</Card>
</Col>
))}
</Row>
);
}

export default NodeMetrics;
Loading

0 comments on commit c5b5bfe

Please sign in to comment.