Service layer to metrics using USE_SPAN_METRIC feature flag (#3196)

* refactor: remove the dependency of services using redux

* refactor: seperated columns and unit test case

* refactor: move the constant to other file

* refactor: updated test case

* refactor: removed the duplicate enum

* fix: removed the inline function

* fix: removed the inline function

* refactor: removed the magic string

* fix: change the name from matrics to metrics

* fix: one on one mapping of props

* refactor: created a hook to getting services through api call

* fix: linter error

* refactor: renamed the file according to functionality

* refactor: renamed more file according to functionality

* refactor: generic querybuilderWithFormula

* refactor: added generic datasource

* refactor: dynamic disabled in getQueryBuilderQueriesWithFormula

* refactor: generic legend for building query with formulas

* feat: added new TopOperationMetrics component for key operation

* refactor: added feature flag for key operation

* refactor: shifted types and fixed typos

* refactor: separated types and renamed file

* refactor: one on one mapping

* refactor: removed unwanted interfaces and renamed files

* refactor: separated types

* chore: done with basic struction and moving up the files

* chore: moved some files to proper places

* feat: added the support for metrics in service layer

* refactor: shifted SkipOnBoardingModal logic to parent

* refactor: created object to send as an augument for getQueryRangeRequestData

* refactor: changes from columns to getColumns

* refactor: updated the utils function getServiceListFromQuery

* refactor: added memo to getQueryRangeRequestData in serive metrics application

* refactor: separated constants from ServiceMetricsQuery.ts

* refactor: separated mock data and updated test case

* refactor: added useMemo on getColumns

* refactor: made the use of useErrorNotification for show error

* refactor: handled the error case

* refactor: one on one mapping

* chore: useGetQueriesRange hooks type is updated

* refactor: review changes

* chore: update type for columnconstants

* chore: reverted back the changes lost in merge conflicts

---------

Co-authored-by: Vishal Sharma <makeavish786@gmail.com>
Co-authored-by: Palash Gupta <palashgdev@gmail.com>
Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
This commit is contained in:
Rajat Dabade 2023-08-02 15:00:58 +05:30 committed by GitHub
parent 68ab022836
commit 562621a117
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 1034 additions and 145 deletions

View File

@ -51,6 +51,7 @@ const themeColors = {
snowWhite: '#fafafa',
gamboge: '#D89614',
bckgGrey: '#1d1d1d',
lightBlue: '#177ddc',
};
export { themeColors };

View File

@ -8,7 +8,7 @@ import {
} from 'types/common/queryBuilder';
import { DataType, FORMULA, MetricsType, WidgetKeys } from '../constant';
import { IServiceName } from '../Tabs/types';
import { DatabaseCallProps, DatabaseCallsRPSProps } from '../types';
import {
getQueryBuilderQueries,
getQueryBuilderQuerieswithFormula,
@ -103,8 +103,8 @@ export const databaseCallsAvgDuration = ({
const legends = ['', ''];
const disabled = [true, true];
const legendFormula = 'Average Duration';
const expression = FORMULA.DATABASE_CALLS_AVG_DURATION;
const legendFormulas = ['Average Duration'];
const expressions = [FORMULA.DATABASE_CALLS_AVG_DURATION];
const aggregateOperators = [
MetricAggregateOperator.SUM,
MetricAggregateOperator.SUM,
@ -116,18 +116,9 @@ export const databaseCallsAvgDuration = ({
additionalItems,
legends,
disabled,
expression,
legendFormula,
expressions,
legendFormulas,
aggregateOperators,
dataSource,
});
};
interface DatabaseCallsRPSProps extends DatabaseCallProps {
legend: '{{db_system}}';
}
interface DatabaseCallProps {
servicename: IServiceName['servicename'];
tagFilterItems: TagFilterItem[];
}

View File

@ -83,22 +83,18 @@ export const externalCallErrorPercent = ({
},
...tagFilterItems,
];
const legendFormulas = [legend];
const expressions = [FORMULA.ERROR_PERCENTAGE];
const disabled = [true, true];
const autocompleteData = [autocompleteDataA, autocompleteDataB];
const legendFormula = legend;
const expression = FORMULA.ERROR_PERCENTAGE;
const autocompleteData: BaseAutocompleteData[] = [
autocompleteDataA,
autocompleteDataB,
const additionalItems = [additionalItemsA, additionalItemsB];
const aggregateOperators = [
MetricAggregateOperator.SUM,
MetricAggregateOperator.SUM,
];
const additionalItems: TagFilterItem[][] = [
additionalItemsA,
additionalItemsB,
];
const legends = Array(2).fill(legend);
const aggregateOperators = Array(2).fill(MetricAggregateOperator.SUM);
const disabled = Array(2).fill(true);
const legends = [legend, legend];
const dataSource = DataSource.METRICS;
return getQueryBuilderQuerieswithFormula({
@ -107,8 +103,8 @@ export const externalCallErrorPercent = ({
legends,
groupBy,
disabled,
expression,
legendFormula,
expressions,
legendFormulas,
aggregateOperators,
dataSource,
});
@ -130,11 +126,10 @@ export const externalCallDuration = ({
key: WidgetKeys.SignozExternalCallLatencyCount,
type: null,
};
const expression = FORMULA.DATABASE_CALLS_AVG_DURATION;
const legendFormula = 'Average Duration';
const expressions = [FORMULA.DATABASE_CALLS_AVG_DURATION];
const legendFormulas = ['Average Duration'];
const legend = '';
const disabled = Array(2).fill(true);
const disabled = [true, true];
const additionalItemsA: TagFilterItem[] = [
{
id: '',
@ -150,28 +145,25 @@ export const externalCallDuration = ({
...tagFilterItems,
];
const autocompleteData: BaseAutocompleteData[] = [
autocompleteDataA,
autocompleteDataB,
];
const autocompleteData = [autocompleteDataA, autocompleteDataB];
const additionalItems: TagFilterItem[][] = [
additionalItemsA,
additionalItemsA,
const additionalItems = [additionalItemsA, additionalItemsA];
const legends = [legend, legend];
const aggregateOperators = [
MetricAggregateOperator.SUM,
MetricAggregateOperator.SUM,
];
const legends = Array(2).fill(legend);
const aggregateOperators = Array(2).fill(MetricAggregateOperator.SUM);
const dataSource = DataSource.METRICS;
return getQueryBuilderQuerieswithFormula({
autocompleteData,
additionalItems,
legends,
disabled,
expression,
legendFormula,
expressions,
legendFormulas,
aggregateOperators,
dataSource: DataSource.METRICS,
dataSource,
});
};
@ -234,8 +226,8 @@ export const externalCallDurationByAddress = ({
key: WidgetKeys.SignozExternalCallLatencyCount,
type: null,
};
const expression = FORMULA.DATABASE_CALLS_AVG_DURATION;
const legendFormula = legend;
const expressions = [FORMULA.DATABASE_CALLS_AVG_DURATION];
const legendFormulas = [legend];
const disabled = [true, true];
const additionalItemsA: TagFilterItem[] = [
{
@ -252,18 +244,13 @@ export const externalCallDurationByAddress = ({
...tagFilterItems,
];
const autocompleteData: BaseAutocompleteData[] = [
autocompleteDataA,
autocompleteDataB,
const autocompleteData = [autocompleteDataA, autocompleteDataB];
const additionalItems = [additionalItemsA, additionalItemsA];
const legends = [legend, legend];
const aggregateOperators = [
MetricAggregateOperator.SUM,
MetricAggregateOperator.SUM,
];
const additionalItems: TagFilterItem[][] = [
additionalItemsA,
additionalItemsA,
];
const legends = Array(2).fill(legend);
const aggregateOperators = Array(2).fill(MetricAggregateOperator.SUM_RATE);
const dataSource = DataSource.METRICS;
return getQueryBuilderQuerieswithFormula({
@ -272,8 +259,8 @@ export const externalCallDurationByAddress = ({
legends,
groupBy,
disabled,
expression,
legendFormula,
expressions,
legendFormulas,
aggregateOperators,
dataSource,
});

View File

@ -67,18 +67,16 @@ export const getQueryBuilderQuerieswithFormula = ({
legends,
groupBy = [],
disabled,
expression,
legendFormula,
expressions,
legendFormulas,
aggregateOperators,
dataSource,
}: BuilderQuerieswithFormulaProps): QueryBuilderData => ({
queryFormulas: [
{
...initialFormulaBuilderFormValues,
expression,
legend: legendFormula,
},
],
queryFormulas: expressions.map((expression, index) => ({
...initialFormulaBuilderFormValues,
expression,
legend: legendFormulas[index],
})),
queryData: autocompleteData.map((_, index) => ({
...initialQueryBuilderFormValuesMap.metrics,
aggregateOperator: aggregateOperators[index],

View File

@ -224,8 +224,8 @@ export const errorPercentage = ({
const additionalItems = [additionalItemsA, additionalItemsB];
const legends = [GraphTitle.ERROR_PERCENTAGE];
const disabled = [true, true];
const expression = FORMULA.ERROR_PERCENTAGE;
const legendFormula = GraphTitle.ERROR_PERCENTAGE;
const expressions = [FORMULA.ERROR_PERCENTAGE];
const legendFormulas = [GraphTitle.ERROR_PERCENTAGE];
const aggregateOperators = [
MetricAggregateOperator.SUM_RATE,
MetricAggregateOperator.SUM_RATE,
@ -237,8 +237,8 @@ export const errorPercentage = ({
additionalItems,
legends,
disabled,
expression,
legendFormula,
expressions,
legendFormulas,
aggregateOperators,
dataSource,
});

View File

@ -124,8 +124,8 @@ export const topOperationQueries = ({
MetricAggregateOperator.SUM_RATE,
MetricAggregateOperator.SUM_RATE,
];
const expression = 'D*100/E';
const legendFormula = GraphTitle.ERROR_PERCENTAGE;
const expressions = ['D*100/E'];
const legendFormulas = [GraphTitle.ERROR_PERCENTAGE];
const dataSource = DataSource.METRICS;
return getQueryBuilderQuerieswithFormula({
@ -134,8 +134,8 @@ export const topOperationQueries = ({
disabled,
legends,
aggregateOperators,
expression,
legendFormula,
expressions,
legendFormulas,
dataSource,
groupBy,
});

View File

@ -4,12 +4,13 @@ import { topOperationQueries } from 'container/MetricsApplication/MetricsPageQue
import { QueryTable } from 'container/QueryTable';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useStepInterval } from 'hooks/queryBuilder/useStepInterval';
import { useNotifications } from 'hooks/useNotifications';
import useResourceAttribute from 'hooks/useResourceAttribute';
import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { isEmpty } from 'lodash-es';
import { ReactNode, useMemo, useState } from 'react';
import { ReactNode, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
import { AppState } from 'store/reducers';
@ -18,18 +19,19 @@ import { GlobalReducer } from 'types/reducer/globalTime';
import { v4 as uuid } from 'uuid';
import { IServiceName } from '../types';
import { title } from './config';
import ColumnWithLink from './TableRenderer/ColumnWithLink';
import { getTableColumnRenderer } from './TableRenderer/TableColumnRenderer';
function TopOperationMetrics(): JSX.Element {
const { servicename } = useParams<IServiceName>();
const [errorMessage, setErrorMessage] = useState<string | undefined>('');
const { notifications } = useNotifications();
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const { queries } = useResourceAttribute();
const selectedTraceTags = JSON.stringify(
@ -80,7 +82,7 @@ function TopOperationMetrics(): JSX.Element {
enabled: !isEmptyWidget,
refetchOnMount: false,
onError: (error) => {
setErrorMessage(error.message);
notifications.error({ message: error.message });
},
},
);
@ -104,13 +106,8 @@ function TopOperationMetrics(): JSX.Element {
[servicename, minTime, maxTime, selectedTraceTags],
);
if (errorMessage) {
return <div>{errorMessage}</div>;
}
return (
<QueryTable
title={title}
query={updatedQuery}
queryTableData={queryTableData}
loading={isLoading}

View File

@ -36,8 +36,8 @@ export interface BuilderQuerieswithFormulaProps {
legends: string[];
disabled: boolean[];
groupBy?: BaseAutocompleteData[];
expression: string;
legendFormula: string;
expressions: string[];
legendFormulas: string[];
additionalItems: TagFilterItem[][];
aggregateOperators: MetricAggregateOperator[];
dataSource: DataSource;

View File

@ -34,6 +34,7 @@ export enum KeyOperationTableHeader {
P99 = 'P99',
NUM_OF_CALLS = 'Number of Calls',
ERROR_RATE = 'Error Rate',
OPERATION_PR_SECOND = 'Op/s',
}
export enum DataType {

View File

@ -1,4 +1,7 @@
import { Widgets } from 'types/api/dashboard/getAll';
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { IServiceName } from './Tabs/types';
export interface GetWidgetQueryBuilderProps {
query: Widgets['query'];
@ -13,3 +16,12 @@ export interface NavigateToTraceProps {
maxTime: number;
selectedTraceTags: string;
}
export interface DatabaseCallsRPSProps extends DatabaseCallProps {
legend: '{{db_system}}';
}
export interface DatabaseCallProps {
servicename: IServiceName['servicename'];
tagFilterItems: TagFilterItem[];
}

View File

@ -0,0 +1,24 @@
export enum ColumnKey {
Application = 'serviceName',
P99 = 'p99',
ErrorRate = 'errorRate',
Operations = 'callRate',
}
export const ColumnTitle: Record<ColumnKey, string> = {
[ColumnKey.Application]: 'Application',
[ColumnKey.P99]: 'P99 latency',
[ColumnKey.ErrorRate]: 'Error Rate (% of total)',
[ColumnKey.Operations]: 'Operations Per Second',
};
export enum ColumnWidth {
Application = 200,
P99 = 150,
ErrorRate = 150,
Operations = 150,
}
export const SORTING_ORDER = 'descend';
export const SEARCH_PLACEHOLDER = 'Search by service';

View File

@ -0,0 +1,34 @@
import { SearchOutlined } from '@ant-design/icons';
import type { ColumnType } from 'antd/es/table';
import ROUTES from 'constants/routes';
import { routeConfig } from 'container/SideNav/config';
import { getQueryString } from 'container/SideNav/helper';
import { Link } from 'react-router-dom';
import { ServicesList } from 'types/api/metrics/getService';
import { filterDropdown } from '../Filter/FilterDropdown';
import { Name } from '../styles';
export const getColumnSearchProps = (
dataIndex: keyof ServicesList,
search: string,
): ColumnType<ServicesList> => ({
filterDropdown,
filterIcon: <SearchOutlined />,
onFilter: (value: string | number | boolean, record: ServicesList): boolean =>
record[dataIndex]
.toString()
.toLowerCase()
.includes(value.toString().toLowerCase()),
render: (metrics: string): JSX.Element => {
const urlParams = new URLSearchParams(search);
const avialableParams = routeConfig[ROUTES.SERVICE_METRICS];
const queryString = getQueryString(avialableParams, urlParams);
return (
<Link to={`${ROUTES.APPLICATION}/${metrics}?${queryString.join('')}`}>
<Name>{metrics}</Name>
</Link>
);
},
});

View File

@ -0,0 +1,54 @@
import type { ColumnsType } from 'antd/es/table';
import { ServicesList } from 'types/api/metrics/getService';
import {
ColumnKey,
ColumnTitle,
ColumnWidth,
SORTING_ORDER,
} from './ColumnContants';
import { getColumnSearchProps } from './GetColumnSearchProps';
export const getColumns = (
search: string,
isMetricData: boolean,
): ColumnsType<ServicesList> => [
{
title: ColumnTitle[ColumnKey.Application],
dataIndex: ColumnKey.Application,
width: ColumnWidth.Application,
key: ColumnKey.Application,
...getColumnSearchProps('serviceName', search),
},
{
title: `${ColumnTitle[ColumnKey.P99]}${
isMetricData ? ' (in ns)' : ' (in ms)'
}`,
dataIndex: ColumnKey.P99,
key: ColumnKey.P99,
width: ColumnWidth.P99,
defaultSortOrder: SORTING_ORDER,
sorter: (a: ServicesList, b: ServicesList): number => a.p99 - b.p99,
render: (value: number): string => {
if (Number.isNaN(value)) return '0.00';
return isMetricData ? value.toFixed(2) : (value / 1000000).toFixed(2);
},
},
{
title: ColumnTitle[ColumnKey.ErrorRate],
dataIndex: ColumnKey.ErrorRate,
key: ColumnKey.ErrorRate,
width: 150,
sorter: (a: ServicesList, b: ServicesList): number =>
a.errorRate - b.errorRate,
render: (value: number): string => value.toFixed(2),
},
{
title: ColumnTitle[ColumnKey.Operations],
dataIndex: ColumnKey.Operations,
key: ColumnKey.Operations,
width: ColumnWidth.Operations,
sorter: (a: ServicesList, b: ServicesList): number => a.callRate - b.callRate,
render: (value: number): string => value.toFixed(2),
},
];

View File

@ -0,0 +1,41 @@
import { SearchOutlined } from '@ant-design/icons';
import { Button, Card, Input, Space } from 'antd';
import type { FilterDropdownProps } from 'antd/es/table/interface';
import { SEARCH_PLACEHOLDER } from '../Columns/ColumnContants';
export const filterDropdown = ({
setSelectedKeys,
selectedKeys,
confirm,
}: FilterDropdownProps): JSX.Element => {
const handleSearch = (): void => {
confirm();
};
const selectedKeysHandler = (e: React.ChangeEvent<HTMLInputElement>): void => {
setSelectedKeys(e.target.value ? [e.target.value] : []);
};
return (
<Card size="small">
<Space align="start" direction="vertical">
<Input
placeholder={SEARCH_PLACEHOLDER}
value={selectedKeys[0]}
onChange={selectedKeysHandler}
allowClear
onPressEnter={handleSearch}
/>
<Button
type="primary"
onClick={handleSearch}
icon={<SearchOutlined />}
size="small"
>
Search
</Button>
</Space>
</Card>
);
};

View File

@ -0,0 +1,67 @@
import { ResizeTable } from 'components/ResizeTable';
import { useGetQueriesRange } from 'hooks/queryBuilder/useGetQueriesRange';
import { useNotifications } from 'hooks/useNotifications';
import { useMemo } from 'react';
import { useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { AppState } from 'store/reducers';
import { ServicesList } from 'types/api/metrics/getService';
import { GlobalReducer } from 'types/reducer/globalTime';
import { getColumns } from '../Columns/ServiceColumn';
import { ServiceMetricsTableProps } from '../types';
import { getServiceListFromQuery } from '../utils';
function ServiceMetricTable({
topLevelOperations,
queryRangeRequestData,
}: ServiceMetricsTableProps): JSX.Element {
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const { notifications } = useNotifications();
const queries = useGetQueriesRange(queryRangeRequestData, {
queryKey: [
`GetMetricsQueryRange-${queryRangeRequestData[0].selectedTime}-${globalSelectedInterval}`,
maxTime,
minTime,
globalSelectedInterval,
],
keepPreviousData: true,
enabled: true,
refetchOnMount: false,
onError: (error) => {
notifications.error({
message: error.message,
});
},
});
const isLoading = queries.some((query) => query.isLoading);
const services: ServicesList[] = useMemo(
() =>
getServiceListFromQuery({
queries,
topLevelOperations,
isLoading,
}),
[isLoading, queries, topLevelOperations],
);
const { search } = useLocation();
const tableColumns = useMemo(() => getColumns(search, true), [search]);
return (
<ResizeTable
columns={tableColumns}
loading={isLoading}
dataSource={services}
rowKey="serviceName"
/>
);
}
export default ServiceMetricTable;

View File

@ -0,0 +1,36 @@
import { useMemo } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { ServiceMetricsProps } from '../types';
import { getQueryRangeRequestData } from '../utils';
import ServiceMetricTable from './ServiceMetricTable';
function ServiceMetricsApplication({
topLevelOperations,
}: ServiceMetricsProps): JSX.Element {
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const queryRangeRequestData = useMemo(
() =>
getQueryRangeRequestData({
topLevelOperations,
minTime,
maxTime,
globalSelectedInterval,
}),
[globalSelectedInterval, maxTime, minTime, topLevelOperations],
);
return (
<ServiceMetricTable
topLevelOperations={topLevelOperations}
queryRangeRequestData={queryRangeRequestData}
/>
);
}
export default ServiceMetricsApplication;

View File

@ -0,0 +1,208 @@
import { ServiceDataProps } from 'api/metrics/getTopLevelOperations';
import { OPERATORS } from 'constants/queryBuilder';
import {
DataType,
KeyOperationTableHeader,
MetricsType,
WidgetKeys,
} from 'container/MetricsApplication/constant';
import { getQueryBuilderQuerieswithFormula } from 'container/MetricsApplication/MetricsPageQueries/MetricsPageQueriesFactory';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import {
DataSource,
MetricAggregateOperator,
QueryBuilderData,
} from 'types/common/queryBuilder';
export const serviceMetricsQuery = (
topLevelOperation: [keyof ServiceDataProps, string[]],
): QueryBuilderData => {
const p99AutoCompleteData: BaseAutocompleteData = {
dataType: DataType.FLOAT64,
isColumn: true,
key: WidgetKeys.Signoz_latency_bucket,
type: null,
};
const errorRateAutoCompleteData: BaseAutocompleteData = {
dataType: DataType.FLOAT64,
isColumn: true,
key: WidgetKeys.SignozCallsTotal,
type: null,
};
const operationPrSecondAutoCompleteData: BaseAutocompleteData = {
dataType: DataType.FLOAT64,
isColumn: true,
key: WidgetKeys.SignozCallsTotal,
type: null,
};
const autocompleteData = [
p99AutoCompleteData,
errorRateAutoCompleteData,
errorRateAutoCompleteData,
operationPrSecondAutoCompleteData,
];
const p99AdditionalItems: TagFilterItem[] = [
{
id: '',
key: {
dataType: DataType.STRING,
isColumn: false,
key: WidgetKeys.Service_name,
type: MetricsType.Resource,
},
op: OPERATORS.IN,
value: [topLevelOperation[0].toString()],
},
{
id: '',
key: {
dataType: DataType.STRING,
isColumn: false,
key: WidgetKeys.Operation,
type: MetricsType.Tag,
},
op: OPERATORS.IN,
value: [...topLevelOperation[1]],
},
];
const errorRateAdditionalItemsA: TagFilterItem[] = [
{
id: '',
key: {
dataType: DataType.STRING,
isColumn: false,
key: WidgetKeys.Service_name,
type: MetricsType.Resource,
},
op: OPERATORS.IN,
value: [topLevelOperation[0].toString()],
},
{
id: '',
key: {
dataType: DataType.INT64,
isColumn: false,
key: WidgetKeys.StatusCode,
type: MetricsType.Tag,
},
op: OPERATORS.IN,
value: ['STATUS_CODE_ERROR'],
},
{
id: '',
key: {
dataType: DataType.STRING,
isColumn: false,
key: WidgetKeys.Operation,
type: MetricsType.Tag,
},
op: OPERATORS.IN,
value: [...topLevelOperation[1]],
},
];
const errorRateAdditionalItemsB: TagFilterItem[] = [
{
id: '',
key: {
dataType: DataType.STRING,
isColumn: false,
key: WidgetKeys.Service_name,
type: MetricsType.Resource,
},
op: OPERATORS.IN,
value: [topLevelOperation[0].toString()],
},
{
id: '',
key: {
dataType: DataType.STRING,
isColumn: false,
key: WidgetKeys.Operation,
type: MetricsType.Tag,
},
op: OPERATORS.IN,
value: [...topLevelOperation[1]],
},
];
const operationPrSecondAdditionalItems: TagFilterItem[] = [
{
id: '',
key: {
dataType: DataType.STRING,
isColumn: false,
key: WidgetKeys.Service_name,
type: MetricsType.Resource,
},
op: OPERATORS.IN,
value: [topLevelOperation[0].toString()],
},
{
id: '',
key: {
dataType: DataType.STRING,
isColumn: false,
key: WidgetKeys.Operation,
type: MetricsType.Tag,
},
op: OPERATORS.IN,
value: [...topLevelOperation[1]],
},
];
const additionalItems = [
p99AdditionalItems,
errorRateAdditionalItemsA,
errorRateAdditionalItemsB,
operationPrSecondAdditionalItems,
];
const aggregateOperators = [
MetricAggregateOperator.HIST_QUANTILE_99,
MetricAggregateOperator.SUM_RATE,
MetricAggregateOperator.SUM_RATE,
MetricAggregateOperator.SUM_RATE,
];
const disabled = [false, true, true, false];
const legends = [
KeyOperationTableHeader.P99,
KeyOperationTableHeader.ERROR_RATE,
KeyOperationTableHeader.ERROR_RATE,
KeyOperationTableHeader.OPERATION_PR_SECOND,
];
const expressions = ['B*100/C'];
const legendFormulas = ['Error Rate'];
const groupBy: BaseAutocompleteData[] = [
{
dataType: DataType.STRING,
isColumn: false,
key: WidgetKeys.Service_name,
type: MetricsType.Tag,
},
];
const dataSource = DataSource.METRICS;
return getQueryBuilderQuerieswithFormula({
autocompleteData,
additionalItems,
disabled,
legends,
aggregateOperators,
expressions,
legendFormulas,
groupBy,
dataSource,
});
};

View File

@ -0,0 +1,59 @@
import localStorageGet from 'api/browser/localstorage/get';
import localStorageSet from 'api/browser/localstorage/set';
import Spinner from 'components/Spinner';
import { SKIP_ONBOARDING } from 'constants/onboarding';
import useGetTopLevelOperations from 'hooks/useGetTopLevelOperations';
import useResourceAttribute from 'hooks/useResourceAttribute';
import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
import { useMemo, useState } from 'react';
import { QueryKey } from 'react-query';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { Tags } from 'types/reducer/trace';
import SkipOnBoardingModal from '../SkipOnBoardModal';
import ServiceMetricsApplication from './ServiceMetricsApplication';
function ServicesUsingMetrics(): JSX.Element {
const { maxTime, minTime, selectedTime: globalSelectedInterval } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const { queries } = useResourceAttribute();
const selectedTags = useMemo(
() => (convertRawQueriesToTraceSelectedTags(queries) as Tags[]) || [],
[queries],
);
const queryKey: QueryKey = [
minTime,
maxTime,
selectedTags,
globalSelectedInterval,
];
const { data, isLoading, isError } = useGetTopLevelOperations(queryKey);
const [skipOnboarding, setSkipOnboarding] = useState(
localStorageGet(SKIP_ONBOARDING) === 'true',
);
const onContinueClick = (): void => {
localStorageSet(SKIP_ONBOARDING, 'true');
setSkipOnboarding(true);
};
const topLevelOperations = Object.entries(data || {});
if (isLoading === false && !skipOnboarding && isError === true) {
return <SkipOnBoardingModal onContinueClick={onContinueClick} />;
}
if (isLoading) {
return <Spinner tip="Loading..." />;
}
return <ServiceMetricsApplication topLevelOperations={topLevelOperations} />;
}
export default ServicesUsingMetrics;

View File

@ -0,0 +1,50 @@
import { render, screen, waitFor } from '@testing-library/react';
import ROUTES from 'constants/routes';
import { BrowserRouter } from 'react-router-dom';
import { services } from './__mocks__/getServices';
import ServiceTraceTable from './ServiceTracesTable';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: (): { pathname: string } => ({
pathname: `${process.env.FRONTEND_API_ENDPOINT}${ROUTES.APPLICATION}/`,
}),
}));
describe('Metrics Component', () => {
it('renders without errors', async () => {
render(
<BrowserRouter>
<ServiceTraceTable services={services} loading={false} />
</BrowserRouter>,
);
await waitFor(() => {
expect(screen.getByText(/application/i)).toBeInTheDocument();
expect(screen.getByText(/p99 latency \(in ms\)/i)).toBeInTheDocument();
expect(screen.getByText(/error rate \(% of total\)/i)).toBeInTheDocument();
expect(screen.getByText(/operations per second/i)).toBeInTheDocument();
});
});
it('renders if the data is loaded in the table', async () => {
render(
<BrowserRouter>
<ServiceTraceTable services={services} loading={false} />
</BrowserRouter>,
);
expect(screen.getByText('frontend')).toBeInTheDocument();
});
it('renders no data when required conditions are met', async () => {
render(
<BrowserRouter>
<ServiceTraceTable services={[]} loading={false} />
</BrowserRouter>,
);
expect(screen.getByText('No data')).toBeInTheDocument();
});
});

View File

@ -0,0 +1,26 @@
import { ResizeTable } from 'components/ResizeTable';
import { useMemo } from 'react';
import { useLocation } from 'react-router-dom';
import { getColumns } from '../Columns/ServiceColumn';
import ServiceTableProps from '../types';
function ServiceTraceTable({
services,
loading,
}: ServiceTableProps): JSX.Element {
const { search } = useLocation();
const tableColumns = useMemo(() => getColumns(search, false), [search]);
return (
<ResizeTable
columns={tableColumns}
loading={loading}
dataSource={services}
rowKey="serviceName"
/>
);
}
export default ServiceTraceTable;

View File

@ -0,0 +1,22 @@
import { ServicesList } from 'types/api/metrics/getService';
export const services: ServicesList[] = [
{
serviceName: 'frontend',
p99: 1261498140,
avgDuration: 768497850.9803921,
numCalls: 255,
callRate: 0.9444444444444444,
numErrors: 0,
errorRate: 0,
},
{
serviceName: 'customer',
p99: 890150740.0000001,
avgDuration: 369612035.2941176,
numCalls: 255,
callRate: 0.9444444444444444,
numErrors: 0,
errorRate: 0,
},
];

View File

@ -0,0 +1,60 @@
import localStorageGet from 'api/browser/localstorage/get';
import localStorageSet from 'api/browser/localstorage/set';
import { SKIP_ONBOARDING } from 'constants/onboarding';
import useErrorNotification from 'hooks/useErrorNotification';
import { useQueryService } from 'hooks/useQueryService';
import useResourceAttribute from 'hooks/useResourceAttribute';
import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
import { useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { Tags } from 'types/reducer/trace';
import SkipOnBoardingModal from '../SkipOnBoardModal';
import ServiceTraceTable from './ServiceTracesTable';
function ServiceTraces(): JSX.Element {
const { maxTime, minTime, selectedTime } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const { queries } = useResourceAttribute();
const selectedTags = useMemo(
() => (convertRawQueriesToTraceSelectedTags(queries) as Tags[]) || [],
[queries],
);
const { data, error, isLoading, isError } = useQueryService({
minTime,
maxTime,
selectedTime,
selectedTags,
});
useErrorNotification(error);
const services = data || [];
const [skipOnboarding, setSkipOnboarding] = useState(
localStorageGet(SKIP_ONBOARDING) === 'true',
);
const onContinueClick = (): void => {
localStorageSet(SKIP_ONBOARDING, 'true');
setSkipOnboarding(true);
};
if (
services.length === 0 &&
isLoading === false &&
!skipOnboarding &&
isError === true
) {
return <SkipOnBoardingModal onContinueClick={onContinueClick} />;
}
return <ServiceTraceTable services={services} loading={isLoading} />;
}
export default ServiceTraces;

View File

@ -0,0 +1,48 @@
import { Button, Typography } from 'antd';
import Modal from 'components/Modal';
function SkipOnBoardingModal({ onContinueClick }: Props): JSX.Element {
return (
<Modal
title="Setup instrumentation"
isModalVisible
closable={false}
footer={[
<Button key="submit" type="primary" onClick={onContinueClick}>
Continue without instrumentation
</Button>,
]}
>
<>
<iframe
width="100%"
height="265"
src="https://www.youtube.com/embed/J1Bof55DOb4"
frameBorder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
title="youtube_video"
/>
<div>
<Typography>No instrumentation data.</Typography>
<Typography>
Please instrument your application as mentioned&nbsp;
<a
href="https://signoz.io/docs/instrumentation/overview"
target="_blank"
rel="noreferrer"
>
here
</a>
</Typography>
</div>
</>
</Modal>
);
}
interface Props {
onContinueClick: () => void;
}
export default SkipOnBoardingModal;

View File

@ -0,0 +1,19 @@
import { FeatureKeys } from 'constants/features';
import useFeatureFlag from 'hooks/useFeatureFlag';
import ServiceMetrics from './ServiceMetrics';
import ServiceTraces from './ServiceTraces';
import { Container } from './styles';
function Services(): JSX.Element {
const isSpanMetricEnabled = useFeatureFlag(FeatureKeys.USE_SPAN_METRICS)
?.active;
return (
<Container>
{isSpanMetricEnabled ? <ServiceMetrics /> : <ServiceTraces />}
</Container>
);
}
export default Services;

View File

@ -0,0 +1,15 @@
import { Typography } from 'antd';
import { themeColors } from 'constants/theme';
import styled from 'styled-components';
export const Container = styled.div`
margin-top: 2rem;
`;
export const Name = styled(Typography)`
&&& {
font-weight: 600;
color: ${themeColors.lightBlue};
cursor: pointer;
}
`;

View File

@ -0,0 +1,34 @@
import { ServiceDataProps } from 'api/metrics/getTopLevelOperations';
import { Time } from 'container/TopNav/DateTimeSelection/config';
import { UseQueryResult } from 'react-query';
import { GetQueryResultsProps } from 'store/actions/dashboard/getQueryResults';
import { SuccessResponse } from 'types/api';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { ServicesList } from 'types/api/metrics/getService';
export default interface ServiceTableProps {
services: ServicesList[];
loading: boolean;
}
export interface ServiceMetricsProps {
topLevelOperations: [keyof ServiceDataProps, string[]][];
}
export interface ServiceMetricsTableProps {
topLevelOperations: [keyof ServiceDataProps, string[]][];
queryRangeRequestData: GetQueryResultsProps[];
}
export interface GetQueryRangeRequestDataProps {
topLevelOperations: [keyof ServiceDataProps, string[]][];
maxTime: number;
minTime: number;
globalSelectedInterval: Time;
}
export interface GetServiceListFromQueryProps {
queries: UseQueryResult<SuccessResponse<MetricRangePayloadProps>, Error>[];
topLevelOperations: [keyof ServiceDataProps, string[]][];
isLoading: boolean;
}

View File

@ -0,0 +1,99 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory';
import { updateStepInterval } from 'hooks/queryBuilder/useStepInterval';
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
import { GetQueryResultsProps } from 'store/actions/dashboard/getQueryResults';
import { ServicesList } from 'types/api/metrics/getService';
import { QueryDataV3 } from 'types/api/widgets/getQuery';
import { EQueryType } from 'types/common/dashboard';
import { v4 as uuid } from 'uuid';
import { serviceMetricsQuery } from './ServiceMetrics/ServiceMetricsQuery';
import {
GetQueryRangeRequestDataProps,
GetServiceListFromQueryProps,
} from './types';
export function getSeriesValue(
queryArray: QueryDataV3[],
queryName: string,
): string {
const queryObject = queryArray.find((item) => item.queryName === queryName);
const series = queryObject ? queryObject.series : 0;
return series ? series[0].values[0].value : '0';
}
export const getQueryRangeRequestData = ({
topLevelOperations,
maxTime,
minTime,
globalSelectedInterval,
}: GetQueryRangeRequestDataProps): GetQueryResultsProps[] => {
const requestData: GetQueryResultsProps[] = [];
topLevelOperations.forEach((operation) => {
const serviceMetricsWidget = getWidgetQueryBuilder({
query: {
queryType: EQueryType.QUERY_BUILDER,
promql: [],
builder: serviceMetricsQuery(operation),
clickhouse_sql: [],
id: uuid(),
},
panelTypes: PANEL_TYPES.TABLE,
});
const updatedQuery = updateStepInterval(
serviceMetricsWidget.query,
maxTime,
minTime,
);
requestData.push({
selectedTime: serviceMetricsWidget?.timePreferance,
graphType: serviceMetricsWidget?.panelTypes,
query: updatedQuery,
globalSelectedInterval,
variables: getDashboardVariables(),
});
});
return requestData;
};
export const getServiceListFromQuery = ({
queries,
topLevelOperations,
isLoading,
}: GetServiceListFromQueryProps): ServicesList[] => {
const services: ServicesList[] = [];
if (!isLoading) {
queries.forEach((query, index) => {
// handling error case if query fails
if (query.isError) {
const serviceData: ServicesList = {
serviceName: topLevelOperations[index][0].toString(),
p99: 0,
callRate: 0,
errorRate: 0,
avgDuration: 0,
numCalls: 0,
numErrors: 0,
};
services.push(serviceData);
}
if (query.data) {
const queryArray = query.data.payload.data.newResult.data.result;
const serviceData: ServicesList = {
serviceName: topLevelOperations[index][0].toString(),
p99: parseFloat(getSeriesValue(queryArray, 'A')),
callRate: parseFloat(getSeriesValue(queryArray, 'D')),
errorRate: parseFloat(getSeriesValue(queryArray, 'F1')),
avgDuration: 0,
numCalls: 0,
numErrors: 0,
};
services.push(serviceData);
}
});
}
return services;
};

View File

@ -5,9 +5,7 @@ export enum ColumnKey {
Operations = 'callRate',
}
export const ColumnTitle: {
[key in ColumnKey]: string;
} = {
export const ColumnTitle: Record<ColumnKey, string> = {
[ColumnKey.Application]: 'Application',
[ColumnKey.P99]: 'P99 latency (in ms)',
[ColumnKey.ErrorRate]: 'Error Rate (% of total)',

View File

@ -0,0 +1,35 @@
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useMemo } from 'react';
import {
QueryKey,
useQueries,
UseQueryOptions,
UseQueryResult,
} from 'react-query';
import {
GetMetricQueryRange,
GetQueryResultsProps,
} from 'store/actions/dashboard/getQueryResults';
import { SuccessResponse } from 'types/api';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
export const useGetQueriesRange = (
requestData: GetQueryResultsProps[],
options: UseQueryOptions<SuccessResponse<MetricRangePayloadProps>, Error>,
): UseQueryResult<SuccessResponse<MetricRangePayloadProps>, Error>[] => {
const queryKey = useMemo(() => {
if (options?.queryKey) {
return [...options.queryKey];
}
return [REACT_QUERY_KEY.GET_QUERY_RANGE, requestData];
}, [options?.queryKey, requestData]);
const queryData = requestData.map((request, index) => ({
queryFn: async (): Promise<SuccessResponse<MetricRangePayloadProps>> =>
GetMetricQueryRange(request),
...options,
queryKey: [...queryKey, index] as QueryKey,
}));
return useQueries(queryData);
};

View File

@ -0,0 +1,16 @@
import getTopLevelOperations, {
ServiceDataProps,
} from 'api/metrics/getTopLevelOperations';
import { QueryKey, useQuery, UseQueryResult } from 'react-query';
type UseGetTopLevelOperations = (
queryKey: QueryKey,
) => UseQueryResult<ServiceDataProps>;
const useGetTopLevelOperations: UseGetTopLevelOperations = (queryKey) =>
useQuery<ServiceDataProps>({
queryKey,
queryFn: getTopLevelOperations,
});
export default useGetTopLevelOperations;

View File

@ -1,68 +1,18 @@
import { Space } from 'antd';
import localStorageGet from 'api/browser/localstorage/get';
import localStorageSet from 'api/browser/localstorage/set';
import ReleaseNote from 'components/ReleaseNote';
import { SKIP_ONBOARDING } from 'constants/onboarding';
import ResourceAttributesFilter from 'container/ResourceAttributesFilter';
import ServicesTable from 'container/ServiceTable';
import SkipOnBoardingModal from 'container/ServiceTable/SkipOnBoardModal';
import useErrorNotification from 'hooks/useErrorNotification';
import { useQueryService } from 'hooks/useQueryService';
import useResourceAttribute from 'hooks/useResourceAttribute';
import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
import { useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import ServicesApplication from 'container/ServiceApplication';
import { useLocation } from 'react-router-dom';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { Tags } from 'types/reducer/trace';
function Metrics(): JSX.Element {
const { minTime, maxTime, selectedTime } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const location = useLocation();
const { queries } = useResourceAttribute();
const [skipOnboarding, setSkipOnboarding] = useState(
localStorageGet(SKIP_ONBOARDING) === 'true',
);
const onContinueClick = (): void => {
localStorageSet(SKIP_ONBOARDING, 'true');
setSkipOnboarding(true);
};
const selectedTags = useMemo(
() => (convertRawQueriesToTraceSelectedTags(queries, '') as Tags[]) || [],
[queries],
);
const { data, error, isLoading, isError } = useQueryService({
minTime,
maxTime,
selectedTime,
selectedTags,
});
useErrorNotification(error);
if (
data?.length === 0 &&
isLoading === false &&
!skipOnboarding &&
isError === true
) {
return <SkipOnBoardingModal onContinueClick={onContinueClick} />;
}
return (
<Space direction="vertical" style={{ width: '100%' }}>
<ReleaseNote path={location.pathname} />
<ResourceAttributesFilter />
<ServicesTable services={data || []} isLoading={isLoading} />
<ServicesApplication />
</Space>
);
}

View File

@ -1,3 +1,4 @@
import { AxiosError } from 'axios';
import { Tags } from 'types/reducer/trace';
export interface Props {
@ -17,3 +18,9 @@ export interface ServicesList {
}
export type PayloadProps = ServicesList[];
export interface QueryServiceProps {
data: PayloadProps | undefined;
error: AxiosError | null;
isLoading: boolean;
}