diff --git a/packages/grafana-ui/src/types/data.ts b/packages/grafana-ui/src/types/data.ts index 678efc40f00..e629e76043a 100644 --- a/packages/grafana-ui/src/types/data.ts +++ b/packages/grafana-ui/src/types/data.ts @@ -21,7 +21,7 @@ export interface QueryResultMeta { requestId?: string; // Used in Explore for highlighting - search?: string; + searchWords?: string[]; // Used in Explore to show limit applied to search result limit?: number; diff --git a/packages/grafana-ui/src/types/datasource.ts b/packages/grafana-ui/src/types/datasource.ts index 3be0bae09dd..2b40064d7d9 100644 --- a/packages/grafana-ui/src/types/datasource.ts +++ b/packages/grafana-ui/src/types/datasource.ts @@ -194,7 +194,7 @@ export abstract class ExploreDataSourceApi< TOptions extends DataSourceJsonData = DataSourceJsonData > extends DataSourceApi { modifyQuery?(query: TQuery, action: QueryFixAction): TQuery; - getHighlighterExpression?(query: TQuery): string; + getHighlighterExpression?(query: TQuery): string[]; languageProvider?: any; } diff --git a/public/app/core/logs_model.ts b/public/app/core/logs_model.ts index eb292c033f8..fd7e5c638a9 100644 --- a/public/app/core/logs_model.ts +++ b/public/app/core/logs_model.ts @@ -446,7 +446,7 @@ export function processLogSeriesRow( const timeLocal = time.format('YYYY-MM-DD HH:mm:ss'); const logLevel = getLogLevel(message); const hasAnsi = hasAnsiCodes(message); - const search = series.meta && series.meta.search ? series.meta.search : ''; + const searchWords = series.meta && series.meta.searchWords ? series.meta.searchWords : []; return { logLevel, @@ -455,10 +455,10 @@ export function processLogSeriesRow( timeLocal, uniqueLabels, hasAnsi, + searchWords, entry: hasAnsi ? ansicolor.strip(message) : message, raw: message, labels: series.labels, - searchWords: search ? [search] : [], timestamp: ts, }; } diff --git a/public/app/core/utils/text.ts b/public/app/core/utils/text.ts index faf801b45be..6db844e736e 100644 --- a/public/app/core/utils/text.ts +++ b/public/app/core/utils/text.ts @@ -6,7 +6,7 @@ import xss from 'xss'; * See https://github.com/bvaughn/react-highlight-words#props */ export function findHighlightChunksInText({ searchWords, textToHighlight }) { - return findMatchesInText(textToHighlight, searchWords.join(' ')); + return searchWords.reduce((acc, term) => [...acc, ...findMatchesInText(textToHighlight, term)], []); } const cleanNeedle = (needle: string): string => { diff --git a/public/app/features/explore/LogRow.tsx b/public/app/features/explore/LogRow.tsx index 19930a8a339..8e3de04749b 100644 --- a/public/app/features/explore/LogRow.tsx +++ b/public/app/features/explore/LogRow.tsx @@ -133,7 +133,7 @@ export class LogRow extends PureComponent { const { entry, hasAnsi, raw } = row; const previewHighlights = highlighterExpressions && !_.isEqual(highlighterExpressions, row.searchWords); const highlights = previewHighlights ? highlighterExpressions : row.searchWords; - const needsHighlighter = highlights && highlights.length > 0 && highlights[0].length > 0; + const needsHighlighter = highlights && highlights.length > 0 && highlights[0] && highlights[0].length > 0; const highlightClassName = classnames('logs-row__match-highlight', { 'logs-row__match-highlight--preview': previewHighlights, }); diff --git a/public/app/features/explore/QueryRow.tsx b/public/app/features/explore/QueryRow.tsx index 06034ecdbff..fc4a5e025e9 100644 --- a/public/app/features/explore/QueryRow.tsx +++ b/public/app/features/explore/QueryRow.tsx @@ -98,7 +98,7 @@ export class QueryRow extends PureComponent { const { datasourceInstance } = this.props; if (datasourceInstance.getHighlighterExpression) { const { exploreId } = this.props; - const expressions = [datasourceInstance.getHighlighterExpression(value)]; + const expressions = datasourceInstance.getHighlighterExpression(value); this.props.highlightLogsExpressionAction({ exploreId, expressions }); } }, 500); diff --git a/public/app/plugins/datasource/loki/datasource.test.ts b/public/app/plugins/datasource/loki/datasource.test.ts index 8b970f05f63..8200cf0f4a1 100644 --- a/public/app/plugins/datasource/loki/datasource.test.ts +++ b/public/app/plugins/datasource/loki/datasource.test.ts @@ -61,7 +61,7 @@ describe('LokiDatasource', () => { backendSrvMock.datasourceRequest = jest.fn(() => Promise.resolve(testResp)); const options = getQueryOptions({ - targets: [{ expr: 'foo', refId: 'B' }], + targets: [{ expr: '{} foo', refId: 'B' }], }); const res = await ds.query(options); @@ -69,7 +69,7 @@ describe('LokiDatasource', () => { const seriesData = res.data[0] as SeriesData; expect(seriesData.rows[0][1]).toBe('hello'); expect(seriesData.meta.limit).toBe(20); - expect(seriesData.meta.search).toBe('(?i)foo'); + expect(seriesData.meta.searchWords).toEqual(['(?i)foo']); done(); }); }); diff --git a/public/app/plugins/datasource/loki/datasource.ts b/public/app/plugins/datasource/loki/datasource.ts index 3e7e5f3b396..04f84658491 100644 --- a/public/app/plugins/datasource/loki/datasource.ts +++ b/public/app/plugins/datasource/loki/datasource.ts @@ -6,7 +6,7 @@ import * as dateMath from '@grafana/ui/src/utils/datemath'; import { addLabelToSelector } from 'app/plugins/datasource/prometheus/add_label_to_query'; import LanguageProvider from './language_provider'; import { logStreamToSeriesData } from './result_transformer'; -import { formatQuery, parseQuery } from './query_utils'; +import { formatQuery, parseQuery, getHighlighterExpressionsFromQuery } from './query_utils'; // Types import { @@ -69,12 +69,14 @@ export class LokiDatasource extends DataSourceApi { prepareQueryTarget(target: LokiQuery, options: DataQueryRequest) { const interpolated = this.templateSrv.replace(target.expr); + const { query, regexp } = parseQuery(interpolated); const start = this.getTime(options.range.from, false); const end = this.getTime(options.range.to, true); const refId = target.refId; return { ...DEFAULT_QUERY_PARAMS, - ...parseQuery(interpolated), + query, + regexp, start, end, limit: this.maxLines, @@ -126,14 +128,15 @@ export class LokiDatasource extends DataSourceApi { for (let i = 0; i < results.length; i++) { const result = results[i]; - if (result.data) { const refId = queryTargets[i].refId; for (const stream of result.data.streams || []) { const seriesData = logStreamToSeriesData(stream); seriesData.refId = refId; seriesData.meta = { - search: queryTargets[i].regexp, + searchWords: getHighlighterExpressionsFromQuery( + formatQuery(queryTargets[i].query, queryTargets[i].regexp) + ), limit: this.maxLines, }; series.push(seriesData); @@ -160,7 +163,7 @@ export class LokiDatasource extends DataSourceApi { modifyQuery(query: LokiQuery, action: any): LokiQuery { const parsed = parseQuery(query.expr || ''); - let selector = parsed.query; + let { query: selector } = parsed; switch (action.type) { case 'ADD_FILTER': { selector = addLabelToSelector(selector, action.key, action.value); @@ -173,8 +176,8 @@ export class LokiDatasource extends DataSourceApi { return { ...query, expr: expression }; } - getHighlighterExpression(query: LokiQuery): string { - return parseQuery(query.expr).regexp; + getHighlighterExpression(query: LokiQuery): string[] { + return getHighlighterExpressionsFromQuery(query.expr); } getTime(date, roundUp) { diff --git a/public/app/plugins/datasource/loki/query_utils.test.ts b/public/app/plugins/datasource/loki/query_utils.test.ts index 3685a3dd358..964f8678a79 100644 --- a/public/app/plugins/datasource/loki/query_utils.test.ts +++ b/public/app/plugins/datasource/loki/query_utils.test.ts @@ -1,56 +1,87 @@ -import { parseQuery } from './query_utils'; +import { parseQuery, getHighlighterExpressionsFromQuery } from './query_utils'; +import { LokiExpression } from './types'; describe('parseQuery', () => { it('returns empty for empty string', () => { expect(parseQuery('')).toEqual({ query: '', regexp: '', - }); + } as LokiExpression); }); it('returns regexp for strings without query', () => { expect(parseQuery('test')).toEqual({ - query: '', - regexp: '(?i)test', - }); + query: 'test', + regexp: '', + } as LokiExpression); }); it('returns query for strings without regexp', () => { expect(parseQuery('{foo="bar"}')).toEqual({ query: '{foo="bar"}', regexp: '', - }); + } as LokiExpression); }); it('returns query for strings with query and search string', () => { expect(parseQuery('x {foo="bar"}')).toEqual({ query: '{foo="bar"}', regexp: '(?i)x', - }); + } as LokiExpression); }); it('returns query for strings with query and regexp', () => { expect(parseQuery('{foo="bar"} x|y')).toEqual({ query: '{foo="bar"}', regexp: '(?i)x|y', - }); + } as LokiExpression); }); it('returns query for selector with two labels', () => { expect(parseQuery('{foo="bar", baz="42"}')).toEqual({ query: '{foo="bar", baz="42"}', regexp: '', - }); + } as LokiExpression); }); it('returns query and regexp with quantifiers', () => { expect(parseQuery('{foo="bar"} \\.java:[0-9]{1,5}')).toEqual({ query: '{foo="bar"}', regexp: '(?i)\\.java:[0-9]{1,5}', - }); + } as LokiExpression); expect(parseQuery('\\.java:[0-9]{1,5} {foo="bar"}')).toEqual({ query: '{foo="bar"}', regexp: '(?i)\\.java:[0-9]{1,5}', - }); + } as LokiExpression); + }); + + it('returns query with filter operands as is', () => { + expect(parseQuery('{foo="bar"} |= "x|y"')).toEqual({ + query: '{foo="bar"} |= "x|y"', + regexp: '', + } as LokiExpression); + expect(parseQuery('{foo="bar"} |~ "42"')).toEqual({ + query: '{foo="bar"} |~ "42"', + regexp: '', + } as LokiExpression); + }); +}); + +describe('getHighlighterExpressionsFromQuery', () => { + it('returns no expressions for empty query', () => { + expect(getHighlighterExpressionsFromQuery('')).toEqual([]); + }); + it('returns a single expressions for legacy query', () => { + expect(getHighlighterExpressionsFromQuery('{} x')).toEqual(['(?i)x']); + expect(getHighlighterExpressionsFromQuery('{foo="bar"} x')).toEqual(['(?i)x']); + }); + it('returns an expression for query with filter', () => { + expect(getHighlighterExpressionsFromQuery('{foo="bar"} |= "x"')).toEqual(['x']); + }); + it('returns expressions for query with filter chain', () => { + expect(getHighlighterExpressionsFromQuery('{foo="bar"} |= "x" |~ "y"')).toEqual(['x', 'y']); + }); + it('returns drops expressions for query with negative filter chain', () => { + expect(getHighlighterExpressionsFromQuery('{foo="bar"} |= "x" != "y"')).toEqual(['x']); }); }); diff --git a/public/app/plugins/datasource/loki/query_utils.ts b/public/app/plugins/datasource/loki/query_utils.ts index 7d2c6234f05..54fcca8809a 100644 --- a/public/app/plugins/datasource/loki/query_utils.ts +++ b/public/app/plugins/datasource/loki/query_utils.ts @@ -1,22 +1,70 @@ +import { LokiExpression } from './types'; + const selectorRegexp = /(?:^|\s){[^{]*}/g; const caseInsensitive = '(?i)'; // Golang mode modifier for Loki, doesn't work in JavaScript -export function parseQuery(input: string) { +export function parseQuery(input: string): LokiExpression { input = input || ''; const match = input.match(selectorRegexp); - let query = ''; - let regexp = input; + let query = input; + let regexp = ''; if (match) { - query = match[0].trim(); regexp = input.replace(selectorRegexp, '').trim(); + // Keep old-style regexp, otherwise take whole query + if (regexp && regexp.search(/\|=|\|~|!=|!~/) === -1) { + query = match[0].trim(); + if (!regexp.startsWith(caseInsensitive)) { + regexp = `${caseInsensitive}${regexp}`; + } + } else { + regexp = ''; + } } - if (regexp) { - regexp = caseInsensitive + regexp; - } - return { query, regexp }; + return { regexp, query }; } export function formatQuery(selector: string, search: string): string { return `${selector || ''} ${search || ''}`.trim(); } + +/** + * Returns search terms from a LogQL query. + * E.g., `{} |= foo |=bar != baz` returns `['foo', 'bar']`. + */ +export function getHighlighterExpressionsFromQuery(input: string): string[] { + const parsed = parseQuery(input); + // Legacy syntax + if (parsed.regexp) { + return [parsed.regexp]; + } + let expression = input; + const results = []; + // Consume filter expression from left to right + while (expression) { + const filterStart = expression.search(/\|=|\|~|!=|!~/); + // Nothing more to search + if (filterStart === -1) { + break; + } + // Drop terms for negative filters + const skip = expression.substr(filterStart).search(/!=|!~/) === 0; + expression = expression.substr(filterStart + 2); + if (skip) { + continue; + } + // Check if there is more chained + const filterEnd = expression.search(/\|=|\|~|!=|!~/); + let filterTerm; + if (filterEnd === -1) { + filterTerm = expression.trim(); + } else { + filterTerm = expression.substr(0, filterEnd); + expression = expression.substr(filterEnd); + } + + // Unwrap the filter term by removing quotes + results.push(filterTerm.replace(/^\s*"/g, '').replace(/"\s*$/g, '')); + } + return results; +} diff --git a/public/app/plugins/datasource/loki/types.ts b/public/app/plugins/datasource/loki/types.ts index c4cfa3fcdc4..4c973f8a79e 100644 --- a/public/app/plugins/datasource/loki/types.ts +++ b/public/app/plugins/datasource/loki/types.ts @@ -22,3 +22,8 @@ export interface LokiLogsStreamEntry { // Legacy, was renamed to ts timestamp?: string; } + +export interface LokiExpression { + regexp: string; + query: string; +}