From 7948bca710c1ab1184515e03ef890168f765a7a7 Mon Sep 17 00:00:00 2001 From: Pranshu Chittora Date: Tue, 3 May 2022 15:41:40 +0530 Subject: [PATCH] feat: resource attributes based filter for metrics (#1022) * feat: resource attributes based filtering enabled --- frontend/src/api/apiV1.ts | 4 +- frontend/src/api/index.ts | 6 +- .../src/api/metrics/getResourceAttributes.ts | 47 ++++ frontend/src/api/metrics/getService.ts | 8 +- .../src/api/metrics/getServiceOverview.ts | 10 +- frontend/src/api/metrics/getTopEndPoints.ts | 9 +- frontend/src/constants/resourceAttributes.ts | 18 ++ .../ListOfDashboard/SearchFilter/index.tsx | 1 + .../ResourceAttributesFilter/QueryChip.tsx | 32 +++ .../ResourceAttributesFilter.Machine.ts | 61 +++++ ...esourceAttributesFilter.Machine.typegen.ts | 32 +++ .../ResourceAttributesFilter/index.tsx | 222 ++++++++++++++++++ .../ResourceAttributesFilter/styles.ts | 32 +++ .../ResourceAttributesFilter/types.ts | 11 + .../ResourceAttributesFilter/utils.ts | 58 +++++ .../MetricsApplication/Tabs/Application.tsx | 20 +- .../MetricsApplication/Tabs/DBCall.tsx | 11 +- .../MetricsApplication/Tabs/External.tsx | 15 +- .../MetricsApplication/TopEndpointsTable.tsx | 11 +- .../container/MetricsApplication/index.tsx | 26 +- frontend/src/container/MetricsTable/index.tsx | 6 +- frontend/src/lib/resourceAttributes.ts | 71 ++++++ .../src/pages/MetricApplication/index.tsx | 30 ++- frontend/src/pages/Metrics/index.tsx | 40 +++- .../store/actions/metrics/getInitialData.ts | 4 + .../src/store/actions/metrics/getService.ts | 3 + .../metrics/setResourceAttributeQueries.ts | 55 +++++ frontend/src/store/reducers/metric.ts | 19 ++ frontend/src/types/actions/metrics.ts | 14 +- .../src/types/api/metrics/getDBOverview.ts | 3 + .../api/metrics/getResourceAttributes.ts | 8 + frontend/src/types/api/metrics/getService.ts | 3 + .../src/types/api/metrics/getTopEndPoints.ts | 3 + frontend/src/types/reducer/metrics.ts | 3 + frontend/src/types/reducer/trace.ts | 7 +- 35 files changed, 841 insertions(+), 62 deletions(-) create mode 100644 frontend/src/api/metrics/getResourceAttributes.ts create mode 100644 frontend/src/constants/resourceAttributes.ts create mode 100644 frontend/src/container/MetricsApplication/ResourceAttributesFilter/QueryChip.tsx create mode 100644 frontend/src/container/MetricsApplication/ResourceAttributesFilter/ResourceAttributesFilter.Machine.ts create mode 100644 frontend/src/container/MetricsApplication/ResourceAttributesFilter/ResourceAttributesFilter.Machine.typegen.ts create mode 100644 frontend/src/container/MetricsApplication/ResourceAttributesFilter/index.tsx create mode 100644 frontend/src/container/MetricsApplication/ResourceAttributesFilter/styles.ts create mode 100644 frontend/src/container/MetricsApplication/ResourceAttributesFilter/types.ts create mode 100644 frontend/src/container/MetricsApplication/ResourceAttributesFilter/utils.ts create mode 100644 frontend/src/lib/resourceAttributes.ts create mode 100644 frontend/src/store/actions/metrics/setResourceAttributeQueries.ts create mode 100644 frontend/src/types/api/metrics/getResourceAttributes.ts diff --git a/frontend/src/api/apiV1.ts b/frontend/src/api/apiV1.ts index 22054bf229..5145443b2a 100644 --- a/frontend/src/api/apiV1.ts +++ b/frontend/src/api/apiV1.ts @@ -1,4 +1,6 @@ const apiV1 = '/api/v1/'; -export const apiV2 = '/api/alertmanager'; + +export const apiV2 = '/api/v2/'; +export const apiAlertManager = '/api/alertmanager'; export default apiV1; diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index fd8e77c84a..d7e04ddbbc 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -8,7 +8,7 @@ import { ENVIRONMENT } from 'constants/env'; import { LOCALSTORAGE } from 'constants/localStorage'; import store from 'store'; -import apiV1, { apiV2 } from './apiV1'; +import apiV1, { apiAlertManager, apiV2 } from './apiV1'; import { Logout } from './utils'; const interceptorsResponse = ( @@ -67,6 +67,10 @@ instance.interceptors.response.use(interceptorsResponse, interceptorRejected); instance.interceptors.request.use(interceptorsRequestResponse); export const AxiosAlertManagerInstance = axios.create({ + baseURL: `${ENVIRONMENT.baseURL}${apiAlertManager}`, +}); + +export const ApiV2Instance = axios.create({ baseURL: `${ENVIRONMENT.baseURL}${apiV2}`, }); diff --git a/frontend/src/api/metrics/getResourceAttributes.ts b/frontend/src/api/metrics/getResourceAttributes.ts new file mode 100644 index 0000000000..5be45af6f1 --- /dev/null +++ b/frontend/src/api/metrics/getResourceAttributes.ts @@ -0,0 +1,47 @@ +import { ApiV2Instance as axios } from 'api'; +import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; +import { AxiosError } from 'axios'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { + TagKeysPayloadProps, + TagValueProps, + TagValuesPayloadProps, +} from 'types/api/metrics/getResourceAttributes'; + +export const getResourceAttributesTagKeys = async (): Promise< + SuccessResponse | ErrorResponse +> => { + try { + const response = await axios.get( + '/metrics/autocomplete/tagKey?metricName=signoz_calls_total&match=resource_', + ); + + return { + statusCode: 200, + error: null, + message: response.data.status, + payload: response.data, + }; + } catch (error) { + return ErrorResponseHandler(error as AxiosError); + } +}; + +export const getResourceAttributesTagValues = async ( + props: TagValueProps, +): Promise | ErrorResponse> => { + try { + const response = await axios.get( + `/metrics/autocomplete/tagValue?metricName=signoz_calls_total&tagKey=${props}`, + ); + + return { + statusCode: 200, + error: null, + message: response.data.status, + payload: response.data, + }; + } catch (error) { + return ErrorResponseHandler(error as AxiosError); + } +}; diff --git a/frontend/src/api/metrics/getService.ts b/frontend/src/api/metrics/getService.ts index 29909b2905..d3bb27c741 100644 --- a/frontend/src/api/metrics/getService.ts +++ b/frontend/src/api/metrics/getService.ts @@ -8,9 +8,11 @@ const getService = async ( props: Props, ): Promise | ErrorResponse> => { try { - const response = await axios.get( - `/services?&start=${props.start}&end=${props.end}`, - ); + const response = await axios.post(`/services`, { + start: `${props.start}`, + end: `${props.end}`, + tags: props.selectedTags, + }); return { statusCode: 200, diff --git a/frontend/src/api/metrics/getServiceOverview.ts b/frontend/src/api/metrics/getServiceOverview.ts index 3ceb794e0e..ea0ddb4062 100644 --- a/frontend/src/api/metrics/getServiceOverview.ts +++ b/frontend/src/api/metrics/getServiceOverview.ts @@ -8,9 +8,13 @@ const getServiceOverview = async ( props: Props, ): Promise | ErrorResponse> => { try { - const response = await axios.get( - `/service/overview?&start=${props.start}&end=${props.end}&service=${props.service}&step=${props.step}`, - ); + const response = await axios.post(`/service/overview`, { + start: `${props.start}`, + end: `${props.end}`, + service: props.service, + step: props.step, + tags: props.selectedTags, + }); return { statusCode: 200, diff --git a/frontend/src/api/metrics/getTopEndPoints.ts b/frontend/src/api/metrics/getTopEndPoints.ts index a2973d1866..db78aae9e3 100644 --- a/frontend/src/api/metrics/getTopEndPoints.ts +++ b/frontend/src/api/metrics/getTopEndPoints.ts @@ -8,9 +8,12 @@ const getTopEndPoints = async ( props: Props, ): Promise | ErrorResponse> => { try { - const response = await axios.get( - `/service/top_endpoints?&start=${props.start}&end=${props.end}&service=${props.service}`, - ); + const response = await axios.post(`/service/top_endpoints`, { + start: `${props.start}`, + end: `${props.end}`, + service: props.service, + tags: props.selectedTags, + }); return { statusCode: 200, diff --git a/frontend/src/constants/resourceAttributes.ts b/frontend/src/constants/resourceAttributes.ts new file mode 100644 index 0000000000..4e82cef590 --- /dev/null +++ b/frontend/src/constants/resourceAttributes.ts @@ -0,0 +1,18 @@ +import { OperatorValues } from 'types/reducer/trace'; + +export const OperatorConversions: Array<{ + label: string; + metricValue: string; + traceValue: OperatorValues; +}> = [ + { + label: 'IN', + metricValue: '=~', + traceValue: 'in', + }, + { + label: 'Not IN', + metricValue: '!~', + traceValue: 'not in', + }, +]; diff --git a/frontend/src/container/ListOfDashboard/SearchFilter/index.tsx b/frontend/src/container/ListOfDashboard/SearchFilter/index.tsx index d8acdc2c42..144bbb5b4b 100644 --- a/frontend/src/container/ListOfDashboard/SearchFilter/index.tsx +++ b/frontend/src/container/ListOfDashboard/SearchFilter/index.tsx @@ -189,6 +189,7 @@ function SearchFilter({ value={selectedValues} onFocus={handleFocus} onBlur={handleBlur} + showSearch > {optionsData.options && Array.isArray(optionsData.options) && diff --git a/frontend/src/container/MetricsApplication/ResourceAttributesFilter/QueryChip.tsx b/frontend/src/container/MetricsApplication/ResourceAttributesFilter/QueryChip.tsx new file mode 100644 index 0000000000..09c5d27471 --- /dev/null +++ b/frontend/src/container/MetricsApplication/ResourceAttributesFilter/QueryChip.tsx @@ -0,0 +1,32 @@ +import { convertMetricKeyToTrace } from 'lib/resourceAttributes'; +import React from 'react'; + +import { QueryChipContainer, QueryChipItem } from './styles'; +import { IResourceAttributeQuery } from './types'; + +interface IQueryChipProps { + queryData: IResourceAttributeQuery; + onClose: (id: string) => void; + disabled: boolean; +} + +export default function QueryChip({ + queryData, + onClose, + disabled, +}: IQueryChipProps): JSX.Element { + return ( + + {convertMetricKeyToTrace(queryData.tagKey)} + {queryData.operator} + { + if (!disabled) onClose(queryData.id); + }} + > + {queryData.tagValue.join(', ')} + + + ); +} diff --git a/frontend/src/container/MetricsApplication/ResourceAttributesFilter/ResourceAttributesFilter.Machine.ts b/frontend/src/container/MetricsApplication/ResourceAttributesFilter/ResourceAttributesFilter.Machine.ts new file mode 100644 index 0000000000..3b9078f76b --- /dev/null +++ b/frontend/src/container/MetricsApplication/ResourceAttributesFilter/ResourceAttributesFilter.Machine.ts @@ -0,0 +1,61 @@ +import { createMachine } from 'xstate'; + +export const ResourceAttributesFilterMachine = + /** @xstate-layout N4IgpgJg5mDOIC5QBECGsAWAjA9qgThAAQDKYBAxhkQIIB2xAYgJYA2ALmPgHQAqqUANJgAngGIAcgFEAGr0SgADjljN2zHHQUgAHogAcAFgAM3AOz6ATAEYAzJdsA2Y4cOWAnABoQIxAFpDR2tuQ319AFYTcKdbFycAX3jvNExcAmIySmp6JjZOHn4hUTFNACFWAFd8bWVVdU1tPQQzY1MXY2tDdzNHM3dHd0NvXwR7biMTa313S0i+63DE5PRsPEJScnwqWgYiFg4uPgFhcQAlKRIpeSQQWrUNLRumx3Czbg8TR0sbS31jfUcw38fW47gBHmm4XCVms3SWIBSq3SGyyO1yBx4AHlFFxUOwcPhJLJrkoVPcGk9ENYFuF3i5YR0wtEHECEAEgiEmV8zH1DLYzHZ4Yi0utMltsrt9vluNjcfjCWVKtUbnd6o9QE1rMYBtxbGFvsZ3NrZj1WdYOfotUZLX0XEFHEKViKMpttjk9nlDrL8HiCWJzpcSbcyWrGoh3NCQj0zK53P1ph1WeFLLqnJZ2s5vmZLA6kginWsXaj3VLDoUAGqoSpgEp0cpVGohh5hhDWDy0sz8zruakzamWVm-Qyg362V5-AZOayO1KFlHitEejFHKCV6v+i5XRt1ZuU1s52zjNOOaZfdOWIY+RDZ0Hc6ZmKEXqyLPPCudit2Sz08ACSEFYNbSHI27kuquiIOEjiONwjJgrM3RWJYZisgEIJgnYPTmuEdi2OaiR5nQOAQHA2hvsiH4Sui0qFCcIGhnuLSmP0YJuJ2xjJsmKELG8XZTK0tjdHG06vgW5GupRS7St6vrKqSO4UhqVL8TBWp8o4eqdl0A5Xmy3G6gK56-B4uERDOSKiuJi6lgUAhrhUYB0buimtrEKZBDYrxaS0OZca8+ltheybOI4hivGZzrzp+VGHH+AGOQp4EIHy+ghNYnawtG4TsbYvk8QKfHGAJfQ9uF76WSW37xWBTSGJ0qXpd0vRZdEKGPqC2YeO2-zfO4+HxEAA */ + createMachine({ + tsTypes: {} as import('./ResourceAttributesFilter.Machine.typegen').Typegen0, + initial: 'Idle', + states: { + TagKey: { + on: { + NEXT: { + actions: 'onSelectOperator', + target: 'Operator', + }, + onBlur: { + actions: 'onBlurPurge', + target: 'Idle', + }, + RESET: { + target: 'Idle', + }, + }, + }, + Operator: { + on: { + NEXT: { + actions: 'onSelectTagValue', + target: 'TagValue', + }, + onBlur: { + actions: 'onBlurPurge', + target: 'Idle', + }, + RESET: { + target: 'Idle', + }, + }, + }, + TagValue: { + on: { + onBlur: { + actions: ['onValidateQuery', 'onBlurPurge'], + target: 'Idle', + }, + RESET: { + target: 'Idle', + }, + }, + }, + Idle: { + on: { + NEXT: { + actions: 'onSelectTagKey', + description: 'Select Category', + target: 'TagKey', + }, + }, + }, + }, + id: 'Dashboard Search And Filter', + }); diff --git a/frontend/src/container/MetricsApplication/ResourceAttributesFilter/ResourceAttributesFilter.Machine.typegen.ts b/frontend/src/container/MetricsApplication/ResourceAttributesFilter/ResourceAttributesFilter.Machine.typegen.ts new file mode 100644 index 0000000000..e7f7ee3de7 --- /dev/null +++ b/frontend/src/container/MetricsApplication/ResourceAttributesFilter/ResourceAttributesFilter.Machine.typegen.ts @@ -0,0 +1,32 @@ +// This file was automatically generated. Edits will be overwritten + +export interface Typegen0 { + '@@xstate/typegen': true; + eventsCausingActions: { + onSelectOperator: 'NEXT'; + onBlurPurge: 'onBlur'; + onSelectTagValue: 'NEXT'; + onValidateQuery: 'onBlur'; + onSelectTagKey: 'NEXT'; + }; + internalEvents: { + 'xstate.init': { type: 'xstate.init' }; + }; + invokeSrcNameMap: {}; + missingImplementations: { + actions: + | 'onSelectOperator' + | 'onBlurPurge' + | 'onSelectTagValue' + | 'onValidateQuery' + | 'onSelectTagKey'; + services: never; + guards: never; + delays: never; + }; + eventsCausingServices: {}; + eventsCausingGuards: {}; + eventsCausingDelays: {}; + matchesStates: 'TagKey' | 'Operator' | 'TagValue' | 'Idle'; + tags: never; +} diff --git a/frontend/src/container/MetricsApplication/ResourceAttributesFilter/index.tsx b/frontend/src/container/MetricsApplication/ResourceAttributesFilter/index.tsx new file mode 100644 index 0000000000..403b88ae4a --- /dev/null +++ b/frontend/src/container/MetricsApplication/ResourceAttributesFilter/index.tsx @@ -0,0 +1,222 @@ +import { CloseCircleFilled } from '@ant-design/icons'; +import { useMachine } from '@xstate/react'; +import { Button, Select, Spin } from 'antd'; +import ROUTES from 'constants/routes'; +import history from 'lib/history'; +import { convertMetricKeyToTrace } from 'lib/resourceAttributes'; +import { map } from 'lodash-es'; +import React, { useEffect, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { ResetInitialData } from 'store/actions/metrics/resetInitialData'; +import { SetResourceAttributeQueries } from 'store/actions/metrics/setResourceAttributeQueries'; +import { AppState } from 'store/reducers'; +import AppReducer from 'types/reducer/app'; +import MetricReducer from 'types/reducer/metrics'; +import { v4 as uuid } from 'uuid'; + +import QueryChip from './QueryChip'; +import { ResourceAttributesFilterMachine } from './ResourceAttributesFilter.Machine'; +import { QueryChipItem, SearchContainer } from './styles'; +import { IOption, IResourceAttributeQuery } from './types'; +import { createQuery, GetTagKeys, GetTagValues, OperatorSchema } from './utils'; + +function ResourceAttributesFilter(): JSX.Element | null { + const dispatch = useDispatch(); + const [disabled, setDisabled] = useState( + !(history.location.pathname === ROUTES.APPLICATION), + ); + + useEffect(() => { + const unListen = history.listen(({ pathname }) => { + setDisabled(!(pathname === ROUTES.APPLICATION)); + }); + return (): void => { + if (!history.location.pathname.startsWith(`${ROUTES.APPLICATION}/`)) { + dispatch(ResetInitialData()); + } + unListen(); + }; + }, [dispatch]); + + const { isDarkMode } = useSelector((state) => state.app); + const { resourceAttributeQueries } = useSelector( + (state) => state.metrics, + ); + const [loading, setLoading] = useState(true); + const [selectedValues, setSelectedValues] = useState([]); + const [staging, setStaging] = useState([]); + const [queries, setQueries] = useState([]); + const [optionsData, setOptionsData] = useState<{ + mode: undefined | 'tags' | 'multiple'; + options: IOption[]; + }>({ + mode: undefined, + options: [], + }); + + const dispatchQueries = (updatedQueries: IResourceAttributeQuery[]): void => { + dispatch(SetResourceAttributeQueries(updatedQueries)); + }; + const handleLoading = (isLoading: boolean): void => { + setLoading(isLoading); + if (isLoading) { + setOptionsData({ mode: undefined, options: [] }); + } + }; + const [state, send] = useMachine(ResourceAttributesFilterMachine, { + actions: { + onSelectTagKey: () => { + handleLoading(true); + GetTagKeys() + .then((tagKeys) => setOptionsData({ options: tagKeys, mode: undefined })) + .finally(() => { + handleLoading(false); + }); + }, + onSelectOperator: () => { + setOptionsData({ options: OperatorSchema, mode: undefined }); + }, + onSelectTagValue: () => { + handleLoading(true); + + GetTagValues(staging[0]) + .then((tagValuesOptions) => + setOptionsData({ options: tagValuesOptions, mode: 'multiple' }), + ) + .finally(() => { + handleLoading(false); + }); + }, + onBlurPurge: () => { + setSelectedValues([]); + setStaging([]); + }, + onValidateQuery: (): void => { + if (staging.length < 2 || selectedValues.length === 0) { + return; + } + + const generatedQuery = createQuery([...staging, selectedValues]); + if (generatedQuery) { + dispatchQueries([...queries, generatedQuery]); + } + }, + }, + }); + + useEffect(() => { + setQueries(resourceAttributeQueries); + }, [resourceAttributeQueries]); + + const handleFocus = (): void => { + if (state.value === 'Idle') { + send('NEXT'); + } + }; + + const handleBlur = (): void => { + send('onBlur'); + }; + const handleChange = (value: never): void => { + if (!optionsData.mode) { + setStaging((prevStaging) => [...prevStaging, value]); + setSelectedValues([]); + send('NEXT'); + return; + } + + setSelectedValues([...value]); + }; + + const handleClose = (id: string): void => { + dispatchQueries(queries.filter((queryData) => queryData.id !== id)); + }; + + const handleClearAll = (): void => { + send('RESET'); + dispatchQueries([]); + setStaging([]); + setSelectedValues([]); + }; + const disabledAndEmpty = !!( + !queries.length && + !staging.length && + !selectedValues.length && + disabled + ); + const disabledOrEmpty = !!( + queries.length || + staging.length || + selectedValues.length || + disabled + ); + + if (disabledAndEmpty) { + return null; + } + + return ( + +
+ {map( + queries, + (query): JSX.Element => { + return ( + + ); + }, + )} + {map(staging, (item, idx) => { + return ( + + {idx === 0 ? convertMetricKeyToTrace(item) : item} + + ); + })} +
+ {!disabled && ( +