mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
(feat/explore): Support for new LogQL filtering syntax (#16674)
* (feat/explore): Support for new LogQL filtering syntax
Loki is adding syntax to support chained filtering.
This PR adapts Grafana to support this.
- Send only `query` parameter in loki request
- Automatically wrap search text in simple syntax, e.g., `{} foo` is
sent as `{} |~ "foo"`.
* Adapted to regexp parameter staying on in Loki
* Dont wrap single regexp in new filter syntax
* Fix datasource test
* Fallback regexp parameter for legacy queries
* Fix search highlighting
* Make highlighting work for filter chains
* Fix datasource test
This commit is contained in:
@@ -21,7 +21,7 @@ export interface QueryResultMeta {
|
|||||||
requestId?: string;
|
requestId?: string;
|
||||||
|
|
||||||
// Used in Explore for highlighting
|
// Used in Explore for highlighting
|
||||||
search?: string;
|
searchWords?: string[];
|
||||||
|
|
||||||
// Used in Explore to show limit applied to search result
|
// Used in Explore to show limit applied to search result
|
||||||
limit?: number;
|
limit?: number;
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ export abstract class ExploreDataSourceApi<
|
|||||||
TOptions extends DataSourceJsonData = DataSourceJsonData
|
TOptions extends DataSourceJsonData = DataSourceJsonData
|
||||||
> extends DataSourceApi<TQuery, TOptions> {
|
> extends DataSourceApi<TQuery, TOptions> {
|
||||||
modifyQuery?(query: TQuery, action: QueryFixAction): TQuery;
|
modifyQuery?(query: TQuery, action: QueryFixAction): TQuery;
|
||||||
getHighlighterExpression?(query: TQuery): string;
|
getHighlighterExpression?(query: TQuery): string[];
|
||||||
languageProvider?: any;
|
languageProvider?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -446,7 +446,7 @@ export function processLogSeriesRow(
|
|||||||
const timeLocal = time.format('YYYY-MM-DD HH:mm:ss');
|
const timeLocal = time.format('YYYY-MM-DD HH:mm:ss');
|
||||||
const logLevel = getLogLevel(message);
|
const logLevel = getLogLevel(message);
|
||||||
const hasAnsi = hasAnsiCodes(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 {
|
return {
|
||||||
logLevel,
|
logLevel,
|
||||||
@@ -455,10 +455,10 @@ export function processLogSeriesRow(
|
|||||||
timeLocal,
|
timeLocal,
|
||||||
uniqueLabels,
|
uniqueLabels,
|
||||||
hasAnsi,
|
hasAnsi,
|
||||||
|
searchWords,
|
||||||
entry: hasAnsi ? ansicolor.strip(message) : message,
|
entry: hasAnsi ? ansicolor.strip(message) : message,
|
||||||
raw: message,
|
raw: message,
|
||||||
labels: series.labels,
|
labels: series.labels,
|
||||||
searchWords: search ? [search] : [],
|
|
||||||
timestamp: ts,
|
timestamp: ts,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import xss from 'xss';
|
|||||||
* See https://github.com/bvaughn/react-highlight-words#props
|
* See https://github.com/bvaughn/react-highlight-words#props
|
||||||
*/
|
*/
|
||||||
export function findHighlightChunksInText({ searchWords, textToHighlight }) {
|
export function findHighlightChunksInText({ searchWords, textToHighlight }) {
|
||||||
return findMatchesInText(textToHighlight, searchWords.join(' '));
|
return searchWords.reduce((acc, term) => [...acc, ...findMatchesInText(textToHighlight, term)], []);
|
||||||
}
|
}
|
||||||
|
|
||||||
const cleanNeedle = (needle: string): string => {
|
const cleanNeedle = (needle: string): string => {
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ export class LogRow extends PureComponent<Props, State> {
|
|||||||
const { entry, hasAnsi, raw } = row;
|
const { entry, hasAnsi, raw } = row;
|
||||||
const previewHighlights = highlighterExpressions && !_.isEqual(highlighterExpressions, row.searchWords);
|
const previewHighlights = highlighterExpressions && !_.isEqual(highlighterExpressions, row.searchWords);
|
||||||
const highlights = previewHighlights ? 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', {
|
const highlightClassName = classnames('logs-row__match-highlight', {
|
||||||
'logs-row__match-highlight--preview': previewHighlights,
|
'logs-row__match-highlight--preview': previewHighlights,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ export class QueryRow extends PureComponent<QueryRowProps> {
|
|||||||
const { datasourceInstance } = this.props;
|
const { datasourceInstance } = this.props;
|
||||||
if (datasourceInstance.getHighlighterExpression) {
|
if (datasourceInstance.getHighlighterExpression) {
|
||||||
const { exploreId } = this.props;
|
const { exploreId } = this.props;
|
||||||
const expressions = [datasourceInstance.getHighlighterExpression(value)];
|
const expressions = datasourceInstance.getHighlighterExpression(value);
|
||||||
this.props.highlightLogsExpressionAction({ exploreId, expressions });
|
this.props.highlightLogsExpressionAction({ exploreId, expressions });
|
||||||
}
|
}
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ describe('LokiDatasource', () => {
|
|||||||
backendSrvMock.datasourceRequest = jest.fn(() => Promise.resolve(testResp));
|
backendSrvMock.datasourceRequest = jest.fn(() => Promise.resolve(testResp));
|
||||||
|
|
||||||
const options = getQueryOptions<LokiQuery>({
|
const options = getQueryOptions<LokiQuery>({
|
||||||
targets: [{ expr: 'foo', refId: 'B' }],
|
targets: [{ expr: '{} foo', refId: 'B' }],
|
||||||
});
|
});
|
||||||
|
|
||||||
const res = await ds.query(options);
|
const res = await ds.query(options);
|
||||||
@@ -69,7 +69,7 @@ describe('LokiDatasource', () => {
|
|||||||
const seriesData = res.data[0] as SeriesData;
|
const seriesData = res.data[0] as SeriesData;
|
||||||
expect(seriesData.rows[0][1]).toBe('hello');
|
expect(seriesData.rows[0][1]).toBe('hello');
|
||||||
expect(seriesData.meta.limit).toBe(20);
|
expect(seriesData.meta.limit).toBe(20);
|
||||||
expect(seriesData.meta.search).toBe('(?i)foo');
|
expect(seriesData.meta.searchWords).toEqual(['(?i)foo']);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 { addLabelToSelector } from 'app/plugins/datasource/prometheus/add_label_to_query';
|
||||||
import LanguageProvider from './language_provider';
|
import LanguageProvider from './language_provider';
|
||||||
import { logStreamToSeriesData } from './result_transformer';
|
import { logStreamToSeriesData } from './result_transformer';
|
||||||
import { formatQuery, parseQuery } from './query_utils';
|
import { formatQuery, parseQuery, getHighlighterExpressionsFromQuery } from './query_utils';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import {
|
import {
|
||||||
@@ -69,12 +69,14 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> {
|
|||||||
|
|
||||||
prepareQueryTarget(target: LokiQuery, options: DataQueryRequest<LokiQuery>) {
|
prepareQueryTarget(target: LokiQuery, options: DataQueryRequest<LokiQuery>) {
|
||||||
const interpolated = this.templateSrv.replace(target.expr);
|
const interpolated = this.templateSrv.replace(target.expr);
|
||||||
|
const { query, regexp } = parseQuery(interpolated);
|
||||||
const start = this.getTime(options.range.from, false);
|
const start = this.getTime(options.range.from, false);
|
||||||
const end = this.getTime(options.range.to, true);
|
const end = this.getTime(options.range.to, true);
|
||||||
const refId = target.refId;
|
const refId = target.refId;
|
||||||
return {
|
return {
|
||||||
...DEFAULT_QUERY_PARAMS,
|
...DEFAULT_QUERY_PARAMS,
|
||||||
...parseQuery(interpolated),
|
query,
|
||||||
|
regexp,
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
limit: this.maxLines,
|
limit: this.maxLines,
|
||||||
@@ -126,14 +128,15 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> {
|
|||||||
|
|
||||||
for (let i = 0; i < results.length; i++) {
|
for (let i = 0; i < results.length; i++) {
|
||||||
const result = results[i];
|
const result = results[i];
|
||||||
|
|
||||||
if (result.data) {
|
if (result.data) {
|
||||||
const refId = queryTargets[i].refId;
|
const refId = queryTargets[i].refId;
|
||||||
for (const stream of result.data.streams || []) {
|
for (const stream of result.data.streams || []) {
|
||||||
const seriesData = logStreamToSeriesData(stream);
|
const seriesData = logStreamToSeriesData(stream);
|
||||||
seriesData.refId = refId;
|
seriesData.refId = refId;
|
||||||
seriesData.meta = {
|
seriesData.meta = {
|
||||||
search: queryTargets[i].regexp,
|
searchWords: getHighlighterExpressionsFromQuery(
|
||||||
|
formatQuery(queryTargets[i].query, queryTargets[i].regexp)
|
||||||
|
),
|
||||||
limit: this.maxLines,
|
limit: this.maxLines,
|
||||||
};
|
};
|
||||||
series.push(seriesData);
|
series.push(seriesData);
|
||||||
@@ -160,7 +163,7 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> {
|
|||||||
|
|
||||||
modifyQuery(query: LokiQuery, action: any): LokiQuery {
|
modifyQuery(query: LokiQuery, action: any): LokiQuery {
|
||||||
const parsed = parseQuery(query.expr || '');
|
const parsed = parseQuery(query.expr || '');
|
||||||
let selector = parsed.query;
|
let { query: selector } = parsed;
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case 'ADD_FILTER': {
|
case 'ADD_FILTER': {
|
||||||
selector = addLabelToSelector(selector, action.key, action.value);
|
selector = addLabelToSelector(selector, action.key, action.value);
|
||||||
@@ -173,8 +176,8 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> {
|
|||||||
return { ...query, expr: expression };
|
return { ...query, expr: expression };
|
||||||
}
|
}
|
||||||
|
|
||||||
getHighlighterExpression(query: LokiQuery): string {
|
getHighlighterExpression(query: LokiQuery): string[] {
|
||||||
return parseQuery(query.expr).regexp;
|
return getHighlighterExpressionsFromQuery(query.expr);
|
||||||
}
|
}
|
||||||
|
|
||||||
getTime(date, roundUp) {
|
getTime(date, roundUp) {
|
||||||
|
|||||||
@@ -1,56 +1,87 @@
|
|||||||
import { parseQuery } from './query_utils';
|
import { parseQuery, getHighlighterExpressionsFromQuery } from './query_utils';
|
||||||
|
import { LokiExpression } from './types';
|
||||||
|
|
||||||
describe('parseQuery', () => {
|
describe('parseQuery', () => {
|
||||||
it('returns empty for empty string', () => {
|
it('returns empty for empty string', () => {
|
||||||
expect(parseQuery('')).toEqual({
|
expect(parseQuery('')).toEqual({
|
||||||
query: '',
|
query: '',
|
||||||
regexp: '',
|
regexp: '',
|
||||||
});
|
} as LokiExpression);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns regexp for strings without query', () => {
|
it('returns regexp for strings without query', () => {
|
||||||
expect(parseQuery('test')).toEqual({
|
expect(parseQuery('test')).toEqual({
|
||||||
query: '',
|
query: 'test',
|
||||||
regexp: '(?i)test',
|
regexp: '',
|
||||||
});
|
} as LokiExpression);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns query for strings without regexp', () => {
|
it('returns query for strings without regexp', () => {
|
||||||
expect(parseQuery('{foo="bar"}')).toEqual({
|
expect(parseQuery('{foo="bar"}')).toEqual({
|
||||||
query: '{foo="bar"}',
|
query: '{foo="bar"}',
|
||||||
regexp: '',
|
regexp: '',
|
||||||
});
|
} as LokiExpression);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns query for strings with query and search string', () => {
|
it('returns query for strings with query and search string', () => {
|
||||||
expect(parseQuery('x {foo="bar"}')).toEqual({
|
expect(parseQuery('x {foo="bar"}')).toEqual({
|
||||||
query: '{foo="bar"}',
|
query: '{foo="bar"}',
|
||||||
regexp: '(?i)x',
|
regexp: '(?i)x',
|
||||||
});
|
} as LokiExpression);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns query for strings with query and regexp', () => {
|
it('returns query for strings with query and regexp', () => {
|
||||||
expect(parseQuery('{foo="bar"} x|y')).toEqual({
|
expect(parseQuery('{foo="bar"} x|y')).toEqual({
|
||||||
query: '{foo="bar"}',
|
query: '{foo="bar"}',
|
||||||
regexp: '(?i)x|y',
|
regexp: '(?i)x|y',
|
||||||
});
|
} as LokiExpression);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns query for selector with two labels', () => {
|
it('returns query for selector with two labels', () => {
|
||||||
expect(parseQuery('{foo="bar", baz="42"}')).toEqual({
|
expect(parseQuery('{foo="bar", baz="42"}')).toEqual({
|
||||||
query: '{foo="bar", baz="42"}',
|
query: '{foo="bar", baz="42"}',
|
||||||
regexp: '',
|
regexp: '',
|
||||||
});
|
} as LokiExpression);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns query and regexp with quantifiers', () => {
|
it('returns query and regexp with quantifiers', () => {
|
||||||
expect(parseQuery('{foo="bar"} \\.java:[0-9]{1,5}')).toEqual({
|
expect(parseQuery('{foo="bar"} \\.java:[0-9]{1,5}')).toEqual({
|
||||||
query: '{foo="bar"}',
|
query: '{foo="bar"}',
|
||||||
regexp: '(?i)\\.java:[0-9]{1,5}',
|
regexp: '(?i)\\.java:[0-9]{1,5}',
|
||||||
});
|
} as LokiExpression);
|
||||||
expect(parseQuery('\\.java:[0-9]{1,5} {foo="bar"}')).toEqual({
|
expect(parseQuery('\\.java:[0-9]{1,5} {foo="bar"}')).toEqual({
|
||||||
query: '{foo="bar"}',
|
query: '{foo="bar"}',
|
||||||
regexp: '(?i)\\.java:[0-9]{1,5}',
|
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']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,22 +1,70 @@
|
|||||||
|
import { LokiExpression } from './types';
|
||||||
|
|
||||||
const selectorRegexp = /(?:^|\s){[^{]*}/g;
|
const selectorRegexp = /(?:^|\s){[^{]*}/g;
|
||||||
const caseInsensitive = '(?i)'; // Golang mode modifier for Loki, doesn't work in JavaScript
|
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 || '';
|
input = input || '';
|
||||||
const match = input.match(selectorRegexp);
|
const match = input.match(selectorRegexp);
|
||||||
let query = '';
|
let query = input;
|
||||||
let regexp = input;
|
let regexp = '';
|
||||||
|
|
||||||
if (match) {
|
if (match) {
|
||||||
query = match[0].trim();
|
|
||||||
regexp = input.replace(selectorRegexp, '').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) {
|
return { regexp, query };
|
||||||
regexp = caseInsensitive + regexp;
|
|
||||||
}
|
|
||||||
return { query, regexp };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatQuery(selector: string, search: string): string {
|
export function formatQuery(selector: string, search: string): string {
|
||||||
return `${selector || ''} ${search || ''}`.trim();
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,3 +22,8 @@ export interface LokiLogsStreamEntry {
|
|||||||
// Legacy, was renamed to ts
|
// Legacy, was renamed to ts
|
||||||
timestamp?: string;
|
timestamp?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LokiExpression {
|
||||||
|
regexp: string;
|
||||||
|
query: string;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user