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:
parent
1aeafa34d1
commit
d26f3cdd03
@ -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 -->
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user