Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Loki: Implement step editor #69648

Merged
merged 16 commits into from Jun 16, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 4 additions & 1 deletion pkg/tsdb/loki/parse_query.go
Expand Up @@ -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)

Expand Down
21 changes: 15 additions & 6 deletions pkg/tsdb/loki/step.go
Expand Up @@ -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
Expand All @@ -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)

safeStep := timeRange / 11000
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
}

chosenStep := durationMax(step, safeStep)
step, err := intervalv2.ParseIntervalStringToTimeDuration(interpolateVariables(*queryStep, interval, timeRange))
ivanahuckova marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return step, err
}

return ceilMs(chosenStep)
return time.Duration(step.Nanoseconds() * resolution), nil
}
145 changes: 110 additions & 35 deletions pkg/tsdb/loki/step_test.go
Expand Up @@ -8,50 +8,125 @@ 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))
})
t.Run("step with range variable", func(t *testing.T) {
queryStep := "$__range"
step, err := calculateStep(time.Second*7, time.Second, 2, &queryStep)
require.NoError(t, err)
require.Equal(t, time.Second*2, 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))
})
t.Run("step with interval variable", func(t *testing.T) {
queryStep := "$__interval"
step, err := calculateStep(time.Second*7, time.Second, 2, &queryStep)
require.NoError(t, err)
require.Equal(t, time.Second*14, 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))
})
// 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("resolution should happen", func(t *testing.T) {
require.Equal(t, time.Second*5, calculateStep(time.Second*1, time.Second*100, 5))
})
// 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("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))
// 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("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 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("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("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("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("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("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)
require.Equal(t, time.Second*2, calculateStep(time.Second*0, time.Second*22000, 0))
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)
})
})
}
Expand Up @@ -61,14 +61,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: e.currentTarget.value.trimEnd() });
ivanahuckova marked this conversation as resolved.
Show resolved Hide resolved
onRunQuery();
}

const queryType = query.queryType ?? (query.instant ? LokiQueryType.Instant : LokiQueryType.Range);
ivanahuckova marked this conversation as resolved.
Show resolved Hide resolved
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
Expand All @@ -87,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"
Expand All @@ -99,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 && (
ivanahuckova marked this conversation as resolved.
Show resolved Hide resolved
<>
<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"
Expand All @@ -132,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));

Expand All @@ -153,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;
}

Expand Down