fix: Logs Live Tail is fixed (#2380)

* logs is updated

* fix: log live tail is updated

* fix: live tail is fixed

* chore: build is fixed

* chore: useEffect is removed

* chore: getLogsAggregate callback is added in the useEffect
This commit is contained in:
palashgdev 2023-03-01 17:18:02 +05:30 committed by GitHub
parent 5e5e81d81d
commit 50270281e3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 155 additions and 95 deletions

View File

@ -8,40 +8,50 @@ import { Button, Popover, Select, Space } from 'antd';
import { LiveTail } from 'api/logs/livetail'; import { LiveTail } from 'api/logs/livetail';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { useIsDarkMode } from 'hooks/useDarkMode'; import { useIsDarkMode } from 'hooks/useDarkMode';
import { useNotifications } from 'hooks/useNotifications';
import getStep from 'lib/getStep';
import { throttle } from 'lodash-es'; import { throttle } from 'lodash-es';
import React, { useCallback, useEffect, useMemo, useRef } from 'react'; import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { connect, useDispatch, useSelector } from 'react-redux';
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { getLogsAggregate } from 'store/actions/logs/getLogsAggregate';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { UPDATE_AUTO_REFRESH_DISABLED } from 'types/actions/globalTime'; import { UPDATE_AUTO_REFRESH_DISABLED } from 'types/actions/globalTime';
import { import {
FLUSH_LOGS, FLUSH_LOGS,
PUSH_LIVE_TAIL_EVENT, PUSH_LIVE_TAIL_EVENT,
SET_LIVE_TAIL_START_TIME, SET_LIVE_TAIL_START_TIME,
SET_LOADING,
TOGGLE_LIVE_TAIL, TOGGLE_LIVE_TAIL,
} from 'types/actions/logs'; } from 'types/actions/logs';
import { TLogsLiveTailState } from 'types/api/logs/liveTail'; import { TLogsLiveTailState } from 'types/api/logs/liveTail';
import { ILog } from 'types/api/logs/log';
import { GlobalReducer } from 'types/reducer/globalTime'; import { GlobalReducer } from 'types/reducer/globalTime';
import { ILogsReducer } from 'types/reducer/logs'; import { ILogsReducer } from 'types/reducer/logs';
import { TIME_PICKER_OPTIONS } from './config'; import { TIME_PICKER_OPTIONS } from './config';
import { StopContainer, TimePickerCard, TimePickerSelect } from './styles'; import { StopContainer, TimePickerCard, TimePickerSelect } from './styles';
const { Option } = Select; function LogLiveTail({ getLogsAggregate }: Props): JSX.Element {
function LogLiveTail(): JSX.Element {
const { const {
liveTail, liveTail,
searchFilter: { queryString }, searchFilter: { queryString },
liveTailStartRange, liveTailStartRange,
logs, logs,
idEnd,
idStart,
} = useSelector<AppState, ILogsReducer>((state) => state.logs); } = useSelector<AppState, ILogsReducer>((state) => state.logs);
const isDarkMode = useIsDarkMode(); const isDarkMode = useIsDarkMode();
const { selectedAutoRefreshInterval } = useSelector<AppState, GlobalReducer>( const { selectedAutoRefreshInterval } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime, (state) => state.globalTime,
); );
const { notifications } = useNotifications();
const dispatch = useDispatch(); const dispatch = useDispatch<Dispatch<AppActions>>();
const handleLiveTail = (toggleState: TLogsLiveTailState): void => { const handleLiveTail = (toggleState: TLogsLiveTailState): void => {
dispatch({ dispatch({
type: TOGGLE_LIVE_TAIL, type: TOGGLE_LIVE_TAIL,
@ -53,7 +63,7 @@ function LogLiveTail(): JSX.Element {
}); });
}; };
const batchedEventsRef = useRef<Record<string, unknown>[]>([]); const batchedEventsRef = useRef<ILog[]>([]);
const pushLiveLog = useCallback(() => { const pushLiveLog = useCallback(() => {
dispatch({ dispatch({
@ -75,47 +85,76 @@ function LogLiveTail(): JSX.Element {
[pushLiveLogThrottled], [pushLiveLogThrottled],
); );
const firstLogsId = useMemo(() => logs[0]?.id, [logs]);
// This ref depicts thats whether the live tail is played from paused state or not. // This ref depicts thats whether the live tail is played from paused state or not.
const liveTailSourceRef = useRef<EventSource | null>(null); const liveTailSourceRef = useRef<EventSource>();
useEffect(() => { useEffect(() => {
if (liveTail === 'PLAYING') { if (liveTail === 'PLAYING') {
// console.log('Starting Live Tail', logs.length);
const timeStamp = dayjs().subtract(liveTailStartRange, 'minute').valueOf(); const timeStamp = dayjs().subtract(liveTailStartRange, 'minute').valueOf();
const queryParams = new URLSearchParams({ const queryParams = new URLSearchParams({
...(queryString ? { q: queryString } : {}), ...(queryString ? { q: queryString } : {}),
timestampStart: (timeStamp * 1e6) as never, timestampStart: (timeStamp * 1e6) as never,
...(liveTailSourceRef.current && logs.length > 0 ...(liveTailSourceRef.current && firstLogsId
? { ? {
idGt: logs[0].id, idGt: firstLogsId,
} }
: {}), : {}),
}); });
if (liveTailSourceRef.current) {
liveTailSourceRef.current.close();
}
const source = LiveTail(queryParams.toString()); const source = LiveTail(queryParams.toString());
liveTailSourceRef.current = source; liveTailSourceRef.current = source;
source.onmessage = function connectionMessage(e): void { source.onmessage = function connectionMessage(e): void {
batchLiveLog(e); batchLiveLog(e);
}; };
// source.onopen = function connectionOpen(): void { };
source.onerror = function connectionError(event: unknown): void { source.onerror = function connectionError(event: unknown): void {
console.error(event); console.error(event);
source.close(); source.close();
dispatch({ dispatch({
type: TOGGLE_LIVE_TAIL, type: TOGGLE_LIVE_TAIL,
payload: 'STOPPED',
});
dispatch({
type: SET_LOADING,
payload: false, payload: false,
}); });
notifications.error({
message: 'Live tail stopped due to some error.',
});
}; };
} else if (liveTailSourceRef.current && liveTailSourceRef.current.close) {
liveTailSourceRef.current?.close();
} }
if (liveTail === 'STOPPED') { if (liveTail === 'STOPPED') {
liveTailSourceRef.current = null; liveTailSourceRef.current = undefined;
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [liveTail]); }, [liveTail, queryString, notifications, dispatch]);
const handleLiveTailStart = (): void => { const handleLiveTailStart = (): void => {
handleLiveTail('PLAYING'); handleLiveTail('PLAYING');
const startTime =
dayjs().subtract(liveTailStartRange, 'minute').valueOf() * 1e6;
const endTime = dayjs().valueOf() * 1e6;
getLogsAggregate({
timestampStart: startTime,
timestampEnd: endTime,
step: getStep({
start: startTime,
end: endTime,
inputFormat: 'ns',
}),
q: queryString,
...(idStart ? { idGt: idStart } : {}),
...(idEnd ? { idLt: idEnd } : {}),
});
if (!liveTailSourceRef.current) { if (!liveTailSourceRef.current) {
dispatch({ dispatch({
type: FLUSH_LOGS, type: FLUSH_LOGS,
@ -129,16 +168,18 @@ function LogLiveTail(): JSX.Element {
disabled={liveTail === 'PLAYING'} disabled={liveTail === 'PLAYING'}
value={liveTailStartRange} value={liveTailStartRange}
onChange={(value): void => { onChange={(value): void => {
dispatch({ if (typeof value === 'number') {
type: SET_LIVE_TAIL_START_TIME, dispatch({
payload: value, type: SET_LIVE_TAIL_START_TIME,
}); payload: value,
});
}
}} }}
> >
{TIME_PICKER_OPTIONS.map((optionData) => ( {TIME_PICKER_OPTIONS.map((optionData) => (
<Option key={optionData.label} value={optionData.value}> <Select.Option key={optionData.label} value={optionData.value}>
Last {optionData.label} Last {optionData.label}
</Option> </Select.Option>
))} ))}
</TimePickerSelect> </TimePickerSelect>
), ),
@ -149,13 +190,28 @@ function LogLiveTail(): JSX.Element {
selectedAutoRefreshInterval, selectedAutoRefreshInterval,
]); ]);
const onLiveTailStop = (): void => {
handleLiveTail('STOPPED');
dispatch({
type: UPDATE_AUTO_REFRESH_DISABLED,
payload: false,
});
dispatch({
type: SET_LOADING,
payload: false,
});
if (liveTailSourceRef.current) {
liveTailSourceRef.current.close();
}
};
return ( return (
<TimePickerCard> <TimePickerCard>
<Space size={0} align="center"> <Space size={0} align="center">
{liveTail === 'PLAYING' ? ( {liveTail === 'PLAYING' ? (
<Button <Button
type="primary" type="primary"
onClick={(): void => handleLiveTail('PAUSED')} onClick={onLiveTailStop}
title="Pause live tail" title="Pause live tail"
style={{ background: green[6] }} style={{ background: green[6] }}
> >
@ -174,11 +230,7 @@ function LogLiveTail(): JSX.Element {
)} )}
{liveTail !== 'STOPPED' && ( {liveTail !== 'STOPPED' && (
<Button <Button type="dashed" onClick={onLiveTailStop} title="Exit live tail">
type="dashed"
onClick={(): void => handleLiveTail('STOPPED')}
title="Exit live tail"
>
<StopContainer isDarkMode={isDarkMode} /> <StopContainer isDarkMode={isDarkMode} />
</Button> </Button>
)} )}
@ -196,4 +248,16 @@ function LogLiveTail(): JSX.Element {
); );
} }
export default LogLiveTail; interface DispatchProps {
getLogsAggregate: typeof getLogsAggregate;
}
type Props = DispatchProps;
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
getLogsAggregate: bindActionCreators(getLogsAggregate, dispatch),
});
export default connect(null, mapDispatchToProps)(LogLiveTail);

View File

@ -2,20 +2,20 @@ import { blue } from '@ant-design/colors';
import Graph from 'components/Graph'; import Graph from 'components/Graph';
import Spinner from 'components/Spinner'; import Spinner from 'components/Spinner';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import useInterval from 'hooks/useInterval';
import getStep from 'lib/getStep'; import getStep from 'lib/getStep';
import React, { memo, useEffect, useMemo, useRef } from 'react'; import React, { useMemo } from 'react';
import { connect, useSelector } from 'react-redux'; import { connect, useSelector } from 'react-redux';
import { bindActionCreators, Dispatch } from 'redux'; import { bindActionCreators } from 'redux';
import { ThunkDispatch } from 'redux-thunk'; import { ThunkDispatch } from 'redux-thunk';
import { getLogsAggregate } from 'store/actions/logs/getLogsAggregate'; import { getLogsAggregate } from 'store/actions/logs/getLogsAggregate';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
import AppActions from 'types/actions'; import AppActions from 'types/actions';
import { GlobalReducer } from 'types/reducer/globalTime';
import { ILogsReducer } from 'types/reducer/logs'; import { ILogsReducer } from 'types/reducer/logs';
import { Container } from './styles'; import { Container } from './styles';
function LogsAggregate({ getLogsAggregate }: LogsAggregateProps): JSX.Element { function LogsAggregate({ getLogsAggregate }: DispatchProps): JSX.Element {
const { const {
searchFilter: { queryString }, searchFilter: { queryString },
idEnd, idEnd,
@ -26,57 +26,30 @@ function LogsAggregate({ getLogsAggregate }: LogsAggregateProps): JSX.Element {
liveTailStartRange, liveTailStartRange,
} = useSelector<AppState, ILogsReducer>((state) => state.logs); } = useSelector<AppState, ILogsReducer>((state) => state.logs);
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>( useInterval(
(state) => state.globalTime, () => {
const startTime =
dayjs().subtract(liveTailStartRange, 'minute').valueOf() * 1e6;
const endTime = dayjs().valueOf() * 1e6;
getLogsAggregate({
timestampStart: startTime,
timestampEnd: endTime,
step: getStep({
start: startTime,
end: endTime,
inputFormat: 'ns',
}),
q: queryString,
...(idStart ? { idGt: idStart } : {}),
...(idEnd ? { idLt: idEnd } : {}),
});
},
60000,
liveTail === 'PLAYING',
); );
const reFetchIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
useEffect(() => {
switch (liveTail) {
case 'STOPPED': {
if (reFetchIntervalRef.current) {
clearInterval(reFetchIntervalRef.current);
}
reFetchIntervalRef.current = null;
break;
}
case 'PLAYING': {
const aggregateCall = (): void => {
const startTime =
dayjs().subtract(liveTailStartRange, 'minute').valueOf() * 1e6;
const endTime = dayjs().valueOf() * 1e6;
getLogsAggregate({
timestampStart: startTime,
timestampEnd: endTime,
step: getStep({
start: startTime,
end: endTime,
inputFormat: 'ns',
}),
q: queryString,
...(idStart ? { idGt: idStart } : {}),
...(idEnd ? { idLt: idEnd } : {}),
});
};
aggregateCall();
reFetchIntervalRef.current = setInterval(aggregateCall, 60000);
break;
}
case 'PAUSED': {
if (reFetchIntervalRef.current) {
clearInterval(reFetchIntervalRef.current);
}
break;
}
default: {
break;
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getLogsAggregate, maxTime, minTime, liveTail]);
const graphData = useMemo( const graphData = useMemo(
() => ({ () => ({
labels: logsAggregate.map((s) => new Date(s.timestamp / 1000000)), labels: logsAggregate.map((s) => new Date(s.timestamp / 1000000)),
@ -107,14 +80,8 @@ function LogsAggregate({ getLogsAggregate }: LogsAggregateProps): JSX.Element {
); );
} }
interface LogsAggregateProps {
getLogsAggregate: (arg0: Parameters<typeof getLogsAggregate>[0]) => void;
}
interface DispatchProps { interface DispatchProps {
getLogsAggregate: ( getLogsAggregate: typeof getLogsAggregate;
props: Parameters<typeof getLogsAggregate>[0],
) => (dispatch: Dispatch<AppActions>) => void;
} }
const mapDispatchToProps = ( const mapDispatchToProps = (
@ -123,4 +90,4 @@ const mapDispatchToProps = (
getLogsAggregate: bindActionCreators(getLogsAggregate, dispatch), getLogsAggregate: bindActionCreators(getLogsAggregate, dispatch),
}); });
export default connect(null, mapDispatchToProps)(memo(LogsAggregate)); export default connect(null, mapDispatchToProps)(LogsAggregate);

View File

@ -74,6 +74,7 @@ function SearchFilter({
const handleSearch = useCallback( const handleSearch = useCallback(
(customQuery: string) => { (customQuery: string) => {
getLogsFields(); getLogsFields();
const { maxTime, minTime } = globalTime;
if (liveTail === 'PLAYING') { if (liveTail === 'PLAYING') {
dispatch({ dispatch({
@ -87,9 +88,24 @@ function SearchFilter({
type: TOGGLE_LIVE_TAIL, type: TOGGLE_LIVE_TAIL,
payload: liveTail, payload: liveTail,
}); });
} else { dispatch({
const { maxTime, minTime } = globalTime; type: SET_LOADING,
payload: false,
});
getLogsAggregate({
timestampStart: minTime,
timestampEnd: maxTime,
step: getStep({
start: minTime,
end: maxTime,
inputFormat: 'ns',
}),
q: customQuery,
...(idStart ? { idGt: idStart } : {}),
...(idEnd ? { idLt: idEnd } : {}),
});
} else {
getLogs({ getLogs({
q: customQuery, q: customQuery,
limit: logLinesPerPage, limit: logLinesPerPage,

View File

@ -1,6 +1,10 @@
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
function useInterval(callback: () => void, delay: number): void { function useInterval(
callback: () => void,
delay: number,
enabled = true,
): void {
const savedCallback = useRef<() => void>(); const savedCallback = useRef<() => void>();
useEffect(() => { useEffect(() => {
@ -14,9 +18,18 @@ function useInterval(callback: () => void, delay: number): void {
} }
} }
const id = setInterval(tick, delay); let id: NodeJS.Timer;
return (): void => clearInterval(id);
}, [delay]); if (enabled) {
id = setInterval(tick, delay);
}
return (): void => {
if (id) {
clearInterval(id);
}
};
}, [delay, enabled]);
} }
export default useInterval; export default useInterval;