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
This commit is contained in:
Piotr Jamróz
2021-10-18 12:22:41 +02:00
committed by GitHub
parent 52b69a9a80
commit 8939636492
17 changed files with 199 additions and 257 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,7 +9,6 @@ import { getUrlStateFromPaneState, makeExplorePaneState } from './utils';
import { ThunkResult } from '../../../types'; import { ThunkResult } from '../../../types';
import { TimeSrv } from '../../dashboard/services/TimeSrv'; import { TimeSrv } from '../../dashboard/services/TimeSrv';
import { PanelModel } from 'app/features/dashboard/state'; import { PanelModel } from 'app/features/dashboard/state';
import store from '../../../core/store';
// //
// Actions and Payloads // Actions and Payloads
@@ -24,12 +23,6 @@ export const richHistoryUpdatedAction = createAction<any>('explore/richHistoryUp
export const localStorageFullAction = createAction('explore/localStorageFullAction'); export const localStorageFullAction = createAction('explore/localStorageFullAction');
export const richHistoryLimitExceededAction = createAction('explore/richHistoryLimitExceededAction'); 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. * 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 * Global Explore state that handles multiple Explore areas and the split state
*/ */
@@ -176,7 +167,6 @@ export const initialExploreState: ExploreState = {
richHistory: [], richHistory: [],
localStorageFull: false, localStorageFull: false,
richHistoryLimitExceededWarningShown: 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)) { if (resetExploreAction.match(action)) {
const payload: ResetExplorePayload = action.payload; const payload: ResetExplorePayload = action.payload;
const leftState = state[ExploreId.left]; const leftState = state[ExploreId.left];

View File

@@ -3,7 +3,6 @@ import {
addResultsToCache, addResultsToCache,
cancelQueries, cancelQueries,
cancelQueriesAction, cancelQueriesAction,
changeAutoLogsVolume,
clearCache, clearCache,
importQueries, importQueries,
loadLogsVolumeData, loadLogsVolumeData,
@@ -338,7 +337,6 @@ describe('reducer', () => {
explore: { explore: {
[ExploreId.left]: { [ExploreId.left]: {
...defaultInitialState.explore[ExploreId.left], ...defaultInitialState.explore[ExploreId.left],
autoLoadLogsVolume: false,
datasourceInstance: { datasourceInstance: {
query: jest.fn(), query: jest.fn(),
meta: { meta: {
@@ -356,63 +354,6 @@ describe('reducer', () => {
getState = store.getState; 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 () => { it('should cancel any unfinished logs volume queries', async () => {
setupQueryResponse(getState()); setupQueryResponse(getState());
let unsubscribes: Function[] = []; let unsubscribes: Function[] = [];

View File

@@ -1,6 +1,7 @@
import { mergeMap, throttleTime } from 'rxjs/operators'; import { mergeMap, throttleTime } from 'rxjs/operators';
import { identity, Observable, of, SubscriptionLike, Unsubscribable } from 'rxjs'; import { identity, Observable, of, SubscriptionLike, Unsubscribable } from 'rxjs';
import { import {
AbsoluteTimeRange,
DataQuery, DataQuery,
DataQueryErrorType, DataQueryErrorType,
DataQueryResponse, DataQueryResponse,
@@ -32,18 +33,13 @@ import { notifyApp } from '../../../core/actions';
import { runRequest } from '../../query/state/runRequest'; import { runRequest } from '../../query/state/runRequest';
import { decorateData } from '../utils/decorators'; import { decorateData } from '../utils/decorators';
import { createErrorNotification } from '../../../core/copy/appNotification'; import { createErrorNotification } from '../../../core/copy/appNotification';
import { import { localStorageFullAction, richHistoryLimitExceededAction, richHistoryUpdatedAction, stateSave } from './main';
localStorageFullAction,
richHistoryLimitExceededAction,
richHistoryUpdatedAction,
stateSave,
storeAutoLoadLogsVolumeAction,
} 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 { createCacheKey, createEmptyQueryResponse, getResultsFromCache } from './utils'; import { createCacheKey, createEmptyQueryResponse, getResultsFromCache } from './utils';
import { config } from '@grafana/runtime'; import { config } from '@grafana/runtime';
import deepEqual from 'fast-deep-equal';
// //
// Actions and Payloads // Actions and Payloads
@@ -124,6 +120,8 @@ const storeLogsVolumeDataProviderAction = createAction<StoreLogsVolumeDataProvid
'explore/storeLogsVolumeDataProviderAction' 'explore/storeLogsVolumeDataProviderAction'
); );
const cleanLogsVolumeAction = createAction<{ exploreId: ExploreId }>('explore/cleanLogsVolumeAction');
export interface StoreLogsVolumeDataSubscriptionPayload { export interface StoreLogsVolumeDataSubscriptionPayload {
exploreId: ExploreId; exploreId: ExploreId;
logsVolumeDataSubscription?: SubscriptionLike; logsVolumeDataSubscription?: SubscriptionLike;
@@ -324,7 +322,7 @@ export const runQueries = (
dispatch(clearCache(exploreId)); dispatch(clearCache(exploreId));
} }
const { richHistory, autoLoadLogsVolume } = getState().explore; const { richHistory } = getState().explore;
const exploreItemState = getState().explore[exploreId]!; const exploreItemState = getState().explore[exploreId]!;
const { const {
datasourceInstance, 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); const logsVolumeDataProvider = datasourceInstance.getLogsVolumeDataProvider(transaction.request);
dispatch( dispatch(
storeLogsVolumeDataProviderAction({ storeLogsVolumeDataProviderAction({
@@ -476,8 +482,9 @@ export const runQueries = (
logsVolumeDataProvider, logsVolumeDataProvider,
}) })
); );
if (autoLoadLogsVolume && logsVolumeDataProvider) { const { logsVolumeData, absoluteRange } = getState().explore[exploreId]!;
dispatch(loadLogsVolumeData(exploreId)); if (!canReuseLogsVolumeData(logsVolumeData, queries, absoluteRange)) {
dispatch(cleanLogsVolumeAction({ exploreId }));
} }
} else { } else {
dispatch( 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. * Reset queries to the given queries. Any modifications will be discarded.
* Use this action for clicks on query examples. Triggers a query run. * 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. * Initializes loading logs volume data and stores emitted value.
*/ */
@@ -697,7 +710,12 @@ export const queryReducer = (state: ExploreItemState, action: AnyAction): Explor
...state, ...state,
logsVolumeDataProvider, logsVolumeDataProvider,
logsVolumeDataSubscription: undefined, logsVolumeDataSubscription: undefined,
// clear previous data, with a new provider the previous data becomes stale };
}
if (cleanLogsVolumeAction.match(action)) {
return {
...state,
logsVolumeData: undefined, logsVolumeData: undefined,
}; };
} }

View File

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

View File

@@ -14,12 +14,18 @@ import {
toDataFrame, toDataFrame,
} from '@grafana/data'; } from '@grafana/data';
import { LokiQuery } from '../types'; import { LokiQuery } from '../types';
import { Observable } from 'rxjs'; import { Observable, throwError, timeout } from 'rxjs';
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
import LokiDatasource, { isMetricsQuery } from '../datasource'; import LokiDatasource, { isMetricsQuery } from '../datasource';
import { LogLevelColor } from '../../../../core/logs_model'; import { LogLevelColor } from '../../../../core/logs_model';
import { BarAlignment, GraphDrawStyle, StackingMode } from '@grafana/schema'; 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( export function createLokiLogsVolumeProvider(
datasource: LokiDatasource, datasource: LokiDatasource,
dataQueryRequest: DataQueryRequest<LokiQuery> dataQueryRequest: DataQueryRequest<LokiQuery>
@@ -30,6 +36,7 @@ export function createLokiLogsVolumeProvider(
.map((target) => { .map((target) => {
return { return {
...target, ...target,
instant: false,
expr: `sum by (level) (count_over_time(${target.expr}[$__interval]))`, expr: `sum by (level) (count_over_time(${target.expr}[$__interval]))`,
}; };
}); });
@@ -42,28 +49,44 @@ export function createLokiLogsVolumeProvider(
data: [], data: [],
}); });
const subscription = datasource.query(logsVolumeRequest).subscribe({ const subscription = datasource
complete: () => { .query(logsVolumeRequest)
const aggregatedLogsVolume = aggregateRawLogsVolume(rawLogsVolume); .pipe(
observer.next({ timeout({
state: LoadingState.Done, each: TIMEOUT,
error: undefined, with: () => throwError(new Error('Request timed-out. Please make your query more specific and try again.')),
data: aggregatedLogsVolume, })
}); )
observer.complete(); .subscribe({
}, complete: () => {
next: (dataQueryResponse: DataQueryResponse) => { const aggregatedLogsVolume = aggregateRawLogsVolume(rawLogsVolume);
rawLogsVolume = rawLogsVolume.concat(dataQueryResponse.data.map(toDataFrame)); if (aggregatedLogsVolume[0]) {
}, aggregatedLogsVolume[0].meta = {
error: (error) => { custom: {
observer.next({ targets: dataQueryRequest.targets,
state: LoadingState.Error, absoluteRange: { from: dataQueryRequest.range.from.valueOf(), to: dataQueryRequest.range.to.valueOf() },
error: error, },
data: [], };
}); }
observer.error(error); 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 () => { return () => {
subscription?.unsubscribe(); subscription?.unsubscribe();
}; };

View File

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