Commit 4c103663 by Ivana Huckova Committed by GitHub

Loki/Explore: Add query type selector (#28817)

* Create query type switcher

* Add and update tests

* Add handling in datasource

* Refactor

* Update tests, when checking higlighting, suppy logs

* Remove both option as redundant

* Add tooltip, remove old comments

* Remove unused importts

* Remove console.log, update width

* Update public/app/plugins/datasource/loki/components/LokiExploreExtraField.tsx

Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com>

* Update tests

* Prettier fixes

* Fix test

Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com>
Co-authored-by: Zoltán Bedi <zoltan.bedi@gmail.com>
parent 9ba8114b
import React from 'react';
import { shallow } from 'enzyme';
import { LokiExploreExtraField, LokiExploreExtraFieldProps } from './LokiExploreExtraField';
import { render, screen } from '@testing-library/react';
import { LokiExploreExtraFieldProps, LokiExploreExtraField } from './LokiExploreExtraField';
const setup = (propOverrides?: LokiExploreExtraFieldProps) => {
const label = 'Loki Explore Extra Field';
const value = '123';
const type = 'number';
const min = 0;
const onChangeFunc = jest.fn();
const queryType = 'range';
const lineLimitValue = '1';
const onLineLimitChange = jest.fn();
const onQueryTypeChange = jest.fn();
const onKeyDownFunc = jest.fn();
const props: any = {
label,
value,
type,
min,
onChangeFunc,
queryType,
lineLimitValue,
onLineLimitChange,
onQueryTypeChange,
onKeyDownFunc,
};
Object.assign(props, propOverrides);
return shallow(<LokiExploreExtraField {...props} />);
return render(<LokiExploreExtraField {...props} />);
};
describe('LokiExploreExtraField', () => {
it('should render component', () => {
const wrapper = setup();
expect(wrapper).toMatchSnapshot();
it('should render step field', () => {
setup();
expect(screen.getByTestId('lineLimitField')).toBeInTheDocument();
});
it('should render query type field', () => {
setup();
expect(screen.getByTestId('queryTypeField')).toBeInTheDocument();
});
});
// Libraries
import React, { memo } from 'react';
import { css, cx } from 'emotion';
// Types
import { InlineFormLabel } from '@grafana/ui';
import { InlineFormLabel, RadioButtonGroup } from '@grafana/ui';
export interface LokiExploreExtraFieldProps {
label: string;
onChangeFunc: (e: React.SyntheticEvent<HTMLInputElement>) => void;
lineLimitValue: string;
queryType: string;
onLineLimitChange: (e: React.SyntheticEvent<HTMLInputElement>) => void;
onKeyDownFunc: (e: React.KeyboardEvent<HTMLInputElement>) => void;
value: string;
type?: string;
min?: number;
onQueryTypeChange: (value: string) => void;
}
export function LokiExploreExtraField(props: LokiExploreExtraFieldProps) {
const { label, onChangeFunc, onKeyDownFunc, value, type, min } = props;
const { onLineLimitChange, onKeyDownFunc, lineLimitValue, queryType, onQueryTypeChange } = props;
const rangeOptions = [
{ value: 'range', label: 'Range' },
{ value: 'instant', label: 'Instant' },
];
return (
<div className="gf-form-inline">
<div className="gf-form">
<InlineFormLabel width={5}>{label}</InlineFormLabel>
<div aria-label="Loki extra field" className="gf-form-inline">
{/*Query type field*/}
<div
data-testid="queryTypeField"
className={cx(
'gf-form explore-input-margin',
css`
flex-wrap: nowrap;
`
)}
aria-label="Query type field"
>
<InlineFormLabel
tooltip="Choose the type of query you would like to run. An instant query queries against a single point in time. A range query queries over a range of time."
width="auto"
>
Query type
</InlineFormLabel>
<RadioButtonGroup options={rangeOptions} value={queryType} onChange={onQueryTypeChange} />
</div>
{/*Line limit field*/}
<div
data-testid="lineLimitField"
className={cx(
'gf-form',
css`
flex-wrap: nowrap;
`
)}
aria-label="Line limit field"
>
<InlineFormLabel width={5}>Line limit</InlineFormLabel>
<input
type={type}
type="number"
className="gf-form-input width-4"
placeholder={'auto'}
onChange={onChangeFunc}
min={0}
onChange={onLineLimitChange}
onKeyDown={onKeyDownFunc}
min={min}
value={value}
value={lineLimitValue}
/>
</div>
</div>
......
......@@ -20,6 +20,17 @@ export function LokiExploreQueryEditor(props: Props) {
onChange(nextQuery);
}
function onQueryTypeChange(value: string) {
const { query, onChange } = props;
let nextQuery;
if (value === 'instant') {
nextQuery = { ...query, instant: true, range: false };
} else {
nextQuery = { ...query, instant: false, range: true };
}
onChange(nextQuery);
}
function preprocessMaxLines(value: string): number {
if (value.length === 0) {
// empty input - falls back to dataSource.maxLines limit
......@@ -58,12 +69,11 @@ export function LokiExploreQueryEditor(props: Props) {
range={range}
ExtraFieldElement={
<LokiExploreExtraField
label={'Line limit'}
onChangeFunc={onMaxLinesChange}
queryType={query.instant ? 'instant' : 'range'}
lineLimitValue={query?.maxLines?.toString() || ''}
onQueryTypeChange={onQueryTypeChange}
onLineLimitChange={onMaxLinesChange}
onKeyDownFunc={onReturnKeyDown}
value={query?.maxLines?.toString() || ''}
type={'number'}
min={0}
/>
}
/>
......
......@@ -150,7 +150,7 @@ export class LokiQueryFieldForm extends React.PureComponent<LokiQueryFieldFormPr
return (
<>
<div className="gf-form-inline gf-form-inline--xs-view-flex-column flex-grow-1">
<div className="gf-form flex-shrink-0">
<div className="gf-form flex-shrink-0 min-width-5">
<ButtonCascader
options={logLabelOptions || []}
disabled={buttonDisabled}
......@@ -161,7 +161,7 @@ export class LokiQueryFieldForm extends React.PureComponent<LokiQueryFieldFormPr
{chooserText}
</ButtonCascader>
</div>
<div className="gf-form gf-form--grow flex-shrink-1 min-width-15 explore-input-margin">
<div className="gf-form gf-form--grow flex-shrink-1 min-width-15">
<QueryField
additionalPlugins={this.plugins}
cleanText={cleanText}
......@@ -176,8 +176,8 @@ export class LokiQueryFieldForm extends React.PureComponent<LokiQueryFieldFormPr
syntaxLoaded={syntaxLoaded}
/>
</div>
{ExtraFieldElement}
</div>
{ExtraFieldElement}
</>
);
}
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`LokiExploreExtraField should render component 1`] = `
<div
className="gf-form-inline"
>
<div
className="gf-form"
>
<Component
width={5}
>
Loki Explore Extra Field
</Component>
<input
className="gf-form-input width-4"
min={0}
onChange={[MockFunction]}
onKeyDown={[MockFunction]}
placeholder="auto"
type="number"
value="123"
/>
</div>
</div>
`;
......@@ -4,12 +4,11 @@ exports[`LokiExploreQueryEditor should render component 1`] = `
<Component
ExtraFieldElement={
<Memo(LokiExploreExtraField)
label="Line limit"
min={0}
onChangeFunc={[Function]}
lineLimitValue="0"
onKeyDownFunc={[Function]}
type="number"
value="0"
onLineLimitChange={[Function]}
onQueryTypeChange={[Function]}
queryType="range"
/>
}
data={
......
import { of, throwError } from 'rxjs';
import { take } from 'rxjs/operators';
import { omit } from 'lodash';
import { AnnotationQueryRequest, CoreApp, DataFrame, dateTime, FieldCache, TimeRange } from '@grafana/data';
import { AnnotationQueryRequest, CoreApp, DataFrame, dateTime, FieldCache, TimeRange, TimeSeries } from '@grafana/data';
import { BackendSrvRequest, FetchResponse } from '@grafana/runtime';
import LokiDatasource from './datasource';
......@@ -26,7 +25,7 @@ const timeSrvStub = {
}),
};
const testResponse: FetchResponse<LokiResponse> = {
const testLogsResponse: FetchResponse<LokiResponse> = {
data: {
data: {
resultType: LokiResultType.Stream,
......@@ -49,6 +48,29 @@ const testResponse: FetchResponse<LokiResponse> = {
config: ({} as unknown) as BackendSrvRequest,
};
const testMetricsResponse: FetchResponse<LokiResponse> = {
data: {
data: {
resultType: LokiResultType.Matrix,
result: [
{
metric: {},
values: [[1605715380, '1.1']],
},
],
},
status: 'success',
},
ok: true,
headers: ({} as unknown) as Headers,
redirected: false,
status: 200,
statusText: 'OK',
type: 'basic',
url: '',
config: ({} as unknown) as BackendSrvRequest,
};
describe('LokiDatasource', () => {
const fetchMock = jest.spyOn(backendSrv, 'fetch');
......@@ -96,7 +118,7 @@ describe('LokiDatasource', () => {
});
});
describe('when querying with limits', () => {
describe('when doing logs queries with limits', () => {
const runLimitTest = async ({
maxDataPoints = 123,
queryMaxLines,
......@@ -121,7 +143,7 @@ describe('LokiDatasource', () => {
const options = getQueryOptions<LokiQuery>({ targets: [{ expr, refId: 'B', maxLines: queryMaxLines }] });
options.maxDataPoints = maxDataPoints;
fetchMock.mockImplementation(() => of(testResponse));
fetchMock.mockImplementation(() => of(testLogsResponse));
await expect(ds.query(options).pipe(take(1))).toEmitValuesWith(() => {
expect(fetchMock.mock.calls.length).toBe(1);
......@@ -151,10 +173,10 @@ describe('LokiDatasource', () => {
});
describe('when querying', () => {
function setup(expr: string, app: CoreApp) {
function setup(expr: string, app: CoreApp, instant?: boolean, range?: boolean) {
const ds = createLokiDSForTests();
const options = getQueryOptions<LokiQuery>({
targets: [{ expr, refId: 'B' }],
targets: [{ expr, refId: 'B', instant, range }],
app,
});
ds.runInstantQuery = jest.fn(() => of({ data: [] }));
......@@ -162,68 +184,95 @@ describe('LokiDatasource', () => {
return { ds, options };
}
it('should run range and instant query in Explore if running metric query', async () => {
const { ds, options } = setup('rate({job="grafana"}[10m])', CoreApp.Explore);
const metricsQuery = 'rate({job="grafana"}[10m])';
const logsQuery = '{job="grafana"} |= "foo"';
it('should run logs instant if only instant is selected', async () => {
const { ds, options } = setup(logsQuery, CoreApp.Explore, true, false);
await ds.query(options).toPromise();
expect(ds.runInstantQuery).toBeCalled();
expect(ds.runRangeQuery).not.toBeCalled();
});
it('should run metrics instant if only instant is selected', async () => {
const { ds, options } = setup(metricsQuery, CoreApp.Explore, true, false);
await ds.query(options).toPromise();
expect(ds.runInstantQuery).toBeCalled();
expect(ds.runRangeQuery).not.toBeCalled();
});
it('should run only logs range query if only range is selected', async () => {
const { ds, options } = setup(logsQuery, CoreApp.Explore, false, true);
await ds.query(options).toPromise();
expect(ds.runInstantQuery).not.toBeCalled();
expect(ds.runRangeQuery).toBeCalled();
});
it('should run only metrics range query if only range is selected', async () => {
const { ds, options } = setup(metricsQuery, CoreApp.Explore, false, true);
await ds.query(options).toPromise();
expect(ds.runInstantQuery).not.toBeCalled();
expect(ds.runRangeQuery).toBeCalled();
});
it('should run only logs range query if no query type is selected in Explore', async () => {
const { ds, options } = setup(logsQuery, CoreApp.Explore);
await ds.query(options).toPromise();
expect(ds.runInstantQuery).not.toBeCalled();
expect(ds.runRangeQuery).toBeCalled();
});
it('should run only range query in Explore if running logs query', async () => {
const { ds, options } = setup('{job="grafana"}', CoreApp.Explore);
it('should run only metrics range query if no query type is selected in Explore', async () => {
const { ds, options } = setup(metricsQuery, CoreApp.Explore);
await ds.query(options).toPromise();
expect(ds.runInstantQuery).not.toBeCalled();
expect(ds.runRangeQuery).toBeCalled();
});
it('should run only range query in Dashboard', async () => {
const { ds, options } = setup('rate({job="grafana"}[10m])', CoreApp.Dashboard);
it('should run only logs range query in Dashboard', async () => {
const { ds, options } = setup(logsQuery, CoreApp.Dashboard);
await ds.query(options).toPromise();
expect(ds.runInstantQuery).not.toBeCalled();
expect(ds.runRangeQuery).toBeCalled();
});
it('should return series data for both queries in Explore if metrics query', async () => {
it('should run only metrics range query in Dashboard', async () => {
const { ds, options } = setup(metricsQuery, CoreApp.Dashboard);
await ds.query(options).toPromise();
expect(ds.runInstantQuery).not.toBeCalled();
expect(ds.runRangeQuery).toBeCalled();
});
it('should return series data for metrics range queries', async () => {
const ds = createLokiDSForTests();
const options = getQueryOptions<LokiQuery>({
targets: [{ expr: 'rate({job="grafana"} |= "foo" [10m])', refId: 'B' }],
targets: [{ expr: metricsQuery, refId: 'B', range: true }],
app: CoreApp.Explore,
});
fetchMock
.mockImplementationOnce(() => of(testResponse))
.mockImplementation(() => of(omit(testResponse, 'data.status')));
fetchMock.mockImplementation(() => of(testMetricsResponse));
await expect(ds.query(options)).toEmitValuesWith(received => {
// first result always comes from runInstantQuery
const firstResult = received[0];
expect(firstResult).toEqual({ data: [], key: 'B_instant' });
const result = received[0];
const timeSeries = result.data[0] as TimeSeries;
// second result always comes from runRangeQuery
const secondResult = received[1];
const dataFrame = secondResult.data[0] as DataFrame;
const fieldCache = new FieldCache(dataFrame);
expect(fieldCache.getFieldByName('line')?.values.get(0)).toBe('hello');
expect(dataFrame.meta?.limit).toBe(500);
expect(dataFrame.meta?.searchWords).toEqual([]);
expect(timeSeries.meta?.preferredVisualisationType).toBe('graph');
expect(timeSeries.refId).toBe('B');
expect(timeSeries.datapoints[0]).toEqual([1.1, 1605715380000]);
});
});
it('should return series data for range query in Dashboard', async () => {
it('should return series data for logs range query', async () => {
const ds = createLokiDSForTests();
const options = getQueryOptions<LokiQuery>({
targets: [{ expr: '{job="grafana"} |= "foo"', refId: 'B' }],
targets: [{ expr: logsQuery, refId: 'B' }],
});
fetchMock
.mockImplementationOnce(() => of(testResponse))
.mockImplementation(() => of(omit(testResponse, 'data.status')));
fetchMock.mockImplementation(() => of(testLogsResponse));
await expect(ds.query(options)).toEmitValuesWith(received => {
// first result will come from runRangeQuery
const firstResult = received[0];
const dataFrame = firstResult.data[0] as DataFrame;
const result = received[0];
const dataFrame = result.data[0] as DataFrame;
const fieldCache = new FieldCache(dataFrame);
expect(fieldCache.getFieldByName('line')?.values.get(0)).toBe('hello');
......
......@@ -24,13 +24,17 @@ import {
QueryResultMeta,
ScopedVars,
TimeRange,
CoreApp,
} from '@grafana/data';
import { getTemplateSrv, TemplateSrv, BackendSrvRequest, FetchError, getBackendSrv } from '@grafana/runtime';
import { addLabelToQuery } from 'app/plugins/datasource/prometheus/add_label_to_query';
import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { convertToWebSocketUrl } from 'app/core/utils/explore';
import { lokiResultsToTableModel, lokiStreamResultToDataFrame, processRangeQueryResponse } from './result_transformer';
import {
lokiResultsToTableModel,
lokiStreamResultToDataFrame,
lokiStreamsToDataFrames,
processRangeQueryResponse,
} from './result_transformer';
import { getHighlighterExpressionsFromQuery } from './query_utils';
import {
......@@ -99,12 +103,11 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> {
}));
for (const target of filteredTargets) {
// In explore we want to show result of metrics instant query in a table under the graph panel to mimic behaviour of prometheus.
// We don't want to do that in dashboards though as user would have to pick the correct data frame.
if (options.app === CoreApp.Explore && isMetricsQuery(target.expr)) {
if (target.instant) {
subQueries.push(this.runInstantQuery(target, options, filteredTargets.length));
} else {
subQueries.push(this.runRangeQuery(target, options, filteredTargets.length));
}
subQueries.push(this.runRangeQuery(target, options, filteredTargets.length));
}
// No valid targets, return the empty result to save a round trip.
......@@ -124,12 +127,14 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> {
responseListLength: number
): Observable<DataQueryResponse> => {
const timeNs = this.getTime(options.range.to, true);
const queryLimit = isMetricsQuery(target.expr) ? options.maxDataPoints : target.maxLines;
const query = {
query: target.expr,
time: `${timeNs + (1e9 - (timeNs % 1e9))}`,
limit: Math.min(options.maxDataPoints || Infinity, this.maxLines),
limit: Math.min(queryLimit || Infinity, this.maxLines),
};
/** Show results of Loki instant queries only in table */
/** Used only for results of metrics instant queries */
const meta: QueryResultMeta = {
preferredVisualisationType: 'table',
};
......@@ -138,7 +143,14 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> {
map((response: { data: LokiResponse }) => {
if (response.data.data.resultType === LokiResultType.Stream) {
return {
data: [],
data: response.data
? lokiStreamsToDataFrames(
response.data as LokiStreamResponse,
target,
query.limit,
this.instanceSettings.jsonData
)
: [],
key: `${target.refId}_instant`,
};
}
......
......@@ -61,10 +61,10 @@ describe('loki result transformer', () => {
});
});
describe('lokiStreamsToDataframes', () => {
describe('lokiStreamsToDataFrames', () => {
it('should enhance data frames', () => {
jest.spyOn(ResultTransformer, 'enhanceDataFrame');
const dataFrames = ResultTransformer.lokiStreamsToDataframes(lokiResponse, { refId: 'B' }, 500, {
const dataFrames = ResultTransformer.lokiStreamsToDataFrames(lokiResponse, { refId: 'B' }, 500, {
derivedFields: [
{
matcherRegex: 'trace=(w+)',
......
......@@ -305,7 +305,7 @@ function lokiStatsToMetaStat(stats: LokiStats | undefined): QueryResultMetaStat[
return result;
}
export function lokiStreamsToDataframes(
export function lokiStreamsToDataFrames(
response: LokiStreamResponse,
target: { refId: string; expr?: string },
limit: number,
......@@ -472,7 +472,7 @@ export function processRangeQueryResponse(
switch (response.data.resultType) {
case LokiResultType.Stream:
return of({
data: lokiStreamsToDataframes(response as LokiStreamResponse, target, limit, config, reverse),
data: lokiStreamsToDataFrames(response as LokiStreamResponse, target, limit, config, reverse),
key: `${target.refId}_log`,
});
......
......@@ -30,6 +30,8 @@ export interface LokiQuery extends DataQuery {
legendFormat?: string;
valueWithRefId?: boolean;
maxLines?: number;
range?: boolean;
instant?: boolean;
}
export interface LokiOptions extends DataSourceJsonData {
......
......@@ -23,7 +23,7 @@ export const PromExploreExtraField: React.FC<PromExploreExtraFieldProps> = memo(
return (
<div aria-label="Prometheus extra field" className="gf-form-inline">
{/*QueryTypeField */}
{/*Query type field*/}
<div
data-testid="queryTypeField"
className={cx(
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment