Skip to content

Commit 5728b53

Browse files
authored
feat: correlation feature (#394)
1 parent e204794 commit 5728b53

26 files changed

+3455
-6
lines changed

src/api/query.ts

+22-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Axios } from './axios';
22
import { LOG_QUERY_URL } from './constants';
33
import { Log, LogsQuery, LogsResponseWithHeaders } from '@/@types/parseable/api/query';
44
import timeRangeUtils from '@/utils/timeRangeUtils';
5-
import { QueryBuilder } from '@/utils/queryBuilder';
5+
import { CorrelationQueryBuilder, QueryBuilder } from '@/utils/queryBuilder';
66

77
const { formatDateAsCastType } = timeRangeUtils;
88
type QueryLogs = {
@@ -13,6 +13,15 @@ type QueryLogs = {
1313
pageOffset: number;
1414
};
1515

16+
type CorrelationLogs = {
17+
streamNames: string[];
18+
startTime: Date;
19+
endTime: Date;
20+
limit: number;
21+
correlationCondition?: string;
22+
selectedFields?: string[];
23+
};
24+
1625
// to optimize query performace, it has been decided to round off the time at the given level
1726
const optimizeTime = (date: Date) => {
1827
const tempDate = new Date(date);
@@ -53,6 +62,18 @@ export const getQueryLogsWithHeaders = (logsQuery: QueryLogs) => {
5362
return Axios().post<LogsResponseWithHeaders>(endPoint, formQueryOpts(logsQuery), {});
5463
};
5564

65+
export const getCorrelationQueryLogsWithHeaders = (logsQuery: CorrelationLogs) => {
66+
const queryBuilder = new CorrelationQueryBuilder(logsQuery);
67+
const endPoint = LOG_QUERY_URL({ fields: true }, queryBuilder.getResourcePath());
68+
return Axios().post<LogsResponseWithHeaders>(endPoint, queryBuilder.getCorrelationQuery(), {});
69+
};
70+
71+
export const getStreamDataWithHeaders = (logsQuery: CorrelationLogs) => {
72+
const queryBuilder = new CorrelationQueryBuilder(logsQuery);
73+
const endPoint = LOG_QUERY_URL({ fields: true }, queryBuilder.getResourcePath());
74+
return Axios().post<LogsResponseWithHeaders>(endPoint, queryBuilder.getQuery(), {});
75+
};
76+
5677
// ------ Custom sql query
5778

5879
const makeCustomQueryRequestData = (logsQuery: LogsQuery, query: string) => {
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { forwardRef } from 'react';
2+
3+
export const CorrelationIcon = forwardRef<
4+
SVGSVGElement,
5+
{
6+
stroke?: string;
7+
strokeWidth?: number;
8+
}
9+
>(({ stroke, strokeWidth }, ref) => (
10+
<svg ref={ref} height="1.2rem" width="1.2rem" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
11+
<path
12+
d="M13.3333 17.3333L14.6667 18.6667C15.0203 19.0203 15.4999 19.219 16 19.219C16.5001 19.219 16.9797 19.0203 17.3333 18.6667L22.6667 13.3333C23.0203 12.9797 23.219 12.5001 23.219 12C23.219 11.4999 23.0203 11.0203 22.6667 10.6667L17.3333 5.33333C16.9797 4.97971 16.5001 4.78105 16 4.78105C15.4999 4.78105 15.0203 4.97971 14.6667 5.33333L9.33333 10.6667C8.97971 11.0203 8.78105 11.4999 8.78105 12C8.78105 12.5001 8.97971 12.9797 9.33333 13.3333L10.6667 14.6667"
13+
stroke={stroke}
14+
strokeWidth={strokeWidth}
15+
strokeLinecap="round"
16+
strokeLinejoin="round"
17+
/>
18+
<path
19+
d="M10.6667 6.66667L9.33333 5.33333C8.97971 4.97971 8.5001 4.78105 8 4.78105C7.4999 4.78105 7.02029 4.97971 6.66667 5.33333L1.33333 10.6667C0.979711 11.0203 0.781049 11.4999 0.781049 12C0.781049 12.5001 0.979711 12.9797 1.33333 13.3333L6.66667 18.6667C7.02029 19.0203 7.4999 19.219 8 19.219C8.5001 19.219 8.97971 19.0203 9.33333 18.6667L14.6667 13.3333C15.0203 12.9797 15.219 12.5001 15.219 12C15.219 11.4999 15.0203 11.0203 14.6667 10.6667L13.3333 9.33333"
20+
stroke={stroke}
21+
strokeWidth={strokeWidth}
22+
strokeLinecap="round"
23+
strokeLinejoin="round"
24+
/>
25+
</svg>
26+
));
27+
28+
CorrelationIcon.displayName = 'CorrelationIcon';

src/components/Navbar/index.tsx

+23-2
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,14 @@ import { FC, useCallback, useEffect } from 'react';
1313
import { useLocation, useParams } from 'react-router-dom';
1414
import { useNavigate } from 'react-router-dom';
1515
import { useDisclosure } from '@mantine/hooks';
16-
import { HOME_ROUTE, CLUSTER_ROUTE, USERS_MANAGEMENT_ROUTE, STREAM_ROUTE, DASHBOARDS_ROUTE } from '@/constants/routes';
16+
import {
17+
HOME_ROUTE,
18+
CLUSTER_ROUTE,
19+
USERS_MANAGEMENT_ROUTE,
20+
STREAM_ROUTE,
21+
DASHBOARDS_ROUTE,
22+
CORRELATION_ROUTE,
23+
} from '@/constants/routes';
1724
import InfoModal from './infoModal';
1825
import { getStreamsSepcificAccess, getUserSepcificStreams } from './rolesHandler';
1926
import Cookies from 'js-cookie';
@@ -26,6 +33,7 @@ import UserModal from './UserModal';
2633
import { signOutHandler } from '@/utils';
2734
import { appStoreReducers, useAppStore } from '@/layouts/MainLayout/providers/AppProvider';
2835
import _ from 'lodash';
36+
import { CorrelationIcon } from './components/CorrelationIcon';
2937

3038
const { setUserRoles, setUserSpecificStreams, setUserAccessMap, changeStream, setStreamSpecificUserAccess } =
3139
appStoreReducers;
@@ -49,6 +57,12 @@ const navItems = [
4957
path: '/explore',
5058
route: STREAM_ROUTE,
5159
},
60+
{
61+
icon: CorrelationIcon,
62+
label: 'Correlation',
63+
path: '/correlation',
64+
route: CORRELATION_ROUTE,
65+
},
5266
];
5367

5468
const previlagedActions = [
@@ -167,7 +181,14 @@ const Navbar: FC = () => {
167181
onClick={() => navigateToPage(navItem.route)}
168182
key={index}>
169183
<Tooltip label={navItem.label} position="right">
170-
<navItem.icon stroke={isActiveItem ? 1.4 : 1.2} size={'1.2rem'} />
184+
{navItem.label === 'Correlation' ? (
185+
<navItem.icon
186+
stroke={isActiveItem ? '#000000' : '#858e96'}
187+
strokeWidth={isActiveItem ? 1.4 : 1.2}
188+
/>
189+
) : (
190+
<navItem.icon stroke={isActiveItem ? '1.4' : '1.2'} size="1.2rem" />
191+
)}
171192
</Tooltip>
172193
</Stack>
173194
);

src/constants/routes.ts

+2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export const OIDC_NOT_CONFIGURED_ROUTE = '/oidc-not-configured';
1111
export const CLUSTER_ROUTE = '/cluster';
1212
export const STREAM_ROUTE = '/:streamName/:view?';
1313
export const DASHBOARDS_ROUTE = '/dashboards';
14+
export const CORRELATION_ROUTE = '/correlation';
1415

1516
export const STREAM_VIEWS = ['explore', 'manage', 'live-tail'];
1617

@@ -27,4 +28,5 @@ export const PATHS = {
2728
cluster: '/cluster',
2829
manage: '/:streamName/:view?',
2930
dashboards: '/dashboards',
31+
correlation: '/correlation',
3032
} as { [key: string]: string };

src/hooks/useCorrelationQueryLogs.tsx

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { getCorrelationQueryLogsWithHeaders } from '@/api/query';
2+
import { StatusCodes } from 'http-status-codes';
3+
import useMountedState from './useMountedState';
4+
import { useAppStore } from '@/layouts/MainLayout/providers/AppProvider';
5+
import _ from 'lodash';
6+
import { AxiosError } from 'axios';
7+
import { useStreamStore } from '@/pages/Stream/providers/StreamProvider';
8+
import {
9+
CORRELATION_LOAD_LIMIT,
10+
correlationStoreReducers,
11+
useCorrelationStore,
12+
} from '@/pages/Correlation/providers/CorrelationProvider';
13+
import { notifyError } from '@/utils/notification';
14+
import { useQuery } from 'react-query';
15+
import { LogsResponseWithHeaders } from '@/@types/parseable/api/query';
16+
17+
const { setStreamData } = correlationStoreReducers;
18+
19+
export const useCorrelationQueryLogs = () => {
20+
const [error, setError] = useMountedState<string | null>(null);
21+
const [{ selectedFields, correlationCondition, fields }, setCorrelationStore] = useCorrelationStore((store) => store);
22+
const [streamInfo] = useStreamStore((store) => store.info);
23+
const [currentStream] = useAppStore((store) => store.currentStream);
24+
const timePartitionColumn = _.get(streamInfo, 'time_partition', 'p_timestamp');
25+
const [timeRange] = useAppStore((store) => store.timeRange);
26+
const [
27+
{
28+
tableOpts: { currentOffset },
29+
},
30+
] = useCorrelationStore((store) => store);
31+
const streamNames = Object.keys(fields);
32+
33+
const defaultQueryOpts = {
34+
startTime: timeRange.startTime,
35+
endTime: timeRange.endTime,
36+
limit: CORRELATION_LOAD_LIMIT,
37+
pageOffset: currentOffset,
38+
timePartitionColumn,
39+
selectedFields: _.flatMap(selectedFields, (values, key) => _.map(values, (value) => `${key}.${value}`)) || [],
40+
correlationCondition: correlationCondition,
41+
};
42+
43+
const {
44+
isLoading: logsLoading,
45+
isRefetching: logsRefetching,
46+
refetch: getCorrelationData,
47+
} = useQuery(
48+
['fetch-logs', defaultQueryOpts],
49+
async () => {
50+
const queryOpts = { ...defaultQueryOpts, streamNames };
51+
const response = await getCorrelationQueryLogsWithHeaders(queryOpts);
52+
return [response];
53+
},
54+
{
55+
enabled: false,
56+
refetchOnWindowFocus: false,
57+
onSuccess: async (responses) => {
58+
responses.map((data: { data: LogsResponseWithHeaders; status: StatusCodes }) => {
59+
const logs = data.data;
60+
const isInvalidResponse = _.isEmpty(logs) || _.isNil(logs) || data.status !== StatusCodes.OK;
61+
if (isInvalidResponse) return setError('Failed to query logs');
62+
63+
const { records, fields } = logs;
64+
if (fields.length > 0 && !correlationCondition) {
65+
return setCorrelationStore((store) => setStreamData(store, currentStream || '', records));
66+
} else if (fields.length > 0 && correlationCondition) {
67+
return setCorrelationStore((store) => setStreamData(store, 'correlatedStream', records));
68+
} else {
69+
notifyError({ message: `${currentStream} doesn't have any fields` });
70+
}
71+
});
72+
},
73+
onError: (data: AxiosError) => {
74+
const errorMessage = data.response?.data as string;
75+
setError(_.isString(errorMessage) && !_.isEmpty(errorMessage) ? errorMessage : 'Failed to query logs');
76+
},
77+
},
78+
);
79+
80+
return {
81+
error,
82+
loading: logsLoading || logsRefetching,
83+
getCorrelationData,
84+
};
85+
};

src/hooks/useFetchStreamData.tsx

+104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { getStreamDataWithHeaders } from '@/api/query';
2+
import { StatusCodes } from 'http-status-codes';
3+
import useMountedState from './useMountedState';
4+
import { useAppStore } from '@/layouts/MainLayout/providers/AppProvider';
5+
import _ from 'lodash';
6+
import { AxiosError } from 'axios';
7+
import { useStreamStore } from '@/pages/Stream/providers/StreamProvider';
8+
import {
9+
correlationStoreReducers,
10+
CORRELATION_LOAD_LIMIT,
11+
useCorrelationStore,
12+
} from '@/pages/Correlation/providers/CorrelationProvider';
13+
import { notifyError } from '@/utils/notification';
14+
import { useQuery } from 'react-query';
15+
import { LogsResponseWithHeaders } from '@/@types/parseable/api/query';
16+
import { useRef, useEffect } from 'react';
17+
18+
const { setStreamData } = correlationStoreReducers;
19+
20+
export const useFetchStreamData = () => {
21+
const [error, setError] = useMountedState<string | null>(null);
22+
const [{ selectedFields, correlationCondition, fields, streamData }, setCorrelationStore] = useCorrelationStore(
23+
(store) => store,
24+
);
25+
const [streamInfo] = useStreamStore((store) => store.info);
26+
const [currentStream] = useAppStore((store) => store.currentStream);
27+
const timePartitionColumn = _.get(streamInfo, 'time_partition', 'p_timestamp');
28+
const [timeRange] = useAppStore((store) => store.timeRange);
29+
const [
30+
{
31+
tableOpts: { currentOffset },
32+
},
33+
] = useCorrelationStore((store) => store);
34+
const streamNames = Object.keys(fields);
35+
36+
const prevTimeRangeRef = useRef({ startTime: timeRange.startTime, endTime: timeRange.endTime });
37+
38+
const hasTimeRangeChanged =
39+
prevTimeRangeRef.current.startTime !== timeRange.startTime ||
40+
prevTimeRangeRef.current.endTime !== timeRange.endTime;
41+
42+
useEffect(() => {
43+
prevTimeRangeRef.current = { startTime: timeRange.startTime, endTime: timeRange.endTime };
44+
}, [timeRange.startTime, timeRange.endTime]);
45+
46+
const defaultQueryOpts = {
47+
startTime: timeRange.startTime,
48+
endTime: timeRange.endTime,
49+
limit: CORRELATION_LOAD_LIMIT,
50+
pageOffset: currentOffset,
51+
timePartitionColumn,
52+
selectedFields: _.flatMap(selectedFields, (values, key) => _.map(values, (value) => `${key}.${value}`)) || [],
53+
correlationCondition: correlationCondition,
54+
};
55+
56+
const {
57+
isLoading: logsLoading,
58+
isRefetching: logsRefetching,
59+
refetch: getFetchStreamData,
60+
} = useQuery(
61+
['fetch-logs', defaultQueryOpts],
62+
async () => {
63+
const streamsToFetch = hasTimeRangeChanged
64+
? streamNames
65+
: streamNames.filter((streamName) => !Object.keys(streamData).includes(streamName));
66+
67+
const fetchPromises = streamsToFetch.map((streamName) => {
68+
const queryOpts = { ...defaultQueryOpts, streamNames: [streamName] };
69+
return getStreamDataWithHeaders(queryOpts);
70+
});
71+
return Promise.all(fetchPromises);
72+
},
73+
{
74+
enabled: false,
75+
refetchOnWindowFocus: false,
76+
onSuccess: async (responses) => {
77+
responses.map((data: { data: LogsResponseWithHeaders; status: StatusCodes }) => {
78+
const logs = data.data;
79+
const isInvalidResponse = _.isEmpty(logs) || _.isNil(logs) || data.status !== StatusCodes.OK;
80+
if (isInvalidResponse) return setError('Failed to query logs');
81+
82+
const { records, fields } = logs;
83+
if (fields.length > 0 && !correlationCondition) {
84+
return setCorrelationStore((store) => setStreamData(store, currentStream || '', records));
85+
} else if (fields.length > 0 && correlationCondition) {
86+
return setCorrelationStore((store) => setStreamData(store, 'correlatedStream', records));
87+
} else {
88+
notifyError({ message: `${currentStream} doesn't have any fields` });
89+
}
90+
});
91+
},
92+
onError: (data: AxiosError) => {
93+
const errorMessage = data.response?.data as string;
94+
setError(_.isString(errorMessage) && !_.isEmpty(errorMessage) ? errorMessage : 'Failed to query logs');
95+
},
96+
},
97+
);
98+
99+
return {
100+
error,
101+
loading: logsLoading || logsRefetching,
102+
getFetchStreamData,
103+
};
104+
};
+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { getLogStreamSchema } from '@/api/logStream';
2+
import { AxiosError, isAxiosError } from 'axios';
3+
import _ from 'lodash';
4+
import { useQuery } from 'react-query';
5+
import { useState } from 'react';
6+
import { correlationStoreReducers, useCorrelationStore } from '@/pages/Correlation/providers/CorrelationProvider';
7+
8+
const { setStreamSchema } = correlationStoreReducers;
9+
10+
export const useGetStreamSchema = (opts: { streamName: string }) => {
11+
const { streamName } = opts;
12+
const [, setCorrelationStore] = useCorrelationStore((_store) => null);
13+
14+
const [errorMessage, setErrorMesssage] = useState<string | null>(null);
15+
16+
const { isError, isSuccess, isLoading, isRefetching } = useQuery(
17+
['stream-schema', streamName],
18+
() => getLogStreamSchema(streamName),
19+
{
20+
retry: false,
21+
enabled: streamName !== '' && streamName !== 'correlatedStream',
22+
refetchOnWindowFocus: false,
23+
onSuccess: (data) => {
24+
setErrorMesssage(null);
25+
setCorrelationStore((store) => setStreamSchema(store, data.data, streamName));
26+
},
27+
onError: (data: AxiosError) => {
28+
if (isAxiosError(data) && data.response) {
29+
const error = data.response?.data as string;
30+
typeof error === 'string' && setErrorMesssage(error);
31+
} else if (data.message && typeof data.message === 'string') {
32+
setErrorMesssage(data.message);
33+
}
34+
},
35+
},
36+
);
37+
38+
return {
39+
isSuccess,
40+
isError,
41+
isLoading,
42+
errorMessage,
43+
isRefetching,
44+
};
45+
};

0 commit comments

Comments
 (0)