mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Explore: Add caching for queries run from logs navigation (#34297)
* WIP: Implement simple caching * If results are cached, don't run new query and use those results * Add duplicate key check * Clean up * Clean up * Add tests for caching * Remove unused variables * Update public/app/features/explore/state/query.test.ts Co-authored-by: Piotr Jamróz <pm.jamroz@gmail.com> * Update public/app/features/explore/state/query.test.ts Co-authored-by: Piotr Jamróz <pm.jamroz@gmail.com> * Use decorateData to apply all decorators * Remove unused variables * Change loading stte to Done * Clear cache when running query from navigation Co-authored-by: Piotr Jamróz <pm.jamroz@gmail.com>
This commit is contained in:
parent
b5de6e7a1d
commit
247bdc2f9b
@ -19,7 +19,7 @@ import { ExploreTimeControls } from './ExploreTimeControls';
|
|||||||
import { LiveTailButton } from './LiveTailButton';
|
import { LiveTailButton } from './LiveTailButton';
|
||||||
import { RunButton } from './RunButton';
|
import { RunButton } from './RunButton';
|
||||||
import { LiveTailControls } from './useLiveTailControls';
|
import { LiveTailControls } from './useLiveTailControls';
|
||||||
import { cancelQueries, clearQueries, runQueries } from './state/query';
|
import { cancelQueries, clearQueries, runQueries, clearCache } from './state/query';
|
||||||
import ReturnToDashboardButton from './ReturnToDashboardButton';
|
import ReturnToDashboardButton from './ReturnToDashboardButton';
|
||||||
import { isSplit } from './state/selectors';
|
import { isSplit } from './state/selectors';
|
||||||
|
|
||||||
@ -54,6 +54,7 @@ interface DispatchProps {
|
|||||||
syncTimes: typeof syncTimes;
|
syncTimes: typeof syncTimes;
|
||||||
changeRefreshInterval: typeof changeRefreshInterval;
|
changeRefreshInterval: typeof changeRefreshInterval;
|
||||||
onChangeTimeZone: typeof updateTimeZoneForSession;
|
onChangeTimeZone: typeof updateTimeZoneForSession;
|
||||||
|
clearCache: typeof clearCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = StateProps & DispatchProps & OwnProps;
|
type Props = StateProps & DispatchProps & OwnProps;
|
||||||
@ -68,10 +69,13 @@ export class UnConnectedExploreToolbar extends PureComponent<Props> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onRunQuery = (loading = false) => {
|
onRunQuery = (loading = false) => {
|
||||||
|
const { clearCache, runQueries, cancelQueries, exploreId } = this.props;
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return this.props.cancelQueries(this.props.exploreId);
|
return cancelQueries(exploreId);
|
||||||
} else {
|
} else {
|
||||||
return this.props.runQueries(this.props.exploreId);
|
// We want to give user a chance tu re-run the query even if it is saved in cache
|
||||||
|
clearCache(exploreId);
|
||||||
|
return runQueries(exploreId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -274,6 +278,7 @@ const mapDispatchToProps: DispatchProps = {
|
|||||||
split: splitOpen,
|
split: splitOpen,
|
||||||
syncTimes,
|
syncTimes,
|
||||||
onChangeTimeZone: updateTimeZoneForSession,
|
onChangeTimeZone: updateTimeZoneForSession,
|
||||||
|
clearCache,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ExploreToolbar = hot(module)(connect(mapStateToProps, mapDispatchToProps)(UnConnectedExploreToolbar));
|
export const ExploreToolbar = hot(module)(connect(mapStateToProps, mapDispatchToProps)(UnConnectedExploreToolbar));
|
||||||
|
@ -65,6 +65,8 @@ interface Props {
|
|||||||
onStopScanning?: () => void;
|
onStopScanning?: () => void;
|
||||||
getRowContext?: (row: LogRowModel, options?: RowContextOptions) => Promise<any>;
|
getRowContext?: (row: LogRowModel, options?: RowContextOptions) => Promise<any>;
|
||||||
getFieldLinks: (field: Field, rowIndex: number) => Array<LinkModel<Field>>;
|
getFieldLinks: (field: Field, rowIndex: number) => Array<LinkModel<Field>>;
|
||||||
|
addResultsToCache: () => void;
|
||||||
|
clearCache: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
@ -244,6 +246,8 @@ export class UnthemedLogs extends PureComponent<Props, State> {
|
|||||||
getFieldLinks,
|
getFieldLinks,
|
||||||
theme,
|
theme,
|
||||||
logsQueries,
|
logsQueries,
|
||||||
|
clearCache,
|
||||||
|
addResultsToCache,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -361,6 +365,8 @@ export class UnthemedLogs extends PureComponent<Props, State> {
|
|||||||
loading={loading}
|
loading={loading}
|
||||||
queries={logsQueries ?? []}
|
queries={logsQueries ?? []}
|
||||||
scrollToTopLogs={this.scrollToTopLogs}
|
scrollToTopLogs={this.scrollToTopLogs}
|
||||||
|
addResultsToCache={addResultsToCache}
|
||||||
|
clearCache={clearCache}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{!loading && !hasData && !scanning && (
|
{!loading && !hasData && !scanning && (
|
||||||
|
@ -7,6 +7,7 @@ import { AbsoluteTimeRange, Field, LogRowModel, RawTimeRange } from '@grafana/da
|
|||||||
import { ExploreId, ExploreItemState } from 'app/types/explore';
|
import { ExploreId, ExploreItemState } from 'app/types/explore';
|
||||||
import { StoreState } from 'app/types';
|
import { StoreState } from 'app/types';
|
||||||
import { splitOpen } from './state/main';
|
import { splitOpen } from './state/main';
|
||||||
|
import { addResultsToCache, clearCache } from './state/query';
|
||||||
import { updateTimeRange } from './state/time';
|
import { updateTimeRange } from './state/time';
|
||||||
import { getTimeZone } from '../profile/state/selectors';
|
import { getTimeZone } from '../profile/state/selectors';
|
||||||
import { LiveLogsWithTheme } from './LiveLogs';
|
import { LiveLogsWithTheme } from './LiveLogs';
|
||||||
@ -15,7 +16,7 @@ import { LogsCrossFadeTransition } from './utils/LogsCrossFadeTransition';
|
|||||||
import { LiveTailControls } from './useLiveTailControls';
|
import { LiveTailControls } from './useLiveTailControls';
|
||||||
import { getFieldLinksForExplore } from './utils/links';
|
import { getFieldLinksForExplore } from './utils/links';
|
||||||
|
|
||||||
interface LogsContainerProps {
|
interface LogsContainerProps extends PropsFromRedux {
|
||||||
exploreId: ExploreId;
|
exploreId: ExploreId;
|
||||||
scanRange?: RawTimeRange;
|
scanRange?: RawTimeRange;
|
||||||
width: number;
|
width: number;
|
||||||
@ -26,7 +27,7 @@ interface LogsContainerProps {
|
|||||||
onStopScanning: () => void;
|
onStopScanning: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class LogsContainer extends PureComponent<PropsFromRedux & LogsContainerProps> {
|
export class LogsContainer extends PureComponent<LogsContainerProps> {
|
||||||
onChangeTime = (absoluteRange: AbsoluteTimeRange) => {
|
onChangeTime = (absoluteRange: AbsoluteTimeRange) => {
|
||||||
const { exploreId, updateTimeRange } = this.props;
|
const { exploreId, updateTimeRange } = this.props;
|
||||||
updateTimeRange({ exploreId, absoluteRange });
|
updateTimeRange({ exploreId, absoluteRange });
|
||||||
@ -77,6 +78,8 @@ export class LogsContainer extends PureComponent<PropsFromRedux & LogsContainerP
|
|||||||
width,
|
width,
|
||||||
isLive,
|
isLive,
|
||||||
exploreId,
|
exploreId,
|
||||||
|
addResultsToCache,
|
||||||
|
clearCache,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
if (!logRows) {
|
if (!logRows) {
|
||||||
@ -134,6 +137,8 @@ export class LogsContainer extends PureComponent<PropsFromRedux & LogsContainerP
|
|||||||
width={width}
|
width={width}
|
||||||
getRowContext={this.getLogRowContext}
|
getRowContext={this.getLogRowContext}
|
||||||
getFieldLinks={this.getFieldLinks}
|
getFieldLinks={this.getFieldLinks}
|
||||||
|
addResultsToCache={() => addResultsToCache(exploreId)}
|
||||||
|
clearCache={() => clearCache(exploreId)}
|
||||||
/>
|
/>
|
||||||
</Collapse>
|
</Collapse>
|
||||||
</LogsCrossFadeTransition>
|
</LogsCrossFadeTransition>
|
||||||
@ -180,6 +185,8 @@ function mapStateToProps(state: StoreState, { exploreId }: { exploreId: string }
|
|||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
updateTimeRange,
|
updateTimeRange,
|
||||||
splitOpen,
|
splitOpen,
|
||||||
|
addResultsToCache,
|
||||||
|
clearCache,
|
||||||
};
|
};
|
||||||
|
|
||||||
const connector = connect(mapStateToProps, mapDispatchToProps);
|
const connector = connect(mapStateToProps, mapDispatchToProps);
|
||||||
|
@ -15,6 +15,8 @@ const setup = (propOverrides?: object) => {
|
|||||||
visibleRange: { from: 1619081941000, to: 1619081945930 },
|
visibleRange: { from: 1619081941000, to: 1619081945930 },
|
||||||
onChangeTime: jest.fn(),
|
onChangeTime: jest.fn(),
|
||||||
scrollToTopLogs: jest.fn(),
|
scrollToTopLogs: jest.fn(),
|
||||||
|
addResultsToCache: jest.fn(),
|
||||||
|
clearCache: jest.fn(),
|
||||||
...propOverrides,
|
...propOverrides,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -14,6 +14,8 @@ type Props = {
|
|||||||
logsSortOrder?: LogsSortOrder | null;
|
logsSortOrder?: LogsSortOrder | null;
|
||||||
onChangeTime: (range: AbsoluteTimeRange) => void;
|
onChangeTime: (range: AbsoluteTimeRange) => void;
|
||||||
scrollToTopLogs: () => void;
|
scrollToTopLogs: () => void;
|
||||||
|
addResultsToCache: () => void;
|
||||||
|
clearCache: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type LogsPage = {
|
export type LogsPage = {
|
||||||
@ -30,6 +32,8 @@ function LogsNavigation({
|
|||||||
scrollToTopLogs,
|
scrollToTopLogs,
|
||||||
visibleRange,
|
visibleRange,
|
||||||
queries,
|
queries,
|
||||||
|
clearCache,
|
||||||
|
addResultsToCache,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const [pages, setPages] = useState<LogsPage[]>([]);
|
const [pages, setPages] = useState<LogsPage[]>([]);
|
||||||
const [currentPageIndex, setCurrentPageIndex] = useState(0);
|
const [currentPageIndex, setCurrentPageIndex] = useState(0);
|
||||||
@ -53,6 +57,7 @@ function LogsNavigation({
|
|||||||
let newPages: LogsPage[] = [];
|
let newPages: LogsPage[] = [];
|
||||||
// We want to start new pagination if queries change or if absolute range is different than expected
|
// We want to start new pagination if queries change or if absolute range is different than expected
|
||||||
if (!isEqual(expectedRangeRef.current, absoluteRange) || !isEqual(expectedQueriesRef.current, queries)) {
|
if (!isEqual(expectedRangeRef.current, absoluteRange) || !isEqual(expectedQueriesRef.current, queries)) {
|
||||||
|
clearCache();
|
||||||
setPages([newPage]);
|
setPages([newPage]);
|
||||||
setCurrentPageIndex(0);
|
setCurrentPageIndex(0);
|
||||||
expectedQueriesRef.current = queries;
|
expectedQueriesRef.current = queries;
|
||||||
@ -72,7 +77,14 @@ function LogsNavigation({
|
|||||||
const index = newPages.findIndex((page) => page.queryRange.to === absoluteRange.to);
|
const index = newPages.findIndex((page) => page.queryRange.to === absoluteRange.to);
|
||||||
setCurrentPageIndex(index);
|
setCurrentPageIndex(index);
|
||||||
}
|
}
|
||||||
}, [visibleRange, absoluteRange, logsSortOrder, queries]);
|
addResultsToCache();
|
||||||
|
}, [visibleRange, absoluteRange, logsSortOrder, queries, clearCache, addResultsToCache]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => clearCache();
|
||||||
|
// We can't enforce the eslint rule here because we only want to run when component unmounts.
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
const changeTime = ({ from, to }: AbsoluteTimeRange) => {
|
const changeTime = ({ from, to }: AbsoluteTimeRange) => {
|
||||||
expectedRangeRef.current = { from, to };
|
expectedRangeRef.current = { from, to };
|
||||||
|
@ -37,6 +37,7 @@ const defaultInitialState = {
|
|||||||
label: 'Off',
|
label: 'Off',
|
||||||
value: 0,
|
value: 0,
|
||||||
},
|
},
|
||||||
|
cache: [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -238,6 +238,7 @@ export const paneReducer = (state: ExploreItemState = makeExplorePaneState(), ac
|
|||||||
datasourceMissing: !datasourceInstance,
|
datasourceMissing: !datasourceInstance,
|
||||||
queryResponse: createEmptyQueryResponse(),
|
queryResponse: createEmptyQueryResponse(),
|
||||||
logsHighlighterExpressions: undefined,
|
logsHighlighterExpressions: undefined,
|
||||||
|
cache: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
addQueryRowAction,
|
addQueryRowAction,
|
||||||
|
addResultsToCache,
|
||||||
|
clearCache,
|
||||||
cancelQueries,
|
cancelQueries,
|
||||||
cancelQueriesAction,
|
cancelQueriesAction,
|
||||||
queryReducer,
|
queryReducer,
|
||||||
@ -10,7 +12,17 @@ import {
|
|||||||
} from './query';
|
} from './query';
|
||||||
import { ExploreId, ExploreItemState } from 'app/types';
|
import { ExploreId, ExploreItemState } from 'app/types';
|
||||||
import { interval, of } from 'rxjs';
|
import { interval, of } from 'rxjs';
|
||||||
import { ArrayVector, DataQueryResponse, DefaultTimeZone, MutableDataFrame, RawTimeRange, toUtc } from '@grafana/data';
|
import {
|
||||||
|
ArrayVector,
|
||||||
|
DataQueryResponse,
|
||||||
|
DefaultTimeZone,
|
||||||
|
MutableDataFrame,
|
||||||
|
RawTimeRange,
|
||||||
|
toUtc,
|
||||||
|
PanelData,
|
||||||
|
DataFrame,
|
||||||
|
LoadingState,
|
||||||
|
} from '@grafana/data';
|
||||||
import { thunkTester } from 'test/core/thunk/thunkTester';
|
import { thunkTester } from 'test/core/thunk/thunkTester';
|
||||||
import { makeExplorePaneState } from './utils';
|
import { makeExplorePaneState } from './utils';
|
||||||
import { reducerTester } from '../../../../test/core/redux/reducerTester';
|
import { reducerTester } from '../../../../test/core/redux/reducerTester';
|
||||||
@ -50,6 +62,7 @@ const defaultInitialState = {
|
|||||||
label: 'Off',
|
label: 'Off',
|
||||||
value: 0,
|
value: 0,
|
||||||
},
|
},
|
||||||
|
cache: [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -213,4 +226,95 @@ describe('reducer', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('caching', () => {
|
||||||
|
it('should add response to cache', async () => {
|
||||||
|
const store = configureStore({
|
||||||
|
...(defaultInitialState as any),
|
||||||
|
explore: {
|
||||||
|
[ExploreId.left]: {
|
||||||
|
...defaultInitialState.explore[ExploreId.left],
|
||||||
|
queryResponse: {
|
||||||
|
series: [{ name: 'test name' }] as DataFrame[],
|
||||||
|
state: LoadingState.Done,
|
||||||
|
} as PanelData,
|
||||||
|
absoluteRange: { from: 1621348027000, to: 1621348050000 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await store.dispatch(addResultsToCache(ExploreId.left));
|
||||||
|
|
||||||
|
expect(store.getState().explore[ExploreId.left].cache).toEqual([
|
||||||
|
{ key: 'from=1621348027000&to=1621348050000', value: { series: [{ name: 'test name' }], state: 'Done' } },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not add response to cache if response is still loading', async () => {
|
||||||
|
const store = configureStore({
|
||||||
|
...(defaultInitialState as any),
|
||||||
|
explore: {
|
||||||
|
[ExploreId.left]: {
|
||||||
|
...defaultInitialState.explore[ExploreId.left],
|
||||||
|
queryResponse: { series: [{ name: 'test name' }] as DataFrame[], state: LoadingState.Loading } as PanelData,
|
||||||
|
absoluteRange: { from: 1621348027000, to: 1621348050000 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await store.dispatch(addResultsToCache(ExploreId.left));
|
||||||
|
|
||||||
|
expect(store.getState().explore[ExploreId.left].cache).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not add duplicate response to cache', async () => {
|
||||||
|
const store = configureStore({
|
||||||
|
...(defaultInitialState as any),
|
||||||
|
explore: {
|
||||||
|
[ExploreId.left]: {
|
||||||
|
...defaultInitialState.explore[ExploreId.left],
|
||||||
|
queryResponse: {
|
||||||
|
series: [{ name: 'test name' }] as DataFrame[],
|
||||||
|
state: LoadingState.Done,
|
||||||
|
} as PanelData,
|
||||||
|
absoluteRange: { from: 1621348027000, to: 1621348050000 },
|
||||||
|
cache: [
|
||||||
|
{
|
||||||
|
key: 'from=1621348027000&to=1621348050000',
|
||||||
|
value: { series: [{ name: 'old test name' }], state: LoadingState.Done },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await store.dispatch(addResultsToCache(ExploreId.left));
|
||||||
|
|
||||||
|
expect(store.getState().explore[ExploreId.left].cache).toHaveLength(1);
|
||||||
|
expect(store.getState().explore[ExploreId.left].cache).toEqual([
|
||||||
|
{ key: 'from=1621348027000&to=1621348050000', value: { series: [{ name: 'old test name' }], state: 'Done' } },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clear cache', async () => {
|
||||||
|
const store = configureStore({
|
||||||
|
...(defaultInitialState as any),
|
||||||
|
explore: {
|
||||||
|
[ExploreId.left]: {
|
||||||
|
...defaultInitialState.explore[ExploreId.left],
|
||||||
|
cache: [
|
||||||
|
{
|
||||||
|
key: 'from=1621348027000&to=1621348050000',
|
||||||
|
value: { series: [{ name: 'old test name' }], state: 'Done' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await store.dispatch(clearCache(ExploreId.left));
|
||||||
|
|
||||||
|
expect(store.getState().explore[ExploreId.left].cache).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { map, mergeMap, throttleTime } from 'rxjs/operators';
|
import { mergeMap, throttleTime } from 'rxjs/operators';
|
||||||
import { identity, Unsubscribable } from 'rxjs';
|
import { identity, Unsubscribable, of } from 'rxjs';
|
||||||
import {
|
import {
|
||||||
DataQuery,
|
DataQuery,
|
||||||
DataQueryErrorType,
|
DataQueryErrorType,
|
||||||
@ -27,19 +27,14 @@ import { ExploreId, QueryOptions } from 'app/types/explore';
|
|||||||
import { getTimeZone } from 'app/features/profile/state/selectors';
|
import { getTimeZone } from 'app/features/profile/state/selectors';
|
||||||
import { getShiftedTimeRange } from 'app/core/utils/timePicker';
|
import { getShiftedTimeRange } from 'app/core/utils/timePicker';
|
||||||
import { notifyApp } from '../../../core/actions';
|
import { notifyApp } from '../../../core/actions';
|
||||||
import { preProcessPanelData, runRequest } from '../../query/state/runRequest';
|
import { runRequest } from '../../query/state/runRequest';
|
||||||
import {
|
import { decorateData } from '../utils/decorators';
|
||||||
decorateWithFrameTypeMetadata,
|
|
||||||
decorateWithGraphResult,
|
|
||||||
decorateWithLogsResult,
|
|
||||||
decorateWithTableResult,
|
|
||||||
} from '../utils/decorators';
|
|
||||||
import { createErrorNotification } from '../../../core/copy/appNotification';
|
import { createErrorNotification } from '../../../core/copy/appNotification';
|
||||||
import { richHistoryUpdatedAction, stateSave } from './main';
|
import { richHistoryUpdatedAction, stateSave } from './main';
|
||||||
import { AnyAction, createAction, PayloadAction } from '@reduxjs/toolkit';
|
import { AnyAction, createAction, PayloadAction } from '@reduxjs/toolkit';
|
||||||
import { updateTime } from './time';
|
import { updateTime } from './time';
|
||||||
import { historyUpdatedAction } from './history';
|
import { historyUpdatedAction } from './history';
|
||||||
import { createEmptyQueryResponse } from './utils';
|
import { createEmptyQueryResponse, createCacheKey, getResultsFromCache } from './utils';
|
||||||
|
|
||||||
//
|
//
|
||||||
// Actions and Payloads
|
// Actions and Payloads
|
||||||
@ -164,6 +159,24 @@ export interface ScanStopPayload {
|
|||||||
}
|
}
|
||||||
export const scanStopAction = createAction<ScanStopPayload>('explore/scanStop');
|
export const scanStopAction = createAction<ScanStopPayload>('explore/scanStop');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds query results to cache.
|
||||||
|
* This is currently used to cache last 5 query results for log queries run from logs navigation (pagination).
|
||||||
|
*/
|
||||||
|
export interface AddResultsToCachePayload {
|
||||||
|
exploreId: ExploreId;
|
||||||
|
cacheKey: string;
|
||||||
|
queryResponse: PanelData;
|
||||||
|
}
|
||||||
|
export const addResultsToCacheAction = createAction<AddResultsToCachePayload>('explore/addResultsToCache');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears cache.
|
||||||
|
*/
|
||||||
|
export interface ClearCachePayload {
|
||||||
|
exploreId: ExploreId;
|
||||||
|
}
|
||||||
|
export const clearCacheAction = createAction<ClearCachePayload>('explore/clearCache');
|
||||||
//
|
//
|
||||||
// Action creators
|
// Action creators
|
||||||
//
|
//
|
||||||
@ -309,100 +322,115 @@ export const runQueries = (exploreId: ExploreId, options?: { replaceUrl?: boolea
|
|||||||
history,
|
history,
|
||||||
refreshInterval,
|
refreshInterval,
|
||||||
absoluteRange,
|
absoluteRange,
|
||||||
|
cache,
|
||||||
} = exploreItemState;
|
} = exploreItemState;
|
||||||
|
let newQuerySub;
|
||||||
|
|
||||||
if (!hasNonEmptyQuery(queries)) {
|
const cachedValue = getResultsFromCache(cache, absoluteRange);
|
||||||
dispatch(clearQueriesAction({ exploreId }));
|
|
||||||
dispatch(stateSave({ replace: options?.replaceUrl })); // Remember to save to state and update location
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!datasourceInstance) {
|
// If we have results saved in cache, we are going to use those results instead of running queries
|
||||||
return;
|
if (cachedValue) {
|
||||||
}
|
newQuerySub = of(cachedValue)
|
||||||
|
.pipe(mergeMap((data: PanelData) => decorateData(data, queryResponse, absoluteRange, refreshInterval, queries)))
|
||||||
// Some datasource's query builders allow per-query interval limits,
|
.subscribe((data) => {
|
||||||
// but we're using the datasource interval limit for now
|
if (!data.error) {
|
||||||
const minInterval = datasourceInstance?.interval;
|
dispatch(stateSave());
|
||||||
|
|
||||||
stopQueryState(querySubscription);
|
|
||||||
|
|
||||||
const datasourceId = datasourceInstance?.meta.id;
|
|
||||||
|
|
||||||
const queryOptions: QueryOptions = {
|
|
||||||
minInterval,
|
|
||||||
// maxDataPoints is used in:
|
|
||||||
// Loki - used for logs streaming for buffer size, with undefined it falls back to datasource config if it supports that.
|
|
||||||
// Elastic - limits the number of datapoints for the counts query and for logs it has hardcoded limit.
|
|
||||||
// Influx - used to correctly display logs in graph
|
|
||||||
// TODO:unification
|
|
||||||
// maxDataPoints: mode === ExploreMode.Logs && datasourceId === 'loki' ? undefined : containerWidth,
|
|
||||||
maxDataPoints: containerWidth,
|
|
||||||
liveStreaming: live,
|
|
||||||
};
|
|
||||||
|
|
||||||
const datasourceName = datasourceInstance.name;
|
|
||||||
const timeZone = getTimeZone(getState().user);
|
|
||||||
const transaction = buildQueryTransaction(queries, queryOptions, range, scanning, timeZone);
|
|
||||||
|
|
||||||
let firstResponse = true;
|
|
||||||
dispatch(changeLoadingStateAction({ exploreId, loadingState: LoadingState.Loading }));
|
|
||||||
|
|
||||||
const newQuerySub = runRequest(datasourceInstance, transaction.request)
|
|
||||||
.pipe(
|
|
||||||
// Simple throttle for live tailing, in case of > 1000 rows per interval we spend about 200ms on processing and
|
|
||||||
// rendering. In case this is optimized this can be tweaked, but also it should be only as fast as user
|
|
||||||
// actually can see what is happening.
|
|
||||||
live ? throttleTime(500) : identity,
|
|
||||||
map((data: PanelData) => preProcessPanelData(data, queryResponse)),
|
|
||||||
map(decorateWithFrameTypeMetadata),
|
|
||||||
map(decorateWithGraphResult),
|
|
||||||
map(decorateWithLogsResult({ absoluteRange, refreshInterval, queries })),
|
|
||||||
mergeMap(decorateWithTableResult)
|
|
||||||
)
|
|
||||||
.subscribe(
|
|
||||||
(data) => {
|
|
||||||
if (!data.error && firstResponse) {
|
|
||||||
// Side-effect: Saving history in localstorage
|
|
||||||
const nextHistory = updateHistory(history, datasourceId, queries);
|
|
||||||
const nextRichHistory = addToRichHistory(
|
|
||||||
richHistory || [],
|
|
||||||
datasourceId,
|
|
||||||
datasourceName,
|
|
||||||
queries,
|
|
||||||
false,
|
|
||||||
'',
|
|
||||||
''
|
|
||||||
);
|
|
||||||
dispatch(historyUpdatedAction({ exploreId, history: nextHistory }));
|
|
||||||
dispatch(richHistoryUpdatedAction({ richHistory: nextRichHistory }));
|
|
||||||
|
|
||||||
// We save queries to the URL here so that only successfully run queries change the URL.
|
|
||||||
dispatch(stateSave({ replace: options?.replaceUrl }));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
firstResponse = false;
|
|
||||||
|
|
||||||
dispatch(queryStreamUpdatedAction({ exploreId, response: data }));
|
dispatch(queryStreamUpdatedAction({ exploreId, response: data }));
|
||||||
|
});
|
||||||
|
|
||||||
// Keep scanning for results if this was the last scanning transaction
|
// If we don't have resuls saved in cache, run new queries
|
||||||
if (getState().explore[exploreId]!.scanning) {
|
} else {
|
||||||
if (data.state === LoadingState.Done && data.series.length === 0) {
|
if (!hasNonEmptyQuery(queries)) {
|
||||||
const range = getShiftedTimeRange(-1, getState().explore[exploreId]!.range);
|
dispatch(clearQueriesAction({ exploreId }));
|
||||||
dispatch(updateTime({ exploreId, absoluteRange: range }));
|
dispatch(stateSave({ replace: options?.replaceUrl })); // Remember to save to state and update location
|
||||||
dispatch(runQueries(exploreId));
|
return;
|
||||||
} else {
|
}
|
||||||
// We can stop scanning if we have a result
|
|
||||||
dispatch(scanStopAction({ exploreId }));
|
if (!datasourceInstance) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some datasource's query builders allow per-query interval limits,
|
||||||
|
// but we're using the datasource interval limit for now
|
||||||
|
const minInterval = datasourceInstance?.interval;
|
||||||
|
|
||||||
|
stopQueryState(querySubscription);
|
||||||
|
|
||||||
|
const datasourceId = datasourceInstance?.meta.id;
|
||||||
|
|
||||||
|
const queryOptions: QueryOptions = {
|
||||||
|
minInterval,
|
||||||
|
// maxDataPoints is used in:
|
||||||
|
// Loki - used for logs streaming for buffer size, with undefined it falls back to datasource config if it supports that.
|
||||||
|
// Elastic - limits the number of datapoints for the counts query and for logs it has hardcoded limit.
|
||||||
|
// Influx - used to correctly display logs in graph
|
||||||
|
// TODO:unification
|
||||||
|
// maxDataPoints: mode === ExploreMode.Logs && datasourceId === 'loki' ? undefined : containerWidth,
|
||||||
|
maxDataPoints: containerWidth,
|
||||||
|
liveStreaming: live,
|
||||||
|
};
|
||||||
|
|
||||||
|
const datasourceName = datasourceInstance.name;
|
||||||
|
const timeZone = getTimeZone(getState().user);
|
||||||
|
const transaction = buildQueryTransaction(queries, queryOptions, range, scanning, timeZone);
|
||||||
|
|
||||||
|
let firstResponse = true;
|
||||||
|
dispatch(changeLoadingStateAction({ exploreId, loadingState: LoadingState.Loading }));
|
||||||
|
|
||||||
|
newQuerySub = runRequest(datasourceInstance, transaction.request)
|
||||||
|
.pipe(
|
||||||
|
// Simple throttle for live tailing, in case of > 1000 rows per interval we spend about 200ms on processing and
|
||||||
|
// rendering. In case this is optimized this can be tweaked, but also it should be only as fast as user
|
||||||
|
// actually can see what is happening.
|
||||||
|
live ? throttleTime(500) : identity,
|
||||||
|
mergeMap((data: PanelData) => decorateData(data, queryResponse, absoluteRange, refreshInterval, queries))
|
||||||
|
)
|
||||||
|
.subscribe(
|
||||||
|
(data) => {
|
||||||
|
if (!data.error && firstResponse) {
|
||||||
|
// Side-effect: Saving history in localstorage
|
||||||
|
const nextHistory = updateHistory(history, datasourceId, queries);
|
||||||
|
const nextRichHistory = addToRichHistory(
|
||||||
|
richHistory || [],
|
||||||
|
datasourceId,
|
||||||
|
datasourceName,
|
||||||
|
queries,
|
||||||
|
false,
|
||||||
|
'',
|
||||||
|
''
|
||||||
|
);
|
||||||
|
dispatch(historyUpdatedAction({ exploreId, history: nextHistory }));
|
||||||
|
dispatch(richHistoryUpdatedAction({ richHistory: nextRichHistory }));
|
||||||
|
|
||||||
|
// We save queries to the URL here so that only successfully run queries change the URL.
|
||||||
|
dispatch(stateSave({ replace: options?.replaceUrl }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
firstResponse = false;
|
||||||
|
|
||||||
|
dispatch(queryStreamUpdatedAction({ exploreId, response: data }));
|
||||||
|
|
||||||
|
// Keep scanning for results if this was the last scanning transaction
|
||||||
|
if (getState().explore[exploreId]!.scanning) {
|
||||||
|
if (data.state === LoadingState.Done && data.series.length === 0) {
|
||||||
|
const range = getShiftedTimeRange(-1, getState().explore[exploreId]!.range);
|
||||||
|
dispatch(updateTime({ exploreId, absoluteRange: range }));
|
||||||
|
dispatch(runQueries(exploreId));
|
||||||
|
} else {
|
||||||
|
// We can stop scanning if we have a result
|
||||||
|
dispatch(scanStopAction({ exploreId }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
dispatch(notifyApp(createErrorNotification('Query processing error', error)));
|
||||||
|
dispatch(changeLoadingStateAction({ exploreId, loadingState: LoadingState.Error }));
|
||||||
|
console.error(error);
|
||||||
}
|
}
|
||||||
},
|
);
|
||||||
(error) => {
|
}
|
||||||
dispatch(notifyApp(createErrorNotification('Query processing error', error)));
|
|
||||||
dispatch(changeLoadingStateAction({ exploreId, loadingState: LoadingState.Error }));
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
dispatch(queryStoreSubscriptionAction({ exploreId, querySubscription: newQuerySub }));
|
dispatch(queryStoreSubscriptionAction({ exploreId, querySubscription: newQuerySub }));
|
||||||
};
|
};
|
||||||
@ -439,6 +467,25 @@ export function scanStart(exploreId: ExploreId): ThunkResult<void> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function addResultsToCache(exploreId: ExploreId): ThunkResult<void> {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const queryResponse = getState().explore[exploreId]!.queryResponse;
|
||||||
|
const absoluteRange = getState().explore[exploreId]!.absoluteRange;
|
||||||
|
const cacheKey = createCacheKey(absoluteRange);
|
||||||
|
|
||||||
|
// Save results to cache only when all results recived and loading is done
|
||||||
|
if (queryResponse.state === LoadingState.Done) {
|
||||||
|
dispatch(addResultsToCacheAction({ exploreId, cacheKey, queryResponse }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearCache(exploreId: ExploreId): ThunkResult<void> {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
dispatch(clearCacheAction({ exploreId }));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Reducer
|
// Reducer
|
||||||
//
|
//
|
||||||
@ -629,6 +676,32 @@ export const queryReducer = (state: ExploreItemState, action: AnyAction): Explor
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (addResultsToCacheAction.match(action)) {
|
||||||
|
const CACHE_LIMIT = 5;
|
||||||
|
const { cache } = state;
|
||||||
|
const { queryResponse, cacheKey } = action.payload;
|
||||||
|
|
||||||
|
let newCache = [...cache];
|
||||||
|
const isDuplicateKey = newCache.some((c) => c.key === cacheKey);
|
||||||
|
|
||||||
|
if (!isDuplicateKey) {
|
||||||
|
const newCacheItem = { key: cacheKey, value: queryResponse };
|
||||||
|
newCache = [newCacheItem, ...newCache].slice(0, CACHE_LIMIT);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
cache: newCache,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clearCacheAction.match(action)) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
cache: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
HistoryItem,
|
HistoryItem,
|
||||||
LoadingState,
|
LoadingState,
|
||||||
PanelData,
|
PanelData,
|
||||||
|
AbsoluteTimeRange,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
|
|
||||||
import { ExploreItemState } from 'app/types/explore';
|
import { ExploreItemState } from 'app/types/explore';
|
||||||
@ -49,6 +50,7 @@ export const makeExplorePaneState = (): ExploreItemState => ({
|
|||||||
graphResult: null,
|
graphResult: null,
|
||||||
logsResult: null,
|
logsResult: null,
|
||||||
eventBridge: (null as unknown) as EventBusExtended,
|
eventBridge: (null as unknown) as EventBusExtended,
|
||||||
|
cache: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
export const createEmptyQueryResponse = (): PanelData => ({
|
export const createEmptyQueryResponse = (): PanelData => ({
|
||||||
@ -96,3 +98,25 @@ export function getUrlStateFromPaneState(pane: ExploreItemState): ExploreUrlStat
|
|||||||
range: toRawTimeRange(pane.range),
|
range: toRawTimeRange(pane.range),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createCacheKey(absRange: AbsoluteTimeRange) {
|
||||||
|
const params = {
|
||||||
|
from: absRange.from,
|
||||||
|
to: absRange.to,
|
||||||
|
};
|
||||||
|
|
||||||
|
const cacheKey = Object.entries(params)
|
||||||
|
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v.toString())}`)
|
||||||
|
.join('&');
|
||||||
|
return cacheKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getResultsFromCache(
|
||||||
|
cache: Array<{ key: string; value: PanelData }>,
|
||||||
|
absoluteRange: AbsoluteTimeRange
|
||||||
|
): PanelData | undefined {
|
||||||
|
const cacheKey = createCacheKey(absoluteRange);
|
||||||
|
const cacheIdx = cache.findIndex((c) => c.key === cacheKey);
|
||||||
|
const cacheValue = cacheIdx >= 0 ? cache[cacheIdx].value : undefined;
|
||||||
|
return cacheValue;
|
||||||
|
}
|
||||||
|
@ -11,10 +11,11 @@ import {
|
|||||||
import { config } from '@grafana/runtime';
|
import { config } from '@grafana/runtime';
|
||||||
import { groupBy } from 'lodash';
|
import { groupBy } from 'lodash';
|
||||||
import { Observable, of } from 'rxjs';
|
import { Observable, of } from 'rxjs';
|
||||||
import { map } from 'rxjs/operators';
|
import { map, mergeMap } from 'rxjs/operators';
|
||||||
import { dataFrameToLogsModel } from '../../../core/logs_model';
|
import { dataFrameToLogsModel } from '../../../core/logs_model';
|
||||||
import { refreshIntervalToSortOrder } from '../../../core/utils/explore';
|
import { refreshIntervalToSortOrder } from '../../../core/utils/explore';
|
||||||
import { ExplorePanelData } from '../../../types';
|
import { ExplorePanelData } from '../../../types';
|
||||||
|
import { preProcessPanelData } from '../../query/state/runRequest';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When processing response first we try to determine what kind of dataframes we got as one query can return multiple
|
* When processing response first we try to determine what kind of dataframes we got as one query can return multiple
|
||||||
@ -154,6 +155,23 @@ export const decorateWithLogsResult = (
|
|||||||
return { ...data, logsResult };
|
return { ...data, logsResult };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// decorateData applies all decorators
|
||||||
|
export function decorateData(
|
||||||
|
data: PanelData,
|
||||||
|
queryResponse: PanelData,
|
||||||
|
absoluteRange: AbsoluteTimeRange,
|
||||||
|
refreshInterval: string | undefined,
|
||||||
|
queries: DataQuery[] | undefined
|
||||||
|
): Observable<ExplorePanelData> {
|
||||||
|
return of(data).pipe(
|
||||||
|
map((data: PanelData) => preProcessPanelData(data, queryResponse)),
|
||||||
|
map(decorateWithFrameTypeMetadata),
|
||||||
|
map(decorateWithGraphResult),
|
||||||
|
map(decorateWithLogsResult({ absoluteRange, refreshInterval, queries })),
|
||||||
|
mergeMap(decorateWithTableResult)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if frame contains time series, which for our purpose means 1 time column and 1 or more numeric columns.
|
* Check if frame contains time series, which for our purpose means 1 time column and 1 or more numeric columns.
|
||||||
*/
|
*/
|
||||||
|
@ -149,6 +149,13 @@ export interface ExploreItemState {
|
|||||||
showTable?: boolean;
|
showTable?: boolean;
|
||||||
showTrace?: boolean;
|
showTrace?: boolean;
|
||||||
showNodeGraph?: boolean;
|
showNodeGraph?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We are using caching to store query responses of queries run from logs navigation.
|
||||||
|
* In logs navigation, we do pagination and we don't want our users to unnecessarily run the same queries that they've run just moments before.
|
||||||
|
* We are currently caching last 5 query responses.
|
||||||
|
*/
|
||||||
|
cache: Array<{ key: string; value: PanelData }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExploreUpdateState {
|
export interface ExploreUpdateState {
|
||||||
|
Loading…
Reference in New Issue
Block a user