mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
@@ -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,9 +49,25 @@ export function createLokiLogsVolumeProvider(
|
||||
data: [],
|
||||
});
|
||||
|
||||
const subscription = datasource.query(logsVolumeRequest).subscribe({
|
||||
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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user