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
This commit is contained in:
Vikrant Gupta 2024-08-16 15:07:06 +05:30 committed by GitHub
parent 65280cf4e1
commit 1b9683d699
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 197 additions and 5 deletions

View File

@ -0,0 +1 @@
<svg width="14" height="14" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#prefix__clip0_4062_7291)" stroke-width="1.167" stroke-linecap="round" stroke-linejoin="round"><path d="M7 12.833A5.833 5.833 0 107 1.167a5.833 5.833 0 000 11.666z" fill="#E5484D" stroke="#E5484D"/><path d="M8.75 5.25l-3.5 3.5M5.25 5.25l3.5 3.5" stroke="#121317"/></g><defs><clipPath id="prefix__clip0_4062_7291"><path fill="#fff" d="M0 0h14v14H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 467 B

View File

@ -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<React.SetStateAction<WsDataEvent | undefined>>;
}
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);
}
});
}

View File

@ -12,10 +12,13 @@ export const getMetricsQueryRange = async (
props: QueryRangePayload,
version: string,
signal: AbortSignal,
headers?: Record<string, string>,
): Promise<SuccessResponse<MetricRangePayloadV3> | 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,

View File

@ -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/',
};

View File

@ -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);
}
}
}
}
}

View File

@ -0,0 +1,4 @@
.query-status {
display: flex;
align-items: center;
}

View File

@ -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 <Spin spinning size="small" indicator={<LoadingOutlined spin />} />;
}
if (error) {
return (
<img
src="/Icons/solid-x-circle.svg"
alt="header"
className="error"
style={{ height: '14px', width: '14px' }}
/>
);
}
if (success) {
return (
<CircleCheck className="success" size={14} fill={Color.BG_ROBIN_500} />
);
}
return <div />;
}, [error, loading, success]);
return <div className="query-status">{content}</div>;
}

View File

@ -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<ILog[]>([]);
const [requestData, setRequestData] = useState<Query | null>(null);
const [showFormatMenuItems, setShowFormatMenuItems] = useState(false);
const [queryId, setQueryId] = useState<string>(v4());
const [queryStats, setQueryStats] = useState<WsDataEvent>();
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({
</div>
</div>
)}
{(selectedPanelType === PANEL_TYPES.TIME_SERIES ||
selectedPanelType === PANEL_TYPES.TABLE) && (
<div className="query-stats">
<QueryStatus
loading={isLoading || isFetching}
error={isError}
success={isSuccess}
/>
{queryStats?.read_rows && (
<Typography.Text className="rows">
{getYAxisFormattedValue(queryStats.read_rows?.toString(), 'short')}{' '}
rows
</Typography.Text>
)}
{queryStats?.elapsed_ms && (
<>
<div className="divider" />
<Typography.Text className="time">
{getYAxisFormattedValue(queryStats?.elapsed_ms?.toString(), 'ms')}
</Typography.Text>
</>
)}
</div>
)}
</div>
</div>

View File

@ -46,6 +46,8 @@ jest.mock(
},
);
jest.mock('api/common/getQueryStats', () => jest.fn());
jest.mock('constants/panelTypes', () => ({
AVAILABLE_EXPORT_PANEL_TYPES: ['graph', 'table'],
}));

View File

@ -20,6 +20,7 @@ export const useGetExplorerQueryRange = (
params?: Record<string, unknown>,
isDependentOnQB = true,
keyRef?: MutableRefObject<any>,
headers?: Record<string, string>,
): UseQueryResult<SuccessResponse<MetricRangePayloadProps>, 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,
);
};

View File

@ -13,12 +13,14 @@ type UseGetQueryRange = (
requestData: GetQueryResultsProps,
version: string,
options?: UseQueryOptions<SuccessResponse<MetricRangePayloadProps>, Error>,
headers?: Record<string, string>,
) => UseQueryResult<SuccessResponse<MetricRangePayloadProps>, Error>;
export const useGetQueryRange: UseGetQueryRange = (
requestData,
version,
options,
headers,
) => {
const newRequestData: GetQueryResultsProps = useMemo(
() => ({
@ -45,7 +47,7 @@ export const useGetQueryRange: UseGetQueryRange = (
return useQuery<SuccessResponse<MetricRangePayloadProps>, Error>({
queryFn: async ({ signal }) =>
GetMetricQueryRange(requestData, version, signal),
GetMetricQueryRange(requestData, version, signal, headers),
...options,
queryKey,
});

View File

@ -23,6 +23,7 @@ export async function GetMetricQueryRange(
props: GetQueryResultsProps,
version: string,
signal?: AbortSignal,
headers?: Record<string, string>,
): Promise<SuccessResponse<MetricRangePayloadProps>> {
const { legendMap, queryPayload } = prepareQueryRangePayload(props);
@ -30,6 +31,7 @@ export async function GetMetricQueryRange(
queryPayload,
version || 'v3',
signal,
headers,
);
if (response.statusCode >= 400) {

View File

@ -3,6 +3,7 @@ declare global {
namespace NodeJS {
interface ProcessEnv {
FRONTEND_API_ENDPOINT: string | undefined;
WEBSOCKET_API_ENDPOINT: string | undefined;
}
}
}

View File

@ -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,

View File

@ -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,