Loki: Removal of Resolution in query editors (#101860)

Loki: Remove usage of `Resolution` in Annotations
This commit is contained in:
Sven Grossmann
2025-03-10 11:02:10 +01:00
committed by GitHub
parent 72aa2392cb
commit 8ea6280469
6 changed files with 10 additions and 196 deletions
@@ -173,9 +173,7 @@ The following options are the same for both **Builder** and **Code** mode:
- **Direction** - Determines the search order. **Backward** is a backward search starting at the end of the time range. **Forward** is a forward search starting at the beginning of the time range. The default is **Backward**
- **Step** Sets the step parameter of Loki metrics queries. The default value equals to the value of `$__interval` variable, which is calculated using the time range and the width of the graph (the number of pixels).
- **Resolution** Deprecated. Sets the step parameter of Loki metrics range queries. With a resolution of `1/1`, each pixel corresponds to one data point. `1/2` retrieves one data point for every other pixel, `1/10` retrieves one data point per 10 pixels, and so on. Lower resolutions perform better.
- **Step** Sets the step parameter of Loki metrics queries. The default value equals to the value of `$__auto` variable, which is calculated using the time range and the width of the graph (the number of pixels).
## Create a log query
@@ -263,6 +261,6 @@ For more information about metric queries, refer to the [Loki metric queries doc
[Annotations](ref:annotate-visualizations) overlay rich event information on top of graphs.
You can add annotation queries in the Dashboard menu's Annotations view.
You can use any non-metric Loki query as a source for annotations.
You can only use log queries as a source for annotations.
Grafana automatically uses log content as annotation text and your log stream labels as tags.
You don't need to create any additional mapping.
@@ -1,13 +1,10 @@
// Libraries
import { memo } from 'react';
import { AnnotationQuery } from '@grafana/data';
import { EditorField, EditorRow } from '@grafana/plugin-ui';
import { Input, Stack } from '@grafana/ui';
// Types
import { getNormalizedLokiQuery } from '../queryUtils';
import { LokiQuery, LokiQueryType } from '../types';
import { LokiQuery } from '../types';
import { LokiOptionFields } from './LokiOptionFields';
import { LokiQueryField } from './LokiQueryField';
@@ -27,17 +24,11 @@ export const LokiAnnotationsQueryEditor = memo(function LokiAnnotationQueryEdito
}
const onChangeQuery = (query: LokiQuery) => {
// the current version of annotations only stores an optional boolean
// field `instant` to handle the instant/range switch.
// we need to maintain compatibility for now, so we do the same.
// we explicitly call `getNormalizedLokiQuery` to make sure `queryType`
// is set up correctly.
const instant = getNormalizedLokiQuery(query).queryType === LokiQueryType.Instant;
onAnnotationChange({
...annotation,
expr: query.expr,
maxLines: query.maxLines,
instant,
queryType: 'range',
});
};
@@ -60,7 +51,6 @@ export const LokiAnnotationsQueryEditor = memo(function LokiAnnotationQueryEdito
ExtraFieldElement={
<LokiOptionFields
lineLimitValue={queryWithRefId?.maxLines?.toString() || ''}
resolution={queryWithRefId.resolution || 1}
query={queryWithRefId}
onRunQuery={() => {}}
onChange={onChangeQuery}
@@ -1,17 +1,15 @@
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import { LokiOptionFieldsProps, LokiOptionFields, preprocessMaxLines } from './LokiOptionFields';
const setup = () => {
const lineLimitValue = '1';
const resolution = 1;
const query = { refId: '1', expr: 'query' };
const onChange = jest.fn();
const onRunQuery = jest.fn();
const props: LokiOptionFieldsProps = {
lineLimitValue,
resolution,
query,
onChange,
onRunQuery,
@@ -20,35 +18,6 @@ const setup = () => {
return props;
};
describe('Query Type Field', () => {
it('should render a query type field', () => {
const props = setup();
render(<LokiOptionFields {...props} />);
expect(screen.getByTestId('queryTypeField')).toBeInTheDocument();
});
it('should have a default value of "Range"', () => {
const props = setup();
render(<LokiOptionFields {...props} />);
expect(screen.getByLabelText('Range')).toBeChecked();
expect(screen.getByLabelText('Instant')).not.toBeChecked();
});
it('should call onChange when value is changed', async () => {
const props = setup();
render(<LokiOptionFields {...props} />);
fireEvent.click(screen.getByLabelText('Instant')); // (`userEvent.click()` triggers an error, so switching here to `fireEvent`.)
await waitFor(() => expect(props.onChange).toHaveBeenCalledTimes(1));
});
it('renders as expected when the query type is instant', () => {
const props = setup();
render(<LokiOptionFields {...props} query={{ refId: '1', expr: 'query', instant: true }} />);
expect(screen.getByLabelText('Instant')).toBeChecked();
expect(screen.getByLabelText('Range')).not.toBeChecked();
});
});
describe('Line Limit Field', () => {
it('should render a line limit field', () => {
const props = setup();
@@ -69,26 +38,6 @@ describe('Line Limit Field', () => {
});
});
describe('Resolution Field', () => {
it('should render the resolution field', () => {
const props = setup();
render(<LokiOptionFields {...props} />);
expect(screen.getByRole('combobox')).toBeInTheDocument();
});
it('should have a default value of 1', async () => {
const props = setup();
render(<LokiOptionFields {...props} />);
expect(await screen.findByText('1/1')).toBeInTheDocument();
});
it('displays the expected resolution value', async () => {
const props = setup();
render(<LokiOptionFields {...props} resolution={5} />);
expect(await screen.findByText('1/5')).toBeInTheDocument();
});
});
describe('preprocessMaxLines', () => {
test.each([
{ inputValue: '', expected: undefined },
@@ -1,19 +1,14 @@
// Libraries
import { map } from 'lodash';
import { memo } from 'react';
import * as React from 'react';
// Types
import { SelectableValue } from '@grafana/data';
import { config } from '@grafana/runtime';
import { InlineFormLabel, RadioButtonGroup, InlineField, Input, Select, Stack } from '@grafana/ui';
import { InlineField, Input, Stack } from '@grafana/ui';
import { getLokiQueryType } from '../queryUtils';
import { LokiQuery, LokiQueryDirection, LokiQueryType } from '../types';
export interface LokiOptionFieldsProps {
lineLimitValue: string;
resolution: number;
query: LokiQuery;
onChange: (value: LokiQuery) => void;
onRunQuery: () => void;
@@ -51,41 +46,15 @@ export function getQueryDirectionLabel(direction: LokiQueryDirection) {
return queryDirections.find((queryDirection) => queryDirection.value === direction)?.label ?? 'Unknown';
}
if (config.featureToggles.lokiExperimentalStreaming) {
queryTypeOptions.push({
value: LokiQueryType.Stream,
label: 'Stream',
description: 'Run a query and keep sending results on an interval',
});
}
export const DEFAULT_RESOLUTION: SelectableValue<number> = {
value: 1,
label: '1/1',
};
export 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, resolution, onRunQuery, runOnBlur, onChange } = props;
const { lineLimitValue, onRunQuery, runOnBlur, onChange } = props;
const query = props.query ?? {};
const queryType = getLokiQueryType(query);
function onChangeQueryLimit(value: string) {
const nextQuery = { ...query, maxLines: preprocessMaxLines(value) };
onChange(nextQuery);
}
function onQueryTypeChange(queryType: LokiQueryType) {
const { instant, range, ...rest } = query;
onChange({ ...rest, queryType });
}
function onMaxLinesChange(e: React.SyntheticEvent<HTMLInputElement>) {
if (query.maxLines !== preprocessMaxLines(e.currentTarget.value)) {
onChangeQueryLimit(e.currentTarget.value);
@@ -98,28 +67,8 @@ export function LokiOptionFields(props: LokiOptionFieldsProps) {
}
}
function onResolutionChange(option: SelectableValue<number>) {
const nextQuery = { ...query, resolution: option.value };
onChange(nextQuery);
}
return (
<Stack alignItems="flex-start" gap={0.5} aria-label="Loki extra field">
{/*Query type field*/}
<Stack wrap="nowrap" gap={0} data-testid="queryTypeField" aria-label="Query type field">
<InlineFormLabel width="auto">Query type</InlineFormLabel>
<RadioButtonGroup
options={queryTypeOptions}
value={queryType}
onChange={(type: LokiQueryType) => {
onQueryTypeChange(type);
if (runOnBlur) {
onRunQuery();
}
}}
/>
</Stack>
{/*Line limit field*/}
<Stack wrap="nowrap" gap={0} data-testid="lineLimitField" aria-label="Line limit field">
<InlineField label="Line limit" tooltip={'Upper limit for number of log lines returned by query.'}>
@@ -138,20 +87,6 @@ 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}
aria-label="Select resolution"
/>
</InlineField>
</Stack>
</Stack>
);
@@ -122,35 +122,13 @@ describe('LokiQueryBuilderOptions', () => {
});
it('shows correct options for metric query', async () => {
setup({ expr: 'rate({foo="bar"}[5m]', step: '1m', resolution: 2 });
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();
expect(screen.getByText('Resolution: 1/2')).toBeInTheDocument();
expect(screen.queryByText(/Direction/)).not.toBeInTheDocument();
});
it('does not show resolution field if resolution is not set', async () => {
setup({ expr: 'rate({foo="bar"}[5m]' });
await userEvent.click(screen.getByRole('button', { name: /Options/ }));
expect(screen.queryByText('Resolution')).not.toBeInTheDocument();
});
it('does not show resolution field if resolution is set to default value 1', async () => {
setup({ expr: 'rate({foo="bar"}[5m]', resolution: 1 });
await userEvent.click(screen.getByRole('button', { name: /Options/ }));
expect(screen.queryByText('Resolution')).not.toBeInTheDocument();
});
it('does shows resolution field with warning if resolution is set to non-default value', async () => {
setup({ expr: 'rate({foo="bar"}[5m]', resolution: 2 });
await userEvent.click(screen.getByRole('button', { name: /Options/ }));
expect(screen.getByText('Resolution')).toBeInTheDocument();
expect(
screen.getByText("The 'Resolution' is deprecated. Use 'Step' editor instead to change step parameter.")
).toBeInTheDocument();
});
it.each(['abc', 10])('shows correct options for metric query with invalid step', async (step: string | number) => {
// @ts-expect-error Expected for backward compatibility test
setup({ expr: 'rate({foo="bar"}[5m]', step });
@@ -8,19 +8,17 @@ import {
isValidGrafanaDuration,
LogSortOrderChangeEvent,
LogsSortOrder,
SelectableValue,
store,
} from '@grafana/data';
import { EditorField, EditorRow, QueryOptionGroup } from '@grafana/plugin-ui';
import { config, getAppEvents, reportInteraction } from '@grafana/runtime';
import { Alert, AutoSizeInput, RadioButtonGroup, Select } from '@grafana/ui';
import { config, getAppEvents } from '@grafana/runtime';
import { AutoSizeInput, RadioButtonGroup } from '@grafana/ui';
import {
getQueryDirectionLabel,
preprocessMaxLines,
queryDirections,
queryTypeOptions,
RESOLUTION_OPTIONS,
} from '../../components/LokiOptionFields';
import { getLokiQueryType, isLogsQuery } from '../../queryUtils';
import { LokiQuery, LokiQueryDirection, LokiQueryType, QueryStats } from '../../types';
@@ -70,15 +68,6 @@ export const LokiQueryBuilderOptions = React.memo<Props>(
[onChange, onRunQuery, query]
);
const onResolutionChange = (option: SelectableValue<number>) => {
reportInteraction('grafana_loki_resolution_clicked', {
app,
resolution: option.value,
});
onChange({ ...query, resolution: option.value });
onRunQuery();
};
const onChunkRangeChange = (evt: React.FormEvent<HTMLInputElement>) => {
const value = evt.currentTarget.value;
if (!isValidDuration(value)) {
@@ -209,26 +198,6 @@ export const LokiQueryBuilderOptions = React.memo<Props>(
onCommitChange={onStepChange}
/>
</EditorField>
{query.resolution !== undefined && query.resolution > 1 && (
<>
<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>
<Alert
severity="warning"
title="The 'Resolution' is deprecated. Use 'Step' editor instead to change step parameter."
/>
</>
)}
</>
)}
{config.featureToggles.lokiQuerySplittingConfig && config.featureToggles.lokiQuerySplitting && (
@@ -261,7 +230,6 @@ function getCollapsedInfo(
direction: LokiQueryDirection | undefined
): string[] {
const queryTypeLabel = queryTypeOptions.find((x) => x.value === queryType);
const resolutionLabel = RESOLUTION_OPTIONS.find((x) => x.value === (query.resolution ?? 1));
const items: string[] = [];
@@ -278,10 +246,6 @@ function getCollapsedInfo(
if (query.step) {
items.push(`Step: ${isValidStep ? query.step : 'Invalid value'}`);
}
if (query.resolution) {
items.push(`Resolution: ${resolutionLabel?.label}`);
}
}
return items;