diff --git a/public/app/plugins/datasource/loki/querySplitting.test.ts b/public/app/plugins/datasource/loki/querySplitting.test.ts index 6cf98353d40..f6faeee5c67 100644 --- a/public/app/plugins/datasource/loki/querySplitting.test.ts +++ b/public/app/plugins/datasource/loki/querySplitting.test.ts @@ -1,7 +1,6 @@ import { of } from 'rxjs'; -import { getQueryOptions } from 'test/helpers/getQueryOptions'; -import { dateTime, LoadingState } from '@grafana/data'; +import { DataQueryRequest, dateTime, LoadingState } from '@grafana/data'; import { createLokiDatasource } from './__mocks__/datasource'; import { getMockFrames } from './__mocks__/frames'; @@ -27,10 +26,19 @@ describe('runSplitQuery()', () => { to: dateTime('2023-02-10T06:00:00.000Z'), }, }; - const request = getQueryOptions({ - targets: [{ expr: 'count_over_time({a="b"}[1m])', refId: 'A' }], - range, - }); + + const createRequest = (targets: LokiQuery[], overrides?: Partial>) => { + const request = { + range, + targets, + intervalMs: 60000, + requestId: 'TEST', + } as DataQueryRequest; + + Object.assign(request, overrides); + return request; + }; + const request = createRequest([{ expr: 'count_over_time({a="b"}[1m])', refId: 'A' }]); beforeEach(() => { datasource = createLokiDatasource(); jest.spyOn(datasource, 'runQuery').mockReturnValue(of({ data: [] })); @@ -44,10 +52,7 @@ describe('runSplitQuery()', () => { }); test('Metric queries with maxLines of 0 will execute', async () => { - const request = getQueryOptions({ - targets: [{ expr: 'count_over_time({a="b"}[1m])', refId: 'A', maxLines: 0 }], - range, - }); + const request = createRequest([{ expr: 'count_over_time({a="b"}[1m])', refId: 'A', maxLines: 0 }]); await expect(runSplitQuery(datasource, request)).toEmitValuesWith(() => { // 3 days, 3 chunks, 3 requests. expect(datasource.runQuery).toHaveBeenCalledTimes(3); @@ -55,10 +60,7 @@ describe('runSplitQuery()', () => { }); test('Log queries with maxLines of 0 will NOT execute', async () => { - const request = getQueryOptions({ - targets: [{ expr: '{a="b"}', refId: 'A', maxLines: 0 }], - range, - }); + const request = createRequest([{ expr: '{a="b"}', refId: 'A', maxLines: 0 }]); await expect(runSplitQuery(datasource, request)).toEmitValuesWith(() => { // Will not request a log query with maxLines of 0 expect(datasource.runQuery).toHaveBeenCalledTimes(0); @@ -220,13 +222,10 @@ describe('runSplitQuery()', () => { jest.useRealTimers(); }); test('Ignores hidden queries', async () => { - const request = getQueryOptions({ - targets: [ - { expr: 'count_over_time({a="b"}[1m])', refId: 'A', hide: true }, - { expr: '{a="b"}', refId: 'B' }, - ], - range, - }); + const request = createRequest([ + { expr: 'count_over_time({a="b"}[1m])', refId: 'A', hide: true }, + { expr: '{a="b"}', refId: 'B' }, + ]); await expect(runSplitQuery(datasource, request)).toEmitValuesWith(() => { expect(logsTimeSplit.splitTimeRange).toHaveBeenCalled(); expect(metricTimeSplit.splitTimeRange).not.toHaveBeenCalled(); @@ -253,13 +252,10 @@ describe('runSplitQuery()', () => { }); }); test('Ignores empty queries', async () => { - const request = getQueryOptions({ - targets: [ - { expr: 'count_over_time({a="b"}[1m])', refId: 'A' }, - { expr: '', refId: 'B' }, - ], - range, - }); + const request = createRequest([ + { expr: 'count_over_time({a="b"}[1m])', refId: 'A' }, + { expr: '', refId: 'B' }, + ]); await expect(runSplitQuery(datasource, request)).toEmitValuesWith(() => { expect(logsTimeSplit.splitTimeRange).not.toHaveBeenCalled(); expect(metricTimeSplit.splitTimeRange).toHaveBeenCalled(); @@ -288,10 +284,7 @@ describe('runSplitQuery()', () => { }); describe('Dynamic maxLines for logs requests', () => { - const request = getQueryOptions({ - targets: [{ expr: '{a="b"}', refId: 'A', maxLines: 4 }], - range, - }); + const request = createRequest([{ expr: '{a="b"}', refId: 'A', maxLines: 4 }]); const { logFrameA } = getMockFrames(); beforeEach(() => { jest.spyOn(datasource, 'runQuery').mockReturnValue(of({ data: [logFrameA], refId: 'A' })); @@ -324,52 +317,40 @@ describe('runSplitQuery()', () => { jest.spyOn(datasource, 'runQuery').mockReturnValue(of({ data: [], refId: 'A' })); }); test('Sends logs and metric queries individually', async () => { - const request = getQueryOptions({ - targets: [ - { expr: '{a="b"}', refId: 'A' }, - { expr: 'count_over_time({a="b"}[1m])', refId: 'B' }, - ], - range, - }); + const request = createRequest([ + { expr: '{a="b"}', refId: 'A' }, + { expr: 'count_over_time({a="b"}[1m])', refId: 'B' }, + ]); await expect(runSplitQuery(datasource, request)).toEmitValuesWith(() => { // 3 days, 3 chunks, 1x Metric + 1x Log, 6 requests. expect(datasource.runQuery).toHaveBeenCalledTimes(6); }); }); test('Groups metric queries', async () => { - const request = getQueryOptions({ - targets: [ - { expr: 'count_over_time({a="b"}[1m])', refId: 'A' }, - { expr: 'count_over_time({c="d"}[1m])', refId: 'B' }, - ], - range, - }); + const request = createRequest([ + { expr: 'count_over_time({a="b"}[1m])', refId: 'A' }, + { expr: 'count_over_time({c="d"}[1m])', refId: 'B' }, + ]); await expect(runSplitQuery(datasource, request)).toEmitValuesWith(() => { // 3 days, 3 chunks, 1x2 Metric, 3 requests. expect(datasource.runQuery).toHaveBeenCalledTimes(3); }); }); test('Groups logs queries', async () => { - const request = getQueryOptions({ - targets: [ - { expr: '{a="b"}', refId: 'A' }, - { expr: '{c="d"}', refId: 'B' }, - ], - range, - }); + const request = createRequest([ + { expr: '{a="b"}', refId: 'A' }, + { expr: '{c="d"}', refId: 'B' }, + ]); await expect(runSplitQuery(datasource, request)).toEmitValuesWith(() => { // 3 days, 3 chunks, 1x2 Logs, 3 requests. expect(datasource.runQuery).toHaveBeenCalledTimes(3); }); }); test('Groups instant queries', async () => { - const request = getQueryOptions({ - targets: [ - { expr: 'count_over_time({a="b"}[1m])', refId: 'A', queryType: LokiQueryType.Instant }, - { expr: 'count_over_time({c="d"}[1m])', refId: 'B', queryType: LokiQueryType.Instant }, - ], - range, - }); + const request = createRequest([ + { expr: 'count_over_time({a="b"}[1m])', refId: 'A', queryType: LokiQueryType.Instant }, + { expr: 'count_over_time({c="d"}[1m])', refId: 'B', queryType: LokiQueryType.Instant }, + ]); await expect(runSplitQuery(datasource, request)).toEmitValuesWith(() => { // Instant queries are omitted from splitting expect(datasource.runQuery).toHaveBeenCalledTimes(1); @@ -377,13 +358,10 @@ describe('runSplitQuery()', () => { }); test('Respects maxLines of logs queries', async () => { const { logFrameA } = getMockFrames(); - const request = getQueryOptions({ - targets: [ - { expr: '{a="b"}', refId: 'A', maxLines: logFrameA.fields[0].values.length }, - { expr: 'count_over_time({a="b"}[1m])', refId: 'B' }, - ], - range, - }); + const request = createRequest([ + { expr: '{a="b"}', refId: 'A', maxLines: logFrameA.fields[0].values.length }, + { expr: 'count_over_time({a="b"}[1m])', refId: 'B' }, + ]); jest.spyOn(datasource, 'runQuery').mockReturnValue(of({ data: [], refId: 'B' })); jest.spyOn(datasource, 'runQuery').mockReturnValueOnce(of({ data: [logFrameA], refId: 'A' })); @@ -393,14 +371,11 @@ describe('runSplitQuery()', () => { }); }); test('Groups multiple queries into logs, queries, instant', async () => { - const request = getQueryOptions({ - targets: [ - { expr: 'count_over_time({a="b"}[1m])', refId: 'A', queryType: LokiQueryType.Instant }, - { expr: '{c="d"}', refId: 'B' }, - { expr: 'count_over_time({c="d"}[1m])', refId: 'C' }, - ], - range, - }); + const request = createRequest([ + { expr: 'count_over_time({a="b"}[1m])', refId: 'A', queryType: LokiQueryType.Instant }, + { expr: '{c="d"}', refId: 'B' }, + { expr: 'count_over_time({c="d"}[1m])', refId: 'C' }, + ]); await expect(runSplitQuery(datasource, request)).toEmitValuesWith(() => { // 3 days, 3 chunks, 3x Logs + 3x Metric + (1x Instant), 7 requests. expect(datasource.runQuery).toHaveBeenCalledTimes(7); @@ -421,70 +396,70 @@ describe('runSplitQuery()', () => { jest.spyOn(datasource, 'runQuery').mockReturnValue(of({ data: [], refId: 'A' })); }); test('with 30m splitDuration runs 2 queries', async () => { - const request = getQueryOptions({ + const request = { targets: [{ expr: '{a="b"}', refId: 'A', splitDuration: '30m' }], range: range1h, - }); + } as DataQueryRequest; await expect(runSplitQuery(datasource, request)).toEmitValuesWith(() => { expect(datasource.runQuery).toHaveBeenCalledTimes(2); }); }); test('with 1h splitDuration runs 1 queries', async () => { - const request = getQueryOptions({ + const request = { targets: [{ expr: '{a="b"}', refId: 'A', splitDuration: '1h' }], range: range1h, - }); + } as DataQueryRequest; await expect(runSplitQuery(datasource, request)).toEmitValuesWith(() => { expect(datasource.runQuery).toHaveBeenCalledTimes(1); }); }); test('with 1h splitDuration and 2 targets runs 1 queries', async () => { - const request = getQueryOptions({ + const request = { targets: [ { expr: '{a="b"}', refId: 'A', splitDuration: '1h' }, { expr: '{a="b"}', refId: 'B', splitDuration: '1h' }, ], range: range1h, - }); + } as DataQueryRequest; await expect(runSplitQuery(datasource, request)).toEmitValuesWith(() => { expect(datasource.runQuery).toHaveBeenCalledTimes(1); }); }); test('with 1h/30m splitDuration and 2 targets runs 3 queries', async () => { - const request = getQueryOptions({ + const request = { targets: [ { expr: '{a="b"}', refId: 'A', splitDuration: '1h' }, { expr: '{a="b"}', refId: 'B', splitDuration: '30m' }, ], range: range1h, - }); + } as DataQueryRequest; await expect(runSplitQuery(datasource, request)).toEmitValuesWith(() => { // 2 x 30m + 1 x 1h expect(datasource.runQuery).toHaveBeenCalledTimes(3); }); }); test('with mixed splitDuration runs the expected amount of queries', async () => { - const request = getQueryOptions({ - targets: [ + const request = createRequest( + [ { expr: 'count_over_time({c="d"}[1m])', refId: 'A', splitDuration: '15m' }, { expr: '{a="b"}', refId: 'B', splitDuration: '15m' }, { expr: '{a="b"}', refId: 'C', splitDuration: '1h' }, ], - range: range1h, - }); + { range: range1h } + ); await expect(runSplitQuery(datasource, request)).toEmitValuesWith(() => { // 4 * 15m + 4 * 15m + 1 * 1h expect(datasource.runQuery).toHaveBeenCalledTimes(9); }); }); test('with 1h/30m splitDuration and 1 log and 2 metric target runs 3 queries', async () => { - const request = getQueryOptions({ - targets: [ + const request = createRequest( + [ { expr: '{a="b"}', refId: 'A', splitDuration: '1h' }, { expr: 'count_over_time({c="d"}[1m])', refId: 'C', splitDuration: '30m' }, ], - range: range1h, - }); + { range: range1h } + ); await expect(runSplitQuery(datasource, request)).toEmitValuesWith(() => { // 2 x 30m + 1 x 1h expect(datasource.runQuery).toHaveBeenCalledTimes(3); @@ -502,26 +477,26 @@ describe('runSplitQuery()', () => { }, }; test('Groups logs queries by resolution', async () => { - const request = getQueryOptions({ - targets: [ + const request = createRequest( + [ { expr: '{a="b"}', refId: 'A', resolution: 3 }, { expr: '{a="b"}', refId: 'B', resolution: 5 }, ], - range: range1d, - }); + { range: range1d } + ); await expect(runSplitQuery(datasource, request)).toEmitValuesWith(() => { // A, B expect(datasource.runQuery).toHaveBeenCalledTimes(2); }); }); test('Groups metric queries with no step by calculated stepMs', async () => { - const request = getQueryOptions({ - targets: [ + const request = createRequest( + [ { expr: 'count_over_time({a="b"}[1m])', refId: 'A', resolution: 3 }, { expr: 'count_over_time{a="b"}[1m])', refId: 'B', resolution: 5 }, ], - range: range1d, - }); + { range: range1d } + ); await expect(runSplitQuery(datasource, request)).toEmitValuesWith(() => { // A, B expect(datasource.runQuery).toHaveBeenCalledTimes(2); @@ -529,21 +504,21 @@ describe('runSplitQuery()', () => { }); test('Groups metric queries with step by stepMs', async () => { - const request = getQueryOptions({ - targets: [ + const request = createRequest( + [ { 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, - }); + { range: range1d } + ); await expect(runSplitQuery(datasource, request)).toEmitValuesWith(() => { // A, B expect(datasource.runQuery).toHaveBeenCalledTimes(2); }); }); test('Groups mixed queries by stepMs', async () => { - const request = getQueryOptions({ - targets: [ + const request = createRequest( + [ { 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 }, @@ -552,26 +527,23 @@ describe('runSplitQuery()', () => { { 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, - }); + { 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({ - 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 - }); + const request = createRequest([ + { 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' }, + ]); await expect(runSplitQuery(datasource, request)).toEmitValuesWith(() => { // 3 * A, 3 * B, 3 * C, 3 * D, 1 * E, 3 * F+G expect(datasource.runQuery).toHaveBeenCalledTimes(16); diff --git a/public/app/plugins/datasource/loki/querybuilder/parsingUtils.ts b/public/app/plugins/datasource/loki/querybuilder/parsingUtils.ts index a4e7215c597..ba642ae3c96 100644 --- a/public/app/plugins/datasource/loki/querybuilder/parsingUtils.ts +++ b/public/app/plugins/datasource/loki/querybuilder/parsingUtils.ts @@ -28,7 +28,7 @@ export function makeError(expr: string, node: SyntaxNode) { * \[\[([\s\S]+?)(?::(\w+))?\]\] [[var2]] or [[var2:fmt2]] * \${(\w+)(?::(\w+))?} ${var3} or ${var3:fmt3} */ -const variableRegex = /\$(\w+)|\[\[([\s\S]+?)(?::(\w+))?\]\]|\${(\w+)(?:\.([^:^\}]+))?(?::([^\}]+))?}/g; +export const variableRegex = /\$(\w+)|\[\[([\s\S]+?)(?::(\w+))?\]\]|\${(\w+)(?:\.([^:^\}]+))?(?::([^\}]+))?}/g; /** * As variables with $ are creating parsing errors, we first replace them with magic string that is parsable and at diff --git a/public/app/plugins/datasource/loki/tracking.test.ts b/public/app/plugins/datasource/loki/tracking.test.ts index 26af7b38541..ea5164d9d00 100644 --- a/public/app/plugins/datasource/loki/tracking.test.ts +++ b/public/app/plugins/datasource/loki/tracking.test.ts @@ -1,6 +1,4 @@ -import { getQueryOptions } from 'test/helpers/getQueryOptions'; - -import { DashboardLoadedEvent, dateTime } from '@grafana/data'; +import { DashboardLoadedEvent, DataQueryRequest, dateTime } from '@grafana/data'; import { reportInteraction } from '@grafana/runtime'; import { QueryEditorMode } from '../prometheus/querybuilder/shared/types'; @@ -27,7 +25,7 @@ const range = { to: dateTime('2023-02-10T06:00:00.000Z'), }, }; -const originalRequest = getQueryOptions({ +const originalRequest = { targets: [ { expr: 'count_over_time({a="b"}[1m])', refId: 'A', ...baseTarget }, { expr: '{a="b"}', refId: 'B', maxLines: 10, ...baseTarget }, @@ -35,26 +33,23 @@ const originalRequest = getQueryOptions({ ], range, app: 'explore', -}); +} as DataQueryRequest; + const requests: LokiGroupedRequest[] = [ { request: { - ...getQueryOptions({ - targets: [{ expr: 'count_over_time({a="b"}[1m])', refId: 'A', ...baseTarget }], - range, - }), + targets: [{ expr: 'count_over_time({a="b"}[1m])', refId: 'A', ...baseTarget }], + range, app: 'explore', - }, + } as DataQueryRequest, partition: partitionTimeRange(true, range, 60000, 24 * 60 * 60 * 1000), }, { request: { - ...getQueryOptions({ - targets: [{ expr: '{a="b"}', refId: 'B', maxLines: 10, ...baseTarget }], - range, - }), + targets: [{ expr: '{a="b"}', refId: 'B', maxLines: 10, ...baseTarget }], + range, app: 'explore', - }, + } as DataQueryRequest, partition: partitionTimeRange(false, range, 60000, 24 * 60 * 60 * 1000), }, ]; diff --git a/public/app/plugins/datasource/loki/tracking.ts b/public/app/plugins/datasource/loki/tracking.ts index f2707b0d72e..a3a3ffa2255 100644 --- a/public/app/plugins/datasource/loki/tracking.ts +++ b/public/app/plugins/datasource/loki/tracking.ts @@ -1,6 +1,5 @@ import { CoreApp, DashboardLoadedEvent, DataQueryRequest, DataQueryResponse } from '@grafana/data'; import { reportInteraction, config } from '@grafana/runtime'; -import { variableRegex } from 'app/features/variables/utils'; import { QueryEditorMode } from '../prometheus/querybuilder/shared/types'; @@ -12,6 +11,7 @@ import { } from './datasource'; import pluginJson from './plugin.json'; import { getNormalizedLokiQuery, isLogsQuery, obfuscate, parseToNodeNamesArray } from './queryUtils'; +import { variableRegex } from './querybuilder/parsingUtils'; import { LokiGroupedRequest, LokiQuery, LokiQueryType } from './types'; type LokiOnDashboardLoadedTrackingEvent = {