-
Notifications
You must be signed in to change notification settings - Fork 104
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Trigger condition parser and resolver for Doc Level Alerts #405
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should the class name be changed to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since currently there is only one concrete implementation of the Trigger Expression Parser have kept the name as is for clarity. However, the base implementation which holds the RPN logic is named as the |
||
triggerExpression: String | ||
) : TriggerExpressionRPNBaseParser(triggerExpression) { | ||
|
||
override fun parse(): TriggerExpressionRPNResolver { | ||
val expression = expressionToParse.replace(" ", "") | ||
|
||
val splitters = ArrayList<String>() | ||
TriggerExpressionOperator.values().forEach { splitters.add(it.value) } | ||
|
||
val breaks = ArrayList<String>().apply { add(expression) } | ||
for (s in splitters) { | ||
val a = ArrayList<String>() | ||
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<String> { | ||
val tokens = input.split(delimeter) | ||
val array = ArrayList<String>() | ||
for (t in tokens) { | ||
array.add(t) | ||
array.add(delimeter) | ||
} | ||
array.removeAt(array.size - 1) | ||
return array | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<String>): ArrayList<ExpressionToken> { | ||
val expTokenStack = Stack<ExpressionToken>() | ||
val outputExpTokens = ArrayList<ExpressionToken>() | ||
|
||
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<TriggerExpressionOperator>() | ||
while (topExpToken != null && topExpToken != TriggerExpressionOperator.PAR_LEFT) { | ||
outputExpTokens.add(topExpToken) | ||
topExpToken = expTokenStack.popExpTokenOrNull<TriggerExpressionOperator>() | ||
} | ||
if (topExpToken != TriggerExpressionOperator.PAR_LEFT) | ||
throw java.lang.IllegalArgumentException("No matching left parenthesis.") | ||
} | ||
else -> { | ||
var op2 = expTokenStack.peekExpTokenOrNull<TriggerExpressionOperator>() | ||
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<TriggerExpressionOperator>() | ||
} | ||
expTokenStack.push(expToken) | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
while (!expTokenStack.isEmpty()) { | ||
expTokenStack.peekExpTokenOrNull<TriggerExpressionOperator>()?.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 <reified T> Stack<ExpressionToken>.popExpTokenOrNull(): T? { | ||
return try { | ||
pop() as T | ||
} catch (e: java.lang.Exception) { | ||
null | ||
} | ||
} | ||
|
||
private inline fun <reified T> Stack<ExpressionToken>.peekExpTokenOrNull(): T? { | ||
return try { | ||
peek() as T | ||
} catch (e: java.lang.Exception) { | ||
null | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<String> = when (this) { | ||
is And -> resolveAnd(docSet1, docSet2) | ||
is Or -> resolveOr(docSet1, docSet2) | ||
is Not -> resolveNot(allDocs, docSet2) | ||
} | ||
|
||
private fun resolveAnd(documentSet1: Set<String>, documentSet2: Set<String>): Set<String> { | ||
return documentSet1.intersect(documentSet2) | ||
} | ||
|
||
private fun resolveOr(documentSet1: Set<String>, documentSet2: Set<String>): Set<String> { | ||
return documentSet1.union(documentSet2) | ||
} | ||
|
||
private fun resolveNot(allDocs: Set<String>, documentSet2: Set<String>): Set<String> { | ||
return allDocs.subtract(documentSet2) | ||
} | ||
|
||
// Operators implemented as operator functions | ||
class And(val docSet1: Set<String>, val docSet2: Set<String>) : TriggerExpression() | ||
class Or(val docSet1: Set<String>, val docSet2: Set<String>) : TriggerExpression() | ||
class Not(val allDocs: Set<String>, val docSet2: Set<String>) : TriggerExpression() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the
if-else if
block moves theobject equals check
to runtime. Kotlin allows this check to be made at compile time usingwhen
control statements.this will allow catching runtime errors we faced today at compile time only.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the suggestion @sbcd90. Agree, control statement could make the flow look much cleaner and I have used it in the Parser/Evaluation logic. However, here since this is not an instance check, but rather the value check with string comparison, I am not sure having control statement would add any value here in terms of performance. Given string value comparison still need to be performed at runtime.
Since we weren't using all the three possible values here, I chose to keep it as if-else block instead.