From 562621a1171a5cbdb7d11a4e24f0c8fe2199b1da Mon Sep 17 00:00:00 2001 From: Rajat Dabade Date: Wed, 2 Aug 2023 15:00:58 +0530 Subject: [PATCH] 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 Co-authored-by: Palash Gupta Co-authored-by: Srikanth Chekuri --- frontend/src/constants/theme.ts | 1 + .../MetricsPageQueries/DBCallQueries.ts | 19 +- .../MetricsPageQueries/ExternalQueries.ts | 83 +++---- .../MetricsPageQueriesFactory.ts | 16 +- .../MetricsPageQueries/OverviewQueries.ts | 8 +- .../MetricsPageQueries/TopOperationQueries.ts | 8 +- .../Tabs/Overview/TopOperationMetrics.tsx | 15 +- .../MetricsApplication/Tabs/types.ts | 4 +- .../container/MetricsApplication/constant.ts | 1 + .../src/container/MetricsApplication/types.ts | 12 + .../Columns/ColumnContants.ts | 24 ++ .../Columns/GetColumnSearchProps.tsx | 34 +++ .../Columns/ServiceColumn.ts | 54 +++++ .../Filter/FilterDropdown.tsx | 41 ++++ .../ServiceMetrics/ServiceMetricTable.tsx | 67 ++++++ .../ServiceMetricsApplication.tsx | 36 +++ .../ServiceMetrics/ServiceMetricsQuery.ts | 208 ++++++++++++++++++ .../ServiceMetrics/index.tsx | 59 +++++ .../ServiceTraces/Service.test.tsx | 50 +++++ .../ServiceTraces/ServiceTracesTable.tsx | 26 +++ .../ServiceTraces/__mocks__/getServices.ts | 22 ++ .../ServiceTraces/index.tsx | 60 +++++ .../SkipOnBoardModal/index.tsx | 48 ++++ .../container/ServiceApplication/index.tsx | 19 ++ .../container/ServiceApplication/styles.ts | 15 ++ .../src/container/ServiceApplication/types.ts | 34 +++ .../src/container/ServiceApplication/utils.ts | 99 +++++++++ .../ServiceTable/Columns/ColumnContants.ts | 4 +- .../hooks/queryBuilder/useGetQueriesRange.ts | 35 +++ .../src/hooks/useGetTopLevelOperations.ts | 16 ++ frontend/src/pages/Services/index.tsx | 54 +---- frontend/src/types/api/metrics/getService.ts | 7 + 32 files changed, 1034 insertions(+), 145 deletions(-) create mode 100644 frontend/src/container/ServiceApplication/Columns/ColumnContants.ts create mode 100644 frontend/src/container/ServiceApplication/Columns/GetColumnSearchProps.tsx create mode 100644 frontend/src/container/ServiceApplication/Columns/ServiceColumn.ts create mode 100644 frontend/src/container/ServiceApplication/Filter/FilterDropdown.tsx create mode 100644 frontend/src/container/ServiceApplication/ServiceMetrics/ServiceMetricTable.tsx create mode 100644 frontend/src/container/ServiceApplication/ServiceMetrics/ServiceMetricsApplication.tsx create mode 100644 frontend/src/container/ServiceApplication/ServiceMetrics/ServiceMetricsQuery.ts create mode 100644 frontend/src/container/ServiceApplication/ServiceMetrics/index.tsx create mode 100644 frontend/src/container/ServiceApplication/ServiceTraces/Service.test.tsx create mode 100644 frontend/src/container/ServiceApplication/ServiceTraces/ServiceTracesTable.tsx create mode 100644 frontend/src/container/ServiceApplication/ServiceTraces/__mocks__/getServices.ts create mode 100644 frontend/src/container/ServiceApplication/ServiceTraces/index.tsx create mode 100644 frontend/src/container/ServiceApplication/SkipOnBoardModal/index.tsx create mode 100644 frontend/src/container/ServiceApplication/index.tsx create mode 100644 frontend/src/container/ServiceApplication/styles.ts create mode 100644 frontend/src/container/ServiceApplication/types.ts create mode 100644 frontend/src/container/ServiceApplication/utils.ts create mode 100644 frontend/src/hooks/queryBuilder/useGetQueriesRange.ts create mode 100644 frontend/src/hooks/useGetTopLevelOperations.ts diff --git a/frontend/src/constants/theme.ts b/frontend/src/constants/theme.ts index fcb8dd171a..18b7db2b18 100644 --- a/frontend/src/constants/theme.ts +++ b/frontend/src/constants/theme.ts @@ -51,6 +51,7 @@ const themeColors = { snowWhite: '#fafafa', gamboge: '#D89614', bckgGrey: '#1d1d1d', + lightBlue: '#177ddc', }; export { themeColors }; diff --git a/frontend/src/container/MetricsApplication/MetricsPageQueries/DBCallQueries.ts b/frontend/src/container/MetricsApplication/MetricsPageQueries/DBCallQueries.ts index 72b9703cdd..9036b81abb 100644 --- a/frontend/src/container/MetricsApplication/MetricsPageQueries/DBCallQueries.ts +++ b/frontend/src/container/MetricsApplication/MetricsPageQueries/DBCallQueries.ts @@ -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[]; -} diff --git a/frontend/src/container/MetricsApplication/MetricsPageQueries/ExternalQueries.ts b/frontend/src/container/MetricsApplication/MetricsPageQueries/ExternalQueries.ts index b140882c46..18038ecf0d 100644 --- a/frontend/src/container/MetricsApplication/MetricsPageQueries/ExternalQueries.ts +++ b/frontend/src/container/MetricsApplication/MetricsPageQueries/ExternalQueries.ts @@ -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, }); diff --git a/frontend/src/container/MetricsApplication/MetricsPageQueries/MetricsPageQueriesFactory.ts b/frontend/src/container/MetricsApplication/MetricsPageQueries/MetricsPageQueriesFactory.ts index 2412dfce47..efe00eec4d 100644 --- a/frontend/src/container/MetricsApplication/MetricsPageQueries/MetricsPageQueriesFactory.ts +++ b/frontend/src/container/MetricsApplication/MetricsPageQueries/MetricsPageQueriesFactory.ts @@ -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], diff --git a/frontend/src/container/MetricsApplication/MetricsPageQueries/OverviewQueries.ts b/frontend/src/container/MetricsApplication/MetricsPageQueries/OverviewQueries.ts index e1137f4cfc..77df3e30f6 100644 --- a/frontend/src/container/MetricsApplication/MetricsPageQueries/OverviewQueries.ts +++ b/frontend/src/container/MetricsApplication/MetricsPageQueries/OverviewQueries.ts @@ -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, }); diff --git a/frontend/src/container/MetricsApplication/MetricsPageQueries/TopOperationQueries.ts b/frontend/src/container/MetricsApplication/MetricsPageQueries/TopOperationQueries.ts index 6f75d9666d..42aac24d9f 100644 --- a/frontend/src/container/MetricsApplication/MetricsPageQueries/TopOperationQueries.ts +++ b/frontend/src/container/MetricsApplication/MetricsPageQueries/TopOperationQueries.ts @@ -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, }); diff --git a/frontend/src/container/MetricsApplication/Tabs/Overview/TopOperationMetrics.tsx b/frontend/src/container/MetricsApplication/Tabs/Overview/TopOperationMetrics.tsx index d205bfbd83..8a044fddba 100644 --- a/frontend/src/container/MetricsApplication/Tabs/Overview/TopOperationMetrics.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/Overview/TopOperationMetrics.tsx @@ -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(); - const [errorMessage, setErrorMessage] = useState(''); + 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
{errorMessage}
; - } - return ( = { + [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'; diff --git a/frontend/src/container/ServiceApplication/Columns/GetColumnSearchProps.tsx b/frontend/src/container/ServiceApplication/Columns/GetColumnSearchProps.tsx new file mode 100644 index 0000000000..4257dc57ec --- /dev/null +++ b/frontend/src/container/ServiceApplication/Columns/GetColumnSearchProps.tsx @@ -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 => ({ + filterDropdown, + filterIcon: , + 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 ( + + {metrics} + + ); + }, +}); diff --git a/frontend/src/container/ServiceApplication/Columns/ServiceColumn.ts b/frontend/src/container/ServiceApplication/Columns/ServiceColumn.ts new file mode 100644 index 0000000000..b290e8409c --- /dev/null +++ b/frontend/src/container/ServiceApplication/Columns/ServiceColumn.ts @@ -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 => [ + { + 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), + }, +]; diff --git a/frontend/src/container/ServiceApplication/Filter/FilterDropdown.tsx b/frontend/src/container/ServiceApplication/Filter/FilterDropdown.tsx new file mode 100644 index 0000000000..1dc4a12d89 --- /dev/null +++ b/frontend/src/container/ServiceApplication/Filter/FilterDropdown.tsx @@ -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): void => { + setSelectedKeys(e.target.value ? [e.target.value] : []); + }; + + return ( + + + + + + + ); +}; diff --git a/frontend/src/container/ServiceApplication/ServiceMetrics/ServiceMetricTable.tsx b/frontend/src/container/ServiceApplication/ServiceMetrics/ServiceMetricTable.tsx new file mode 100644 index 0000000000..154cc4ab11 --- /dev/null +++ b/frontend/src/container/ServiceApplication/ServiceMetrics/ServiceMetricTable.tsx @@ -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 ( + + ); +} + +export default ServiceMetricTable; diff --git a/frontend/src/container/ServiceApplication/ServiceMetrics/ServiceMetricsApplication.tsx b/frontend/src/container/ServiceApplication/ServiceMetrics/ServiceMetricsApplication.tsx new file mode 100644 index 0000000000..4d5889b909 --- /dev/null +++ b/frontend/src/container/ServiceApplication/ServiceMetrics/ServiceMetricsApplication.tsx @@ -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 ( + + ); +} + +export default ServiceMetricsApplication; diff --git a/frontend/src/container/ServiceApplication/ServiceMetrics/ServiceMetricsQuery.ts b/frontend/src/container/ServiceApplication/ServiceMetrics/ServiceMetricsQuery.ts new file mode 100644 index 0000000000..7214a7c912 --- /dev/null +++ b/frontend/src/container/ServiceApplication/ServiceMetrics/ServiceMetricsQuery.ts @@ -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, + }); +}; diff --git a/frontend/src/container/ServiceApplication/ServiceMetrics/index.tsx b/frontend/src/container/ServiceApplication/ServiceMetrics/index.tsx new file mode 100644 index 0000000000..740168f96c --- /dev/null +++ b/frontend/src/container/ServiceApplication/ServiceMetrics/index.tsx @@ -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 ; + } + + if (isLoading) { + return ; + } + + return ; +} + +export default ServicesUsingMetrics; diff --git a/frontend/src/container/ServiceApplication/ServiceTraces/Service.test.tsx b/frontend/src/container/ServiceApplication/ServiceTraces/Service.test.tsx new file mode 100644 index 0000000000..2e94296d8d --- /dev/null +++ b/frontend/src/container/ServiceApplication/ServiceTraces/Service.test.tsx @@ -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( + + + , + ); + + 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( + + + , + ); + + expect(screen.getByText('frontend')).toBeInTheDocument(); + }); + + it('renders no data when required conditions are met', async () => { + render( + + + , + ); + + expect(screen.getByText('No data')).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/container/ServiceApplication/ServiceTraces/ServiceTracesTable.tsx b/frontend/src/container/ServiceApplication/ServiceTraces/ServiceTracesTable.tsx new file mode 100644 index 0000000000..b5c4f6e7a1 --- /dev/null +++ b/frontend/src/container/ServiceApplication/ServiceTraces/ServiceTracesTable.tsx @@ -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 ( + + ); +} + +export default ServiceTraceTable; diff --git a/frontend/src/container/ServiceApplication/ServiceTraces/__mocks__/getServices.ts b/frontend/src/container/ServiceApplication/ServiceTraces/__mocks__/getServices.ts new file mode 100644 index 0000000000..c7ffdf0d46 --- /dev/null +++ b/frontend/src/container/ServiceApplication/ServiceTraces/__mocks__/getServices.ts @@ -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, + }, +]; diff --git a/frontend/src/container/ServiceApplication/ServiceTraces/index.tsx b/frontend/src/container/ServiceApplication/ServiceTraces/index.tsx new file mode 100644 index 0000000000..370697af00 --- /dev/null +++ b/frontend/src/container/ServiceApplication/ServiceTraces/index.tsx @@ -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 ; + } + + return ; +} + +export default ServiceTraces; diff --git a/frontend/src/container/ServiceApplication/SkipOnBoardModal/index.tsx b/frontend/src/container/ServiceApplication/SkipOnBoardModal/index.tsx new file mode 100644 index 0000000000..aedc3d4e43 --- /dev/null +++ b/frontend/src/container/ServiceApplication/SkipOnBoardModal/index.tsx @@ -0,0 +1,48 @@ +import { Button, Typography } from 'antd'; +import Modal from 'components/Modal'; + +function SkipOnBoardingModal({ onContinueClick }: Props): JSX.Element { + return ( + + Continue without instrumentation + , + ]} + > + <> +