Compare commits

...

10 Commits

Author SHA1 Message Date
rahulkeswani101
cf0bf861b5 Merge branch 'SIG-5730' of https://github.com/SigNoz/signoz into SIG-5730 2024-08-27 14:24:19 +05:30
rahulkeswani101
9d74ad83fc fix: updated the test cases 2024-08-27 14:18:42 +05:30
Ankit Nayan
42f7be643d change card height to 300px 2024-08-27 12:00:39 +05:30
rahulkeswani101
0faa5e32e7 feat: added skeleton for each charts in infra metrics tab 2024-08-26 23:32:55 +05:30
rahulkeswani101
d562ff4eca Merge branch 'develop' into SIG-5730 2024-08-26 22:37:20 +05:30
Ankit Nayan
1bf30ce440 fix: clusterName, podName variables not working 2024-08-26 21:36:38 +05:30
Ankit Nayan
7cea83635e Merge branch 'SIG-5730' of https://github.com/SigNoz/signoz into SIG-5730 2024-08-26 19:04:06 +05:30
Ankit Nayan
6e3910a369 chore: cleanup query_range params 2024-08-26 19:02:56 +05:30
rahulkeswani101
247cd782d5 feat: added yaxis unit for the charts 2024-08-26 18:50:05 +05:30
rahulkeswani101
917aef0ed8 feat: added new tab for infra metrics in logs detailed page 2024-08-26 16:57:15 +05:30
21 changed files with 778 additions and 0 deletions

View File

@@ -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()]),

View File

@@ -2,6 +2,7 @@ export const VIEW_TYPES = {
OVERVIEW: 'OVERVIEW',
JSON: 'JSON',
CONTEXT: 'CONTEXT',
INFRAMETRICS: 'INFRAMETRICS',
} as const;
export type VIEWS = typeof VIEW_TYPES[keyof typeof VIEW_TYPES];

View File

@@ -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 {
@@ -22,6 +23,7 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useNotifications } from 'hooks/useNotifications';
import {
Activity,
Braces,
Copy,
Filter,
@@ -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">
<Activity size={14} />
InfraMetrics
</div>
</Radio.Button>
</Radio.Group>
{selectedView === VIEW_TYPES.JSON && (
@@ -246,6 +259,7 @@ function LogDetail({
isEdit={isEdit}
/>
)}
{selectedView === VIEW_TYPES.INFRAMETRICS && <InfraMetrics logData={log} />}
</Drawer>
);
}

View File

@@ -0,0 +1,19 @@
.infra-metrics-card {
margin: 1rem 0;
height: 300px;
padding: 10px;
.ant-card-body {
padding: 0;
}
.chart-container {
overflow: hidden;
width: 100%;
height: 100%;
}
}
.no-data-card {
height: 100px;
}

View File

@@ -0,0 +1,124 @@
import './InfraMetrics.styles.scss';
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 { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
import getStartEndRangeTime from 'lib/getStartEndRangeTime';
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 { InfraMetricsSProps } from '../LogDetailedView.types';
import { cardTitles, getQueryPayload } from './constants';
function InfraMetrics({ logData }: InfraMetricsSProps): JSX.Element {
const { start, end } = getStartEndRangeTime({
type: 'GLOBAL_TIME',
interval: '3h',
});
const minTimeScale = (parseInt(start, 10) * 1e3) / 1000;
const maxTimeScale = (parseInt(end, 10) * 1e3) / 1000;
const clusterName = logData.resources_string?.['k8s.cluster.name']
? (logData.resources_string?.['k8s.cluster.name'] as string)
: '';
const podName = logData.resources_string?.['k8s.pod.name']
? (logData.resources_string?.['k8s.pod.name'] as string)
: '';
const queries = useQueries(
getQueryPayload(clusterName, podName).map((payload) => ({
queryKey: ['metrics', payload, ENTITY_VERSION_V4],
queryFn: (): Promise<SuccessResponse<MetricRangePayloadProps>> =>
GetMetricQueryRange(payload, ENTITY_VERSION_V4),
})),
);
const isDarkMode = useIsDarkMode();
const graphRef = useRef<HTMLDivElement>(null);
const dimensions = useResizeObserver(graphRef);
const chartData = useMemo(
() => queries.map(({ data }) => getUPlotChartData(data?.payload)),
[queries],
);
const getYAxisUnit = (idx: number): string => {
if (idx === 1 || idx === 3) {
return 'bytes';
}
if (idx === 2) {
return 'binBps';
}
return '';
};
const options = useMemo(
() =>
queries.map(({ data }, idx) =>
getUPlotChartOptions({
apiResponse: data?.payload,
isDarkMode,
dimensions,
minTimeScale,
maxTimeScale,
softMax: null,
softMin: null,
yAxisUnit: getYAxisUnit(idx),
}),
),
[queries, isDarkMode, dimensions, minTimeScale, maxTimeScale],
);
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="chart-container">
<Uplot options={options[idx]} data={chartData[idx]} />
</div>
);
};
return (
<div>
<Row gutter={24}>
{queries.map((query, idx) => (
<Col span={12} key={cardTitles[idx]}>
<Typography.Text>{cardTitles[idx]}</Typography.Text>
<Card
bordered
className={cx('infra-metrics-card', {
'no-data-card':
!query.isLoading && !query?.data?.payload?.data?.result?.length,
})}
ref={graphRef}
>
{renderCardContent(query, idx)}
</Card>
</Col>
))}
</Row>
</div>
);
}
export default InfraMetrics;

