mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Elasticsearch: Enable full range log volume histogram (#41202)
* Rename "Logs volume" labels to "Log volume" Code references are kept intact as there's a lot of them, it could be renamed in a separate PR just with renaming * Add log level docs * Remove feature flag to enable log volume by default * Update error message * Update docs * Fix unit test * Fix unit test Queries are now run automatically * Add extra param for Loki API * Remove "Load volume" button * Update documentation about log volume * Move comment * Make reload button more accessible * Update docs/sources/explore/logs-integration.md Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> * Hide full range log volume for Loki behind the feature toggle Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>
This commit is contained in:
@@ -66,7 +66,7 @@ You can use the Loki query editor to create log and metric queries.
|
||||
|
||||
### Log browser
|
||||
|
||||
With Loki log browser you can easily navigate trough your list of labels and values and construct the query of your choice. Log browser has multi-step selection:
|
||||
With Loki log browser you can easily navigate through your list of labels and values and construct the query of your choice. Log browser has multi-step selection:
|
||||
|
||||
1. Choose the labels you would like to consider for your search.
|
||||
2. Pick the values for selected labels. Log browser supports facetting and therefore it shows you only possible label combinations.
|
||||
|
||||
@@ -21,7 +21,11 @@ During an infrastructure monitoring and incident response, you can dig deeper in
|
||||
|
||||
### Logs visualization
|
||||
|
||||
Results of log queries are shown as histograms in the graph and individual logs are displayed below. If the data source does not send histogram data for the requested time range, the logs model computes a time series based on the log row counts bucketed by an automatically calculated time interval and the start of the histogram is then anchored by the first log row's timestamp from the result. The end of the time series is anchored to the time picker's **To** range.
|
||||
Results of log queries are shown as histograms in the graph and individual logs are explained in the following sections.
|
||||
|
||||
If the data source supports a full range log volume histogram, the graph with log distribution for all entered log queries is shown automatically. This feature is currently supported by Elasticsearch data source.
|
||||
|
||||
If the data source does not support loading full range log volume histogram, the logs model computes a time series based on the log row counts bucketed by an automatically calculated time interval, and the first log row's timestamp then anchors the start of the histogram from the result. The end of the time series is anchored to the time picker's **To** range.
|
||||
|
||||
#### Log level
|
||||
|
||||
|
||||
@@ -52,7 +52,6 @@ export interface FeatureToggles {
|
||||
recordedQueries: boolean;
|
||||
newNavigation: boolean;
|
||||
fullRangeLogsVolume: boolean;
|
||||
autoLoadFullRangeLogsVolume: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -68,7 +68,6 @@ export class GrafanaBootConfig implements GrafanaConfig {
|
||||
recordedQueries: false,
|
||||
newNavigation: false,
|
||||
fullRangeLogsVolume: false,
|
||||
autoLoadFullRangeLogsVolume: false,
|
||||
};
|
||||
licenseInfo: LicenseInfo = {} as LicenseInfo;
|
||||
rendererAvailable = false;
|
||||
|
||||
@@ -54,7 +54,8 @@ export const LogLevelColor = {
|
||||
[LogLevel.unknown]: getThemeColor('#8e8e8e', '#dde4ed'),
|
||||
};
|
||||
|
||||
const SECOND = 1000;
|
||||
const MILLISECOND = 1;
|
||||
const SECOND = 1000 * MILLISECOND;
|
||||
const MINUTE = 60 * SECOND;
|
||||
const HOUR = 60 * MINUTE;
|
||||
const DAY = 24 * HOUR;
|
||||
@@ -633,7 +634,8 @@ export function queryLogsVolume<T extends DataQuery>(
|
||||
logsVolumeRequest: DataQueryRequest<T>,
|
||||
options: LogsVolumeQueryOptions<T>
|
||||
): Observable<DataQueryResponse> {
|
||||
const intervalInfo = getIntervalInfo(logsVolumeRequest.scopedVars);
|
||||
const timespan = options.range.to.valueOf() - options.range.from.valueOf();
|
||||
const intervalInfo = getIntervalInfo(logsVolumeRequest.scopedVars, timespan);
|
||||
logsVolumeRequest.interval = intervalInfo.interval;
|
||||
logsVolumeRequest.scopedVars.__interval = { value: intervalInfo.interval, text: intervalInfo.interval };
|
||||
if (intervalInfo.intervalMs !== undefined) {
|
||||
@@ -692,11 +694,15 @@ export function queryLogsVolume<T extends DataQuery>(
|
||||
});
|
||||
}
|
||||
|
||||
function getIntervalInfo(scopedVars: ScopedVars): { interval: string; intervalMs?: number } {
|
||||
function getIntervalInfo(scopedVars: ScopedVars, timespanMs: number): { interval: string; intervalMs?: number } {
|
||||
if (scopedVars.__interval) {
|
||||
let intervalMs: number = scopedVars.__interval_ms.value;
|
||||
let interval = '';
|
||||
if (intervalMs > HOUR) {
|
||||
// below 5 seconds we force the resolution to be per 1ms as interval in scopedVars is not less than 10ms
|
||||
if (timespanMs < SECOND * 5) {
|
||||
intervalMs = MILLISECOND;
|
||||
interval = '1ms';
|
||||
} else if (intervalMs > HOUR) {
|
||||
intervalMs = DAY;
|
||||
interval = '1d';
|
||||
} else if (intervalMs > MINUTE) {
|
||||
|
||||
@@ -69,8 +69,6 @@ interface Props extends Themeable2 {
|
||||
getFieldLinks: (field: Field, rowIndex: number) => Array<LinkModel<Field>>;
|
||||
addResultsToCache: () => void;
|
||||
clearCache: () => void;
|
||||
loadingLogsVolumeAvailable: boolean;
|
||||
onClickLoadLogsVolume: () => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
@@ -270,8 +268,6 @@ class UnthemedLogs extends PureComponent<Props, State> {
|
||||
logsQueries,
|
||||
clearCache,
|
||||
addResultsToCache,
|
||||
onClickLoadLogsVolume,
|
||||
loadingLogsVolumeAvailable,
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
@@ -351,18 +347,6 @@ class UnthemedLogs extends PureComponent<Props, State> {
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
<div>
|
||||
{loadingLogsVolumeAvailable && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
aria-label="Load volume button"
|
||||
title="Execute a query to show full range logs volume"
|
||||
onClick={onClickLoadLogsVolume}
|
||||
icon="graph-bar"
|
||||
className={styles.headerButton}
|
||||
>
|
||||
Load volume
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="secondary"
|
||||
disabled={isFlipping}
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
AbsoluteTimeRange,
|
||||
Field,
|
||||
hasLogsContextSupport,
|
||||
hasLogsVolumeSupport,
|
||||
LoadingState,
|
||||
LogRowModel,
|
||||
RawTimeRange,
|
||||
@@ -14,7 +13,7 @@ import {
|
||||
import { ExploreId, ExploreItemState } from 'app/types/explore';
|
||||
import { StoreState } from 'app/types';
|
||||
import { splitOpen } from './state/main';
|
||||
import { addResultsToCache, clearCache, loadLogsVolumeData } from './state/query';
|
||||
import { addResultsToCache, clearCache } from './state/query';
|
||||
import { updateTimeRange } from './state/time';
|
||||
import { getTimeZone } from '../profile/state/selectors';
|
||||
import { LiveLogsWithTheme } from './LiveLogs';
|
||||
@@ -22,7 +21,6 @@ import { Logs } from './Logs';
|
||||
import { LogsCrossFadeTransition } from './utils/LogsCrossFadeTransition';
|
||||
import { LiveTailControls } from './useLiveTailControls';
|
||||
import { getFieldLinksForExplore } from './utils/links';
|
||||
import { config } from 'app/core/config';
|
||||
|
||||
interface LogsContainerProps extends PropsFromRedux {
|
||||
width: number;
|
||||
@@ -69,7 +67,6 @@ class LogsContainer extends PureComponent<LogsContainerProps> {
|
||||
|
||||
render() {
|
||||
const {
|
||||
datasourceInstance,
|
||||
loading,
|
||||
loadingState,
|
||||
logRows,
|
||||
@@ -90,8 +87,6 @@ class LogsContainer extends PureComponent<LogsContainerProps> {
|
||||
exploreId,
|
||||
addResultsToCache,
|
||||
clearCache,
|
||||
logsVolumeDataProvider,
|
||||
loadLogsVolumeData,
|
||||
} = this.props;
|
||||
|
||||
if (!logRows) {
|
||||
@@ -151,12 +146,6 @@ class LogsContainer extends PureComponent<LogsContainerProps> {
|
||||
getFieldLinks={this.getFieldLinks}
|
||||
addResultsToCache={() => addResultsToCache(exploreId)}
|
||||
clearCache={() => clearCache(exploreId)}
|
||||
loadingLogsVolumeAvailable={
|
||||
hasLogsVolumeSupport(datasourceInstance) &&
|
||||
!!logsVolumeDataProvider &&
|
||||
!config.featureToggles.autoLoadFullRangeLogsVolume
|
||||
}
|
||||
onClickLoadLogsVolume={() => loadLogsVolumeData(exploreId)}
|
||||
/>
|
||||
</Collapse>
|
||||
</LogsCrossFadeTransition>
|
||||
@@ -207,7 +196,6 @@ const mapDispatchToProps = {
|
||||
splitOpen,
|
||||
addResultsToCache,
|
||||
clearCache,
|
||||
loadLogsVolumeData,
|
||||
};
|
||||
|
||||
const connector = connect(mapStateToProps, mapDispatchToProps);
|
||||
|
||||
@@ -27,7 +27,7 @@ function renderPanel(logsVolumeData?: DataQueryResponse) {
|
||||
describe('LogsVolumePanel', () => {
|
||||
it('shows loading message', () => {
|
||||
renderPanel({ state: LoadingState.Loading, error: undefined, data: [] });
|
||||
expect(screen.getByText('Logs volume is loading...')).toBeInTheDocument();
|
||||
expect(screen.getByText('Log volume is loading...')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows no volume data', () => {
|
||||
@@ -42,12 +42,12 @@ describe('LogsVolumePanel', () => {
|
||||
|
||||
it('shows error message', () => {
|
||||
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('Failed to load log volume for this query')).toBeInTheDocument();
|
||||
expect(screen.getByText('Test error message')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not show the panel when there is no volume data', () => {
|
||||
renderPanel(undefined);
|
||||
expect(screen.queryByText('Logs volume')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Log volume')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AbsoluteTimeRange, DataQueryResponse, GrafanaTheme2, LoadingState, SplitOpen, TimeZone } from '@grafana/data';
|
||||
import { Alert, Button, Collapse, TooltipDisplayMode, useStyles2, useTheme2 } from '@grafana/ui';
|
||||
import { AbsoluteTimeRange, DataQueryResponse, LoadingState, SplitOpen, TimeZone } from '@grafana/data';
|
||||
import { Alert, Button, Collapse, InlineField, TooltipDisplayMode, useStyles2, useTheme2 } from '@grafana/ui';
|
||||
import { ExploreGraph } from './ExploreGraph';
|
||||
import React from 'react';
|
||||
import { css } from '@emotion/css';
|
||||
@@ -27,12 +27,12 @@ export function LogsVolumePanel(props: Props) {
|
||||
return null;
|
||||
} else if (logsVolumeData?.error) {
|
||||
return (
|
||||
<Alert title="Failed to load volume logs for this query">
|
||||
<Alert title="Failed to load log volume 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>;
|
||||
LogsVolumePanelContent = <span>Log volume is loading...</span>;
|
||||
} else if (logsVolumeData?.data) {
|
||||
if (logsVolumeData.data.length > 0) {
|
||||
LogsVolumePanelContent = (
|
||||
@@ -59,15 +59,14 @@ export function LogsVolumePanel(props: Props) {
|
||||
|
||||
if (zoomRatio !== undefined && zoomRatio < 1) {
|
||||
zoomLevelInfo = (
|
||||
<>
|
||||
<span className={styles.zoomInfo}>Reload logs volume</span>
|
||||
<Button size="xs" icon="sync" variant="secondary" onClick={onLoadLogsVolume} />
|
||||
</>
|
||||
<InlineField label="Reload log volume" transparent>
|
||||
<Button size="xs" icon="sync" variant="secondary" onClick={onLoadLogsVolume} id="reload-volume" />
|
||||
</InlineField>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Collapse label="Logs volume" isOpen={true} loading={logsVolumeData?.state === LoadingState.Loading}>
|
||||
<Collapse label="Log volume" isOpen={true} loading={logsVolumeData?.state === LoadingState.Loading}>
|
||||
<div style={{ height }} className={styles.contentContainer}>
|
||||
{LogsVolumePanelContent}
|
||||
</div>
|
||||
@@ -76,7 +75,7 @@ export function LogsVolumePanel(props: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
const getStyles = () => {
|
||||
return {
|
||||
zoomInfoContainer: css`
|
||||
display: flex;
|
||||
@@ -85,10 +84,6 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
right: 5px;
|
||||
top: 5px;
|
||||
`,
|
||||
zoomInfo: css`
|
||||
padding: 8px;
|
||||
font-size: ${theme.typography.bodySmall.fontSize};
|
||||
`,
|
||||
contentContainer: css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -3,19 +3,6 @@ import { noop } from 'lodash';
|
||||
import { shallow } from 'enzyme';
|
||||
import { SecondaryActions } from './SecondaryActions';
|
||||
|
||||
import { config } from '@grafana/runtime';
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...((jest.requireActual('@grafana/runtime') as unknown) as object),
|
||||
config: {
|
||||
...((jest.requireActual('@grafana/runtime') as unknown) as any).config,
|
||||
featureToggles: {
|
||||
fullRangeLogsVolume: true,
|
||||
autoLoadFullRangeLogsVolume: false,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const addQueryRowButtonSelector = '[aria-label="Add row button"]';
|
||||
const richHistoryButtonSelector = '[aria-label="Rich history button"]';
|
||||
const queryInspectorButtonSelector = '[aria-label="Query inspector button"]';
|
||||
@@ -79,8 +66,4 @@ describe('SecondaryActions', () => {
|
||||
wrapper.find(queryInspectorButtonSelector).simulate('click');
|
||||
expect(onClickQueryInspector).toBeCalled();
|
||||
});
|
||||
|
||||
it('does not render load logs volume button when auto loading is enabled', () => {
|
||||
config.featureToggles.autoLoadFullRangeLogsVolume = true;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
cancelQueriesAction,
|
||||
clearCache,
|
||||
importQueries,
|
||||
loadLogsVolumeData,
|
||||
queryReducer,
|
||||
runQueries,
|
||||
scanStartAction,
|
||||
@@ -33,18 +32,6 @@ import { reducerTester } from '../../../../test/core/redux/reducerTester';
|
||||
import { configureStore } from '../../../store/configureStore';
|
||||
import { setTimeSrv } from '../../dashboard/services/TimeSrv';
|
||||
import Mock = jest.Mock;
|
||||
import { config } from '@grafana/runtime';
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...((jest.requireActual('@grafana/runtime') as unknown) as object),
|
||||
config: {
|
||||
...((jest.requireActual('@grafana/runtime') as unknown) as any).config,
|
||||
featureToggles: {
|
||||
fullRangeLogsVolume: true,
|
||||
autoLoadFullRangeLogsVolume: false,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const t = toUtc();
|
||||
const testRange = {
|
||||
@@ -376,29 +363,21 @@ describe('reducer', () => {
|
||||
|
||||
it('should cancel any unfinished logs volume queries', async () => {
|
||||
await dispatch(runQueries(ExploreId.left));
|
||||
// no subscriptions created yet
|
||||
expect(unsubscribes).toHaveLength(0);
|
||||
|
||||
await dispatch(loadLogsVolumeData(ExploreId.left));
|
||||
// first query is run automatically
|
||||
// loading in progress - one subscription created, not cleaned up yet
|
||||
expect(unsubscribes).toHaveLength(1);
|
||||
expect(unsubscribes[0]).not.toBeCalled();
|
||||
|
||||
setupQueryResponse(getState());
|
||||
await dispatch(runQueries(ExploreId.left));
|
||||
// new query was run - first subscription is cleaned up, no new subscriptions yet
|
||||
expect(unsubscribes).toHaveLength(1);
|
||||
// a new query is run while log volume query is not resolve yet...
|
||||
expect(unsubscribes[0]).toBeCalled();
|
||||
|
||||
await dispatch(loadLogsVolumeData(ExploreId.left));
|
||||
// new subscription is created, only the old was was cleaned up
|
||||
// first subscription is cleaned up, a new subscription is created automatically
|
||||
expect(unsubscribes).toHaveLength(2);
|
||||
expect(unsubscribes[0]).toBeCalled();
|
||||
expect(unsubscribes[1]).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should load logs volume after running the query', async () => {
|
||||
config.featureToggles.autoLoadFullRangeLogsVolume = true;
|
||||
await dispatch(runQueries(ExploreId.left));
|
||||
expect(unsubscribes).toHaveLength(1);
|
||||
});
|
||||
|
||||
@@ -38,7 +38,6 @@ 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';
|
||||
|
||||
//
|
||||
@@ -478,7 +477,7 @@ export const runQueries = (
|
||||
})
|
||||
);
|
||||
dispatch(cleanLogsVolumeAction({ exploreId }));
|
||||
} else if (config.featureToggles.fullRangeLogsVolume && hasLogsVolumeSupport(datasourceInstance)) {
|
||||
} else if (hasLogsVolumeSupport(datasourceInstance)) {
|
||||
const logsVolumeDataProvider = datasourceInstance.getLogsVolumeDataProvider(transaction.request);
|
||||
dispatch(
|
||||
storeLogsVolumeDataProviderAction({
|
||||
@@ -489,9 +488,7 @@ export const runQueries = (
|
||||
const { logsVolumeData, absoluteRange } = getState().explore[exploreId]!;
|
||||
if (!canReuseLogsVolumeData(logsVolumeData, queries, absoluteRange)) {
|
||||
dispatch(cleanLogsVolumeAction({ exploreId }));
|
||||
if (config.featureToggles.autoLoadFullRangeLogsVolume) {
|
||||
dispatch(loadLogsVolumeData(exploreId));
|
||||
}
|
||||
dispatch(loadLogsVolumeData(exploreId));
|
||||
}
|
||||
} else {
|
||||
dispatch(
|
||||
|
||||
@@ -147,10 +147,7 @@ export const decorateWithLogsResult = (
|
||||
const sortOrder = refreshIntervalToSortOrder(options.refreshInterval);
|
||||
const sortedNewResults = sortLogsResult(newResults, sortOrder);
|
||||
const rows = sortedNewResults.rows;
|
||||
const series =
|
||||
config.featureToggles.fullRangeLogsVolume && options.fullRangeLogsVolumeAvailable
|
||||
? undefined
|
||||
: sortedNewResults.series;
|
||||
const series = options.fullRangeLogsVolumeAvailable ? undefined : sortedNewResults.series;
|
||||
const logsResult = { ...sortedNewResults, rows, series };
|
||||
|
||||
return { ...data, logsResult };
|
||||
|
||||
@@ -12,9 +12,9 @@ import {
|
||||
MutableDataFrame,
|
||||
FieldType,
|
||||
} from '@grafana/data';
|
||||
import { BackendSrvRequest, FetchResponse } from '@grafana/runtime';
|
||||
import { BackendSrvRequest, FetchResponse, config } from '@grafana/runtime';
|
||||
|
||||
import LokiDatasource from './datasource';
|
||||
import LokiDatasource, { RangeQueryOptions } from './datasource';
|
||||
import { LokiQuery, LokiResponse, LokiResultType } from './types';
|
||||
import { getQueryOptions } from 'test/helpers/getQueryOptions';
|
||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||
@@ -29,6 +29,12 @@ jest.mock('@grafana/runtime', () => ({
|
||||
// @ts-ignore
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
getBackendSrv: () => backendSrv,
|
||||
config: {
|
||||
...((jest.requireActual('@grafana/runtime') as unknown) as any).config,
|
||||
featureToggles: {
|
||||
fullRangeLogsVolume: true,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const rawRange = {
|
||||
@@ -164,6 +170,30 @@ describe('LokiDatasource', () => {
|
||||
// Step is in seconds (1 ms === 0.001 s)
|
||||
expect(req.step).toEqual(0.001);
|
||||
});
|
||||
|
||||
describe('log volume hint', () => {
|
||||
let options: RangeQueryOptions;
|
||||
|
||||
beforeEach(() => {
|
||||
const raw = { from: 'now', to: 'now-1h' };
|
||||
const range = { from: dateTime(), to: dateTime(), raw: raw };
|
||||
options = ({
|
||||
range,
|
||||
} as unknown) as RangeQueryOptions;
|
||||
});
|
||||
|
||||
it('should add volume hint param for log volume queries', () => {
|
||||
const target = { expr: '{job="grafana"}', refId: 'B', volumeQuery: true };
|
||||
const req = ds.createRangeQuery(target, options as any, 1000);
|
||||
expect(req.hint).toBe('logvolhist');
|
||||
});
|
||||
|
||||
it('should not add volume hint param for regular queries', () => {
|
||||
const target = { expr: '{job="grafana"}', refId: 'B', volumeQuery: false };
|
||||
const req = ds.createRangeQuery(target, options as any, 1000);
|
||||
expect(req.hint).not.toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when doing logs queries with limits', () => {
|
||||
@@ -922,34 +952,55 @@ describe('LokiDatasource', () => {
|
||||
});
|
||||
|
||||
describe('logs volume data provider', () => {
|
||||
it('creates provider for logs query', () => {
|
||||
const ds = createLokiDSForTests();
|
||||
const options = getQueryOptions<LokiQuery>({
|
||||
targets: [{ expr: '{label=value}', refId: 'A' }],
|
||||
describe('when feature toggle is enabled', () => {
|
||||
beforeEach(() => {
|
||||
config.featureToggles.fullRangeLogsVolume = true;
|
||||
});
|
||||
|
||||
expect(ds.getLogsVolumeDataProvider(options)).toBeDefined();
|
||||
it('creates provider for logs query', () => {
|
||||
const ds = createLokiDSForTests();
|
||||
const options = getQueryOptions<LokiQuery>({
|
||||
targets: [{ expr: '{label=value}', refId: 'A' }],
|
||||
});
|
||||
|
||||
expect(ds.getLogsVolumeDataProvider(options)).toBeDefined();
|
||||
});
|
||||
|
||||
it('does not create provider for metrics query', () => {
|
||||
const ds = createLokiDSForTests();
|
||||
const options = getQueryOptions<LokiQuery>({
|
||||
targets: [{ expr: 'rate({label=value}[1m])', refId: 'A' }],
|
||||
});
|
||||
|
||||
expect(ds.getLogsVolumeDataProvider(options)).not.toBeDefined();
|
||||
});
|
||||
|
||||
it('creates provider if at least one query is a logs query', () => {
|
||||
const ds = createLokiDSForTests();
|
||||
const options = getQueryOptions<LokiQuery>({
|
||||
targets: [
|
||||
{ expr: 'rate({label=value}[1m])', refId: 'A' },
|
||||
{ expr: '{label=value}', refId: 'B' },
|
||||
],
|
||||
});
|
||||
|
||||
expect(ds.getLogsVolumeDataProvider(options)).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('does not create provider for metrics query', () => {
|
||||
const ds = createLokiDSForTests();
|
||||
const options = getQueryOptions<LokiQuery>({
|
||||
targets: [{ expr: 'rate({label=value}[1m])', refId: 'A' }],
|
||||
describe('when feature toggle is disabled', () => {
|
||||
beforeEach(() => {
|
||||
config.featureToggles.fullRangeLogsVolume = false;
|
||||
});
|
||||
|
||||
expect(ds.getLogsVolumeDataProvider(options)).not.toBeDefined();
|
||||
});
|
||||
it('does not create a provider for logs query', () => {
|
||||
const ds = createLokiDSForTests();
|
||||
const options = getQueryOptions<LokiQuery>({
|
||||
targets: [{ expr: '{label=value}', refId: 'A' }],
|
||||
});
|
||||
|
||||
it('creates provider if at least one query is a logs query', () => {
|
||||
const ds = createLokiDSForTests();
|
||||
const options = getQueryOptions<LokiQuery>({
|
||||
targets: [
|
||||
{ expr: 'rate({label=value}[1m])', refId: 'A' },
|
||||
{ expr: '{label=value}', refId: 'B' },
|
||||
],
|
||||
expect(ds.getLogsVolumeDataProvider(options)).not.toBeDefined();
|
||||
});
|
||||
|
||||
expect(ds.getLogsVolumeDataProvider(options)).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -59,6 +59,7 @@ import { RowContextOptions } from '@grafana/ui/src/components/Logs/LogRowContext
|
||||
import syntax from './syntax';
|
||||
import { DEFAULT_RESOLUTION } from './components/LokiOptionFields';
|
||||
import { queryLogsVolume } from 'app/core/logs_model';
|
||||
import config from 'app/core/config';
|
||||
|
||||
export type RangeQueryOptions = DataQueryRequest<LokiQuery> | AnnotationQueryRequest<LokiQuery>;
|
||||
export const DEFAULT_MAX_LINES = 1000;
|
||||
@@ -118,6 +119,10 @@ export class LokiDatasource
|
||||
}
|
||||
|
||||
getLogsVolumeDataProvider(request: DataQueryRequest<LokiQuery>): Observable<DataQueryResponse> | undefined {
|
||||
if (!config.featureToggles.fullRangeLogsVolume) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const isLogsVolumeAvailable = request.targets.some((target) => target.expr && !isMetricsQuery(target.expr));
|
||||
if (!isLogsVolumeAvailable) {
|
||||
return undefined;
|
||||
@@ -130,6 +135,7 @@ export class LokiDatasource
|
||||
return {
|
||||
...target,
|
||||
instant: false,
|
||||
volumeQuery: true,
|
||||
expr: `sum by (level) (count_over_time(${target.expr}[$__interval]))`,
|
||||
};
|
||||
});
|
||||
@@ -242,9 +248,12 @@ export class LokiDatasource
|
||||
};
|
||||
}
|
||||
|
||||
const hint: { hint?: 'logvolhist' } = target.volumeQuery ? { hint: 'logvolhist' } : {};
|
||||
|
||||
return {
|
||||
...DEFAULT_QUERY_PARAMS,
|
||||
...range,
|
||||
...hint,
|
||||
query,
|
||||
limit,
|
||||
};
|
||||
|
||||
@@ -14,6 +14,8 @@ export interface LokiRangeQueryRequest {
|
||||
end?: number;
|
||||
step?: number;
|
||||
direction?: 'BACKWARD' | 'FORWARD';
|
||||
// extra info passed to Loki API when a log volume query is run to distinguish it from any other query
|
||||
hint?: 'logvolhist';
|
||||
}
|
||||
|
||||
export enum LokiResultType {
|
||||
@@ -33,6 +35,7 @@ export interface LokiQuery extends DataQuery {
|
||||
resolution?: number;
|
||||
range?: boolean;
|
||||
instant?: boolean;
|
||||
volumeQuery?: boolean;
|
||||
}
|
||||
|
||||
export interface LokiOptions extends DataSourceJsonData {
|
||||
|
||||
Reference in New Issue
Block a user