Loki: add support for resolution (#36710)

* Add input to specify min step

* Add stepInterval to as input to component

* Add onBlur to Input component

* Loki: add functionality for min step

* Loki: change name on props to step to make it more clear

* Loki: add resolution as a query option

* Loki: Add min,max,exact as step options

* Loki: add functionality for different step modes

* Loki: fix bug where step function isn't working

* Loki: fix bug where exact step isn't working

* Loki: change width of step input field

* Loki: add tests for adjustInterval function

* Loki: add check for max step oprio to make sure it's not below the safe interval

* Loki: fix bug with some tests

* Loki: fix bug with tests

* Explore: add tooltip to loki step function

* Loki: remove resolution as a logs option

* Loki: update snapshots

* Fix failing tests

* Loki: add select component for choosing resolution

* Loki: add functionality for calculating correct interval with resolution applied

* Loki: remove functionality for step mode

* Loki: remove tests for step mode

* Loki: add tooltip to line limit and resolution

* Loki: add backend support for resolution

* Loki: fixed backend bug where resolution was undefined

* Loki: add check for resolution
This commit is contained in:
Olof Bourghardt
2021-08-16 14:02:13 +02:00
committed by GitHub
parent 1aeafa34d1
commit d26f3cdd03
14 changed files with 106 additions and 32 deletions

View File

@@ -36,6 +36,7 @@ export const LokiAnnotationsQueryEditor = memo(function LokiAnnotationQueryEdito
<LokiOptionFields
queryType={queryWithRefId.instant ? 'instant' : 'range'}
lineLimitValue={queryWithRefId?.maxLines?.toString() || ''}
resolution={queryWithRefId.resolution || 1}
query={queryWithRefId}
onRunQuery={() => {}}
onChange={onChange}

View File

@@ -27,6 +27,7 @@ export function LokiExploreQueryEditor(props: Props) {
<LokiOptionFields
queryType={query.instant ? 'instant' : 'range'}
lineLimitValue={query?.maxLines?.toString() || ''}
resolution={query.resolution || 1}
query={query}
onRunQuery={onRunQuery}
onChange={onChange}

View File

@@ -1,14 +1,16 @@
// Libraries
import React, { memo } from 'react';
import { css, cx } from '@emotion/css';
import { LokiQuery } from '../types';
import { SelectableValue } from '@grafana/data';
import { map } from 'lodash';
// Types
import { InlineFormLabel, RadioButtonGroup, InlineField, Input } from '@grafana/ui';
import { InlineFormLabel, RadioButtonGroup, InlineField, Input, Select } from '@grafana/ui';
import { SelectableValue } from '@grafana/data';
import { LokiQuery } from '../types';
export interface LokiOptionFieldsProps {
lineLimitValue: string;
resolution: number;
queryType: LokiQueryType;
query: LokiQuery;
onChange: (value: LokiQuery) => void;
@@ -27,8 +29,20 @@ const queryTypeOptions: Array<SelectableValue<LokiQueryType>> = [
},
];
export const DEFAULT_RESOLUTION: SelectableValue<number> = {
value: 1,
label: '1/1',
};
const RESOLUTION_OPTIONS: Array<SelectableValue<number>> = [DEFAULT_RESOLUTION].concat(
map([2, 3, 4, 5, 10], (value: number) => ({
value,
label: '1/' + value,
}))
);
export function LokiOptionFields(props: LokiOptionFieldsProps) {
const { lineLimitValue, queryType, query, onRunQuery, runOnBlur, onChange } = props;
const { lineLimitValue, resolution, queryType, query, onRunQuery, runOnBlur, onChange } = props;
function onChangeQueryLimit(value: string) {
const nextQuery = { ...query, maxLines: preprocessMaxLines(value) };
@@ -71,6 +85,11 @@ export function LokiOptionFields(props: LokiOptionFieldsProps) {
}
}
function onResolutionChange(option: SelectableValue<number>) {
const nextQuery = { ...query, resolution: option.value };
onChange(nextQuery);
}
return (
<div aria-label="Loki extra field" className="gf-form-inline">
{/*Query type field*/}
@@ -108,7 +127,7 @@ export function LokiOptionFields(props: LokiOptionFieldsProps) {
)}
aria-label="Line limit field"
>
<InlineField label="Line limit">
<InlineField label="Line limit" tooltip={'Upper limit for number of log lines returned by query.'}>
<Input
className="width-4"
placeholder="auto"
@@ -124,6 +143,14 @@ export function LokiOptionFields(props: LokiOptionFieldsProps) {
}}
/>
</InlineField>
<InlineField
label="Resolution"
tooltip={
'Resolution 1/1 sets step parameter of Loki metrics range queries such that each pixel corresponds to one data point. For better performance, lower resolutions can be picked. 1/2 only retrieves a data point for every other pixel, and 1/10 retrieves one data point per 10 pixels.'
}
>
<Select isSearchable={false} onChange={onResolutionChange} options={RESOLUTION_OPTIONS} value={resolution} />
</InlineField>
</div>
</div>
);

View File

@@ -53,6 +53,7 @@ export function LokiQueryEditor(props: LokiQueryEditorProps) {
<LokiOptionFields
queryType={query.instant ? 'instant' : 'range'}
lineLimitValue={query?.maxLines?.toString() || ''}
resolution={query?.resolution || 1}
query={query}
onRunQuery={onRunQuery}
onChange={onChange}

View File

@@ -15,6 +15,7 @@ exports[`LokiExploreQueryEditor should render component 1`] = `
}
}
queryType="range"
resolution={1}
/>
}
data={

View File

@@ -16,6 +16,7 @@ exports[`Render LokiQueryEditor with legend should render 1`] = `
}
}
queryType="range"
resolution={1}
runOnBlur={true}
/>
<div
@@ -81,6 +82,7 @@ exports[`Render LokiQueryEditor with legend should update timerange 1`] = `
}
}
queryType="range"
resolution={1}
runOnBlur={true}
/>
<div

View File

@@ -112,7 +112,7 @@ describe('LokiDatasource', () => {
const req = ds.createRangeQuery(target, options as any, 1000);
expect(req.start).toBeDefined();
expect(req.end).toBeDefined();
expect(adjustIntervalSpy).toHaveBeenCalledWith(1000, expect.anything());
expect(adjustIntervalSpy).toHaveBeenCalledWith(1000, 1, expect.anything());
});
it('should use provided intervalMs', () => {
@@ -127,7 +127,7 @@ describe('LokiDatasource', () => {
const req = ds.createRangeQuery(target, options as any, 1000);
expect(req.start).toBeDefined();
expect(req.end).toBeDefined();
expect(adjustIntervalSpy).toHaveBeenCalledWith(2000, expect.anything());
expect(adjustIntervalSpy).toHaveBeenCalledWith(2000, 1, expect.anything());
});
it('should set the minimal step to 1ms', () => {
@@ -142,7 +142,7 @@ describe('LokiDatasource', () => {
const req = ds.createRangeQuery(target, options as any, 1000);
expect(req.start).toBeDefined();
expect(req.end).toBeDefined();
expect(adjustIntervalSpy).toHaveBeenCalledWith(0.0005, expect.anything());
expect(adjustIntervalSpy).toHaveBeenCalledWith(0.0005, expect.anything(), 1000);
// Step is in seconds (1 ms === 0.001 s)
expect(req.step).toEqual(0.001);
});
@@ -399,7 +399,7 @@ describe('LokiDatasource', () => {
describe('__range, __range_s and __range_ms variables', () => {
const options = {
targets: [{ expr: 'rate(process_cpu_seconds_total[$__range])', refId: 'A' }],
targets: [{ expr: 'rate(process_cpu_seconds_total[$__range])', refId: 'A', stepInterval: '2s' }],
range: {
from: rawRange.from,
to: rawRange.to,
@@ -581,7 +581,7 @@ describe('LokiDatasource', () => {
status: 'success',
},
} as unknown) as FetchResponse;
const { promise } = getTestContext(response);
const { promise } = getTestContext(response, { stepInterval: '15s' });
const res = await promise;
@@ -613,7 +613,7 @@ describe('LokiDatasource', () => {
} as unknown) as FetchResponse;
describe('When tagKeys is set', () => {
it('should only include selected labels', async () => {
const { promise } = getTestContext(response, { tagKeys: 'label2,label3' });
const { promise } = getTestContext(response, { tagKeys: 'label2,label3', stepInterval: '15s' });
const res = await promise;
@@ -624,7 +624,7 @@ describe('LokiDatasource', () => {
});
describe('When textFormat is set', () => {
it('should fromat the text accordingly', async () => {
const { promise } = getTestContext(response, { textFormat: 'hello {{label2}}' });
const { promise } = getTestContext(response, { textFormat: 'hello {{label2}}', stepInterval: '15s' });
const res = await promise;
@@ -634,7 +634,7 @@ describe('LokiDatasource', () => {
});
describe('When titleFormat is set', () => {
it('should fromat the title accordingly', async () => {
const { promise } = getTestContext(response, { titleFormat: 'Title {{label2}}' });
const { promise } = getTestContext(response, { titleFormat: 'Title {{label2}}', stepInterval: '15s' });
const res = await promise;
@@ -781,6 +781,26 @@ describe('LokiDatasource', () => {
});
});
});
describe('adjustInterval', () => {
const dynamicInterval = 15;
const range = 1642;
const resolution = 1;
const ds = createLokiDSForTests();
it('should return the interval as a factor of dynamicInterval and resolution', () => {
let interval = ds.adjustInterval(dynamicInterval, resolution, range);
expect(interval).toBe(resolution * dynamicInterval);
});
it('should not return a value less than the safe interval', () => {
let safeInterval = range / 11000;
if (safeInterval > 1) {
safeInterval = Math.ceil(safeInterval);
}
const unsafeInterval = safeInterval - 0.01;
let interval = ds.adjustInterval(unsafeInterval, resolution, range);
expect(interval).toBeGreaterThanOrEqual(safeInterval);
});
});
});
function createLokiDSForTests(

View File

@@ -51,6 +51,7 @@ import LanguageProvider from './language_provider';
import { serializeParams } from '../../../core/utils/fetch';
import { RowContextOptions } from '@grafana/ui/src/components/Logs/LogRowContextProvider';
import syntax from './syntax';
import { DEFAULT_RESOLUTION } from './components/LokiOptionFields';
export type RangeQueryOptions = DataQueryRequest<LokiQuery> | AnnotationQueryRequest<LokiQuery>;
export const DEFAULT_MAX_LINES = 1000;
@@ -186,8 +187,11 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> {
const startNs = this.getTime(options.range.from, false);
const endNs = this.getTime(options.range.to, true);
const rangeMs = Math.ceil((endNs - startNs) / 1e6);
const resolution = target.resolution || (DEFAULT_RESOLUTION.value as number);
const adjustedInterval =
this.adjustInterval((options as DataQueryRequest<LokiQuery>).intervalMs || 1000, rangeMs) / 1000;
this.adjustInterval((options as DataQueryRequest<LokiQuery>).intervalMs || 1000, resolution, rangeMs) / 1000;
// We want to ceil to 3 decimal places
const step = Math.ceil(adjustedInterval * 1000) / 1000;
@@ -558,14 +562,28 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> {
}
async annotationQuery(options: any): Promise<AnnotationEvent[]> {
const { expr, maxLines, instant, tagKeys = '', titleFormat = '', textFormat = '' } = options.annotation;
const {
expr,
maxLines,
instant,
stepInterval,
tagKeys = '',
titleFormat = '',
textFormat = '',
} = options.annotation;
if (!expr) {
return [];
}
const interpolatedExpr = this.templateSrv.replace(expr, {}, this.interpolateQueryExpr);
const query = { refId: `annotation-${options.annotation.name}`, expr: interpolatedExpr, maxLines, instant };
const query = {
refId: `annotation-${options.annotation.name}`,
expr: interpolatedExpr,
maxLines,
instant,
stepInterval,
};
const { data } = instant
? await this.runInstantQuery(query, options as any).toPromise()
: await this.runRangeQuery(query, options as any).toPromise();
@@ -634,14 +652,16 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> {
return error;
}
adjustInterval(interval: number, range: number) {
adjustInterval(dynamicInterval: number, resolution: number, range: number) {
// Loki will drop queries that might return more than 11000 data points.
// Calibrate interval if it is too small.
if (interval !== 0 && range / interval > 11000) {
interval = Math.ceil(range / 11000);
let safeInterval = range / 11000;
if (safeInterval > 1) {
safeInterval = Math.ceil(safeInterval);
}
// The min interval is set to 1ms
return Math.max(interval, 1);
let adjustedInterval = Math.max(resolution * dynamicInterval, safeInterval);
return adjustedInterval;
}
addAdHocFilters(queryExpr: string) {

View File

@@ -30,6 +30,7 @@ export interface LokiQuery extends DataQuery {
legendFormat?: string;
valueWithRefId?: boolean;
maxLines?: number;
resolution?: number;
range?: boolean;
instant?: boolean;
}