View File

@@ -0,0 +1,405 @@
/* eslint-disable sonarjs/no-duplicate-string */
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
export const getQueryPayload = (
clusterName: string,
podName: string,
): GetQueryResultsProps[] => [
{
selectedTime: 'GLOBAL_TIME',
graphType: PANEL_TYPES.TIME_SERIES,
query: {
builder: {
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.Float64,
id: 'k8s_pod_cpu_utilization--float64----true',
isColumn: true,
key: 'k8s_pod_cpu_utilization',
type: '',
},
aggregateOperator: 'avg',
dataSource: DataSource.METRICS,
disabled: false,
expression: 'A',
filters: {
items: [
{
id: '9a0ffaf3',
key: {
dataType: DataTypes.String,
id: 'k8s_cluster_name--string--tag--false',
isColumn: false,
key: 'k8s_cluster_name',
type: 'tag',
},
op: '=',
value: clusterName,
},
{
id: '9a0ffaf3',
key: {
dataType: DataTypes.String,
id: 'k8s_pod_name--string--tag--false',
isColumn: false,
key: 'k8s_pod_name',
type: 'tag',
},
op: '=',
value: podName,
},
],
op: 'AND',
},
groupBy: [
{
dataType: DataTypes.String,
id: 'k8s_pod_name--string--tag--false',
isColumn: false,
key: 'k8s_pod_name',
type: 'tag',
},
],
having: [],
legend: '{{k8s_pod_name}}',
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: 'sum',
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'avg',
functions: [],
},
],
queryFormulas: [],
},
clickhouse_sql: [
{
disabled: false,
legend: '',
name: 'A',
query: '',
},
],
id: '3fe84db4-8f8b-44ba-b903-2daaab59c756',
promql: [
{
disabled: false,
legend: '',
name: 'A',
query: '',
},
],
queryType: EQueryType.QUERY_BUILDER,
},
globalSelectedInterval: '3h',
variables: {},
formatForWeb: false,
},
{
selectedTime: 'GLOBAL_TIME',
graphType: PANEL_TYPES.TIME_SERIES,
query: {
builder: {
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.Float64,
id: 'k8s_pod_memory_usage--float64----true',
isColumn: true,
key: 'k8s_pod_memory_usage',
type: '',
},
aggregateOperator: 'avg',
dataSource: DataSource.METRICS,
disabled: false,
expression: 'A',
filters: {
items: [
{
id: '9a0ffaf3',
key: {
dataType: DataTypes.String,
id: 'k8s_cluster_name--string--tag--false',
isColumn: false,
key: 'k8s_cluster_name',
type: 'tag',
},
op: '=',
value: clusterName,
},
{
id: '2d8022f6',
key: {
dataType: DataTypes.String,
id: 'k8s_pod_name--string--tag--false',
isColumn: false,
key: 'k8s_pod_name',
type: 'tag',
},
op: '=',
value: podName,
},
],
op: 'AND',
},
groupBy: [
{
dataType: DataTypes.String,
id: 'k8s_pod_name--string--tag--false',
isColumn: false,
key: 'k8s_pod_name',
type: 'tag',
},
],
having: [],
legend: '{{k8s_pod_name}}',
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: 'sum',
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'avg',
functions: [],
},
],
queryFormulas: [],
},
clickhouse_sql: [
{
disabled: false,
legend: '',
name: 'A',
query: '',
},
],
id: '59c73365-4180-4ddd-9406-2e2d8cfbc0d9',
promql: [
{
disabled: false,
legend: '',
name: 'A',
query: '',
},
],
queryType: EQueryType.QUERY_BUILDER,
},
globalSelectedInterval: '3h',
variables: {},
formatForWeb: false,
},
{
selectedTime: 'GLOBAL_TIME',
graphType: PANEL_TYPES.TIME_SERIES,
query: {
builder: {
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.Float64,
id: 'k8s_pod_network_io--float64----true',
isColumn: true,
key: 'k8s_pod_network_io',
type: '',
},
aggregateOperator: 'sum_rate',
dataSource: DataSource.METRICS,
disabled: false,
expression: 'A',
filters: {
items: [
{
id: '9a0ffaf3',
key: {
dataType: DataTypes.String,
id: 'k8s_cluster_name--string--tag--false',
isColumn: false,
key: 'k8s_cluster_name',
type: 'tag',
},
op: '=',
value: clusterName,
},
{
id: 'c32821ed',
key: {
dataType: DataTypes.String,
id: 'k8s_pod_name--string--tag--false',
isColumn: false,
key: 'k8s_pod_name',
type: 'tag',
},
op: '=',
value: podName,
},
],
op: 'AND',
},
groupBy: [
{
dataType: DataTypes.String,
id: 'direction--string--tag--false',
isColumn: false,
key: 'direction',
type: 'tag',
},
{
dataType: DataTypes.String,
id: 'interface--string--tag--false',
isColumn: false,
key: 'interface',
type: 'tag',
},
{
dataType: DataTypes.String,
id: 'k8s_pod_name--string--tag--false',
isColumn: false,
key: 'k8s_pod_name',
type: 'tag',
},
],
having: [],
legend: '{{k8s_pod_name}}-{{interface}}-{{direction}}',
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: 'sum',
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'rate',
functions: [],
},
],
queryFormulas: [],
},
clickhouse_sql: [
{
disabled: false,
legend: '',
name: 'A',
query: '',
},
],
id: '534b461d-d992-4a30-ba17-ed7aac95a55b',
promql: [
{
disabled: false,
legend: '',
name: 'A',
query: '',
},
],
queryType: EQueryType.QUERY_BUILDER,
},
globalSelectedInterval: '3h',
variables: {},
formatForWeb: false,
},
{
selectedTime: 'GLOBAL_TIME',
graphType: PANEL_TYPES.TIME_SERIES,
query: {
builder: {
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.Float64,
id: 'k8s_pod_filesystem_usage--float64----true',
isColumn: true,
key: 'k8s_pod_filesystem_usage',
type: '',
},
aggregateOperator: 'avg',
dataSource: DataSource.METRICS,
disabled: false,
expression: 'A',
filters: {
items: [
{
id: '9a0ffaf3',
key: {
dataType: DataTypes.String,
id: 'k8s_cluster_name--string--tag--false',
isColumn: false,
key: 'k8s_cluster_name',
type: 'tag',
},
op: '=',
value: clusterName,
},
{
id: 'ba47cf47',
key: {
dataType: DataTypes.String,
id: 'k8s_pod_name--string--tag--false',
isColumn: false,
key: 'k8s_pod_name',
type: 'tag',
},
op: '=',
value: podName,
},
],
op: 'AND',
},
groupBy: [
{
dataType: DataTypes.String,
id: 'k8s_pod_name--string--tag--false',
isColumn: false,
key: 'k8s_pod_name',
type: 'tag',
},
],
having: [],
legend: '{{k8s_pod_name}}',
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: 'sum',
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'avg',
functions: [],
},
],
queryFormulas: [],
},
clickhouse_sql: [
{
disabled: false,
legend: '',
name: 'A',
query: '',
},
],
id: '5a709367-ad0b-4a0a-9f7e-884e555f7686',
promql: [
{
disabled: false,
legend: '',
name: 'A',
query: '',
},
],
queryType: EQueryType.QUERY_BUILDER,
},
globalSelectedInterval: '3h',
variables: {},
formatForWeb: false,
},
];
export const cardTitles: string[] = [
'Pod CPU usage',
'Pod memory usage (WSS)',
'Pod network IO',
'Pod filesystem usage',
];

