Loki: Implement step editor (#69648)
* Loki: Implement step editor * Update to keep value * Remove console.log * Remove white space * Update public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderOptions.tsx Co-authored-by: Matias Chomicki <matyax@gmail.com> * Import trim * Update using of step in split queries * Add tests * Add tests * Remove step interpolation --------- Co-authored-by: Matias Chomicki <matyax@gmail.com>
This commit is contained in:
@@ -126,7 +126,10 @@ func parseQuery(queryContext *backend.QueryDataRequest) ([]*lokiQuery, error) {
|
||||
interval := query.Interval
|
||||
timeRange := query.TimeRange.To.Sub(query.TimeRange.From)
|
||||
|
||||
step := calculateStep(interval, timeRange, resolution)
|
||||
step, err := calculateStep(interval, timeRange, resolution, model.Step)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
expr := interpolateVariables(model.Expr, interval, timeRange)
|
||||
|
||||
|
||||
+15
-6
@@ -3,6 +3,8 @@ package loki
|
||||
import (
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/tsdb/intervalv2"
|
||||
)
|
||||
|
||||
// round the duration to the nearest millisecond larger-or-equal-to the duration
|
||||
@@ -20,12 +22,19 @@ func durationMax(d1 time.Duration, d2 time.Duration) time.Duration {
|
||||
}
|
||||
}
|
||||
|
||||
func calculateStep(baseInterval time.Duration, timeRange time.Duration, resolution int64) time.Duration {
|
||||
step := time.Duration(baseInterval.Nanoseconds() * resolution)
|
||||
func calculateStep(interval time.Duration, timeRange time.Duration, resolution int64, queryStep *string) (time.Duration, error) {
|
||||
// If we don't have step from query we calculate it from interval, time range and resolution
|
||||
if queryStep == nil || *queryStep == "" {
|
||||
step := time.Duration(interval.Nanoseconds() * resolution)
|
||||
safeStep := timeRange / 11000
|
||||
chosenStep := durationMax(step, safeStep)
|
||||
return ceilMs(chosenStep), nil
|
||||
}
|
||||
|
||||
safeStep := timeRange / 11000
|
||||
step, err := intervalv2.ParseIntervalStringToTimeDuration(*queryStep)
|
||||
if err != nil {
|
||||
return step, err
|
||||
}
|
||||
|
||||
chosenStep := durationMax(step, safeStep)
|
||||
|
||||
return ceilMs(chosenStep)
|
||||
return time.Duration(step.Nanoseconds() * resolution), nil
|
||||
}
|
||||
|
||||
+96
-35
@@ -8,50 +8,111 @@ import (
|
||||
)
|
||||
|
||||
func TestLokiStep(t *testing.T) {
|
||||
t.Run("base case", func(t *testing.T) {
|
||||
require.Equal(t, time.Second*14, calculateStep(time.Second*7, time.Second, 2))
|
||||
})
|
||||
t.Run("with query step", func(t *testing.T) {
|
||||
t.Run("valid step in go duration format", func(t *testing.T) {
|
||||
queryStep := "1m"
|
||||
step, err := calculateStep(time.Second*7, time.Second, 2, &queryStep)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, time.Minute*2, step)
|
||||
})
|
||||
|
||||
t.Run("step should be at least 1 millisecond", func(t *testing.T) {
|
||||
require.Equal(t, time.Millisecond*1, calculateStep(time.Microsecond*500, time.Second, 1))
|
||||
})
|
||||
t.Run("valid step as number", func(t *testing.T) {
|
||||
queryStep := "30"
|
||||
step, err := calculateStep(time.Second*7, time.Second, 2, &queryStep)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, time.Minute*1, step)
|
||||
})
|
||||
|
||||
t.Run("safeInterval should happen", func(t *testing.T) {
|
||||
// safeInterval
|
||||
require.Equal(t, time.Second*3, calculateStep(time.Second*2, time.Second*33000, 1))
|
||||
})
|
||||
// calculateStep parses a duration with support for unit that Grafana uses (e.g 1d)
|
||||
t.Run("step with 1d", func(t *testing.T) {
|
||||
queryStep := "1d"
|
||||
step, err := calculateStep(time.Second*7, time.Second, 2, &queryStep)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, time.Hour*48, step)
|
||||
})
|
||||
|
||||
t.Run("step should math.Ceil in milliseconds", func(t *testing.T) {
|
||||
require.Equal(t, time.Millisecond*2, calculateStep(time.Microsecond*1234, time.Second*1, 1))
|
||||
})
|
||||
// calculateStep parses a duration with support for unit that Grafana uses (e.g 1w)
|
||||
t.Run("step with 1w", func(t *testing.T) {
|
||||
queryStep := "1w"
|
||||
step, err := calculateStep(time.Second*7, time.Second, 2, &queryStep)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, time.Hour*336, step)
|
||||
})
|
||||
|
||||
t.Run("step should math.Ceil in milliseconds, even if safeInterval happens", func(t *testing.T) {
|
||||
require.Equal(t, time.Millisecond*3001, calculateStep(time.Second*2, time.Second*33001, 1))
|
||||
// Returns error
|
||||
t.Run("invalid step", func(t *testing.T) {
|
||||
queryStep := "invalid"
|
||||
step, err := calculateStep(time.Second*7, time.Second, 2, &queryStep)
|
||||
require.Error(t, err)
|
||||
require.Equal(t, time.Duration(0), step)
|
||||
})
|
||||
})
|
||||
t.Run("with no query step", func(t *testing.T) {
|
||||
t.Run("base case", func(t *testing.T) {
|
||||
step, err := calculateStep(time.Second*7, time.Second, 2, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, time.Second*14, step)
|
||||
})
|
||||
|
||||
t.Run("resolution should happen", func(t *testing.T) {
|
||||
require.Equal(t, time.Second*5, calculateStep(time.Second*1, time.Second*100, 5))
|
||||
})
|
||||
t.Run("step should be at least 1 millisecond", func(t *testing.T) {
|
||||
step, err := calculateStep(time.Microsecond*500, time.Second, 1, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, time.Millisecond*1, step)
|
||||
})
|
||||
|
||||
t.Run("safeInterval check should happen after resolution is used", func(t *testing.T) {
|
||||
require.Equal(t, time.Second*4, calculateStep(time.Second*2, time.Second*33000, 2))
|
||||
})
|
||||
t.Run("safeInterval should happen", func(t *testing.T) {
|
||||
// safeInterval
|
||||
step, err := calculateStep(time.Second*2, time.Second*33000, 1, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, time.Second*3, step)
|
||||
})
|
||||
|
||||
t.Run("survive interval=0", func(t *testing.T) {
|
||||
// interval=0. this should never happen, but we make sure we return something sane
|
||||
// (in this case safeInterval will take care of the problem)
|
||||
require.Equal(t, time.Second*2, calculateStep(time.Second*0, time.Second*22000, 1))
|
||||
})
|
||||
t.Run("step should math.Ceil in milliseconds", func(t *testing.T) {
|
||||
step, err := calculateStep(time.Microsecond*1234, time.Second*1, 1, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, time.Millisecond*2, step)
|
||||
})
|
||||
|
||||
t.Run("survive resolution=0", func(t *testing.T) {
|
||||
// resolution=0. this should never happen, but we make sure we return something sane
|
||||
// (in this case safeInterval will take care of the problem)
|
||||
require.Equal(t, time.Second*2, calculateStep(time.Second*1, time.Second*22000, 0))
|
||||
})
|
||||
t.Run("step should math.Ceil in milliseconds, even if safeInterval happens", func(t *testing.T) {
|
||||
step, err := calculateStep(time.Second*2, time.Second*33001, 1, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, time.Millisecond*3001, step)
|
||||
})
|
||||
|
||||
t.Run("survive interval=0 and resolution=0", func(t *testing.T) {
|
||||
// resolution=0 and interval=0. this should never happen, but we make sure we return something sane
|
||||
// (in this case safeInterval will take care of the problem)
|
||||
require.Equal(t, time.Second*2, calculateStep(time.Second*0, time.Second*22000, 0))
|
||||
t.Run("resolution should happen", func(t *testing.T) {
|
||||
step, err := calculateStep(time.Second*1, time.Second*100, 5, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, time.Second*5, step)
|
||||
})
|
||||
|
||||
t.Run("safeInterval check should happen after resolution is used", func(t *testing.T) {
|
||||
step, err := calculateStep(time.Second*2, time.Second*33000, 2, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, time.Second*4, step)
|
||||
})
|
||||
|
||||
t.Run("survive interval=0", func(t *testing.T) {
|
||||
// interval=0. this should never happen, but we make sure we return something sane
|
||||
// (in this case safeInterval will take care of the problem)
|
||||
step, err := calculateStep(time.Second*0, time.Second*22000, 1, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, time.Second*2, step)
|
||||
})
|
||||
|
||||
t.Run("survive resolution=0", func(t *testing.T) {
|
||||
// resolution=0. this should never happen, but we make sure we return something sane
|
||||
// (in this case safeInterval will take care of the problem)
|
||||
step, err := calculateStep(time.Second*1, time.Second*22000, 0, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, time.Second*2, step)
|
||||
})
|
||||
|
||||
t.Run("survive interval=0 and resolution=0", func(t *testing.T) {
|
||||
// resolution=0 and interval=0. this should never happen, but we make sure we return something sane
|
||||
// (in this case safeInterval will take care of the problem)
|
||||
step, err := calculateStep(time.Second*0, time.Second*22000, 0, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, time.Second*2, step)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -40,7 +40,125 @@ describe('runSplitQuery()', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('Handles and reports rerrors', async () => {
|
||||
test('Correctly splits queries without step', async () => {
|
||||
await expect(runSplitQuery(datasource, request)).toEmitValuesWith(() => {
|
||||
expect(datasource.runQuery).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({
|
||||
requestId: 'TEST_3',
|
||||
intervalMs: 60000,
|
||||
range: expect.objectContaining({
|
||||
from: expect.objectContaining({
|
||||
//2023-02-09T06:00:00.000Z
|
||||
_i: 1675922400000,
|
||||
}),
|
||||
to: expect.objectContaining({
|
||||
// 2023-02-10T06:00:00.000Z
|
||||
_i: 1676008800000,
|
||||
}),
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
expect(datasource.runQuery).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.objectContaining({
|
||||
requestId: 'TEST_2',
|
||||
intervalMs: 60000,
|
||||
range: expect.objectContaining({
|
||||
from: expect.objectContaining({
|
||||
//2023-02-08T05:59:00.000Z
|
||||
_i: 1675835940000,
|
||||
}),
|
||||
to: expect.objectContaining({
|
||||
// 2023-02-09T05:59:00.000Z
|
||||
_i: 1675922340000,
|
||||
}),
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
expect(datasource.runQuery).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
expect.objectContaining({
|
||||
requestId: 'TEST_1',
|
||||
intervalMs: 60000,
|
||||
range: expect.objectContaining({
|
||||
from: expect.objectContaining({
|
||||
//2023-02-08T05:00:00.000Z
|
||||
_i: 1675832400000,
|
||||
}),
|
||||
to: expect.objectContaining({
|
||||
// 2023-02-08T05:58:00.000Z
|
||||
_i: 1675835880000,
|
||||
}),
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('Correctly splits queries with step', async () => {
|
||||
const req = { ...request };
|
||||
req.targets[0].step = '10s';
|
||||
await expect(runSplitQuery(datasource, request)).toEmitValuesWith(() => {
|
||||
expect(datasource.runQuery).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({
|
||||
requestId: 'TEST_3',
|
||||
intervalMs: 60000,
|
||||
range: expect.objectContaining({
|
||||
from: expect.objectContaining({
|
||||
//2023-02-09T06:00:00.000Z
|
||||
_i: 1675922400000,
|
||||
}),
|
||||
to: expect.objectContaining({
|
||||
// 2023-02-10T06:00:00.000Z
|
||||
_i: 1676008800000,
|
||||
}),
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
expect(datasource.runQuery).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.objectContaining({
|
||||
requestId: 'TEST_2',
|
||||
intervalMs: 60000,
|
||||
range: expect.objectContaining({
|
||||
from: expect.objectContaining({
|
||||
//2023-02-08T05:59:50.000Z
|
||||
_i: 1675835990000,
|
||||
}),
|
||||
to: expect.objectContaining({
|
||||
// 2023-02-09T05:59:50.000Z
|
||||
_i: 1675922390000,
|
||||
}),
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
expect(datasource.runQuery).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
expect.objectContaining({
|
||||
requestId: 'TEST_1',
|
||||
intervalMs: 60000,
|
||||
range: expect.objectContaining({
|
||||
from: expect.objectContaining({
|
||||
// 2023-02-08T05:00:00.000Z
|
||||
_i: 1675832400000,
|
||||
}),
|
||||
to: expect.objectContaining({
|
||||
// 2023-02-08T05:59:40.000Z
|
||||
_i: 1675835980000,
|
||||
}),
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('Handles and reports errors', async () => {
|
||||
jest
|
||||
.spyOn(datasource, 'runQuery')
|
||||
.mockReturnValue(of({ state: LoadingState.Error, error: { refId: 'A', message: 'Error' }, data: [] }));
|
||||
@@ -339,7 +457,7 @@ describe('runSplitQuery()', () => {
|
||||
expect(datasource.runQuery).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
test('Groups metric queries by resolution', async () => {
|
||||
test('Groups metric queries with no step by calculated stepMs', async () => {
|
||||
const request = getQueryOptions<LokiQuery>({
|
||||
targets: [
|
||||
{ expr: 'count_over_time({a="b"}[1m])', refId: 'A', resolution: 3 },
|
||||
@@ -352,23 +470,21 @@ describe('runSplitQuery()', () => {
|
||||
expect(datasource.runQuery).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
test('Groups mixed queries by resolution', async () => {
|
||||
|
||||
test('Groups metric queries with step by stepMs', async () => {
|
||||
const request = getQueryOptions<LokiQuery>({
|
||||
targets: [
|
||||
{ expr: '{a="b"}', refId: 'A', resolution: 3 },
|
||||
{ expr: '{a="b"}', refId: 'B', resolution: 5 },
|
||||
{ expr: 'count_over_time({a="b"}[1m])', refId: 'C', resolution: 3 },
|
||||
{ expr: 'count_over_time{a="b"}[1m])', refId: 'D', resolution: 5 },
|
||||
{ expr: '{a="b"}', refId: 'E', resolution: 5, queryType: LokiQueryType.Instant },
|
||||
{ expr: 'count_over_time({a="b"}[1m])', refId: 'A', resolution: 1, step: '10' },
|
||||
{ expr: 'count_over_time{a="b"}[1m])', refId: 'B', resolution: 1, step: '5ms' },
|
||||
],
|
||||
range: range1d,
|
||||
});
|
||||
await expect(runSplitQuery(datasource, request)).toEmitValuesWith(() => {
|
||||
// A, B, C, D, E
|
||||
expect(datasource.runQuery).toHaveBeenCalledTimes(5);
|
||||
// A, B
|
||||
expect(datasource.runQuery).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
test('Chunked groups mixed queries by resolution', async () => {
|
||||
test('Groups mixed queries by stepMs', async () => {
|
||||
const request = getQueryOptions<LokiQuery>({
|
||||
targets: [
|
||||
{ expr: '{a="b"}', refId: 'A', resolution: 3 },
|
||||
@@ -376,12 +492,32 @@ describe('runSplitQuery()', () => {
|
||||
{ expr: 'count_over_time({a="b"}[1m])', refId: 'C', resolution: 3 },
|
||||
{ expr: 'count_over_time{a="b"}[1m])', refId: 'D', resolution: 5 },
|
||||
{ expr: '{a="b"}', refId: 'E', resolution: 5, queryType: LokiQueryType.Instant },
|
||||
{ expr: 'rate({a="b"}[5m])', refId: 'F', resolution: 5, step: '10' },
|
||||
{ expr: 'rate({a="b"} | logfmt[5m])', refId: 'G', resolution: 5, step: '10s' },
|
||||
],
|
||||
range: range1d,
|
||||
});
|
||||
await expect(runSplitQuery(datasource, request)).toEmitValuesWith(() => {
|
||||
// A, B, C, D, E, F+G
|
||||
expect(datasource.runQuery).toHaveBeenCalledTimes(6);
|
||||
});
|
||||
});
|
||||
test('Chunked groups mixed queries by stepMs', async () => {
|
||||
const request = getQueryOptions<LokiQuery>({
|
||||
targets: [
|
||||
{ expr: '{a="b"}', refId: 'A', resolution: 3 },
|
||||
{ expr: '{a="b"}', refId: 'B', resolution: 5 },
|
||||
{ expr: 'count_over_time({a="b"}[1m])', refId: 'C', resolution: 3 },
|
||||
{ expr: 'count_over_time{a="b"}[1m])', refId: 'D', resolution: 5 },
|
||||
{ expr: '{a="b"}', refId: 'E', resolution: 5, queryType: LokiQueryType.Instant },
|
||||
{ expr: 'rate({a="b"}[5m])', refId: 'F', resolution: 5, step: '10' },
|
||||
{ expr: 'rate({a="b"} | logfmt[5m])', refId: 'G', resolution: 5, step: '10s' },
|
||||
],
|
||||
range, // 3 days
|
||||
});
|
||||
await expect(runSplitQuery(datasource, request)).toEmitValuesWith(() => {
|
||||
// 3 * A, 3 * B, 3 * C, 3 * D, 1 * E
|
||||
expect(datasource.runQuery).toHaveBeenCalledTimes(13);
|
||||
// 3 * A, 3 * B, 3 * C, 3 * D, 1 * E, 3 * F+G
|
||||
expect(datasource.runQuery).toHaveBeenCalledTimes(16);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
dateTime,
|
||||
durationToMilliseconds,
|
||||
parseDuration,
|
||||
rangeUtil,
|
||||
TimeRange,
|
||||
} from '@grafana/data';
|
||||
import { LoadingState } from '@grafana/schema';
|
||||
@@ -25,24 +26,15 @@ import { LokiGroupedRequest, LokiQuery, LokiQueryType } from './types';
|
||||
export function partitionTimeRange(
|
||||
isLogsQuery: boolean,
|
||||
originalTimeRange: TimeRange,
|
||||
intervalMs: number,
|
||||
resolution: number,
|
||||
stepMs: number,
|
||||
duration: number
|
||||
): TimeRange[] {
|
||||
// the `step` value that will be finally sent to Loki is rougly the same as `intervalMs`,
|
||||
// but there are some complications.
|
||||
// we need to replicate this algo:
|
||||
//
|
||||
// https://github.com/grafana/grafana/blob/main/pkg/tsdb/loki/step.go#L23
|
||||
const start = originalTimeRange.from.toDate().getTime();
|
||||
const end = originalTimeRange.to.toDate().getTime();
|
||||
|
||||
const safeStep = Math.ceil((end - start) / 11000);
|
||||
const step = Math.max(intervalMs * resolution, safeStep);
|
||||
|
||||
const ranges = isLogsQuery
|
||||
? splitLogsTimeRange(start, end, duration)
|
||||
: splitMetricTimeRange(start, end, step, duration);
|
||||
: splitMetricTimeRange(start, end, stepMs, duration);
|
||||
|
||||
return ranges.map(([start, end]) => {
|
||||
const from = dateTime(start);
|
||||
@@ -243,29 +235,20 @@ export function runSplitQuery(datasource: LokiDatasource, request: DataQueryRequ
|
||||
for (const resolution in resolutionPartition) {
|
||||
requests.push({
|
||||
request: { ...request, targets: resolutionPartition[resolution] },
|
||||
partition: partitionTimeRange(
|
||||
true,
|
||||
request.range,
|
||||
request.intervalMs,
|
||||
Number(resolution),
|
||||
Number(chunkRangeMs)
|
||||
),
|
||||
partition: partitionTimeRange(true, request.range, request.intervalMs, Number(chunkRangeMs)),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const [chunkRangeMs, queries] of Object.entries(rangePartitionedMetricQueries)) {
|
||||
const resolutionPartition = groupBy(queries, (query) => query.resolution || 1);
|
||||
for (const resolution in resolutionPartition) {
|
||||
const stepMsPartition = groupBy(queries, (query) =>
|
||||
calculateStep(request.intervalMs, request.range, query.resolution || 1, query.step)
|
||||
);
|
||||
|
||||
for (const stepMs in stepMsPartition) {
|
||||
requests.push({
|
||||
request: { ...request, targets: resolutionPartition[resolution] },
|
||||
partition: partitionTimeRange(
|
||||
false,
|
||||
request.range,
|
||||
request.intervalMs,
|
||||
Number(resolution),
|
||||
Number(chunkRangeMs)
|
||||
),
|
||||
request: { ...request, targets: stepMsPartition[stepMs] },
|
||||
partition: partitionTimeRange(false, request.range, Number(stepMs), Number(chunkRangeMs)),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -288,3 +271,18 @@ export function runSplitQuery(datasource: LokiDatasource, request: DataQueryRequ
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Replicate from backend for split queries for now, until we can move query splitting to the backend
|
||||
// https://github.com/grafana/grafana/blob/main/pkg/tsdb/loki/step.go#L23
|
||||
function calculateStep(intervalMs: number, range: TimeRange, resolution: number, step: string | undefined) {
|
||||
// If we can parse step,the we use it
|
||||
// Otherwise we will calculate step based on interval
|
||||
const interval_regex = /(-?\d+(?:\.\d+)?)(ms|[Mwdhmsy])/;
|
||||
if (step?.match(interval_regex)) {
|
||||
return rangeUtil.intervalToMs(step) * resolution;
|
||||
}
|
||||
|
||||
const newStep = intervalMs * resolution;
|
||||
const safeStep = Math.round((range.to.valueOf() - range.from.valueOf()) / 11000);
|
||||
return Math.max(newStep, safeStep);
|
||||
}
|
||||
|
||||
+17
-6
@@ -38,8 +38,7 @@ describe('LokiQueryBuilderOptions', () => {
|
||||
});
|
||||
|
||||
it('can change line limit to valid value', async () => {
|
||||
const { props } = setup();
|
||||
props.query.expr = '{foo="bar"}';
|
||||
const { props } = setup({ expr: '{foo="bar"}' });
|
||||
|
||||
await userEvent.click(screen.getByTitle('Click to edit options'));
|
||||
// Second autosize input is a Line limit
|
||||
@@ -54,10 +53,9 @@ describe('LokiQueryBuilderOptions', () => {
|
||||
});
|
||||
|
||||
it('does not change line limit to invalid numeric value', async () => {
|
||||
const { props } = setup();
|
||||
const { props } = setup({ expr: '{foo="bar"}' });
|
||||
// We need to start with some value to be able to change it
|
||||
props.query.maxLines = 10;
|
||||
props.query.expr = '{foo="bar"}';
|
||||
|
||||
await userEvent.click(screen.getByTitle('Click to edit options'));
|
||||
// Second autosize input is a Line limit
|
||||
@@ -72,10 +70,9 @@ describe('LokiQueryBuilderOptions', () => {
|
||||
});
|
||||
|
||||
it('does not change line limit to invalid text value', async () => {
|
||||
const { props } = setup();
|
||||
const { props } = setup({ expr: '{foo="bar"}' });
|
||||
// We need to start with some value to be able to change it
|
||||
props.query.maxLines = 10;
|
||||
props.query.expr = '{foo="bar"}';
|
||||
|
||||
await userEvent.click(screen.getByTitle('Click to edit options'));
|
||||
// Second autosize input is a Line limit
|
||||
@@ -88,6 +85,20 @@ describe('LokiQueryBuilderOptions', () => {
|
||||
maxLines: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('shows correct options for log query', async () => {
|
||||
setup({ expr: '{foo="bar"}' });
|
||||
expect(screen.getByText('Line limit: 20')).toBeInTheDocument();
|
||||
expect(screen.getByText('Type: Range')).toBeInTheDocument();
|
||||
expect(screen.queryByText(/step/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows correct options for metric query', async () => {
|
||||
setup({ expr: 'rate({foo="bar"}[5m]', step: '1m' });
|
||||
expect(screen.queryByText('Line limit: 20')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('Type: Range')).toBeInTheDocument();
|
||||
expect(screen.getByText('Step: 1m')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
function setup(queryOverrides: Partial<LokiQuery> = {}) {
|
||||
|
||||
+50
-23
@@ -1,3 +1,4 @@
|
||||
import { trim } from 'lodash';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { CoreApp, isValidDuration, SelectableValue } from '@grafana/data';
|
||||
@@ -61,14 +62,19 @@ export const LokiQueryBuilderOptions = React.memo<Props>(
|
||||
}
|
||||
}
|
||||
|
||||
let queryType = query.queryType ?? (query.instant ? LokiQueryType.Instant : LokiQueryType.Range);
|
||||
let showMaxLines = isLogsQuery(query.expr);
|
||||
function onStepChange(e: React.SyntheticEvent<HTMLInputElement>) {
|
||||
onChange({ ...query, step: trim(e.currentTarget.value) });
|
||||
onRunQuery();
|
||||
}
|
||||
|
||||
const queryType = query.queryType ?? (query.instant ? LokiQueryType.Instant : LokiQueryType.Range);
|
||||
const isLogQuery = isLogsQuery(query.expr);
|
||||
|
||||
return (
|
||||
<EditorRow>
|
||||
<QueryOptionGroup
|
||||
title="Options"
|
||||
collapsedInfo={getCollapsedInfo(query, queryType, showMaxLines, maxLines)}
|
||||
collapsedInfo={getCollapsedInfo(query, queryType, maxLines, isLogQuery)}
|
||||
queryStats={queryStats}
|
||||
>
|
||||
<EditorField
|
||||
@@ -86,7 +92,7 @@ export const LokiQueryBuilderOptions = React.memo<Props>(
|
||||
<EditorField label="Type">
|
||||
<RadioButtonGroup options={queryTypeOptions} value={queryType} onChange={onQueryTypeChange} />
|
||||
</EditorField>
|
||||
{showMaxLines && (
|
||||
{isLogQuery && (
|
||||
<EditorField label="Line limit" tooltip="Upper limit for number of log lines returned by query.">
|
||||
<AutoSizeInput
|
||||
className="width-4"
|
||||
@@ -98,18 +104,34 @@ export const LokiQueryBuilderOptions = React.memo<Props>(
|
||||
/>
|
||||
</EditorField>
|
||||
)}
|
||||
<EditorField
|
||||
label="Resolution"
|
||||
tooltip="Sets the step parameter of Loki metrics range queries. With a resolution of 1/1, each pixel corresponds to one data point. 1/10 retrieves one data point per 10 pixels. Lower resolutions perform better."
|
||||
>
|
||||
<Select
|
||||
isSearchable={false}
|
||||
onChange={onResolutionChange}
|
||||
options={RESOLUTION_OPTIONS}
|
||||
value={query.resolution || 1}
|
||||
aria-label="Select resolution"
|
||||
/>
|
||||
</EditorField>
|
||||
{!isLogQuery && (
|
||||
<>
|
||||
<EditorField
|
||||
label="Step"
|
||||
tooltip="Use the step parameter when making metric queries to Loki. If not filled, Grafana's calculated interval will be used. Example valid values: 1s, 5m, 10h, 1d."
|
||||
>
|
||||
<AutoSizeInput
|
||||
className="width-6"
|
||||
placeholder={'auto'}
|
||||
type="string"
|
||||
defaultValue={query.step ?? ''}
|
||||
onCommitChange={onStepChange}
|
||||
/>
|
||||
</EditorField>
|
||||
<EditorField
|
||||
label="Resolution"
|
||||
tooltip="Changes the step parameter of Loki metrics range queries. With a resolution of 1/1, each pixel corresponds to one data point. 1/10 retrieves one data point per 10 pixels. Lower resolutions perform better."
|
||||
>
|
||||
<Select
|
||||
isSearchable={false}
|
||||
onChange={onResolutionChange}
|
||||
options={RESOLUTION_OPTIONS}
|
||||
value={query.resolution || 1}
|
||||
aria-label="Select resolution"
|
||||
/>
|
||||
</EditorField>
|
||||
</>
|
||||
)}
|
||||
{config.featureToggles.lokiQuerySplittingConfig && config.featureToggles.lokiQuerySplitting && (
|
||||
<EditorField
|
||||
label="Split Duration"
|
||||
@@ -131,12 +153,7 @@ export const LokiQueryBuilderOptions = React.memo<Props>(
|
||||
}
|
||||
);
|
||||
|
||||
function getCollapsedInfo(
|
||||
query: LokiQuery,
|
||||
queryType: LokiQueryType,
|
||||
showMaxLines: boolean,
|
||||
maxLines: number
|
||||
): string[] {
|
||||
function getCollapsedInfo(query: LokiQuery, queryType: LokiQueryType, maxLines: number, isLogQuery: boolean): string[] {
|
||||
const queryTypeLabel = queryTypeOptions.find((x) => x.value === queryType);
|
||||
const resolutionLabel = RESOLUTION_OPTIONS.find((x) => x.value === (query.resolution ?? 1));
|
||||
|
||||
@@ -152,10 +169,20 @@ function getCollapsedInfo(
|
||||
|
||||
items.push(`Type: ${queryTypeLabel?.label}`);
|
||||
|
||||
if (showMaxLines) {
|
||||
if (isLogQuery) {
|
||||
items.push(`Line limit: ${query.maxLines ?? maxLines}`);
|
||||
}
|
||||
|
||||
if (!isLogQuery) {
|
||||
if (query.step) {
|
||||
items.push(`Step: ${query.step}`);
|
||||
}
|
||||
|
||||
if (query.resolution) {
|
||||
items.push(`Resolution: ${resolutionLabel?.label}`);
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ const requests: LokiGroupedRequest[] = [
|
||||
}),
|
||||
app: 'explore',
|
||||
},
|
||||
partition: partitionTimeRange(true, range, 60000, 1, 24 * 60 * 60 * 1000),
|
||||
partition: partitionTimeRange(true, range, 60000, 24 * 60 * 60 * 1000),
|
||||
},
|
||||
{
|
||||
request: {
|
||||
@@ -54,7 +54,7 @@ const requests: LokiGroupedRequest[] = [
|
||||
}),
|
||||
app: 'explore',
|
||||
},
|
||||
partition: partitionTimeRange(false, range, 60000, 1, 24 * 60 * 60 * 1000),
|
||||
partition: partitionTimeRange(false, range, 60000, 24 * 60 * 60 * 1000),
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user