diff --git a/public/app/plugins/datasource/loki/components/LokiExploreExtraField.test.tsx b/public/app/plugins/datasource/loki/components/LokiExploreExtraField.test.tsx index 5e75d1da317..486d4114758 100644 --- a/public/app/plugins/datasource/loki/components/LokiExploreExtraField.test.tsx +++ b/public/app/plugins/datasource/loki/components/LokiExploreExtraField.test.tsx @@ -1,32 +1,35 @@ 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(); + return render(); }; 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(); }); }); diff --git a/public/app/plugins/datasource/loki/components/LokiExploreExtraField.tsx b/public/app/plugins/datasource/loki/components/LokiExploreExtraField.tsx index 61a32a2ce2c..a8e96ffd9b6 100644 --- a/public/app/plugins/datasource/loki/components/LokiExploreExtraField.tsx +++ b/public/app/plugins/datasource/loki/components/LokiExploreExtraField.tsx @@ -1,33 +1,68 @@ // 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) => void; + lineLimitValue: string; + queryType: string; + onLineLimitChange: (e: React.SyntheticEvent) => void; onKeyDownFunc: (e: React.KeyboardEvent) => 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 ( -
-
- {label} +
+ {/*Query type field*/} +
+ + Query type + + + +
+ {/*Line limit field*/} +
+ Line limit
diff --git a/public/app/plugins/datasource/loki/components/LokiExploreQueryEditor.tsx b/public/app/plugins/datasource/loki/components/LokiExploreQueryEditor.tsx index ebca0ae0afa..01abd8807ae 100644 --- a/public/app/plugins/datasource/loki/components/LokiExploreQueryEditor.tsx +++ b/public/app/plugins/datasource/loki/components/LokiExploreQueryEditor.tsx @@ -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={ } /> diff --git a/public/app/plugins/datasource/loki/components/LokiQueryFieldForm.tsx b/public/app/plugins/datasource/loki/components/LokiQueryFieldForm.tsx index c40f4f9aa3d..931fb679067 100644 --- a/public/app/plugins/datasource/loki/components/LokiQueryFieldForm.tsx +++ b/public/app/plugins/datasource/loki/components/LokiQueryFieldForm.tsx @@ -150,7 +150,7 @@ export class LokiQueryFieldForm extends React.PureComponent
-
+
-
+
- {ExtraFieldElement}
+ {ExtraFieldElement} ); } diff --git a/public/app/plugins/datasource/loki/components/__snapshots__/LokiExploreExtraField.test.tsx.snap b/public/app/plugins/datasource/loki/components/__snapshots__/LokiExploreExtraField.test.tsx.snap deleted file mode 100644 index 0a968e90b02..00000000000 --- a/public/app/plugins/datasource/loki/components/__snapshots__/LokiExploreExtraField.test.tsx.snap +++ /dev/null @@ -1,26 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`LokiExploreExtraField should render component 1`] = ` -
-
- - Loki Explore Extra Field - - -
-
-`; diff --git a/public/app/plugins/datasource/loki/components/__snapshots__/LokiExploreQueryEditor.test.tsx.snap b/public/app/plugins/datasource/loki/components/__snapshots__/LokiExploreQueryEditor.test.tsx.snap index 0a46681c9b3..fc69ac05249 100644 --- a/public/app/plugins/datasource/loki/components/__snapshots__/LokiExploreQueryEditor.test.tsx.snap +++ b/public/app/plugins/datasource/loki/components/__snapshots__/LokiExploreQueryEditor.test.tsx.snap @@ -4,12 +4,11 @@ exports[`LokiExploreQueryEditor should render component 1`] = ` } data={ diff --git a/public/app/plugins/datasource/loki/datasource.test.ts b/public/app/plugins/datasource/loki/datasource.test.ts index 80abc8d10c6..4696f839b8f 100644 --- a/public/app/plugins/datasource/loki/datasource.test.ts +++ b/public/app/plugins/datasource/loki/datasource.test.ts @@ -1,7 +1,6 @@ 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 = { +const testLogsResponse: FetchResponse = { data: { data: { resultType: LokiResultType.Stream, @@ -49,6 +48,29 @@ const testResponse: FetchResponse = { config: ({} as unknown) as BackendSrvRequest, }; +const testMetricsResponse: FetchResponse = { + 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({ 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({ - 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).toBeCalled(); + expect(ds.runRangeQuery).not.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 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 range query in Dashboard', async () => { - const { ds, options } = setup('rate({job="grafana"}[10m])', CoreApp.Dashboard); + 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 return series data for both queries in Explore if metrics query', async () => { + 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 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 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 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({ - 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({ - 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'); diff --git a/public/app/plugins/datasource/loki/datasource.ts b/public/app/plugins/datasource/loki/datasource.ts index 93ba8db9131..9ba4d515c92 100644 --- a/public/app/plugins/datasource/loki/datasource.ts +++ b/public/app/plugins/datasource/loki/datasource.ts @@ -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 { })); 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 { responseListLength: number ): Observable => { 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 { 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`, }; } diff --git a/public/app/plugins/datasource/loki/result_transformer.test.ts b/public/app/plugins/datasource/loki/result_transformer.test.ts index 0f1cc7a79e3..73d20dbfd84 100644 --- a/public/app/plugins/datasource/loki/result_transformer.test.ts +++ b/public/app/plugins/datasource/loki/result_transformer.test.ts @@ -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+)', diff --git a/public/app/plugins/datasource/loki/result_transformer.ts b/public/app/plugins/datasource/loki/result_transformer.ts index 092eb7ebd2f..3788224d568 100644 --- a/public/app/plugins/datasource/loki/result_transformer.ts +++ b/public/app/plugins/datasource/loki/result_transformer.ts @@ -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`, }); diff --git a/public/app/plugins/datasource/loki/types.ts b/public/app/plugins/datasource/loki/types.ts index cb09ff3dd5c..0b751fb73f5 100644 --- a/public/app/plugins/datasource/loki/types.ts +++ b/public/app/plugins/datasource/loki/types.ts @@ -30,6 +30,8 @@ export interface LokiQuery extends DataQuery { legendFormat?: string; valueWithRefId?: boolean; maxLines?: number; + range?: boolean; + instant?: boolean; } export interface LokiOptions extends DataSourceJsonData { diff --git a/public/app/plugins/datasource/prometheus/components/PromExploreExtraField.tsx b/public/app/plugins/datasource/prometheus/components/PromExploreExtraField.tsx index 2c05ed14c31..80bcf7c971f 100644 --- a/public/app/plugins/datasource/prometheus/components/PromExploreExtraField.tsx +++ b/public/app/plugins/datasource/prometheus/components/PromExploreExtraField.tsx @@ -23,7 +23,7 @@ export const PromExploreExtraField: React.FC = memo( return (
- {/*QueryTypeField */} + {/*Query type field*/}