From c18a9a339c92f10658a3c3fa28c30b551dbea4ee Mon Sep 17 00:00:00 2001 From: Saurabh Singh Date: Fri, 15 Apr 2022 01:38:01 -0700 Subject: [PATCH] Add Trigger condition resolver which parses and evaluates the Trigger expression. Signed-off-by: Saurabh Singh --- .../DocumentReturningMonitorRunner.kt | 6 +- .../org/opensearch/alerting/TriggerService.kt | 36 ++---- .../parsers/ExpressionParser.kt | 12 ++ .../parsers/TriggerExpressionParser.kt | 53 ++++++++ .../parsers/TriggerExpressionRPNBaseParser.kt | 114 +++++++++++++++++ .../resolvers/TriggerExpression.kt | 32 +++++ .../resolvers/TriggerExpressionRPNResolver.kt | 103 +++++++++++++++ .../resolvers/TriggerExpressionResolver.kt | 12 ++ .../tokens/ExpressionToken.kt | 8 ++ .../tokens/TriggerExpressionConstant.kt | 26 ++++ .../tokens/TriggerExpressionOperator.kt | 20 +++ .../tokens/TriggerExpressionToken.kt | 11 ++ .../TriggerExpressionParserTests.kt | 71 +++++++++++ .../TriggerExpressionResolverTests.kt | 119 ++++++++++++++++++ 14 files changed, 597 insertions(+), 26 deletions(-) create mode 100644 alerting/src/main/kotlin/org/opensearch/alerting/triggercondition/parsers/ExpressionParser.kt create mode 100644 alerting/src/main/kotlin/org/opensearch/alerting/triggercondition/parsers/TriggerExpressionParser.kt create mode 100644 alerting/src/main/kotlin/org/opensearch/alerting/triggercondition/parsers/TriggerExpressionRPNBaseParser.kt create mode 100644 alerting/src/main/kotlin/org/opensearch/alerting/triggercondition/resolvers/TriggerExpression.kt create mode 100644 alerting/src/main/kotlin/org/opensearch/alerting/triggercondition/resolvers/TriggerExpressionRPNResolver.kt create mode 100644 alerting/src/main/kotlin/org/opensearch/alerting/triggercondition/resolvers/TriggerExpressionResolver.kt create mode 100644 alerting/src/main/kotlin/org/opensearch/alerting/triggercondition/tokens/ExpressionToken.kt create mode 100644 alerting/src/main/kotlin/org/opensearch/alerting/triggercondition/tokens/TriggerExpressionConstant.kt create mode 100644 alerting/src/main/kotlin/org/opensearch/alerting/triggercondition/tokens/TriggerExpressionOperator.kt create mode 100644 alerting/src/main/kotlin/org/opensearch/alerting/triggercondition/tokens/TriggerExpressionToken.kt create mode 100644 alerting/src/test/kotlin/org/opensearch/alerting/triggeraction/TriggerExpressionParserTests.kt create mode 100644 alerting/src/test/kotlin/org/opensearch/alerting/triggeraction/TriggerExpressionResolverTests.kt diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/DocumentReturningMonitorRunner.kt b/alerting/src/main/kotlin/org/opensearch/alerting/DocumentReturningMonitorRunner.kt index 16bb92b5e..a3eaac693 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/DocumentReturningMonitorRunner.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/DocumentReturningMonitorRunner.kt @@ -119,7 +119,7 @@ object DocumentReturningMonitorRunner : MonitorRunner { monitor, idQueryMap, docsToQueries, - queryIds, + queryToDocIds, dryrun ) } @@ -146,11 +146,11 @@ object DocumentReturningMonitorRunner : MonitorRunner { monitor: Monitor, idQueryMap: Map, docsToQueries: Map>, - queryIds: List, + queryToDocIds: Map>, dryrun: Boolean ): DocumentLevelTriggerRunResult { val triggerCtx = DocumentLevelTriggerExecutionContext(monitor, trigger) - val triggerResult = monitorCtx.triggerService!!.runDocLevelTrigger(monitor, trigger, triggerCtx, docsToQueries, queryIds) + val triggerResult = monitorCtx.triggerService!!.runDocLevelTrigger(monitor, trigger, queryToDocIds) logger.info("trigger results") logger.info(triggerResult.triggeredDocs.toString()) diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/TriggerService.kt b/alerting/src/main/kotlin/org/opensearch/alerting/TriggerService.kt index 4d9c281f5..77ea886a8 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/TriggerService.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/TriggerService.kt @@ -8,6 +8,7 @@ package org.opensearch.alerting import org.apache.logging.log4j.LogManager import org.opensearch.alerting.aggregation.bucketselectorext.BucketSelectorIndices.Fields.BUCKET_INDICES import org.opensearch.alerting.aggregation.bucketselectorext.BucketSelectorIndices.Fields.PARENT_BUCKET_PATH +import org.opensearch.alerting.core.model.DocLevelQuery import org.opensearch.alerting.model.AggregationResultBucket import org.opensearch.alerting.model.Alert import org.opensearch.alerting.model.BucketLevelTrigger @@ -18,20 +19,22 @@ import org.opensearch.alerting.model.Monitor import org.opensearch.alerting.model.QueryLevelTrigger import org.opensearch.alerting.model.QueryLevelTriggerRunResult import org.opensearch.alerting.script.BucketLevelTriggerExecutionContext -import org.opensearch.alerting.script.DocumentLevelTriggerExecutionContext import org.opensearch.alerting.script.QueryLevelTriggerExecutionContext import org.opensearch.alerting.script.TriggerScript +import org.opensearch.alerting.triggercondition.parsers.TriggerExpressionParser import org.opensearch.alerting.util.getBucketKeysHash +import org.opensearch.script.Script import org.opensearch.script.ScriptService import org.opensearch.search.aggregations.Aggregation import org.opensearch.search.aggregations.Aggregations import org.opensearch.search.aggregations.support.AggregationPath -import java.time.Instant /** Service that handles executing Triggers */ class TriggerService(val scriptService: ScriptService) { private val logger = LogManager.getLogger(TriggerService::class.java) + private val ALWAYS_RUN = Script("return true") + private val NEVER_RUN = Script("return false") fun isQueryLevelTriggerActionable(ctx: QueryLevelTriggerExecutionContext, result: QueryLevelTriggerRunResult): Boolean { // Suppress actions if the current alert is acknowledged and there are no errors. @@ -60,31 +63,18 @@ class TriggerService(val scriptService: ScriptService) { fun runDocLevelTrigger( monitor: Monitor, trigger: DocumentLevelTrigger, - ctx: DocumentLevelTriggerExecutionContext, - docsToQueries: Map>, - queryIds: List + queryToDocIds: Map> ): DocumentLevelTriggerRunResult { return try { - val triggeredDocs = mutableListOf() + var triggeredDocs = mutableListOf() - val dummyTrigger = QueryLevelTrigger( - name = trigger.name, - severity = trigger.severity, - actions = trigger.actions, - condition = trigger.condition - ) - val dummyExecutionContext = QueryLevelTriggerExecutionContext(monitor, dummyTrigger, emptyList(), Instant.now(), Instant.now()) - - for (doc in docsToQueries.keys) { - val params = trigger.condition.params.toMutableMap() - for (queryId in queryIds) { - params[queryId] = docsToQueries[doc]!!.contains(queryId) + if (trigger.condition.idOrCode.equals(ALWAYS_RUN.idOrCode)) { + for (value in queryToDocIds.values) { + triggeredDocs.addAll(value) } - val triggered = scriptService.compile(trigger.condition, TriggerScript.CONTEXT) - .newInstance(params) - .execute(dummyExecutionContext) - logger.info("trigger val: $triggered") - if (triggered) triggeredDocs.add(doc) + } else if (!trigger.condition.idOrCode.equals(NEVER_RUN.idOrCode)) { + triggeredDocs = TriggerExpressionParser(trigger.condition.idOrCode).parse() + .evaluate(queryToDocIds).toMutableList() } DocumentLevelTriggerRunResult(trigger.name, triggeredDocs, null) diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/triggercondition/parsers/ExpressionParser.kt b/alerting/src/main/kotlin/org/opensearch/alerting/triggercondition/parsers/ExpressionParser.kt new file mode 100644 index 000000000..c0e215000 --- /dev/null +++ b/alerting/src/main/kotlin/org/opensearch/alerting/triggercondition/parsers/ExpressionParser.kt @@ -0,0 +1,12 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.alerting.triggercondition.parsers + +import org.opensearch.alerting.triggercondition.resolvers.TriggerExpressionResolver + +interface ExpressionParser { + fun parse(): TriggerExpressionResolver +} diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/triggercondition/parsers/TriggerExpressionParser.kt b/alerting/src/main/kotlin/org/opensearch/alerting/triggercondition/parsers/TriggerExpressionParser.kt new file mode 100644 index 000000000..835e9b383 --- /dev/null +++ b/alerting/src/main/kotlin/org/opensearch/alerting/triggercondition/parsers/TriggerExpressionParser.kt @@ -0,0 +1,53 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.alerting.triggercondition.parsers + +import org.opensearch.alerting.triggercondition.resolvers.TriggerExpressionRPNResolver +import org.opensearch.alerting.triggercondition.tokens.TriggerExpressionOperator + +/** + * The postfix (Reverse Polish Notation) parser. + * Uses the Shunting-yard algorithm to parse a mathematical expression + * @param triggerExpression String containing the trigger expression for the monitor + */ +class TriggerExpressionParser( + triggerExpression: String +) : TriggerExpressionRPNBaseParser(triggerExpression) { + + override fun parse(): TriggerExpressionRPNResolver { + val expression = expressionToParse.replace(" ", "") + + val splitters = ArrayList() + TriggerExpressionOperator.values().forEach { splitters.add(it.value) } + + val breaks = ArrayList().apply { add(expression) } + for (s in splitters) { + val a = ArrayList() + for (ind in 0 until breaks.size) { + breaks[ind].let { + if (it.length > 1) { + a.addAll(breakString(breaks[ind], s)) + } else a.add(it) + } + } + breaks.clear() + breaks.addAll(a) + } + + return TriggerExpressionRPNResolver(convertInfixToPostfix(breaks)) + } + + private fun breakString(input: String, delimeter: String): ArrayList { + val tokens = input.split(delimeter) + val array = ArrayList() + for (t in tokens) { + array.add(t) + array.add(delimeter) + } + array.removeAt(array.size - 1) + return array + } +} diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/triggercondition/parsers/TriggerExpressionRPNBaseParser.kt b/alerting/src/main/kotlin/org/opensearch/alerting/triggercondition/parsers/TriggerExpressionRPNBaseParser.kt new file mode 100644 index 000000000..6dd6bfc36 --- /dev/null +++ b/alerting/src/main/kotlin/org/opensearch/alerting/triggercondition/parsers/TriggerExpressionRPNBaseParser.kt @@ -0,0 +1,114 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.alerting.triggercondition.parsers + +import org.opensearch.alerting.triggercondition.tokens.ExpressionToken +import org.opensearch.alerting.triggercondition.tokens.TriggerExpressionConstant +import org.opensearch.alerting.triggercondition.tokens.TriggerExpressionOperator +import org.opensearch.alerting.triggercondition.tokens.TriggerExpressionToken +import java.util.Stack + +/** + * This is the abstract base class which holds the trigger expression parsing logic; + * using the Infix to Postfix a.k.a. Reverse Polish Notation (RPN) parser. + * It also uses the Shunting-Yard algorithm to parse the given trigger expression. + * + * @param expressionToParse Complete string containing the trigger expression + */ +abstract class TriggerExpressionRPNBaseParser( + protected val expressionToParse: String +) : ExpressionParser { + /** + * To perform the Infix-to-postfix conversion of the trigger expression + */ + protected fun convertInfixToPostfix(expTokens: List): ArrayList { + val expTokenStack = Stack() + val outputExpTokens = ArrayList() + + for (tokenString in expTokens) { + if (tokenString.isEmpty()) continue + when (val expToken = assignToken(tokenString)) { + is TriggerExpressionToken -> outputExpTokens.add(expToken) + is TriggerExpressionOperator -> { + when (expToken) { + TriggerExpressionOperator.PAR_LEFT -> expTokenStack.push(expToken) + TriggerExpressionOperator.PAR_RIGHT -> { + var topExpToken = expTokenStack.popExpTokenOrNull() + while (topExpToken != null && topExpToken != TriggerExpressionOperator.PAR_LEFT) { + outputExpTokens.add(topExpToken) + topExpToken = expTokenStack.popExpTokenOrNull() + } + if (topExpToken != TriggerExpressionOperator.PAR_LEFT) + throw java.lang.IllegalArgumentException("No matching left parenthesis.") + } + else -> { + var op2 = expTokenStack.peekExpTokenOrNull() + while (op2 != null) { + val c = expToken.precedence.compareTo(op2.precedence) + if (c < 0 || !expToken.rightAssociative && c <= 0) { + outputExpTokens.add(expTokenStack.pop()) + } else { + break + } + op2 = expTokenStack.peekExpTokenOrNull() + } + expTokenStack.push(expToken) + } + } + } + } + } + + while (!expTokenStack.isEmpty()) { + expTokenStack.peekExpTokenOrNull()?.let { + if (it == TriggerExpressionOperator.PAR_LEFT) + throw java.lang.IllegalArgumentException("No matching right parenthesis.") + } + val top = expTokenStack.pop() + outputExpTokens.add(top) + } + + return outputExpTokens + } + + /** + * Looks up and maps the expression token that matches the string version of that expression unit + */ + private fun assignToken(tokenString: String): ExpressionToken { + + // Check "query" string in trigger expression such as in 'query[name="abc"]' + if (tokenString.startsWith(TriggerExpressionConstant.ConstantType.QUERY.ident)) + return TriggerExpressionToken(tokenString) + + // Check operators in trigger expression such as in [&&, ||, !] + for (op in TriggerExpressionOperator.values()) { + if (op.value == tokenString) return op + } + + // Check any constants in trigger expression such as in ["name, "id", "tag", [", "]", "="] + for (con in TriggerExpressionConstant.ConstantType.values()) { + if (tokenString == con.ident) return TriggerExpressionConstant(con) + } + + throw IllegalArgumentException("Error while processing the trigger expression '$tokenString'") + } + + private inline fun Stack.popExpTokenOrNull(): T? { + return try { + pop() as T + } catch (e: java.lang.Exception) { + null + } + } + + private inline fun Stack.peekExpTokenOrNull(): T? { + return try { + peek() as T + } catch (e: java.lang.Exception) { + null + } + } +} diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/triggercondition/resolvers/TriggerExpression.kt b/alerting/src/main/kotlin/org/opensearch/alerting/triggercondition/resolvers/TriggerExpression.kt new file mode 100644 index 000000000..2a3e6c1ff --- /dev/null +++ b/alerting/src/main/kotlin/org/opensearch/alerting/triggercondition/resolvers/TriggerExpression.kt @@ -0,0 +1,32 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.alerting.triggercondition.resolvers + +sealed class TriggerExpression { + + fun resolve(): Set = when (this) { + is And -> resolveAnd(docSet1, docSet2) + is Or -> resolveOr(docSet1, docSet2) + is Not -> resolveNot(allDocs, docSet2) + } + + private fun resolveAnd(documentSet1: Set, documentSet2: Set): Set { + return documentSet1.intersect(documentSet2) + } + + private fun resolveOr(documentSet1: Set, documentSet2: Set): Set { + return documentSet1.union(documentSet2) + } + + private fun resolveNot(allDocs: Set, documentSet2: Set): Set { + return allDocs.subtract(documentSet2) + } + + // Operators implemented as operator functions + class And(val docSet1: Set, val docSet2: Set) : TriggerExpression() + class Or(val docSet1: Set, val docSet2: Set) : TriggerExpression() + class Not(val allDocs: Set, val docSet2: Set) : TriggerExpression() +} diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/triggercondition/resolvers/TriggerExpressionRPNResolver.kt b/alerting/src/main/kotlin/org/opensearch/alerting/triggercondition/resolvers/TriggerExpressionRPNResolver.kt new file mode 100644 index 000000000..749214048 --- /dev/null +++ b/alerting/src/main/kotlin/org/opensearch/alerting/triggercondition/resolvers/TriggerExpressionRPNResolver.kt @@ -0,0 +1,103 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.alerting.triggercondition.resolvers + +import org.opensearch.alerting.core.model.DocLevelQuery +import org.opensearch.alerting.triggercondition.tokens.ExpressionToken +import org.opensearch.alerting.triggercondition.tokens.TriggerExpressionConstant +import org.opensearch.alerting.triggercondition.tokens.TriggerExpressionOperator +import org.opensearch.alerting.triggercondition.tokens.TriggerExpressionToken +import java.util.Optional +import java.util.Stack + +/** + * Solves the Trigger Expression using the Reverse Polish Notation (RPN) based solver + * @param polishNotation an array of expression tokens organized in the RPN order + */ +class TriggerExpressionRPNResolver( + private val polishNotation: ArrayList +) : TriggerExpressionResolver { + + private val eqString by lazy { + val stringBuilder = StringBuilder() + for (expToken in polishNotation) { + when (expToken) { + is TriggerExpressionToken -> stringBuilder.append(expToken.value) + is TriggerExpressionOperator -> stringBuilder.append(expToken.value) + is TriggerExpressionConstant -> stringBuilder.append(expToken.type.ident) + else -> throw Exception() + } + stringBuilder.append(" ") + } + stringBuilder.toString() + } + + override fun toString(): String = eqString + + /** + * Evaluates the trigger expression expressed provided in form of the RPN token array. + * @param queryToDocIds Map to hold the resultant document id per query id + * @return evaluates the final set of document id + */ + override fun evaluate(queryToDocIds: Map>): Set { + val tokenStack = Stack>() + + val allDocIds = mutableSetOf() + for (value in queryToDocIds.values) { + allDocIds.addAll(value) + } + + for (expToken in polishNotation) { + when (expToken) { + is TriggerExpressionToken -> tokenStack.push(resolveQueryExpression(expToken.value, queryToDocIds)) + is TriggerExpressionOperator -> { + val right = tokenStack.pop() + val expr = when (expToken) { + TriggerExpressionOperator.AND -> TriggerExpression.And(tokenStack.pop(), right) + TriggerExpressionOperator.OR -> TriggerExpression.Or(tokenStack.pop(), right) + TriggerExpressionOperator.NOT -> TriggerExpression.Not(allDocIds, right) + else -> throw IllegalArgumentException("No matching operator.") + } + tokenStack.push(expr.resolve()) + } + } + } + return tokenStack.pop() + } + + private fun resolveQueryExpression(queryExpString: String, queryToDocIds: Map>): Set { + if (!queryExpString.startsWith(TriggerExpressionConstant.ConstantType.QUERY.ident)) return emptySet() + val token = queryExpString.substringAfter(TriggerExpressionConstant.ConstantType.BRACKET_LEFT.ident) + .substringBefore(TriggerExpressionConstant.ConstantType.BRACKET_RIGHT.ident) + if (token.isEmpty()) return emptySet() + + val tokens = token.split(TriggerExpressionConstant.ConstantType.EQUALS.ident) + if (tokens.isEmpty() || tokens.size != 2) return emptySet() + + val identifier = tokens[0] + val value = tokens[1] + val documents = mutableSetOf() + when (identifier) { + TriggerExpressionConstant.ConstantType.NAME.ident -> { + val key: Optional = queryToDocIds.keys.stream().filter { it.name == value }.findFirst() + if (key.isPresent) queryToDocIds[key.get()]?.let { doc -> documents.addAll(doc) } + } + + TriggerExpressionConstant.ConstantType.ID.ident -> { + val key: Optional = queryToDocIds.keys.stream().filter { it.id == value }.findFirst() + if (key.isPresent) queryToDocIds[key.get()]?.let { doc -> documents.addAll(doc) } + } + + // Iterate through all the queries with the same Tag + TriggerExpressionConstant.ConstantType.TAG.ident -> { + queryToDocIds.keys.stream().forEach { + if (it.tags.contains(value)) queryToDocIds[it]?.let { it1 -> documents.addAll(it1) } + } + } + } + return documents + } +} diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/triggercondition/resolvers/TriggerExpressionResolver.kt b/alerting/src/main/kotlin/org/opensearch/alerting/triggercondition/resolvers/TriggerExpressionResolver.kt new file mode 100644 index 000000000..faeabad08 --- /dev/null +++ b/alerting/src/main/kotlin/org/opensearch/alerting/triggercondition/resolvers/TriggerExpressionResolver.kt @@ -0,0 +1,12 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.alerting.triggercondition.resolvers + +import org.opensearch.alerting.core.model.DocLevelQuery + +interface TriggerExpressionResolver { + fun evaluate(queryToDocIds: Map>): Set +} diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/triggercondition/tokens/ExpressionToken.kt b/alerting/src/main/kotlin/org/opensearch/alerting/triggercondition/tokens/ExpressionToken.kt new file mode 100644 index 000000000..2085bf2d3 --- /dev/null +++ b/alerting/src/main/kotlin/org/opensearch/alerting/triggercondition/tokens/ExpressionToken.kt @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.alerting.triggercondition.tokens + +interface ExpressionToken diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/triggercondition/tokens/TriggerExpressionConstant.kt b/alerting/src/main/kotlin/org/opensearch/alerting/triggercondition/tokens/TriggerExpressionConstant.kt new file mode 100644 index 000000000..80e662a21 --- /dev/null +++ b/alerting/src/main/kotlin/org/opensearch/alerting/triggercondition/tokens/TriggerExpressionConstant.kt @@ -0,0 +1,26 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.alerting.triggercondition.tokens + +/** + * To define all the tokens which could be part of expression constant such as query[id=new_id], query[name=new_name], + * query[tag=new_tag] + */ +class TriggerExpressionConstant(val type: ConstantType) : ExpressionToken { + + enum class ConstantType(val ident: String) { + QUERY("query"), + + TAG("tag"), + NAME("name"), + ID("id"), + + BRACKET_LEFT("["), + BRACKET_RIGHT("]"), + + EQUALS("=") + } +} diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/triggercondition/tokens/TriggerExpressionOperator.kt b/alerting/src/main/kotlin/org/opensearch/alerting/triggercondition/tokens/TriggerExpressionOperator.kt new file mode 100644 index 000000000..de3c4a0df --- /dev/null +++ b/alerting/src/main/kotlin/org/opensearch/alerting/triggercondition/tokens/TriggerExpressionOperator.kt @@ -0,0 +1,20 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.alerting.triggercondition.tokens + +/** + * To define all the operators used in the trigger expression + */ +enum class TriggerExpressionOperator(val value: String, val precedence: Int, val rightAssociative: Boolean) : ExpressionToken { + + AND("&&", 2, false), + OR("||", 2, false), + + NOT("!", 3, true), + + PAR_LEFT("(", 1, false), + PAR_RIGHT(")", 1, false) +} diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/triggercondition/tokens/TriggerExpressionToken.kt b/alerting/src/main/kotlin/org/opensearch/alerting/triggercondition/tokens/TriggerExpressionToken.kt new file mode 100644 index 000000000..808f7737d --- /dev/null +++ b/alerting/src/main/kotlin/org/opensearch/alerting/triggercondition/tokens/TriggerExpressionToken.kt @@ -0,0 +1,11 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.alerting.triggercondition.tokens + +/** + * To define the tokens in Trigger expression such as query[tag=“sev1"] or query[name=“sev1"] or query[id=“sev1"] + */ +internal data class TriggerExpressionToken(val value: String) : ExpressionToken diff --git a/alerting/src/test/kotlin/org/opensearch/alerting/triggeraction/TriggerExpressionParserTests.kt b/alerting/src/test/kotlin/org/opensearch/alerting/triggeraction/TriggerExpressionParserTests.kt new file mode 100644 index 000000000..2657d1e5a --- /dev/null +++ b/alerting/src/test/kotlin/org/opensearch/alerting/triggeraction/TriggerExpressionParserTests.kt @@ -0,0 +1,71 @@ +package org.opensearch.alerting.triggeraction + +import org.junit.Assert +import org.opensearch.alerting.triggercondition.parsers.TriggerExpressionParser +import org.opensearch.test.OpenSearchTestCase + +class TriggerExpressionParserTests : OpenSearchTestCase() { + + fun `test trigger expression posix parsing simple AND`() { + val eqString = "(query[name=sigma-123] && query[name=sigma-456])" + val equation = TriggerExpressionParser(eqString).parse() + Assert.assertEquals("query[name=sigma-123] query[name=sigma-456] && ", equation.toString()) + } + + fun `test trigger expression posix parsing multiple AND`() { + val eqString = "(query[name=sigma-123] && query[name=sigma-456]) && query[name=sigma-789]" + val equation = TriggerExpressionParser(eqString).parse() + Assert.assertEquals("query[name=sigma-123] query[name=sigma-456] && query[name=sigma-789] && ", equation.toString()) + } + + fun `test trigger expression posix parsing multiple AND with parenthesis`() { + val eqString = "(query[name=sigma-123] && query[name=sigma-456]) && (query[name=sigma-789] && query[name=id-2aw34])" + val equation = TriggerExpressionParser(eqString).parse() + Assert.assertEquals( + "query[name=sigma-123] query[name=sigma-456] && query[name=sigma-789] query[name=id-2aw34] && && ", + equation.toString() + ) + } + + fun `test trigger expression posix parsing simple OR`() { + val eqString = "(query[name=sigma-123] || query[name=sigma-456])" + val equation = TriggerExpressionParser(eqString).parse() + Assert.assertEquals("query[name=sigma-123] query[name=sigma-456] || ", equation.toString()) + } + + fun `test trigger expression posix parsing multiple OR`() { + val eqString = "(query[name=sigma-123] || query[name=sigma-456]) || query[name=sigma-789]" + val equation = TriggerExpressionParser(eqString).parse() + Assert.assertEquals("query[name=sigma-123] query[name=sigma-456] || query[name=sigma-789] || ", equation.toString()) + } + + fun `test trigger expression posix parsing multiple OR with parenthesis`() { + val eqString = "(query[name=sigma-123] || query[name=sigma-456]) || (query[name=sigma-789] || query[name=id-2aw34])" + val equation = TriggerExpressionParser(eqString).parse() + Assert.assertEquals( + "query[name=sigma-123] query[name=sigma-456] || query[name=sigma-789] query[name=id-2aw34] || || ", + equation.toString() + ) + } + + fun `test trigger expression posix parsing simple NOT`() { + val eqString = "(query[name=sigma-123] || !query[name=sigma-456])" + val equation = TriggerExpressionParser(eqString).parse() + Assert.assertEquals("query[name=sigma-123] query[name=sigma-456] ! || ", equation.toString()) + } + + fun `test trigger expression posix parsing multiple NOT`() { + val eqString = "(query[name=sigma-123] && !query[tag=tag-456]) && !(query[name=sigma-789])" + val equation = TriggerExpressionParser(eqString).parse() + Assert.assertEquals("query[name=sigma-123] query[tag=tag-456] ! && query[name=sigma-789] ! && ", equation.toString()) + } + + fun `test trigger expression posix parsing multiple operators with parenthesis`() { + val eqString = "(query[name=sigma-123] && query[tag=sev1]) || !(!query[name=sigma-789] || query[name=id-2aw34])" + val equation = TriggerExpressionParser(eqString).parse() + Assert.assertEquals( + "query[name=sigma-123] query[tag=sev1] && query[name=sigma-789] ! query[name=id-2aw34] || ! || ", + equation.toString() + ) + } +} diff --git a/alerting/src/test/kotlin/org/opensearch/alerting/triggeraction/TriggerExpressionResolverTests.kt b/alerting/src/test/kotlin/org/opensearch/alerting/triggeraction/TriggerExpressionResolverTests.kt new file mode 100644 index 000000000..97d5ea54e --- /dev/null +++ b/alerting/src/test/kotlin/org/opensearch/alerting/triggeraction/TriggerExpressionResolverTests.kt @@ -0,0 +1,119 @@ +package org.opensearch.alerting.triggeraction + +import org.junit.Assert +import org.opensearch.alerting.core.model.DocLevelQuery +import org.opensearch.alerting.triggercondition.parsers.TriggerExpressionParser +import org.opensearch.test.OpenSearchTestCase + +class TriggerExpressionResolverTests : OpenSearchTestCase() { + + fun `test trigger expression evaluation simple AND`() { + val eqString = "(query[name=sigma-123] && query[name=sigma-456])" + val equation = TriggerExpressionParser(eqString).parse() + val queryToDocIds = mutableMapOf>() + queryToDocIds[DocLevelQuery("", "sigma-123", "", emptyList())] = mutableSetOf("1", "2", "3") + queryToDocIds[DocLevelQuery("", "sigma-456", "", emptyList())] = mutableSetOf("1", "2", "3") + Assert.assertEquals("query[name=sigma-123] query[name=sigma-456] && ", equation.toString()) + Assert.assertEquals(mutableSetOf("1", "2", "3"), equation.evaluate(queryToDocIds)) + } + + fun `test trigger expression evaluation simple AND scenario2`() { + val eqString = "(query[name=sigma-123] && query[id=id1456])" + val equation = TriggerExpressionParser(eqString).parse() + val queryToDocIds = mutableMapOf>() + queryToDocIds[DocLevelQuery("", "sigma-123", "", emptyList())] = mutableSetOf("6", "3", "7") + queryToDocIds[DocLevelQuery("id1456", "", "", emptyList())] = mutableSetOf("1", "2", "3") + Assert.assertEquals("query[name=sigma-123] query[id=id1456] && ", equation.toString()) + Assert.assertEquals(mutableSetOf("3"), equation.evaluate(queryToDocIds)) + } + + fun `test trigger expression evaluation simple AND scenario3`() { + val eqString = "(query[name=sigma-123] && query[tag=sev2])" + val equation = TriggerExpressionParser(eqString).parse() + val queryToDocIds = mutableMapOf>() + queryToDocIds[DocLevelQuery("", "sigma-123", "", emptyList())] = mutableSetOf("6", "8", "7") + queryToDocIds[DocLevelQuery("", "", "", mutableListOf("tag=sev2"))] = mutableSetOf("1", "2", "3") + Assert.assertEquals("query[name=sigma-123] query[tag=sev2] && ", equation.toString()) + Assert.assertEquals(emptySet(), equation.evaluate(queryToDocIds)) + } + + fun `test trigger expression evaluation simple OR`() { + val eqString = "(query[name=sigma-123] || query[name=sigma-456])" + val equation = TriggerExpressionParser(eqString).parse() + val queryToDocIds = mutableMapOf>() + queryToDocIds[DocLevelQuery("", "sigma-123", "", emptyList())] = mutableSetOf("1", "2", "3") + queryToDocIds[DocLevelQuery("", "sigma-456", "", emptyList())] = mutableSetOf("1", "2", "3") + Assert.assertEquals("query[name=sigma-123] query[name=sigma-456] || ", equation.toString()) + Assert.assertEquals(mutableSetOf("1", "2", "3"), equation.evaluate(queryToDocIds)) + } + + fun `test trigger expression evaluation simple OR scenario2`() { + val eqString = "(query[name=sigma-123] || query[id=id1456])" + val equation = TriggerExpressionParser(eqString).parse() + val queryToDocIds = mutableMapOf>() + queryToDocIds[DocLevelQuery("", "sigma-123", "", emptyList())] = mutableSetOf("6", "3", "7") + queryToDocIds[DocLevelQuery("id1456", "", "", emptyList())] = mutableSetOf("1", "2", "3") + Assert.assertEquals("query[name=sigma-123] query[id=id1456] || ", equation.toString()) + Assert.assertEquals(mutableSetOf("6", "3", "7", "1", "2", "3"), equation.evaluate(queryToDocIds)) + } + + fun `test trigger expression evaluation simple OR scenario3`() { + val eqString = "(query[name=sigma-123] || query[tag=sev2])" + val equation = TriggerExpressionParser(eqString).parse() + val queryToDocIds = mutableMapOf>() + queryToDocIds[DocLevelQuery("", "sigma-123", "", emptyList())] = mutableSetOf("6", "8", "7") + queryToDocIds[DocLevelQuery("", "", "", mutableListOf("tag=sev2"))] = emptySet() + Assert.assertEquals("query[name=sigma-123] query[tag=sev2] || ", equation.toString()) + Assert.assertEquals(mutableSetOf("6", "8", "7"), equation.evaluate(queryToDocIds)) + } + + fun `test trigger expression evaluation simple NOT`() { + val eqString = "!(query[name=sigma-456])" + val equation = TriggerExpressionParser(eqString).parse() + val queryToDocIds = mutableMapOf>() + queryToDocIds[DocLevelQuery("", "sigma-123", "", emptyList())] = mutableSetOf("1", "2", "3") + queryToDocIds[DocLevelQuery("", "sigma-456", "", emptyList())] = mutableSetOf("4", "5", "6") + Assert.assertEquals("query[name=sigma-456] ! ", equation.toString()) + Assert.assertEquals(mutableSetOf("1", "2", "3"), equation.evaluate(queryToDocIds)) + } + + fun `test trigger expression evaluation AND with NOT`() { + val eqString = "(query[name=sigma-123] && !query[name=sigma-456])" + val equation = TriggerExpressionParser(eqString).parse() + val queryToDocIds = mutableMapOf>() + queryToDocIds[DocLevelQuery("", "sigma-123", "", emptyList())] = mutableSetOf("1", "2", "3", "11") + queryToDocIds[DocLevelQuery("", "sigma-456", "", emptyList())] = mutableSetOf("3", "4", "5") + queryToDocIds[DocLevelQuery("id_new", "", "", emptyList())] = mutableSetOf("11", "12", "13") + Assert.assertEquals("query[name=sigma-123] query[name=sigma-456] ! && ", equation.toString()) + Assert.assertEquals(mutableSetOf("1", "2", "11"), equation.evaluate(queryToDocIds)) + } + + fun `test trigger expression evaluation OR with NOT`() { + val eqString = "(query[name=sigma-123] || !query[id=id1456])" + val equation = TriggerExpressionParser(eqString).parse() + val queryToDocIds = mutableMapOf>() + queryToDocIds[DocLevelQuery("", "sigma-123", "", emptyList())] = mutableSetOf("6", "3", "7") + queryToDocIds[DocLevelQuery("id1456", "", "", emptyList())] = mutableSetOf("11", "12", "15") + queryToDocIds[DocLevelQuery("id_new", "", "", emptyList())] = mutableSetOf("11", "12", "13") + Assert.assertEquals("query[name=sigma-123] query[id=id1456] ! || ", equation.toString()) + Assert.assertEquals(mutableSetOf("6", "3", "7", "13"), equation.evaluate(queryToDocIds)) + } + + fun `test trigger expression evaluation with multiple operators with parenthesis`() { + val eqString = "(query[name=sigma-123] && query[tag=sev1]) || !(!query[name=sigma-789] || query[id=id-2aw34])" + val equation = TriggerExpressionParser(eqString).parse() + + val queryToDocIds = mutableMapOf>() + queryToDocIds[DocLevelQuery("", "sigma-123", "", emptyList())] = mutableSetOf("1", "2", "3") + queryToDocIds[DocLevelQuery("id_random1", "", "", mutableListOf("sev1"))] = mutableSetOf("2", "3", "4") + queryToDocIds[DocLevelQuery("", "sigma-789", "", emptyList())] = mutableSetOf("11", "12", "13") + queryToDocIds[DocLevelQuery("id-2aw34", "", "", emptyList())] = mutableSetOf("13", "14", "15") + + Assert.assertEquals( + "query[name=sigma-123] query[tag=sev1] && query[name=sigma-789] ! query[id=id-2aw34] || ! || ", + equation.toString() + ) + + Assert.assertEquals(mutableSetOf("2", "3", "11", "12"), equation.evaluate(queryToDocIds)) + } +}