Loki: Logs volume improvements (#40327)

* Skip logfmt errors

* Run volume queries as range queries

* Remove auto-skipping parsing errors

* Remove auto-skipping parsing errors

* Cache logs volume results

* Remove auto-loading

* Fix tests

* Clean up

* Disable logs volume in live streaming

* Clean up

* Add logs volume timeout

* Switch tooltip mode

* Increase timeout to 15s

* Fix test

* Change timeout to 10 seconds

* Remove unused code

* Extract styles

* Remove info about zoom level
pull/40564/head
Piotr Jamróz 4 years ago committed by GitHub
parent 52b69a9a80
commit 8939636492
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      public/app/features/explore/Explore.test.tsx
  2. 48
      public/app/features/explore/Explore.tsx
  3. 3
      public/app/features/explore/ExploreToolbar.tsx
  4. 15
      public/app/features/explore/LogsVolumePanel.test.tsx
  5. 117
      public/app/features/explore/LogsVolumePanel.tsx
  6. 1
      public/app/features/explore/QueryRows.test.tsx
  7. 10
      public/app/features/explore/SecondaryActions.test.tsx
  8. 12
      public/app/features/explore/SecondaryActions.tsx
  9. 16
      public/app/features/explore/Wrapper.tsx
  10. 2
      public/app/features/explore/__snapshots__/Explore.test.tsx.snap
  11. 2
      public/app/features/explore/state/datasource.ts
  12. 18
      public/app/features/explore/state/main.ts
  13. 59
      public/app/features/explore/state/query.test.ts
  14. 76
      public/app/features/explore/state/query.ts
  15. 1
      public/app/plugins/datasource/loki/dataProviders/logsVolumeProvider.test.ts
  16. 69
      public/app/plugins/datasource/loki/dataProviders/logsVolumeProvider.ts
  17. 5
      public/app/types/explore.ts

@ -83,11 +83,9 @@ const dummyProps: Props = {
showTrace: true,
showNodeGraph: true,
splitOpen: (() => {}) as any,
autoLoadLogsVolume: false,
logsVolumeData: undefined,
logsVolumeDataProvider: undefined,
loadLogsVolumeData: () => {},
changeAutoLogsVolume: () => {},
};
describe('Explore', () => {

@ -6,7 +6,15 @@ import AutoSizer from 'react-virtualized-auto-sizer';
import memoizeOne from 'memoize-one';
import { selectors } from '@grafana/e2e-selectors';
import { Collapse, CustomScrollbar, ErrorBoundaryAlert, Themeable2, withTheme2 } from '@grafana/ui';
import { AbsoluteTimeRange, DataFrame, DataQuery, GrafanaTheme2, LoadingState, RawTimeRange } from '@grafana/data';
import {
AbsoluteTimeRange,
DataFrame,
DataQuery,
GrafanaTheme2,
hasLogsVolumeSupport,
LoadingState,
RawTimeRange,
} from '@grafana/data';
import LogsContainer from './LogsContainer';
import { QueryRows } from './QueryRows';
@ -16,15 +24,7 @@ import ExploreQueryInspector from './ExploreQueryInspector';
import { splitOpen } from './state/main';
import { changeSize } from './state/explorePane';
import { updateTimeRange } from './state/time';
import {
addQueryRow,
changeAutoLogsVolume,
loadLogsVolumeData,
modifyQueries,
scanStart,
scanStopAction,
setQueries,
} from './state/query';
import { addQueryRow, loadLogsVolumeData, modifyQueries, scanStart, scanStopAction, setQueries } from './state/query';
import { ExploreId, ExploreItemState } from 'app/types/explore';
import { StoreState } from 'app/types';
import { ExploreToolbar } from './ExploreToolbar';
@ -215,31 +215,17 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
}
renderLogsVolume(width: number) {
const {
logsVolumeData,
exploreId,
loadLogsVolumeData,
autoLoadLogsVolume,
changeAutoLogsVolume,
absoluteRange,
timeZone,
splitOpen,
} = this.props;
const { logsVolumeData, exploreId, loadLogsVolumeData, absoluteRange, timeZone, splitOpen } = this.props;
return (
<LogsVolumePanel
exploreId={exploreId}
loadLogsVolumeData={loadLogsVolumeData}
absoluteRange={absoluteRange}
width={width}
logsVolumeData={logsVolumeData}
onUpdateTimeRange={this.onUpdateTimeRange}
timeZone={timeZone}
splitOpen={splitOpen}
autoLoadLogsVolume={autoLoadLogsVolume}
onChangeAutoLogsVolume={(autoLoadLogsVolume) => {
changeAutoLogsVolume(exploreId, autoLoadLogsVolume);
}}
onLoadLogsVolume={() => loadLogsVolumeData(exploreId)}
/>
);
}
@ -317,13 +303,13 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
showTrace,
showNodeGraph,
logsVolumeDataProvider,
loadLogsVolumeData,
} = this.props;
const { openDrawer } = this.state;
const styles = getStyles(theme);
const showPanels = queryResponse && queryResponse.state !== LoadingState.NotStarted;
const showRichHistory = openDrawer === ExploreDrawer.RichHistory;
const showQueryInspector = openDrawer === ExploreDrawer.QueryInspector;
const showLogsVolume = !!logsVolumeDataProvider;
return (
<CustomScrollbar autoHeightMin={'100%'}>
@ -340,9 +326,11 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
addQueryRowButtonHidden={false}
richHistoryButtonActive={showRichHistory}
queryInspectorButtonActive={showQueryInspector}
loadingLogsVolumeAvailable={hasLogsVolumeSupport(datasourceInstance) && !!logsVolumeDataProvider}
onClickAddQueryRowButton={this.onClickAddQueryRowButton}
onClickRichHistoryButton={this.toggleShowRichHistory}
onClickQueryInspectorButton={this.toggleShowQueryInspector}
onClickLoadLogsVolume={() => loadLogsVolumeData(exploreId)}
/>
<ResponseErrorContainer exploreId={exploreId} />
</div>
@ -360,7 +348,7 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
{showMetrics && graphResult && (
<ErrorBoundaryAlert>{this.renderGraphPanel(width)}</ErrorBoundaryAlert>
)}
{showLogsVolume && <ErrorBoundaryAlert>{this.renderLogsVolume(width)}</ErrorBoundaryAlert>}
{<ErrorBoundaryAlert>{this.renderLogsVolume(width)}</ErrorBoundaryAlert>}
{showTable && <ErrorBoundaryAlert>{this.renderTablePanel(width)}</ErrorBoundaryAlert>}
{showLogs && <ErrorBoundaryAlert>{this.renderLogsPanel(width)}</ErrorBoundaryAlert>}
{showNodeGraph && <ErrorBoundaryAlert>{this.renderNodeGraphPanel()}</ErrorBoundaryAlert>}
@ -395,7 +383,7 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
function mapStateToProps(state: StoreState, { exploreId }: ExploreProps) {
const explore = state.explore;
const { syncedTimes, autoLoadLogsVolume } = explore;
const { syncedTimes } = explore;
const item: ExploreItemState = explore[exploreId]!;
const timeZone = getTimeZone(state.user);
const {
@ -423,7 +411,6 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps) {
queryKeys,
isLive,
graphResult,
autoLoadLogsVolume,
logsVolumeDataProvider,
logsVolumeData,
logsResult: logsResult ?? undefined,
@ -448,7 +435,6 @@ const mapDispatchToProps = {
setQueries,
updateTimeRange,
loadLogsVolumeData,
changeAutoLogsVolume,
addQueryRow,
splitOpen,
};

@ -213,7 +213,7 @@ export class UnConnectedExploreToolbar extends PureComponent<Props> {
}
const mapStateToProps = (state: StoreState, { exploreId }: OwnProps) => {
const { syncedTimes, autoLoadLogsVolume } = state.explore;
const { syncedTimes } = state.explore;
const exploreItem: ExploreItemState = state.explore[exploreId]!;
const {
datasourceInstance,
@ -242,7 +242,6 @@ const mapStateToProps = (state: StoreState, { exploreId }: OwnProps) => {
isPaused,
syncedTimes,
containerWidth,
autoLoadLogsVolume,
};
};

@ -1,7 +1,6 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { LogsVolumePanel } from './LogsVolumePanel';
import { ExploreId } from '../../types';
import { DataQueryResponse, LoadingState } from '@grafana/data';
jest.mock('./ExploreGraph', () => {
@ -14,16 +13,13 @@ jest.mock('./ExploreGraph', () => {
function renderPanel(logsVolumeData?: DataQueryResponse) {
render(
<LogsVolumePanel
exploreId={ExploreId.left}
loadLogsVolumeData={() => {}}
absoluteRange={{ from: 0, to: 1 }}
timeZone="timeZone"
splitOpen={() => {}}
width={100}
onUpdateTimeRange={() => {}}
logsVolumeData={logsVolumeData}
autoLoadLogsVolume={false}
onChangeAutoLogsVolume={() => {}}
onLoadLogsVolume={() => {}}
/>
);
}
@ -45,12 +41,13 @@ describe('LogsVolumePanel', () => {
});
it('shows error message', () => {
renderPanel({ state: LoadingState.Error, error: { data: { message: 'Error message' } }, data: [] });
expect(screen.getByText('Failed to load volume logs for this query: Error message')).toBeInTheDocument();
renderPanel({ state: LoadingState.Error, error: { data: { message: 'Test error message' } }, data: [] });
expect(screen.getByText('Failed to load volume logs for this query')).toBeInTheDocument();
expect(screen.getByText('Test error message')).toBeInTheDocument();
});
it('shows button to load logs volume', () => {
it('does not show the panel when there is no volume data', () => {
renderPanel(undefined);
expect(screen.getByText('Load logs volume')).toBeInTheDocument();
expect(screen.queryByText('Logs volume')).not.toBeInTheDocument();
});
});

@ -1,58 +1,35 @@
import { AbsoluteTimeRange, DataQueryResponse, LoadingState, SplitOpen, TimeZone } from '@grafana/data';
import { Button, Collapse, InlineField, InlineFieldRow, InlineSwitch, useTheme2 } from '@grafana/ui';
import { AbsoluteTimeRange, DataQueryResponse, GrafanaTheme2, LoadingState, SplitOpen, TimeZone } from '@grafana/data';
import { Alert, Button, Collapse, TooltipDisplayMode, useStyles2, useTheme2 } from '@grafana/ui';
import { ExploreGraph } from './ExploreGraph';
import React, { useCallback } from 'react';
import { ExploreId } from '../../types';
import React from 'react';
import { css } from '@emotion/css';
type Props = {
exploreId: ExploreId;
loadLogsVolumeData: (exploreId: ExploreId) => void;
logsVolumeData?: DataQueryResponse;
absoluteRange: AbsoluteTimeRange;
timeZone: TimeZone;
splitOpen: SplitOpen;
width: number;
onUpdateTimeRange: (timeRange: AbsoluteTimeRange) => void;
autoLoadLogsVolume: boolean;
onChangeAutoLogsVolume: (value: boolean) => void;
onLoadLogsVolume: () => void;
};
export function LogsVolumePanel(props: Props) {
const {
width,
logsVolumeData,
exploreId,
loadLogsVolumeData,
absoluteRange,
timeZone,
splitOpen,
onUpdateTimeRange,
autoLoadLogsVolume,
onChangeAutoLogsVolume,
} = props;
const { width, logsVolumeData, absoluteRange, timeZone, splitOpen, onUpdateTimeRange, onLoadLogsVolume } = props;
const theme = useTheme2();
const styles = useStyles2(getStyles);
const spacing = parseInt(theme.spacing(2).slice(0, -2), 10);
const height = 150;
let LogsVolumePanelContent;
if (!logsVolumeData) {
LogsVolumePanelContent = (
<Button
onClick={() => {
loadLogsVolumeData(exploreId);
}}
>
Load logs volume
</Button>
);
return null;
} else if (logsVolumeData?.error) {
LogsVolumePanelContent = (
<span>
Failed to load volume logs for this query:{' '}
{logsVolumeData.error.data?.message || logsVolumeData.error.statusText}
</span>
return (
<Alert title="Failed to load volume logs for this query">
{logsVolumeData.error.data?.message || logsVolumeData.error.statusText || logsVolumeData.error.message}
</Alert>
);
} else if (logsVolumeData?.state === LoadingState.Loading) {
LogsVolumePanelContent = <span>Logs volume is loading...</span>;
@ -68,6 +45,7 @@ export function LogsVolumePanel(props: Props) {
onChangeTime={onUpdateTimeRange}
timeZone={timeZone}
splitOpenFn={splitOpen}
tooltipDisplayMode={TooltipDisplayMode.Multi}
/>
);
} else {
@ -75,40 +53,53 @@ export function LogsVolumePanel(props: Props) {
}
}
const handleOnChangeAutoLogsVolume = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
const { target } = event;
if (target) {
onChangeAutoLogsVolume(target.checked);
}
},
[onChangeAutoLogsVolume]
);
const zoomRatio = logsLevelZoomRatio(logsVolumeData, absoluteRange);
let zoomLevelInfo;
if (zoomRatio !== undefined && zoomRatio < 1) {
zoomLevelInfo = (
<>
<span className={styles.zoomInfo}>Reload to show higher resolution</span>
<Button size="xs" icon="sync" variant="secondary" onClick={onLoadLogsVolume} />
</>
);
}
return (
<Collapse label="Logs volume" isOpen={true} loading={logsVolumeData?.state === LoadingState.Loading}>
<div
style={{ height }}
className={css({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
})}
>
<div style={{ height }} className={styles.contentContainer}>
{LogsVolumePanelContent}
</div>
<div
className={css({
display: 'flex',
justifyContent: 'end',
})}
>
<InlineFieldRow>
<InlineField label="Auto-load logs volume" transparent>
<InlineSwitch value={autoLoadLogsVolume} onChange={handleOnChangeAutoLogsVolume} transparent />
</InlineField>
</InlineFieldRow>
</div>
<div className={styles.zoomInfoContainer}>{zoomLevelInfo}</div>
</Collapse>
);
}
const getStyles = (theme: GrafanaTheme2) => {
return {
zoomInfoContainer: css`
display: flex;
justify-content: end;
position: absolute;
right: 5px;
top: 5px;
`,
zoomInfo: css`
padding: 8px;
font-size: ${theme.typography.bodySmall.fontSize};
`,
contentContainer: css`
display: flex;
align-items: center;
justify-content: center;
`,
};
};
function logsLevelZoomRatio(
logsVolumeData: DataQueryResponse | undefined,
selectedTimeRange: AbsoluteTimeRange
): number | undefined {
const dataRange = logsVolumeData && logsVolumeData.data[0] && logsVolumeData.data[0].meta?.custom?.absoluteRange;
return dataRange ? (selectedTimeRange.from - selectedTimeRange.to) / (dataRange.from - dataRange.to) : undefined;
}

@ -48,7 +48,6 @@ function setup(queries: DataQuery[]) {
syncedTimes: false,
right: undefined,
richHistory: [],
autoLoadLogsVolume: false,
localStorageFull: false,
richHistoryLimitExceededWarningShown: false,
};

@ -6,6 +6,7 @@ import { SecondaryActions } from './SecondaryActions';
const addQueryRowButtonSelector = '[aria-label="Add row button"]';
const richHistoryButtonSelector = '[aria-label="Rich history button"]';
const queryInspectorButtonSelector = '[aria-label="Query inspector button"]';
const onClickLoadLogsVolumeSelector = '[aria-label="Load logs volume button"]';
describe('SecondaryActions', () => {
it('should render component two buttons', () => {
@ -14,6 +15,7 @@ describe('SecondaryActions', () => {
onClickAddQueryRowButton={noop}
onClickRichHistoryButton={noop}
onClickQueryInspectorButton={noop}
onClickLoadLogsVolume={noop}
/>
);
expect(wrapper.find(addQueryRowButtonSelector)).toHaveLength(1);
@ -27,6 +29,7 @@ describe('SecondaryActions', () => {
onClickAddQueryRowButton={noop}
onClickRichHistoryButton={noop}
onClickQueryInspectorButton={noop}
onClickLoadLogsVolume={noop}
/>
);
expect(wrapper.find(addQueryRowButtonSelector)).toHaveLength(0);
@ -40,6 +43,7 @@ describe('SecondaryActions', () => {
onClickAddQueryRowButton={noop}
onClickRichHistoryButton={noop}
onClickQueryInspectorButton={noop}
onClickLoadLogsVolume={noop}
/>
);
expect(wrapper.find(addQueryRowButtonSelector).props().disabled).toBe(true);
@ -49,11 +53,14 @@ describe('SecondaryActions', () => {
const onClickAddRow = jest.fn();
const onClickHistory = jest.fn();
const onClickQueryInspector = jest.fn();
const onClickLoadLogsVolumeInspector = jest.fn();
const wrapper = shallow(
<SecondaryActions
onClickAddQueryRowButton={onClickAddRow}
onClickRichHistoryButton={onClickHistory}
onClickQueryInspectorButton={onClickQueryInspector}
loadingLogsVolumeAvailable={true}
onClickLoadLogsVolume={onClickLoadLogsVolumeInspector}
/>
);
@ -65,5 +72,8 @@ describe('SecondaryActions', () => {
wrapper.find(queryInspectorButtonSelector).simulate('click');
expect(onClickQueryInspector).toBeCalled();
wrapper.find(onClickLoadLogsVolumeSelector).simulate('click');
expect(onClickQueryInspector).toBeCalled();
});
});

@ -8,10 +8,12 @@ type Props = {
addQueryRowButtonHidden?: boolean;
richHistoryButtonActive?: boolean;
queryInspectorButtonActive?: boolean;
loadingLogsVolumeAvailable?: boolean;
onClickAddQueryRowButton: () => void;
onClickRichHistoryButton: () => void;
onClickQueryInspectorButton: () => void;
onClickLoadLogsVolume: () => void;
};
const getStyles = (theme: GrafanaTheme2) => {
@ -56,6 +58,16 @@ export function SecondaryActions(props: Props) {
>
Inspector
</Button>
{props.loadingLogsVolumeAvailable && (
<Button
variant="secondary"
aria-label="Load logs volume button"
onClick={props.onClickLoadLogsVolume}
icon="graph-bar"
>
Load logs volume
</Button>
)}
</HorizontalGroup>
</div>
);

@ -2,13 +2,7 @@ import React, { PureComponent } from 'react';
import { connect, ConnectedProps } from 'react-redux';
import { ExploreId, ExploreQueryParams } from 'app/types/explore';
import { ErrorBoundaryAlert } from '@grafana/ui';
import {
AUTO_LOAD_LOGS_VOLUME_SETTING_KEY,
lastSavedUrl,
resetExploreAction,
richHistoryUpdatedAction,
storeAutoLoadLogsVolumeAction,
} from './state/main';
import { lastSavedUrl, resetExploreAction, richHistoryUpdatedAction } from './state/main';
import { getRichHistory } from '../../core/utils/richHistory';
import { ExplorePaneContainer } from './ExplorePaneContainer';
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
@ -16,7 +10,6 @@ import { Branding } from '../../core/components/Branding/Branding';
import { getNavModel } from '../../core/selectors/navModel';
import { StoreState } from 'app/types';
import store from '../../core/store';
interface RouteProps extends GrafanaRouteComponentProps<{}, ExploreQueryParams> {}
interface OwnProps {}
@ -25,14 +18,12 @@ const mapStateToProps = (state: StoreState) => {
return {
navModel: getNavModel(state.navIndex, 'explore'),
exploreState: state.explore,
autoLoadLogsVolume: state.explore.autoLoadLogsVolume,
};
};
const mapDispatchToProps = {
resetExploreAction,
richHistoryUpdatedAction,
storeAutoLoadLogsVolumeAction,
};
const connector = connect(mapStateToProps, mapDispatchToProps);
@ -52,7 +43,6 @@ class WrapperUnconnected extends PureComponent<Props> {
}
componentDidUpdate(prevProps: Props) {
const { autoLoadLogsVolume } = this.props;
const { left, right } = this.props.queryParams;
const hasSplit = Boolean(left) && Boolean(right);
const datasourceTitle = hasSplit
@ -60,10 +50,6 @@ class WrapperUnconnected extends PureComponent<Props> {
: `${this.props.exploreState.left.datasourceInstance?.name}`;
const documentTitle = `${this.props.navModel.main.text} - ${datasourceTitle} - ${Branding.AppTitle}`;
document.title = documentTitle;
if (prevProps.autoLoadLogsVolume !== autoLoadLogsVolume) {
store.set(AUTO_LOAD_LOGS_VOLUME_SETTING_KEY, autoLoadLogsVolume);
}
}
render() {

@ -20,7 +20,9 @@ exports[`Explore should render component 1`] = `
<SecondaryActions
addQueryRowButtonDisabled={false}
addQueryRowButtonHidden={false}
loadingLogsVolumeAvailable={false}
onClickAddQueryRowButton={[Function]}
onClickLoadLogsVolume={[Function]}
onClickQueryInspectorButton={[Function]}
onClickRichHistoryButton={[Function]}
queryInspectorButtonActive={false}

@ -92,6 +92,8 @@ export const datasourceReducer = (state: ExploreItemState, action: AnyAction): E
graphResult: null,
tableResult: null,
logsResult: null,
logsVolumeDataProvider: undefined,
logsVolumeData: undefined,
queryResponse: createEmptyQueryResponse(),
loading: false,
queryKeys: [],

@ -9,7 +9,6 @@ import { getUrlStateFromPaneState, makeExplorePaneState } from './utils';
import { ThunkResult } from '../../../types';
import { TimeSrv } from '../../dashboard/services/TimeSrv';
import { PanelModel } from 'app/features/dashboard/state';
import store from '../../../core/store';
//
// Actions and Payloads
@ -24,12 +23,6 @@ export const richHistoryUpdatedAction = createAction<any>('explore/richHistoryUp
export const localStorageFullAction = createAction('explore/localStorageFullAction');
export const richHistoryLimitExceededAction = createAction('explore/richHistoryLimitExceededAction');
/**
* Stores new value of auto-load logs volume switch. Used only internally. changeAutoLogsVolume() is used to
* update auto-load and load logs volume if it hasn't been loaded.
*/
export const storeAutoLoadLogsVolumeAction = createAction<boolean>('explore/storeAutoLoadLogsVolumeAction');
/**
* Resets state for explore.
*/
@ -163,8 +156,6 @@ export const navigateToExplore = (
};
};
export const AUTO_LOAD_LOGS_VOLUME_SETTING_KEY = 'grafana.explore.logs.autoLoadLogsVolume';
/**
* Global Explore state that handles multiple Explore areas and the split state
*/
@ -176,7 +167,6 @@ export const initialExploreState: ExploreState = {
richHistory: [],
localStorageFull: false,
richHistoryLimitExceededWarningShown: false,
autoLoadLogsVolume: store.getBool(AUTO_LOAD_LOGS_VOLUME_SETTING_KEY, false),
};
/**
@ -245,14 +235,6 @@ export const exploreReducer = (state = initialExploreState, action: AnyAction):
};
}
if (storeAutoLoadLogsVolumeAction.match(action)) {
const autoLoadLogsVolume = action.payload;
return {
...state,
autoLoadLogsVolume,
};
}
if (resetExploreAction.match(action)) {
const payload: ResetExplorePayload = action.payload;
const leftState = state[ExploreId.left];

@ -3,7 +3,6 @@ import {
addResultsToCache,
cancelQueries,
cancelQueriesAction,
changeAutoLogsVolume,
clearCache,
importQueries,
loadLogsVolumeData,
@ -338,7 +337,6 @@ describe('reducer', () => {
explore: {
[ExploreId.left]: {
...defaultInitialState.explore[ExploreId.left],
autoLoadLogsVolume: false,
datasourceInstance: {
query: jest.fn(),
meta: {
@ -356,63 +354,6 @@ describe('reducer', () => {
getState = store.getState;
});
it('should not load logs volume automatically after running the query if auto-loading is disabled', async () => {
setupQueryResponse(getState());
getState().explore.autoLoadLogsVolume = false;
await dispatch(runQueries(ExploreId.left));
expect(getState().explore[ExploreId.left].logsVolumeData).not.toBeDefined();
});
it('should load logs volume automatically after running the query if auto-loading is enabled', async () => {
setupQueryResponse(getState());
getState().explore.autoLoadLogsVolume = true;
await dispatch(runQueries(ExploreId.left));
expect(getState().explore[ExploreId.left].logsVolumeData).toMatchObject({
state: LoadingState.Done,
error: undefined,
data: [{}],
});
});
it('when auto-load is enabled after running the query it should load logs volume data after changing auto-load option', async () => {
setupQueryResponse(getState());
await dispatch(runQueries(ExploreId.left));
expect(getState().explore[ExploreId.left].logsVolumeDataProvider).toBeDefined();
expect(getState().explore[ExploreId.left].logsVolumeData).not.toBeDefined();
await dispatch(changeAutoLogsVolume(ExploreId.left, true));
expect(getState().explore.autoLoadLogsVolume).toEqual(true);
expect(getState().explore[ExploreId.left].logsVolumeData).toMatchObject({
state: LoadingState.Done,
error: undefined,
data: [{}],
});
});
it('should allow loading logs volume on demand if auto-load is disabled', async () => {
setupQueryResponse(getState());
getState().explore.autoLoadLogsVolume = false;
await dispatch(runQueries(ExploreId.left));
expect(getState().explore[ExploreId.left].logsVolumeData).not.toBeDefined();
await dispatch(loadLogsVolumeData(ExploreId.left));
expect(getState().explore.autoLoadLogsVolume).toEqual(false);
expect(getState().explore[ExploreId.left].logsVolumeData).toMatchObject({
state: LoadingState.Done,
error: undefined,
data: [{}],
});
});
it('should cancel any unfinished logs volume queries', async () => {
setupQueryResponse(getState());
let unsubscribes: Function[] = [];

@ -1,6 +1,7 @@
import { mergeMap, throttleTime } from 'rxjs/operators';
import { identity, Observable, of, SubscriptionLike, Unsubscribable } from 'rxjs';
import {
AbsoluteTimeRange,
DataQuery,
DataQueryErrorType,
DataQueryResponse,
@ -32,18 +33,13 @@ import { notifyApp } from '../../../core/actions';
import { runRequest } from '../../query/state/runRequest';
import { decorateData } from '../utils/decorators';
import { createErrorNotification } from '../../../core/copy/appNotification';
import {
localStorageFullAction,
richHistoryLimitExceededAction,
richHistoryUpdatedAction,
stateSave,
storeAutoLoadLogsVolumeAction,
} from './main';
import { localStorageFullAction, richHistoryLimitExceededAction, richHistoryUpdatedAction, stateSave } from './main';
import { AnyAction, createAction, PayloadAction } from '@reduxjs/toolkit';
import { updateTime } from './time';
import { historyUpdatedAction } from './history';
import { createCacheKey, createEmptyQueryResponse, getResultsFromCache } from './utils';
import { config } from '@grafana/runtime';
import deepEqual from 'fast-deep-equal';
//
// Actions and Payloads
@ -124,6 +120,8 @@ const storeLogsVolumeDataProviderAction = createAction<StoreLogsVolumeDataProvid
'explore/storeLogsVolumeDataProviderAction'
);
const cleanLogsVolumeAction = createAction<{ exploreId: ExploreId }>('explore/cleanLogsVolumeAction');
export interface StoreLogsVolumeDataSubscriptionPayload {
exploreId: ExploreId;
logsVolumeDataSubscription?: SubscriptionLike;
@ -324,7 +322,7 @@ export const runQueries = (
dispatch(clearCache(exploreId));
}
const { richHistory, autoLoadLogsVolume } = getState().explore;
const { richHistory } = getState().explore;
const exploreItemState = getState().explore[exploreId]!;
const {
datasourceInstance,
@ -468,7 +466,15 @@ export const runQueries = (
}
);
if (config.featureToggles.fullRangeLogsVolume && hasLogsVolumeSupport(datasourceInstance)) {
if (live) {
dispatch(
storeLogsVolumeDataProviderAction({
exploreId,
logsVolumeDataProvider: undefined,
})
);
dispatch(cleanLogsVolumeAction({ exploreId }));
} else if (config.featureToggles.fullRangeLogsVolume && hasLogsVolumeSupport(datasourceInstance)) {
const logsVolumeDataProvider = datasourceInstance.getLogsVolumeDataProvider(transaction.request);
dispatch(
storeLogsVolumeDataProviderAction({
@ -476,8 +482,9 @@ export const runQueries = (
logsVolumeDataProvider,
})
);
if (autoLoadLogsVolume && logsVolumeDataProvider) {
dispatch(loadLogsVolumeData(exploreId));
const { logsVolumeData, absoluteRange } = getState().explore[exploreId]!;
if (!canReuseLogsVolumeData(logsVolumeData, queries, absoluteRange)) {
dispatch(cleanLogsVolumeAction({ exploreId }));
}
} else {
dispatch(
@ -493,6 +500,29 @@ export const runQueries = (
};
};
/**
* Checks if after changing the time range the existing data can be used to show logs volume.
* It can happen if queries are the same and new time range is within existing data time range.
*/
function canReuseLogsVolumeData(
logsVolumeData: DataQueryResponse | undefined,
queries: DataQuery[],
selectedTimeRange: AbsoluteTimeRange
): boolean {
if (logsVolumeData && logsVolumeData.data[0]) {
// check if queries are the same
if (!deepEqual(logsVolumeData.data[0].meta?.custom?.targets, queries)) {
return false;
}
const dataRange = logsVolumeData && logsVolumeData.data[0] && logsVolumeData.data[0].meta?.custom?.absoluteRange;
// if selected range is within loaded logs volume
if (dataRange && dataRange.from <= selectedTimeRange.from && selectedTimeRange.to <= dataRange.to) {
return true;
}
}
return false;
}
/**
* Reset queries to the given queries. Any modifications will be discarded.
* Use this action for clicks on query examples. Triggers a query run.
@ -543,23 +573,6 @@ export function clearCache(exploreId: ExploreId): ThunkResult<void> {
};
}
/**
* Uses storeLogsVolumeDataProviderAction to update the state and load logs volume when auto-load
* is enabled and logs volume hasn't been loaded yet.
*/
export function changeAutoLogsVolume(exploreId: ExploreId, autoLoadLogsVolume: boolean): ThunkResult<void> {
return (dispatch, getState) => {
dispatch(storeAutoLoadLogsVolumeAction(autoLoadLogsVolume));
const state = getState().explore[exploreId]!;
// load logs volume automatically after switching
const logsVolumeData = state.logsVolumeData;
if (!logsVolumeData?.data && autoLoadLogsVolume) {
dispatch(loadLogsVolumeData(exploreId));
}
};
}
/**
* Initializes loading logs volume data and stores emitted value.
*/
@ -697,7 +710,12 @@ export const queryReducer = (state: ExploreItemState, action: AnyAction): Explor
...state,
logsVolumeDataProvider,
logsVolumeDataSubscription: undefined,
// clear previous data, with a new provider the previous data becomes stale
};
}
if (cleanLogsVolumeAction.match(action)) {
return {
...state,
logsVolumeData: undefined,
};
}

@ -39,6 +39,7 @@ describe('LokiLogsVolumeProvider', () => {
datasourceSetup();
request = ({
targets: [{ expr: '{app="app01"}' }, { expr: '{app="app02"}' }],
range: { from: 0, to: 1 },
} as unknown) as DataQueryRequest<LokiQuery>;
volumeProvider = createLokiLogsVolumeProvider((datasource as unknown) as LokiDatasource, request);
}

@ -14,12 +14,18 @@ import {
toDataFrame,
} from '@grafana/data';
import { LokiQuery } from '../types';
import { Observable } from 'rxjs';
import { Observable, throwError, timeout } from 'rxjs';
import { cloneDeep } from 'lodash';
import LokiDatasource, { isMetricsQuery } from '../datasource';
import { LogLevelColor } from '../../../../core/logs_model';
import { BarAlignment, GraphDrawStyle, StackingMode } from '@grafana/schema';
/**
* Logs volume query may be expensive as it requires counting all logs in the selected range. If such query
* takes too much time it may need be made more specific to limit number of logs processed under the hood.
*/
const TIMEOUT = 10000;
export function createLokiLogsVolumeProvider(
datasource: LokiDatasource,
dataQueryRequest: DataQueryRequest<LokiQuery>
@ -30,6 +36,7 @@ export function createLokiLogsVolumeProvider(
.map((target) => {
return {
...target,
instant: false,
expr: `sum by (level) (count_over_time(${target.expr}[$__interval]))`,
};
});
@ -42,28 +49,44 @@ export function createLokiLogsVolumeProvider(
data: [],
});
const subscription = datasource.query(logsVolumeRequest).subscribe({
complete: () => {
const aggregatedLogsVolume = aggregateRawLogsVolume(rawLogsVolume);
observer.next({
state: LoadingState.Done,
error: undefined,
data: aggregatedLogsVolume,
});
observer.complete();
},
next: (dataQueryResponse: DataQueryResponse) => {
rawLogsVolume = rawLogsVolume.concat(dataQueryResponse.data.map(toDataFrame));
},
error: (error) => {
observer.next({
state: LoadingState.Error,
error: error,
data: [],
});
observer.error(error);
},
});
const subscription = datasource
.query(logsVolumeRequest)
.pipe(
timeout({
each: TIMEOUT,
with: () => throwError(new Error('Request timed-out. Please make your query more specific and try again.')),
})
)
.subscribe({
complete: () => {
const aggregatedLogsVolume = aggregateRawLogsVolume(rawLogsVolume);
if (aggregatedLogsVolume[0]) {
aggregatedLogsVolume[0].meta = {
custom: {
targets: dataQueryRequest.targets,
absoluteRange: { from: dataQueryRequest.range.from.valueOf(), to: dataQueryRequest.range.to.valueOf() },
},
};
}
observer.next({
state: LoadingState.Done,
error: undefined,
data: aggregatedLogsVolume,
});
observer.complete();
},
next: (dataQueryResponse: DataQueryResponse) => {
rawLogsVolume = rawLogsVolume.concat(dataQueryResponse.data.map(toDataFrame));
},
error: (error) => {
observer.next({
state: LoadingState.Error,
error: error,
data: [],
});
observer.error(error);
},
});
return () => {
subscription?.unsubscribe();
};

@ -56,11 +56,6 @@ export interface ExploreState {
* True if a warning message of hitting the exceeded number of items has been shown already.
*/
richHistoryLimitExceededWarningShown: boolean;
/**
* Auto-loading logs volume after running the query
*/
autoLoadLogsVolume: boolean;
}
export interface ExploreItemState {

Loading…
Cancel
Save