From 1b9683d699ea4ed7bdadf880c9912df9dc22422d Mon Sep 17 00:00:00 2001 From: Vikrant Gupta Date: Fri, 16 Aug 2024 15:07:06 +0530 Subject: [PATCH] feat: client changes for query stats (#5706) * feat: changes for the query stats websockets * chore: remove unwanted files * fix: work on random id rather than hash * fix: improve the icons and design * feat: webpack and docker file changes * fix: test cases * chore: format the units * chore: address review comments * chore: update the id to uuid package * fix: build issues * chore: remove docker file changes * chore: remove docker file changes --- frontend/public/Icons/solid-x-circle.svg | 1 + frontend/src/api/common/getQueryStats.ts | 32 ++++++++++ frontend/src/api/metrics/getQueryRange.ts | 10 +++- frontend/src/constants/env.ts | 1 + .../LogsExplorerViews.styles.scss | 39 ++++++++++++ .../LogsExplorerViews/QueryStatus.styles.scss | 4 ++ .../LogsExplorerViews/QueryStatus.tsx | 42 +++++++++++++ .../src/container/LogsExplorerViews/index.tsx | 60 ++++++++++++++++++- .../tests/LogsExplorerViews.test.tsx | 2 + .../queryBuilder/useGetExplorerQueryRange.ts | 2 + .../hooks/queryBuilder/useGetQueryRange.ts | 4 +- frontend/src/lib/dashboard/getQueryResults.ts | 2 + frontend/src/typings/environment.ts | 1 + frontend/webpack.config.js | 1 + frontend/webpack.config.prod.js | 1 + 15 files changed, 197 insertions(+), 5 deletions(-) create mode 100644 frontend/public/Icons/solid-x-circle.svg create mode 100644 frontend/src/api/common/getQueryStats.ts create mode 100644 frontend/src/container/LogsExplorerViews/QueryStatus.styles.scss create mode 100644 frontend/src/container/LogsExplorerViews/QueryStatus.tsx diff --git a/frontend/public/Icons/solid-x-circle.svg b/frontend/public/Icons/solid-x-circle.svg new file mode 100644 index 000000000000..3f189e386589 --- /dev/null +++ b/frontend/public/Icons/solid-x-circle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/api/common/getQueryStats.ts b/frontend/src/api/common/getQueryStats.ts new file mode 100644 index 000000000000..98f25c648634 --- /dev/null +++ b/frontend/src/api/common/getQueryStats.ts @@ -0,0 +1,32 @@ +import getLocalStorageApi from 'api/browser/localstorage/get'; +import { ENVIRONMENT } from 'constants/env'; +import { LOCALSTORAGE } from 'constants/localStorage'; + +export interface WsDataEvent { + read_rows: number; + read_bytes: number; + elapsed_ms: number; +} +interface GetQueryStatsProps { + queryId: string; + setData: React.Dispatch>; +} + +export function getQueryStats(props: GetQueryStatsProps): void { + const { queryId, setData } = props; + + const token = getLocalStorageApi(LOCALSTORAGE.AUTH_TOKEN) || ''; + const socket = new WebSocket( + `${ENVIRONMENT.wsURL}api/v3/query_progress?q=${queryId}`, + token, + ); + + socket.addEventListener('message', (event) => { + try { + const parsedData = JSON.parse(event?.data); + setData(parsedData); + } catch { + setData(event?.data); + } + }); +} diff --git a/frontend/src/api/metrics/getQueryRange.ts b/frontend/src/api/metrics/getQueryRange.ts index 40deb021bc43..631372478dff 100644 --- a/frontend/src/api/metrics/getQueryRange.ts +++ b/frontend/src/api/metrics/getQueryRange.ts @@ -12,10 +12,13 @@ export const getMetricsQueryRange = async ( props: QueryRangePayload, version: string, signal: AbortSignal, + headers?: Record, ): Promise | ErrorResponse> => { try { if (version && version === ENTITY_VERSION_V4) { - const response = await ApiV4Instance.post('/query_range', props, { signal }); + const response = await ApiV4Instance.post('/query_range', props, { + signal, + }); return { statusCode: 200, @@ -26,7 +29,10 @@ export const getMetricsQueryRange = async ( }; } - const response = await ApiV3Instance.post('/query_range', props, { signal }); + const response = await ApiV3Instance.post('/query_range', props, { + signal, + headers, + }); return { statusCode: 200, diff --git a/frontend/src/constants/env.ts b/frontend/src/constants/env.ts index 2c5230dfcc48..adeb98b4a07e 100644 --- a/frontend/src/constants/env.ts +++ b/frontend/src/constants/env.ts @@ -3,4 +3,5 @@ export const ENVIRONMENT = { process?.env?.FRONTEND_API_ENDPOINT || process?.env?.GITPOD_WORKSPACE_URL?.replace('://', '://8080-') || '', + wsURL: process?.env?.WEBSOCKET_API_ENDPOINT || 'ws://localhost:8080/', }; diff --git a/frontend/src/container/LogsExplorerViews/LogsExplorerViews.styles.scss b/frontend/src/container/LogsExplorerViews/LogsExplorerViews.styles.scss index a6142d195c0b..3821e7025c8d 100644 --- a/frontend/src/container/LogsExplorerViews/LogsExplorerViews.styles.scss +++ b/frontend/src/container/LogsExplorerViews/LogsExplorerViews.styles.scss @@ -80,6 +80,36 @@ position: relative; } } + .query-stats { + display: flex; + align-items: center; + gap: 12px; + .rows { + color: var(--bg-vanilla-400); + font-family: 'Geist Mono'; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 18px; /* 150% */ + letter-spacing: 0.36px; + } + + .divider { + width: 1px; + height: 14px; + background: #242834; + } + + .time { + color: var(--bg-vanilla-400); + font-family: 'Geist Mono'; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 18px; /* 150% */ + letter-spacing: 0.36px; + } + } } .logs-actions-container { @@ -149,6 +179,15 @@ background: var(--bg-robin-400); } } + .query-stats { + .rows { + color: var(--bg-ink-400); + } + + .time { + color: var(--bg-ink-400); + } + } } } } diff --git a/frontend/src/container/LogsExplorerViews/QueryStatus.styles.scss b/frontend/src/container/LogsExplorerViews/QueryStatus.styles.scss new file mode 100644 index 000000000000..d6f9579113c2 --- /dev/null +++ b/frontend/src/container/LogsExplorerViews/QueryStatus.styles.scss @@ -0,0 +1,4 @@ +.query-status { + display: flex; + align-items: center; +} diff --git a/frontend/src/container/LogsExplorerViews/QueryStatus.tsx b/frontend/src/container/LogsExplorerViews/QueryStatus.tsx new file mode 100644 index 000000000000..628b3d5fc1ca --- /dev/null +++ b/frontend/src/container/LogsExplorerViews/QueryStatus.tsx @@ -0,0 +1,42 @@ +import './QueryStatus.styles.scss'; + +import { LoadingOutlined } from '@ant-design/icons'; +import { Color } from '@signozhq/design-tokens'; +import { Spin } from 'antd'; +import { CircleCheck } from 'lucide-react'; +import React, { useMemo } from 'react'; + +interface IQueryStatusProps { + loading: boolean; + error: boolean; + success: boolean; +} + +export default function QueryStatus( + props: IQueryStatusProps, +): React.ReactElement { + const { loading, error, success } = props; + + const content = useMemo((): React.ReactElement => { + if (loading) { + return } />; + } + if (error) { + return ( + header + ); + } + if (success) { + return ( + + ); + } + return
; + }, [error, loading, success]); + return
{content}
; +} diff --git a/frontend/src/container/LogsExplorerViews/index.tsx b/frontend/src/container/LogsExplorerViews/index.tsx index c7214ab2600c..35aa3f6e3374 100644 --- a/frontend/src/container/LogsExplorerViews/index.tsx +++ b/frontend/src/container/LogsExplorerViews/index.tsx @@ -1,8 +1,10 @@ /* eslint-disable sonarjs/cognitive-complexity */ import './LogsExplorerViews.styles.scss'; -import { Button } from 'antd'; +import { Button, Typography } from 'antd'; +import { getQueryStats, WsDataEvent } from 'api/common/getQueryStats'; import logEvent from 'api/common/logEvent'; +import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig'; import LogsFormatOptionsMenu from 'components/LogsFormatOptionsMenu/LogsFormatOptionsMenu'; import { DEFAULT_ENTITY_VERSION } from 'constants/app'; import { LOCALSTORAGE } from 'constants/localStorage'; @@ -77,6 +79,8 @@ import { GlobalReducer } from 'types/reducer/globalTime'; import { generateExportToDashboardLink } from 'utils/dashboard/generateExportToDashboardLink'; import { v4 } from 'uuid'; +import QueryStatus from './QueryStatus'; + function LogsExplorerViews({ selectedView, showFrequencyChart, @@ -130,6 +134,8 @@ function LogsExplorerViews({ const [logs, setLogs] = useState([]); const [requestData, setRequestData] = useState(null); const [showFormatMenuItems, setShowFormatMenuItems] = useState(false); + const [queryId, setQueryId] = useState(v4()); + const [queryStats, setQueryStats] = useState(); const handleAxisError = useAxiosError(); @@ -233,7 +239,13 @@ function LogsExplorerViews({ chartQueryKeyRef, ); - const { data, isLoading, isFetching, isError } = useGetExplorerQueryRange( + const { + data, + isLoading, + isFetching, + isError, + isSuccess, + } = useGetExplorerQueryRange( requestData, panelType, DEFAULT_ENTITY_VERSION, @@ -251,6 +263,9 @@ function LogsExplorerViews({ }, undefined, listQueryKeyRef, + { + ...(!isEmpty(queryId) && { 'X-SIGNOZ-QUERY-ID': queryId }), + }, ); const getRequestData = useCallback( @@ -337,6 +352,23 @@ function LogsExplorerViews({ ], ); + useEffect(() => { + setQueryId(v4()); + }, [isError, isSuccess]); + + useEffect(() => { + if ( + !isEmpty(queryId) && + (isLoading || isFetching) && + selectedPanelType !== PANEL_TYPES.LIST + ) { + setQueryStats(undefined); + setTimeout(() => { + getQueryStats({ queryId, setData: setQueryStats }); + }, 500); + } + }, [queryId, isLoading, isFetching, selectedPanelType]); + const logEventCalledRef = useRef(false); useEffect(() => { if (!logEventCalledRef.current && !isUndefined(data?.payload)) { @@ -703,6 +735,30 @@ function LogsExplorerViews({
)} + {(selectedPanelType === PANEL_TYPES.TIME_SERIES || + selectedPanelType === PANEL_TYPES.TABLE) && ( +
+ + {queryStats?.read_rows && ( + + {getYAxisFormattedValue(queryStats.read_rows?.toString(), 'short')}{' '} + rows + + )} + {queryStats?.elapsed_ms && ( + <> +
+ + {getYAxisFormattedValue(queryStats?.elapsed_ms?.toString(), 'ms')} + + + )} +
+ )}
diff --git a/frontend/src/container/LogsExplorerViews/tests/LogsExplorerViews.test.tsx b/frontend/src/container/LogsExplorerViews/tests/LogsExplorerViews.test.tsx index 7cd58316e875..67c3fcfe2c14 100644 --- a/frontend/src/container/LogsExplorerViews/tests/LogsExplorerViews.test.tsx +++ b/frontend/src/container/LogsExplorerViews/tests/LogsExplorerViews.test.tsx @@ -46,6 +46,8 @@ jest.mock( }, ); +jest.mock('api/common/getQueryStats', () => jest.fn()); + jest.mock('constants/panelTypes', () => ({ AVAILABLE_EXPORT_PANEL_TYPES: ['graph', 'table'], })); diff --git a/frontend/src/hooks/queryBuilder/useGetExplorerQueryRange.ts b/frontend/src/hooks/queryBuilder/useGetExplorerQueryRange.ts index 04b9deac1635..13093ab254c8 100644 --- a/frontend/src/hooks/queryBuilder/useGetExplorerQueryRange.ts +++ b/frontend/src/hooks/queryBuilder/useGetExplorerQueryRange.ts @@ -20,6 +20,7 @@ export const useGetExplorerQueryRange = ( params?: Record, isDependentOnQB = true, keyRef?: MutableRefObject, + headers?: Record, ): UseQueryResult, Error> => { const { isEnabledQuery } = useQueryBuilder(); const { selectedTime: globalSelectedInterval, minTime, maxTime } = useSelector< @@ -61,5 +62,6 @@ export const useGetExplorerQueryRange = ( queryKey: [key, globalSelectedInterval, requestData, minTime, maxTime], enabled: isEnabled, }, + headers, ); }; diff --git a/frontend/src/hooks/queryBuilder/useGetQueryRange.ts b/frontend/src/hooks/queryBuilder/useGetQueryRange.ts index 334ee7f6289c..e0c6ea31722c 100644 --- a/frontend/src/hooks/queryBuilder/useGetQueryRange.ts +++ b/frontend/src/hooks/queryBuilder/useGetQueryRange.ts @@ -13,12 +13,14 @@ type UseGetQueryRange = ( requestData: GetQueryResultsProps, version: string, options?: UseQueryOptions, Error>, + headers?: Record, ) => UseQueryResult, Error>; export const useGetQueryRange: UseGetQueryRange = ( requestData, version, options, + headers, ) => { const newRequestData: GetQueryResultsProps = useMemo( () => ({ @@ -45,7 +47,7 @@ export const useGetQueryRange: UseGetQueryRange = ( return useQuery, Error>({ queryFn: async ({ signal }) => - GetMetricQueryRange(requestData, version, signal), + GetMetricQueryRange(requestData, version, signal, headers), ...options, queryKey, }); diff --git a/frontend/src/lib/dashboard/getQueryResults.ts b/frontend/src/lib/dashboard/getQueryResults.ts index 50e66489240a..03ad5f8caa41 100644 --- a/frontend/src/lib/dashboard/getQueryResults.ts +++ b/frontend/src/lib/dashboard/getQueryResults.ts @@ -23,6 +23,7 @@ export async function GetMetricQueryRange( props: GetQueryResultsProps, version: string, signal?: AbortSignal, + headers?: Record, ): Promise> { const { legendMap, queryPayload } = prepareQueryRangePayload(props); @@ -30,6 +31,7 @@ export async function GetMetricQueryRange( queryPayload, version || 'v3', signal, + headers, ); if (response.statusCode >= 400) { diff --git a/frontend/src/typings/environment.ts b/frontend/src/typings/environment.ts index c77832ce4380..58d01f2f6929 100644 --- a/frontend/src/typings/environment.ts +++ b/frontend/src/typings/environment.ts @@ -3,6 +3,7 @@ declare global { namespace NodeJS { interface ProcessEnv { FRONTEND_API_ENDPOINT: string | undefined; + WEBSOCKET_API_ENDPOINT: string | undefined; } } } diff --git a/frontend/webpack.config.js b/frontend/webpack.config.js index 2e5c0d0f4eb6..8fc1188448db 100644 --- a/frontend/webpack.config.js +++ b/frontend/webpack.config.js @@ -38,6 +38,7 @@ const plugins = [ 'process.env': JSON.stringify({ NODE_ENV: process.env.NODE_ENV, FRONTEND_API_ENDPOINT: process.env.FRONTEND_API_ENDPOINT, + WEBSOCKET_API_ENDPOINT: process.env.WEBSOCKET_API_ENDPOINT, INTERCOM_APP_ID: process.env.INTERCOM_APP_ID, SEGMENT_ID: process.env.SEGMENT_ID, POSTHOG_KEY: process.env.POSTHOG_KEY, diff --git a/frontend/webpack.config.prod.js b/frontend/webpack.config.prod.js index 87ef8b714349..f9af80bcf5c5 100644 --- a/frontend/webpack.config.prod.js +++ b/frontend/webpack.config.prod.js @@ -48,6 +48,7 @@ const plugins = [ new webpack.DefinePlugin({ 'process.env': JSON.stringify({ FRONTEND_API_ENDPOINT: process.env.FRONTEND_API_ENDPOINT, + WEBSOCKET_API_ENDPOINT: process.env.WEBSOCKET_API_ENDPOINT, INTERCOM_APP_ID: process.env.INTERCOM_APP_ID, SEGMENT_ID: process.env.SEGMENT_ID, POSTHOG_KEY: process.env.POSTHOG_KEY,