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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 106 additions and 32 deletions

View File

@ -1,12 +1,11 @@
<!-- 8.1.1 START -->
# 8.1.1 (2021-08-09)
### Bug fixes
* **CloudWatch Logs:** Fix crash when no region is selected. [#37639](https://github.com/grafana/grafana/pull/37639), [@aocenas](https://github.com/aocenas)
* **Reporting:** Fix timezone parsing for scheduler (enterprise)
- **CloudWatch Logs:** Fix crash when no region is selected. [#37639](https://github.com/grafana/grafana/pull/37639), [@aocenas](https://github.com/aocenas)
- **Reporting:** Fix timezone parsing for scheduler (enterprise)
<!-- 8.1.1 END -->
<!-- 8.1.0 START -->

View File

@ -10,7 +10,7 @@ weight = 150
Grafana includes built-in support for Prometheus Alertmanager. It is presently in alpha and not accessible unless [alpha plugins are enabled in Grafana settings](https://grafana.com/docs/grafana/latest/administration/configuration/#enable_alpha). Once you add it as a data source, you can use the [Grafana alerting UI](https://grafana.com/docs/grafana/latest/alerting/) to manage silences, contact points as well as notification policies. A drop down option in these pages allows you to switch between Grafana and any configured Alertmanager data sources .
> **Note:** Currently, the [Cortex implementation of Prometheus Alertmanager](https://cortexmetrics.io/docs/proposals/scalable-alertmanager/) is required to edit rules.
> **Note:** Currently, the [Cortex implementation of Prometheus Alertmanager](https://cortexmetrics.io/docs/proposals/scalable-alertmanager/) is required to edit rules.
## Provision the Alertmanager data source

View File

@ -26,7 +26,6 @@ Returns an indicator to check if fine-grained access control is enabled or not.
| -------------------- | ---------------------- |
| status:accesscontrol | services:accesscontrol |
#### Example request
```http
@ -256,7 +255,6 @@ Content-Type: application/json; charset=UTF-8
#### Status codes
| Code | Description |
| ---- | ---------------------------------------------------------------------------------- |
| 200 | Role is updated. |
@ -279,7 +277,6 @@ For example, if a user does not have required permissions for creating users, th
| ----------- | -------------------- |
| roles:write | permissions:delegate |
#### Example request
```http
@ -377,7 +374,6 @@ For example, if a user does not have required permissions for creating users, th
| ------------ | -------------------- |
| roles:delete | permissions:delegate |
#### Example request
```http

View File

@ -10,5 +10,4 @@ list = false
### Bug fixes
* **CloudWatch Logs:** Fix crash when no region is selected. [#37639](https://github.com/grafana/grafana/pull/37639), [@aocenas](https://github.com/aocenas)
- **CloudWatch Logs:** Fix crash when no region is selected. [#37639](https://github.com/grafana/grafana/pull/37639), [@aocenas](https://github.com/aocenas)

View File

@ -56,6 +56,7 @@ type ResponseModel struct {
LegendFormat string `json:"legendFormat"`
Interval string `json:"interval"`
IntervalMS int `json:"intervalMS"`
Resolution int64 `json:"resolution"`
}
func init() {
@ -210,7 +211,12 @@ func (s *Service) parseQuery(dsInfo *datasourceInfo, queryContext *backend.Query
return nil, err
}
step := time.Duration(int64(interval.Value))
var resolution int64 = 1
if model.Resolution >= 1 && model.Resolution <= 5 || model.Resolution == 10 {
resolution = model.Resolution
}
step := time.Duration(int64(interval.Value) * resolution)
qs = append(qs, &lokiQuery{
Expr: model.Expr,

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;
}