mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-14 04:26:02 +08:00
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:
parent
68ab022836
commit
562621a117
@ -51,6 +51,7 @@ const themeColors = {
|
||||
snowWhite: '#fafafa',
|
||||
gamboge: '#D89614',
|
||||
bckgGrey: '#1d1d1d',
|
||||
lightBlue: '#177ddc',
|
||||
};
|
||||
|
||||
export { themeColors };
|
||||
|
@ -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[];
|
||||
}
|
||||
|
@ -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,
|
||||
});
|
||||
|
@ -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],
|
||||
|
@ -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,
|
||||
});
|
||||
|
@ -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,
|
||||
});
|
||||
|
@ -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}
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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[];
|
||||
}
|
||||
|
@ -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';
|
@ -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>
|
||||
);
|
||||
},
|
||||
});
|
@ -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),
|
||||
},
|
||||
];
|
@ -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>
|
||||
);
|
||||
};
|
@ -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;
|
@ -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;
|
@ -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,
|
||||
});
|
||||
};
|
@ -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;
|
@ -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();
|
||||
});
|
||||
});
|
@ -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;
|
@ -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,
|
||||
},
|
||||
];
|
@ -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;
|
@ -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
|
||||
<a
|
||||
href="https://signoz.io/docs/instrumentation/overview"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
here
|
||||
</a>
|
||||
</Typography>
|
||||
</div>
|
||||
</>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
interface Props {
|
||||
onContinueClick: () => void;
|
||||
}
|
||||
|
||||
export default SkipOnBoardingModal;
|
19
frontend/src/container/ServiceApplication/index.tsx
Normal file
19
frontend/src/container/ServiceApplication/index.tsx
Normal 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;
|
15
frontend/src/container/ServiceApplication/styles.ts
Normal file
15
frontend/src/container/ServiceApplication/styles.ts
Normal 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;
|
||||
}
|
||||
`;
|
34
frontend/src/container/ServiceApplication/types.ts
Normal file
34
frontend/src/container/ServiceApplication/types.ts
Normal 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;
|
||||
}
|
99
frontend/src/container/ServiceApplication/utils.ts
Normal file
99
frontend/src/container/ServiceApplication/utils.ts
Normal 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;
|
||||
};
|
@ -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)',
|
||||
|
35
frontend/src/hooks/queryBuilder/useGetQueriesRange.ts
Normal file
35
frontend/src/hooks/queryBuilder/useGetQueriesRange.ts
Normal 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);
|
||||
};
|
16
frontend/src/hooks/useGetTopLevelOperations.ts
Normal file
16
frontend/src/hooks/useGetTopLevelOperations.ts
Normal 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;
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user