mirror of
https://github.com/grafana/grafana.git
synced 2025-08-01 16:05:53 +08:00
256 lines
6.3 KiB
TypeScript
256 lines
6.3 KiB
TypeScript
import { isEqual } from 'lodash';
|
|
import lucene, { AST, BinaryAST, LeftOnlyAST, NodeTerm } from 'lucene';
|
|
|
|
import { AdHocVariableFilter } from '@grafana/data';
|
|
|
|
type ModifierType = '' | '-';
|
|
|
|
/**
|
|
* Checks for the presence of a given label:"value" filter in the query.
|
|
*/
|
|
export function queryHasFilter(query: string, key: string, value: string, modifier: ModifierType = ''): boolean {
|
|
return findFilterNode(query, key, value, modifier) !== null;
|
|
}
|
|
|
|
/**
|
|
* Given a query, find the NodeTerm that matches the given field and value.
|
|
*/
|
|
export function findFilterNode(
|
|
query: string,
|
|
key: string,
|
|
value: string,
|
|
modifier: ModifierType = ''
|
|
): NodeTerm | null {
|
|
const field = `${modifier}${lucene.term.escape(key)}`;
|
|
value = lucene.phrase.escape(value);
|
|
let ast: AST | null = parseQuery(query);
|
|
if (!ast) {
|
|
return null;
|
|
}
|
|
|
|
return findNodeInTree(ast, field, value);
|
|
}
|
|
|
|
function findNodeInTree(ast: AST, field: string, value: string): NodeTerm | null {
|
|
// {}
|
|
if (Object.keys(ast).length === 0) {
|
|
return null;
|
|
}
|
|
// { left: {}, right: {} } or { left: {} }
|
|
if (isAST(ast.left)) {
|
|
return findNodeInTree(ast.left, field, value);
|
|
}
|
|
if (isNodeTerm(ast.left) && ast.left.field === field && ast.left.term === value) {
|
|
return ast.left;
|
|
}
|
|
if (isLeftOnlyAST(ast)) {
|
|
return null;
|
|
}
|
|
if (isNodeTerm(ast.right) && ast.right.field === field && ast.right.term === value) {
|
|
return ast.right;
|
|
}
|
|
if (isBinaryAST(ast.right)) {
|
|
return findNodeInTree(ast.right, field, value);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Adds a label:"value" expression to the query.
|
|
*/
|
|
export function addFilterToQuery(query: string, key: string, value: string, modifier: ModifierType = ''): string {
|
|
if (queryHasFilter(query, key, value, modifier)) {
|
|
return query;
|
|
}
|
|
|
|
key = escapeFilter(key);
|
|
value = escapeFilterValue(value);
|
|
const filter = `${modifier}${key}:"${value}"`;
|
|
|
|
return concatenate(query, filter);
|
|
}
|
|
|
|
/**
|
|
* Merge a query with a filter.
|
|
*/
|
|
function concatenate(query: string, filter: string, condition = 'AND'): string {
|
|
if (!filter) {
|
|
return query;
|
|
}
|
|
return query.trim() === '' ? filter : `${query} ${condition} ${filter}`;
|
|
}
|
|
|
|
/**
|
|
* Adds a label:"value" expression to the query.
|
|
*/
|
|
export function addAddHocFilter(query: string, filter: AdHocVariableFilter, logLevelField?: string): string {
|
|
if (!filter.key || !filter.value) {
|
|
return query;
|
|
}
|
|
|
|
filter = {
|
|
...filter,
|
|
// Type is defined as string, but it can be a number.
|
|
value: filter.value.toString(),
|
|
};
|
|
|
|
let key = filter.key;
|
|
if (logLevelField && key === 'level') {
|
|
key = logLevelField;
|
|
}
|
|
|
|
const equalityFilters = ['=', '!='];
|
|
if (equalityFilters.includes(filter.operator)) {
|
|
return addFilterToQuery(query, key, filter.value, filter.operator === '=' ? '' : '-');
|
|
}
|
|
/**
|
|
* Keys and values in ad hoc filters may contain characters such as
|
|
* colons, which needs to be escaped.
|
|
*/
|
|
key = escapeFilter(key);
|
|
const value = escapeFilterValue(filter.value);
|
|
const regexValue = escapeFilterValue(filter.value, false);
|
|
let addHocFilter = '';
|
|
switch (filter.operator) {
|
|
case '=~':
|
|
addHocFilter = `${key}:/${regexValue}/`;
|
|
break;
|
|
case '!~':
|
|
addHocFilter = `-${key}:/${regexValue}/`;
|
|
break;
|
|
case '>':
|
|
addHocFilter = `${key}:>${value}`;
|
|
break;
|
|
case '<':
|
|
addHocFilter = `${key}:<${value}`;
|
|
break;
|
|
}
|
|
return concatenate(query, addHocFilter);
|
|
}
|
|
|
|
/**
|
|
* Removes a label:"value" expression from the query.
|
|
*/
|
|
export function removeFilterFromQuery(query: string, key: string, value: string, modifier: ModifierType = ''): string {
|
|
const node = findFilterNode(query, key, value, modifier);
|
|
const ast = parseQuery(query);
|
|
if (!node || !ast) {
|
|
return query;
|
|
}
|
|
|
|
return lucene.toString(removeNodeFromTree(ast, node));
|
|
}
|
|
|
|
function removeNodeFromTree(ast: AST, node: NodeTerm): AST {
|
|
// {}
|
|
if (Object.keys(ast).length === 0) {
|
|
return ast;
|
|
}
|
|
// { left: {}, right: {} } or { left: {} }
|
|
if (isAST(ast.left)) {
|
|
ast.left = removeNodeFromTree(ast.left, node);
|
|
return ast;
|
|
}
|
|
if (isNodeTerm(ast.left) && isEqual(ast.left, node)) {
|
|
Object.assign(
|
|
ast,
|
|
{
|
|
left: undefined,
|
|
operator: undefined,
|
|
right: undefined,
|
|
},
|
|
'right' in ast ? ast.right : {}
|
|
);
|
|
return ast;
|
|
}
|
|
if (isLeftOnlyAST(ast)) {
|
|
return ast;
|
|
}
|
|
if (isNodeTerm(ast.right) && isEqual(ast.right, node)) {
|
|
Object.assign(ast, {
|
|
right: undefined,
|
|
operator: undefined,
|
|
});
|
|
return ast;
|
|
}
|
|
if (isBinaryAST(ast.right)) {
|
|
ast.right = removeNodeFromTree(ast.right, node);
|
|
return ast;
|
|
}
|
|
return ast;
|
|
}
|
|
|
|
/**
|
|
* Filters can possibly reserved characters such as colons which are part of the Lucene syntax.
|
|
* Use this function to escape filter keys.
|
|
*/
|
|
export function escapeFilter(value: string) {
|
|
return lucene.term.escape(value);
|
|
}
|
|
|
|
/**
|
|
* Values can possibly reserved special characters such as quotes.
|
|
* Use this function to escape filter values.
|
|
*/
|
|
export function escapeFilterValue(value: string, escapeBackslash = true) {
|
|
if (escapeBackslash) {
|
|
value = value.replace(/\\/g, '\\\\');
|
|
}
|
|
return lucene.phrase.escape(value);
|
|
}
|
|
|
|
/**
|
|
* Normalizes the query by removing whitespace around colons, which breaks parsing.
|
|
*/
|
|
function normalizeQuery(query: string) {
|
|
return query.replace(/(\w+)\s(:)/gi, '$1$2');
|
|
}
|
|
|
|
function isLeftOnlyAST(ast: unknown): ast is LeftOnlyAST {
|
|
if (!ast || typeof ast !== 'object') {
|
|
return false;
|
|
}
|
|
|
|
if ('left' in ast && !('right' in ast)) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function isBinaryAST(ast: unknown): ast is BinaryAST {
|
|
if (!ast || typeof ast !== 'object') {
|
|
return false;
|
|
}
|
|
|
|
if ('left' in ast && 'right' in ast) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function isAST(ast: unknown): ast is AST {
|
|
return isLeftOnlyAST(ast) || isBinaryAST(ast);
|
|
}
|
|
|
|
function isNodeTerm(ast: unknown): ast is NodeTerm {
|
|
if (ast && typeof ast === 'object' && 'term' in ast) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function parseQuery(query: string) {
|
|
try {
|
|
return lucene.parse(normalizeQuery(query));
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function addStringFilterToQuery(query: string, filter: string, contains = true) {
|
|
const expression = `"${escapeFilterValue(filter)}"`;
|
|
return query.trim() ? `${query} ${contains ? 'AND' : 'NOT'} ${expression}` : `${contains ? '' : 'NOT '}${expression}`;
|
|
}
|