mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -15,6 +15,7 @@ exports[`LokiExploreQueryEditor should render component 1`] = `
|
||||
}
|
||||
}
|
||||
queryType="range"
|
||||
resolution={1}
|
||||
/>
|
||||
}
|
||||
data={
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -30,6 +30,7 @@ export interface LokiQuery extends DataQuery {
|
||||
legendFormat?: string;
|
||||
valueWithRefId?: boolean;
|
||||
maxLines?: number;
|
||||
resolution?: number;
|
||||
range?: boolean;
|
||||
instant?: boolean;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user