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,
|
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', () => {
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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: [],
|
||||||
|
|||||||
@@ -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];
|
||||||
|
|||||||
@@ -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[] = [];
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user