Files
Isabella Siu d83db31a23 Elasticsearch: Replace level in adhoc filters with level field name (#100315)
Elasticsearch: replace level in adhoc filters with level field name
2025-02-25 09:16:55 -05:00

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}`;
}