From d779b83715df9de296dc5de65daf0f7d3a152811 Mon Sep 17 00:00:00 2001 From: Vishal Sharma Date: Wed, 8 Feb 2023 12:41:55 +0530 Subject: [PATCH] feat: navigate to trace from metrics (#2191) * feat: navigate to trace from metrics * chore: add sonar back * chore: refactor --- .../MetricsPageQueries/DBCallQueries.ts | 2 +- .../MetricsApplication/Tabs/DBCall.tsx | 66 +++++++++++- .../MetricsApplication/Tabs/External.tsx | 98 ++++++++++++++++- .../MetricsApplication/Tabs/Overview.tsx | 102 +++++++----------- .../container/MetricsApplication/Tabs/util.ts | 75 +++++++++++++ frontend/src/lib/resourceAttributes.ts | 10 +- frontend/src/types/reducer/trace.ts | 7 -- 7 files changed, 274 insertions(+), 86 deletions(-) create mode 100644 frontend/src/container/MetricsApplication/Tabs/util.ts diff --git a/frontend/src/container/MetricsApplication/MetricsPageQueries/DBCallQueries.ts b/frontend/src/container/MetricsApplication/MetricsPageQueries/DBCallQueries.ts index d657fff52e..0b0f9ad4c8 100644 --- a/frontend/src/container/MetricsApplication/MetricsPageQueries/DBCallQueries.ts +++ b/frontend/src/container/MetricsApplication/MetricsPageQueries/DBCallQueries.ts @@ -47,7 +47,7 @@ export const databaseCallsAvgDuration = ({ const metricNameA = 'signoz_db_latency_sum'; const metricNameB = 'signoz_db_latency_count'; const expression = 'A/B'; - const legendFormula = ''; + const legendFormula = 'Average Duration'; const legend = ''; const disabled = true; const additionalItemsA = [ diff --git a/frontend/src/container/MetricsApplication/Tabs/DBCall.tsx b/frontend/src/container/MetricsApplication/Tabs/DBCall.tsx index 9f7f32e2c3..bef9779f54 100644 --- a/frontend/src/container/MetricsApplication/Tabs/DBCall.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/DBCall.tsx @@ -4,8 +4,11 @@ import { databaseCallsAvgDuration, databaseCallsRPS, } from 'container/MetricsApplication/MetricsPageQueries/DBCallQueries'; -import { resourceAttributesToTagFilterItems } from 'lib/resourceAttributes'; -import React, { useMemo } from 'react'; +import { + convertRawQueriesToTraceSelectedTags, + resourceAttributesToTagFilterItems, +} from 'lib/resourceAttributes'; +import React, { useMemo, useState } from 'react'; import { useSelector } from 'react-redux'; import { useParams } from 'react-router-dom'; import { AppState } from 'store/reducers'; @@ -13,9 +16,16 @@ import { Widgets } from 'types/api/dashboard/getAll'; import MetricReducer from 'types/reducer/metrics'; import { Card, GraphContainer, GraphTitle, Row } from '../styles'; +import { Button } from './styles'; +import { + dbSystemTags, + onGraphClickHandler, + onViewTracePopupClick, +} from './util'; function DBCall({ getWidgetQueryBuilder }: DBCallProps): JSX.Element { const { servicename } = useParams<{ servicename?: string }>(); + const [selectedTimeStamp, setSelectedTimeStamp] = useState(0); const { resourceAttributeQueries } = useSelector( (state) => state.metrics, ); @@ -23,6 +33,15 @@ function DBCall({ getWidgetQueryBuilder }: DBCallProps): JSX.Element { () => resourceAttributesToTagFilterItems(resourceAttributeQueries) || [], [resourceAttributeQueries], ); + const selectedTraceTags: string = useMemo( + () => + JSON.stringify( + convertRawQueriesToTraceSelectedTags(resourceAttributeQueries).concat( + ...dbSystemTags, + ) || [], + ), + [resourceAttributeQueries], + ); const legend = '{{db_system}}'; const databaseCallsRPSWidget = useMemo( @@ -39,7 +58,6 @@ function DBCall({ getWidgetQueryBuilder }: DBCallProps): JSX.Element { }), [getWidgetQueryBuilder, servicename, tagFilterItems], ); - const databaseCallsAverageDurationWidget = useMemo( () => getWidgetQueryBuilder({ @@ -57,6 +75,18 @@ function DBCall({ getWidgetQueryBuilder }: DBCallProps): JSX.Element { return ( + Database Calls RPS @@ -65,12 +95,33 @@ function DBCall({ getWidgetQueryBuilder }: DBCallProps): JSX.Element { fullViewOptions={false} widget={databaseCallsRPSWidget} yAxisUnit="reqps" + onClickHandler={(ChartEvent, activeElements, chart, data): void => { + onGraphClickHandler(setSelectedTimeStamp)( + ChartEvent, + activeElements, + chart, + data, + 'database_call_rps', + ); + }} /> + Database Calls Avg Duration @@ -79,6 +130,15 @@ function DBCall({ getWidgetQueryBuilder }: DBCallProps): JSX.Element { fullViewOptions={false} widget={databaseCallsAverageDurationWidget} yAxisUnit="ms" + onClickHandler={(ChartEvent, activeElements, chart, data): void => { + onGraphClickHandler(setSelectedTimeStamp)( + ChartEvent, + activeElements, + chart, + data, + 'database_call_avg_duration', + ); + }} /> diff --git a/frontend/src/container/MetricsApplication/Tabs/External.tsx b/frontend/src/container/MetricsApplication/Tabs/External.tsx index a0bd62bb29..e06cd1ae15 100644 --- a/frontend/src/container/MetricsApplication/Tabs/External.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/External.tsx @@ -6,8 +6,11 @@ import { externalCallErrorPercent, externalCallRpsByAddress, } from 'container/MetricsApplication/MetricsPageQueries/ExternalQueries'; -import { resourceAttributesToTagFilterItems } from 'lib/resourceAttributes'; -import React, { useMemo } from 'react'; +import { + convertRawQueriesToTraceSelectedTags, + resourceAttributesToTagFilterItems, +} from 'lib/resourceAttributes'; +import React, { useMemo, useState } from 'react'; import { useSelector } from 'react-redux'; import { useParams } from 'react-router-dom'; import { AppState } from 'store/reducers'; @@ -15,18 +18,25 @@ import { Widgets } from 'types/api/dashboard/getAll'; import MetricReducer from 'types/reducer/metrics'; import { Card, GraphContainer, GraphTitle, Row } from '../styles'; +import { Button } from './styles'; +import { onGraphClickHandler, onViewTracePopupClick } from './util'; function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element { const { servicename } = useParams<{ servicename?: string }>(); const { resourceAttributeQueries } = useSelector( (state) => state.metrics, ); + const [selectedTimeStamp, setSelectedTimeStamp] = useState(0); const tagFilterItems = useMemo( () => resourceAttributesToTagFilterItems(resourceAttributeQueries) || [], [resourceAttributeQueries], ); + const selectedTraceTags: string = JSON.stringify( + convertRawQueriesToTraceSelectedTags(resourceAttributeQueries) || [], + ); + const legend = '{{address}}'; const externalCallErrorWidget = useMemo( @@ -92,6 +102,18 @@ function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element { <> + External Call Error Percentage @@ -100,12 +122,33 @@ function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element { fullViewOptions={false} widget={externalCallErrorWidget} yAxisUnit="%" + onClickHandler={(ChartEvent, activeElements, chart, data): void => { + onGraphClickHandler(setSelectedTimeStamp)( + ChartEvent, + activeElements, + chart, + data, + 'external_call_error_percentage', + ); + }} /> + External Call duration @@ -114,6 +157,15 @@ function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element { fullViewOptions={false} widget={externalCallDurationWidget} yAxisUnit="ms" + onClickHandler={(ChartEvent, activeElements, chart, data): void => { + onGraphClickHandler(setSelectedTimeStamp)( + ChartEvent, + activeElements, + chart, + data, + 'external_call_duration', + ); + }} /> @@ -122,6 +174,18 @@ function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element { + External Call RPS(by Address) @@ -130,12 +194,33 @@ function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element { fullViewOptions={false} widget={externalCallRPSWidget} yAxisUnit="reqps" + onClickHandler={(ChartEvent, activeElements, chart, data): void => { + onGraphClickHandler(setSelectedTimeStamp)( + ChartEvent, + activeElements, + chart, + data, + 'external_call_rps_by_address', + ); + }} /> + External Call duration(by Address) @@ -144,6 +229,15 @@ function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element { fullViewOptions={false} widget={externalCallDurationAddressWidget} yAxisUnit="ms" + onClickHandler={(ChartEvent, activeElements, chart, data): void => { + onGraphClickHandler(setSelectedTimeStamp)( + ChartEvent, + activeElements, + chart, + data, + 'external_call_duration_by_address', + ); + }} /> diff --git a/frontend/src/container/MetricsApplication/Tabs/Overview.tsx b/frontend/src/container/MetricsApplication/Tabs/Overview.tsx index 1b9a3b972c..d51dae088a 100644 --- a/frontend/src/container/MetricsApplication/Tabs/Overview.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/Overview.tsx @@ -1,4 +1,3 @@ -import { ActiveElement, Chart, ChartData, ChartEvent } from 'chart.js'; import Graph from 'components/Graph'; import { METRICS_PAGE_QUERY_PARAM } from 'constants/query'; import ROUTES from 'constants/routes'; @@ -10,7 +9,7 @@ import { convertRawQueriesToTraceSelectedTags, resourceAttributesToTagFilterItems, } from 'lib/resourceAttributes'; -import React, { useCallback, useMemo, useRef } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useParams } from 'react-router-dom'; import { UpdateTimeInterval } from 'store/actions'; @@ -25,10 +24,11 @@ import { import { Card, Col, GraphContainer, GraphTitle, Row } from '../styles'; import TopOperationsTable from '../TopOperationsTable'; import { Button } from './styles'; +import { onGraphClickHandler, onViewTracePopupClick } from './util'; function Application({ getWidgetQueryBuilder }: DashboardProps): JSX.Element { const { servicename } = useParams<{ servicename?: string }>(); - const selectedTimeStamp = useRef(0); + const [selectedTimeStamp, setSelectedTimeStamp] = useState(0); const dispatch = useDispatch(); const { @@ -39,7 +39,7 @@ function Application({ getWidgetQueryBuilder }: DashboardProps): JSX.Element { } = useSelector((state) => state.metrics); const selectedTraceTags: string = JSON.stringify( - convertRawQueriesToTraceSelectedTags(resourceAttributeQueries, 'array') || [], + convertRawQueriesToTraceSelectedTags(resourceAttributeQueries) || [], ); const tagFilterItems = useMemo( @@ -77,58 +77,6 @@ function Application({ getWidgetQueryBuilder }: DashboardProps): JSX.Element { [servicename, topLevelOperations, tagFilterItems, getWidgetQueryBuilder], ); - const onTracePopupClick = (timestamp: number): void => { - const currentTime = timestamp; - const tPlusOne = timestamp + 1 * 60 * 1000; - - const urlParams = new URLSearchParams(); - urlParams.set(METRICS_PAGE_QUERY_PARAM.startTime, currentTime.toString()); - urlParams.set(METRICS_PAGE_QUERY_PARAM.endTime, tPlusOne.toString()); - - history.replace( - `${ - ROUTES.TRACE - }?${urlParams.toString()}&selected={"serviceName":["${servicename}"]}&filterToFetchData=["duration","status","serviceName"]&spanAggregateCurrentPage=1&selectedTags=${selectedTraceTags}&&isFilterExclude={"serviceName":false}&userSelectedFilter={"status":["error","ok"],"serviceName":["${servicename}"]}&spanAggregateCurrentPage=1`, - ); - }; - - const onClickHandler = async ( - event: ChartEvent, - elements: ActiveElement[], - chart: Chart, - data: ChartData, - from: string, - ): Promise => { - if (event.native) { - const points = chart.getElementsAtEventForMode( - event.native, - 'nearest', - { intersect: true }, - true, - ); - - const id = `${from}_button`; - const buttonElement = document.getElementById(id); - - if (points.length !== 0) { - const firstPoint = points[0]; - - if (data.labels) { - const time = data?.labels[firstPoint.index] as Date; - - if (buttonElement) { - buttonElement.style.display = 'block'; - buttonElement.style.left = `${firstPoint.element.x}px`; - buttonElement.style.top = `${firstPoint.element.y}px`; - selectedTimeStamp.current = time.getTime(); - } - } - } else if (buttonElement && buttonElement.style.display === 'block') { - buttonElement.style.display = 'none'; - } - } - }; - const onDragSelect = useCallback( (start: number, end: number) => { const startTimestamp = Math.trunc(start); @@ -162,9 +110,11 @@ function Application({ getWidgetQueryBuilder }: DashboardProps): JSX.Element { type="default" size="small" id="Service_button" - onClick={(): void => { - onTracePopupClick(selectedTimeStamp.current); - }} + onClick={onViewTracePopupClick( + servicename, + selectedTraceTags, + selectedTimeStamp, + )} > View Traces @@ -173,7 +123,13 @@ function Application({ getWidgetQueryBuilder }: DashboardProps): JSX.Element { { - onClickHandler(ChartEvent, activeElements, chart, data, 'Service'); + onGraphClickHandler(setSelectedTimeStamp)( + ChartEvent, + activeElements, + chart, + data, + 'Service', + ); }} name="service_latency" type="line" @@ -230,9 +186,11 @@ function Application({ getWidgetQueryBuilder }: DashboardProps): JSX.Element { type="default" size="small" id="Rate_button" - onClick={(): void => { - onTracePopupClick(selectedTimeStamp.current); - }} + onClick={onViewTracePopupClick( + servicename, + selectedTraceTags, + selectedTimeStamp, + )} > View Traces @@ -243,7 +201,13 @@ function Application({ getWidgetQueryBuilder }: DashboardProps): JSX.Element { name="operations_per_sec" fullViewOptions={false} onClickHandler={(event, element, chart, data): void => { - onClickHandler(event, element, chart, data, 'Rate'); + onGraphClickHandler(setSelectedTimeStamp)( + event, + element, + chart, + data, + 'Rate', + ); }} widget={operationPerSecWidget} yAxisUnit="ops" @@ -260,7 +224,7 @@ function Application({ getWidgetQueryBuilder }: DashboardProps): JSX.Element { size="small" id="Error_button" onClick={(): void => { - onErrorTrackHandler(selectedTimeStamp.current); + onErrorTrackHandler(selectedTimeStamp); }} > View Traces @@ -273,7 +237,13 @@ function Application({ getWidgetQueryBuilder }: DashboardProps): JSX.Element { name="error_percentage_%" fullViewOptions={false} onClickHandler={(ChartEvent, activeElements, chart, data): void => { - onClickHandler(ChartEvent, activeElements, chart, data, 'Error'); + onGraphClickHandler(setSelectedTimeStamp)( + ChartEvent, + activeElements, + chart, + data, + 'Error', + ); }} widget={errorPercentageWidget} yAxisUnit="%" diff --git a/frontend/src/container/MetricsApplication/Tabs/util.ts b/frontend/src/container/MetricsApplication/Tabs/util.ts new file mode 100644 index 0000000000..376d2b4770 --- /dev/null +++ b/frontend/src/container/MetricsApplication/Tabs/util.ts @@ -0,0 +1,75 @@ +import { ActiveElement, Chart, ChartData, ChartEvent } from 'chart.js'; +import { METRICS_PAGE_QUERY_PARAM } from 'constants/query'; +import ROUTES from 'constants/routes'; +import history from 'lib/history'; +import { Tags } from 'types/reducer/trace'; + +export const dbSystemTags: Tags[] = [ + { + Key: ['db.system.(string)'], + StringValues: [''], + NumberValues: [], + BoolValues: [], + Operator: 'Exists', + }, +]; + +export function onViewTracePopupClick( + servicename: string | undefined, + selectedTraceTags: string, + timestamp: number, +): VoidFunction { + return (): void => { + const currentTime = timestamp; + const tPlusOne = timestamp + 1 * 60 * 1000; + + const urlParams = new URLSearchParams(); + urlParams.set(METRICS_PAGE_QUERY_PARAM.startTime, currentTime.toString()); + urlParams.set(METRICS_PAGE_QUERY_PARAM.endTime, tPlusOne.toString()); + + history.replace( + `${ + ROUTES.TRACE + }?${urlParams.toString()}&selected={"serviceName":["${servicename}"]}&filterToFetchData=["duration","status","serviceName"]&spanAggregateCurrentPage=1&selectedTags=${selectedTraceTags}&&isFilterExclude={"serviceName":false}&userSelectedFilter={"status":["error","ok"],"serviceName":["${servicename}"]}&spanAggregateCurrentPage=1`, + ); + }; +} + +export function onGraphClickHandler( + setSelectedTimeStamp: React.Dispatch>, +) { + return async ( + event: ChartEvent, + elements: ActiveElement[], + chart: Chart, + data: ChartData, + from: string, + ): Promise => { + if (event.native) { + const points = chart.getElementsAtEventForMode( + event.native, + 'nearest', + { intersect: true }, + true, + ); + const id = `${from}_button`; + const buttonElement = document.getElementById(id); + + if (points.length !== 0) { + const firstPoint = points[0]; + + if (data.labels) { + const time = data?.labels[firstPoint.index] as Date; + if (buttonElement) { + buttonElement.style.display = 'block'; + buttonElement.style.left = `${firstPoint.element.x}px`; + buttonElement.style.top = `${firstPoint.element.y}px`; + setSelectedTimeStamp(time.getTime()); + } + } + } else if (buttonElement && buttonElement.style.display === 'block') { + buttonElement.style.display = 'none'; + } + } + }; +} diff --git a/frontend/src/lib/resourceAttributes.ts b/frontend/src/lib/resourceAttributes.ts index 33b8fd5323..1d21799ad9 100644 --- a/frontend/src/lib/resourceAttributes.ts +++ b/frontend/src/lib/resourceAttributes.ts @@ -1,7 +1,7 @@ import { OperatorConversions } from 'constants/resourceAttributes'; import { IResourceAttributeQuery } from 'container/MetricsApplication/ResourceAttributesFilter/types'; import { IQueryBuilderTagFilterItems } from 'types/api/dashboard/getAll'; -import { OperatorValues, Tags, TagsAPI } from 'types/reducer/trace'; +import { OperatorValues, Tags } from 'types/reducer/trace'; /** * resource_x_y -> x.y @@ -35,13 +35,9 @@ export const convertOperatorLabelToTraceOperator = ( export const convertRawQueriesToTraceSelectedTags = ( queries: IResourceAttributeQuery[], - keyType: 'string' | 'array' = 'string', -): Tags[] | TagsAPI[] => +): Tags[] => queries.map((query) => ({ - Key: - keyType === 'array' - ? [convertMetricKeyToTrace(query.tagKey)] - : (convertMetricKeyToTrace(query.tagKey) as never), + Key: [convertMetricKeyToTrace(query.tagKey)], Operator: convertOperatorLabelToTraceOperator(query.operator), StringValues: query.tagValue, NumberValues: [], diff --git a/frontend/src/types/reducer/trace.ts b/frontend/src/types/reducer/trace.ts index ac27a156e7..fda615160c 100644 --- a/frontend/src/types/reducer/trace.ts +++ b/frontend/src/types/reducer/trace.ts @@ -54,13 +54,6 @@ export interface Tags { BoolValues: boolean[]; } -export interface TagsAPI { - Key: string; - Operator: OperatorValues; - StringValues: string[]; - NumberValues: number[]; - BoolValues: boolean[]; -} export type OperatorValues = | 'NotIn' | 'In'