blob: 344bba7906ff41662de83a5378df149d72d4fa15 [file] [log] [blame]
// Copyright 2023 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
// the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
import { LogEntry } from '../../shared/interfaces';
import { FilterCondition, ConditionType } from './log-filter.models';
export class LogFilter {
/**
* Generates a structured representation of filter conditions which can be
* used to filter log entries.
*
* @param {string} searchQuery - The search query string provided.
* @returns {function[]} An array of filter functions, each representing a
* set of conditions grouped by logical operators, for filtering log
* entries.
*/
static parseSearchQuery(searchQuery: string): FilterCondition[] {
const filters: FilterCondition[] = [];
const orGroups = searchQuery.split(/\s*\|\s*/);
for (let i = 0; i < orGroups.length; i++) {
let orGroup = orGroups[i];
if (orGroup.includes('(') && !orGroup.includes(')')) {
let j = i + 1;
while (j < orGroups.length && !orGroups[j].includes(')')) {
orGroup += ` | ${orGroups[j]}`;
j++;
}
if (j < orGroups.length) {
orGroup += ` | ${orGroups[j]}`;
i = j;
}
}
const andConditions = orGroup.match(
/\([^()]*\)|"[^"]+"|[^\s:]+:[^\s]+|[^\s]+/g,
);
const andFilters: FilterCondition[] = [];
if (andConditions) {
for (const condition of andConditions) {
if (condition.startsWith('(') && condition.endsWith(')')) {
const nestedConditions = condition.slice(1, -1).trim();
andFilters.push(...this.parseSearchQuery(nestedConditions));
} else if (condition.startsWith('"') && condition.endsWith('"')) {
const exactPhrase = condition.slice(1, -1).trim();
andFilters.push({
type: ConditionType.ExactPhraseSearch,
exactPhrase,
});
} else if (condition.startsWith('!')) {
const column = condition.slice(1, condition.indexOf(':'));
const value = condition.slice(condition.indexOf(':') + 1);
andFilters.push({
type: ConditionType.NotExpression,
expression: {
type: ConditionType.ColumnSearch,
column,
value,
},
});
} else if (condition.endsWith(':')) {
const column = condition.slice(0, condition.indexOf(':'));
andFilters.push({
type: ConditionType.ColumnSearch,
column,
});
} else if (condition.includes(':')) {
const column = condition.slice(0, condition.indexOf(':'));
const value = condition.slice(condition.indexOf(':') + 1);
andFilters.push({
type: ConditionType.ColumnSearch,
column,
value,
});
} else {
andFilters.push({
type: ConditionType.StringSearch,
searchString: condition,
});
}
}
}
if (andFilters.length > 0) {
if (andFilters.length === 1) {
filters.push(andFilters[0]);
} else {
filters.push({
type: ConditionType.AndExpression,
expressions: andFilters,
});
}
}
}
if (filters.length === 0) {
filters.push({
type: ConditionType.StringSearch,
searchString: '',
});
}
if (filters.length > 1) {
return [
{
type: ConditionType.OrExpression,
expressions: filters,
},
];
}
return filters;
}
/**
* Takes a condition node, which represents a specific filter condition, and
* recursively generates a filter function that can be applied to log
* entries.
*
* @param {FilterCondition} condition - A filter condition to convert to a
* function.
* @returns {function} A function for filtering log entries based on the
* input condition and its logical operators.
*/
static createFilterFunction(
condition: FilterCondition,
): (logEntry: LogEntry) => boolean {
switch (condition.type) {
case ConditionType.StringSearch:
return (logEntry) =>
this.checkStringInColumns(logEntry, condition.searchString);
case ConditionType.ExactPhraseSearch:
return (logEntry) =>
this.checkExactPhraseInColumns(logEntry, condition.exactPhrase);
case ConditionType.ColumnSearch:
return (logEntry) =>
this.checkColumn(logEntry, condition.column, condition.value);
case ConditionType.NotExpression: {
const innerFilter = this.createFilterFunction(condition.expression);
return (logEntry) => !innerFilter(logEntry);
}
case ConditionType.AndExpression: {
const andFilters = condition.expressions.map((expr) =>
this.createFilterFunction(expr),
);
return (logEntry) => andFilters.every((filter) => filter(logEntry));
}
case ConditionType.OrExpression: {
const orFilters = condition.expressions.map((expr) =>
this.createFilterFunction(expr),
);
return (logEntry) => orFilters.some((filter) => filter(logEntry));
}
default:
// Return a filter that matches all entries
return () => true;
}
}
/**
* Checks if the column exists in a log entry and then performs a value
* search on the column's value.
*
* @param {LogEntry} logEntry - The log entry to be searched.
* @param {string} column - The name of the column (log entry field) to be
* checked for filtering.
* @param {string} value - An optional string that represents the value used
* for filtering.
* @returns {boolean} True if the specified column exists in the log entry,
* or if a value is provided, returns true if the value matches a
* substring of the column's value (case-insensitive).
*/
private static checkColumn(
logEntry: LogEntry,
column: string,
value?: string,
): boolean {
const fieldData = logEntry.fields.find((field) => field.key === column);
if (!fieldData) return false;
if (value === undefined) {
return true;
}
const searchRegex = new RegExp(value, 'i');
return searchRegex.test(fieldData.value.toString());
}
/**
* Checks if the provided search string exists in any of the log entry
* columns (excluding `severity`).
*
* @param {LogEntry} logEntry - The log entry to be searched.
* @param {string} searchString - The search string to be matched against
* the log entry fields.
* @returns {boolean} True if the search string is found in any of the log
* entry fields, otherwise false.
*/
private static checkStringInColumns(
logEntry: LogEntry,
searchString: string,
): boolean {
const escapedSearchString = this.escapeRegEx(searchString);
const columnsToSearch = logEntry.fields.filter(
(field) => field.key !== 'severity',
);
return columnsToSearch.some((field) =>
new RegExp(escapedSearchString, 'i').test(field.value.toString()),
);
}
/**
* Checks if the exact phrase exists in any of the log entry columns
* (excluding `severity`).
*
* @param {LogEntry} logEntry - The log entry to be searched.
* @param {string} exactPhrase - The exact phrase to search for within the
* log entry columns.
* @returns {boolean} True if the exact phrase is found in any column,
* otherwise false.
*/
private static checkExactPhraseInColumns(
logEntry: LogEntry,
exactPhrase: string,
): boolean {
const escapedExactPhrase = this.escapeRegEx(exactPhrase);
const searchRegex = new RegExp(escapedExactPhrase, 'i');
const columnsToSearch = logEntry.fields.filter(
(field) => field.key !== 'severity',
);
return columnsToSearch.some((field) =>
searchRegex.test(field.value.toString()),
);
}
private static escapeRegEx(text: string) {
return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
}
}