View File

@@ -23,3 +23,7 @@ export interface IFieldAttributes {
export interface JSONViewProps {
logData: ILog;
}
export interface InfraMetricsSProps {
logData: ILog;
}

View File

@@ -28,6 +28,20 @@ const lodsQueryServerRequest = (): void =>
),
);
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
// mocking the graph components in this test as this should be handled separately
jest.mock(
'container/TimeSeriesView/TimeSeriesView',

View File

@@ -9,6 +9,20 @@ import store from 'store';
import ChangeHistory from '../index';
import { pipelineData, pipelineDataHistory } from './testUtils';
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
const queryClient = new QueryClient({
defaultOptions: {
queries: {

View File

@@ -9,6 +9,20 @@ import store from 'store';
import { pipelineMockData } from '../mocks/pipeline';
import AddNewPipeline from '../PipelineListsView/AddNewPipeline';
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
export function matchMedia(): void {
Object.defineProperty(window, 'matchMedia', {
writable: true,

View File

@@ -9,6 +9,20 @@ import { pipelineMockData } from '../mocks/pipeline';
import AddNewProcessor from '../PipelineListsView/AddNewProcessor';
import { matchMedia } from './AddNewPipeline.test';
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
beforeAll(() => {
matchMedia();
});

View File

@@ -6,6 +6,20 @@ import { MemoryRouter } from 'react-router-dom';
import i18n from 'ReactI18';
import store from 'store';
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
describe('PipelinePage container test', () => {
it('should render DeleteAction section', () => {
const { asFragment } = render(

View File

@@ -6,6 +6,20 @@ import { MemoryRouter } from 'react-router-dom';
import i18n from 'ReactI18';
import store from 'store';
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
describe('PipelinePage container test', () => {
it('should render DragAction section', () => {
const { asFragment } = render(

View File

@@ -6,6 +6,20 @@ import { MemoryRouter } from 'react-router-dom';
import i18n from 'ReactI18';
import store from 'store';
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
describe('PipelinePage container test', () => {
it('should render EditAction section', () => {
const { asFragment } = render(

View File

@@ -8,6 +8,20 @@ import store from 'store';
import { pipelineMockData } from '../mocks/pipeline';
import PipelineActions from '../PipelineListsView/TableComponents/PipelineActions';
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
describe('PipelinePage container test', () => {
it('should render PipelineActions section', () => {
const { asFragment } = render(

View File

@@ -9,6 +9,20 @@ import { pipelineMockData } from '../mocks/pipeline';
import PipelineExpandView from '../PipelineListsView/PipelineExpandView';
import { matchMedia } from './AddNewPipeline.test';
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
beforeAll(() => {
matchMedia();
});

View File

@@ -11,6 +11,20 @@ import store from 'store';
import { pipelineApiResponseMockData } from '../mocks/pipeline';
import PipelineListsView from '../PipelineListsView';
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
const samplePipelinePreviewResponse = {
isLoading: false,
logs: [

View File

@@ -11,6 +11,20 @@ import { v4 } from 'uuid';
import PipelinePageLayout from '../Layouts/Pipeline';
import { matchMedia } from './AddNewPipeline.test';
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
beforeAll(() => {
matchMedia();
});

View File

@@ -7,6 +7,20 @@ import store from 'store';
import TagInput from '../components/TagInput';
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
describe('Pipeline Page', () => {
it('should render TagInput section', () => {
const { asFragment } = render(

View File

@@ -11,6 +11,20 @@ import {
getTableColumn,
} from '../PipelineListsView/utils';
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
describe('Utils testing of Pipeline Page', () => {
test('it should be check form field of add pipeline', () => {
expect(pipelineFields.length).toBe(3);

View File

@@ -26,6 +26,21 @@ import { Query } from 'types/api/queryBuilder/queryBuilderData';
import LogsExplorer from '../index';
const queryRangeURL = 'http://localhost/api/v3/query_range';
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
// mocking the graph components in this test as this should be handled separately
jest.mock(
'container/TimeSeriesView/TimeSeriesView',