diff --git a/public/app/features/explore/Explore.test.tsx b/public/app/features/explore/Explore.test.tsx index a29aaaf9d18..4e1d3b6658b 100644 --- a/public/app/features/explore/Explore.test.tsx +++ b/public/app/features/explore/Explore.test.tsx @@ -83,11 +83,9 @@ const dummyProps: Props = { showTrace: true, showNodeGraph: true, splitOpen: (() => {}) as any, - autoLoadLogsVolume: false, logsVolumeData: undefined, logsVolumeDataProvider: undefined, loadLogsVolumeData: () => {}, - changeAutoLogsVolume: () => {}, }; describe('Explore', () => { diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 12911ff1860..8270e44b0b6 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -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 { } renderLogsVolume(width: number) { - const { - logsVolumeData, - exploreId, - loadLogsVolumeData, - autoLoadLogsVolume, - changeAutoLogsVolume, - absoluteRange, - timeZone, - splitOpen, - } = this.props; + const { logsVolumeData, exploreId, loadLogsVolumeData, absoluteRange, timeZone, splitOpen } = this.props; return ( { - changeAutoLogsVolume(exploreId, autoLoadLogsVolume); - }} + onLoadLogsVolume={() => loadLogsVolumeData(exploreId)} /> ); } @@ -317,13 +303,13 @@ export class Explore extends React.PureComponent { 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 ( @@ -340,9 +326,11 @@ export class Explore extends React.PureComponent { addQueryRowButtonHidden={false} richHistoryButtonActive={showRichHistory} queryInspectorButtonActive={showQueryInspector} + loadingLogsVolumeAvailable={hasLogsVolumeSupport(datasourceInstance) && !!logsVolumeDataProvider} onClickAddQueryRowButton={this.onClickAddQueryRowButton} onClickRichHistoryButton={this.toggleShowRichHistory} onClickQueryInspectorButton={this.toggleShowQueryInspector} + onClickLoadLogsVolume={() => loadLogsVolumeData(exploreId)} /> @@ -360,7 +348,7 @@ export class Explore extends React.PureComponent { {showMetrics && graphResult && ( {this.renderGraphPanel(width)} )} - {showLogsVolume && {this.renderLogsVolume(width)}} + {{this.renderLogsVolume(width)}} {showTable && {this.renderTablePanel(width)}} {showLogs && {this.renderLogsPanel(width)}} {showNodeGraph && {this.renderNodeGraphPanel()}} @@ -395,7 +383,7 @@ export class Explore extends React.PureComponent { 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, }; diff --git a/public/app/features/explore/ExploreToolbar.tsx b/public/app/features/explore/ExploreToolbar.tsx index 533c1c5b298..a959db53162 100644 --- a/public/app/features/explore/ExploreToolbar.tsx +++ b/public/app/features/explore/ExploreToolbar.tsx @@ -213,7 +213,7 @@ export class UnConnectedExploreToolbar extends PureComponent { } 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, }; }; diff --git a/public/app/features/explore/LogsVolumePanel.test.tsx b/public/app/features/explore/LogsVolumePanel.test.tsx index 3c100bd2abc..85e05f2e274 100644 --- a/public/app/features/explore/LogsVolumePanel.test.tsx +++ b/public/app/features/explore/LogsVolumePanel.test.tsx @@ -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( {}} 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(); }); }); diff --git a/public/app/features/explore/LogsVolumePanel.tsx b/public/app/features/explore/LogsVolumePanel.tsx index 2baa72832e7..caf140c19c2 100644 --- a/public/app/features/explore/LogsVolumePanel.tsx +++ b/public/app/features/explore/LogsVolumePanel.tsx @@ -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 = ( - - ); + return null; } else if (logsVolumeData?.error) { - LogsVolumePanelContent = ( - - Failed to load volume logs for this query:{' '} - {logsVolumeData.error.data?.message || logsVolumeData.error.statusText} - + return ( + + {logsVolumeData.error.data?.message || logsVolumeData.error.statusText || logsVolumeData.error.message} + ); } else if (logsVolumeData?.state === LoadingState.Loading) { LogsVolumePanelContent = Logs volume is loading...; @@ -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) => { - const { target } = event; - if (target) { - onChangeAutoLogsVolume(target.checked); - } - }, - [onChangeAutoLogsVolume] - ); + const zoomRatio = logsLevelZoomRatio(logsVolumeData, absoluteRange); + let zoomLevelInfo; + + if (zoomRatio !== undefined && zoomRatio < 1) { + zoomLevelInfo = ( + <> + Reload to show higher resolution + + {props.loadingLogsVolumeAvailable && ( + + )} ); diff --git a/public/app/features/explore/Wrapper.tsx b/public/app/features/explore/Wrapper.tsx index 84c4491e69d..abc725c59fb 100644 --- a/public/app/features/explore/Wrapper.tsx +++ b/public/app/features/explore/Wrapper.tsx @@ -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 { } 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 { : `${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() { diff --git a/public/app/features/explore/__snapshots__/Explore.test.tsx.snap b/public/app/features/explore/__snapshots__/Explore.test.tsx.snap index 193e62eaaa0..fc9e55f9bf0 100644 --- a/public/app/features/explore/__snapshots__/Explore.test.tsx.snap +++ b/public/app/features/explore/__snapshots__/Explore.test.tsx.snap @@ -20,7 +20,9 @@ exports[`Explore should render component 1`] = ` ('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('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]; diff --git a/public/app/features/explore/state/query.test.ts b/public/app/features/explore/state/query.test.ts index 4d5c161567b..8d501fffc1c 100644 --- a/public/app/features/explore/state/query.test.ts +++ b/public/app/features/explore/state/query.test.ts @@ -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[] = []; diff --git a/public/app/features/explore/state/query.ts b/public/app/features/explore/state/query.ts index 18c8d035b40..306d3d446cf 100644 --- a/public/app/features/explore/state/query.ts +++ b/public/app/features/explore/state/query.ts @@ -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('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 { }; } -/** - * 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 { - 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, }; } diff --git a/public/app/plugins/datasource/loki/dataProviders/logsVolumeProvider.test.ts b/public/app/plugins/datasource/loki/dataProviders/logsVolumeProvider.test.ts index 0367139bff8..a528048b01a 100644 --- a/public/app/plugins/datasource/loki/dataProviders/logsVolumeProvider.test.ts +++ b/public/app/plugins/datasource/loki/dataProviders/logsVolumeProvider.test.ts @@ -39,6 +39,7 @@ describe('LokiLogsVolumeProvider', () => { datasourceSetup(); request = ({ targets: [{ expr: '{app="app01"}' }, { expr: '{app="app02"}' }], + range: { from: 0, to: 1 }, } as unknown) as DataQueryRequest; volumeProvider = createLokiLogsVolumeProvider((datasource as unknown) as LokiDatasource, request); } diff --git a/public/app/plugins/datasource/loki/dataProviders/logsVolumeProvider.ts b/public/app/plugins/datasource/loki/dataProviders/logsVolumeProvider.ts index 49f0f6aac2d..3b89cce2952 100644 --- a/public/app/plugins/datasource/loki/dataProviders/logsVolumeProvider.ts +++ b/public/app/plugins/datasource/loki/dataProviders/logsVolumeProvider.ts @@ -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 @@ -30,6 +36,7 @@ export function createLokiLogsVolumeProvider( .map((target) => { return { ...target, + instant: false, expr: `sum by (level) (count_over_time(${target.expr}[$__interval]))`, }; }); @@ -42,28 +49,44 @@ export function createLokiLogsVolumeProvider( data: [], }); - const subscription = datasource.query(logsVolumeRequest).subscribe({ - complete: () => { - const aggregatedLogsVolume = aggregateRawLogsVolume(rawLogsVolume); - observer.next({ - state: LoadingState.Done, - error: undefined, - data: aggregatedLogsVolume, - }); - observer.complete(); - }, - next: (dataQueryResponse: DataQueryResponse) => { - rawLogsVolume = rawLogsVolume.concat(dataQueryResponse.data.map(toDataFrame)); - }, - error: (error) => { - observer.next({ - state: LoadingState.Error, - error: error, - data: [], - }); - observer.error(error); - }, - }); + const subscription = datasource + .query(logsVolumeRequest) + .pipe( + timeout({ + each: TIMEOUT, + with: () => throwError(new Error('Request timed-out. Please make your query more specific and try again.')), + }) + ) + .subscribe({ + complete: () => { + const aggregatedLogsVolume = aggregateRawLogsVolume(rawLogsVolume); + if (aggregatedLogsVolume[0]) { + aggregatedLogsVolume[0].meta = { + custom: { + targets: dataQueryRequest.targets, + absoluteRange: { from: dataQueryRequest.range.from.valueOf(), to: dataQueryRequest.range.to.valueOf() }, + }, + }; + } + observer.next({ + state: LoadingState.Done, + error: undefined, + data: aggregatedLogsVolume, + }); + observer.complete(); + }, + next: (dataQueryResponse: DataQueryResponse) => { + rawLogsVolume = rawLogsVolume.concat(dataQueryResponse.data.map(toDataFrame)); + }, + error: (error) => { + observer.next({ + state: LoadingState.Error, + error: error, + data: [], + }); + observer.error(error); + }, + }); return () => { subscription?.unsubscribe(); }; diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index cb54e1f4cc9..0544a8d0255 100644 --- a/public/app/types/explore.ts +++ b/public/app/types/explore.ts @@ -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 {