MQE: Support `&&(and)`, `||(or)` bool operators. (#12965)

This commit is contained in:
Wan Kai 2025-01-14 16:22:29 +08:00 committed by GitHub
parent 1dde137f8e
commit 8bcaabb43c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 286 additions and 16 deletions

View File

@ -163,6 +163,28 @@ and get result:
### Compare Operation Rules and Result Type
Same as the [Binary Operation Rules](#binary-operation-rules).
## Bool Operation
Bool Operation takes two `compare` expressions and performs a logical operation on their results.
The following table lists the bool operations supported by MQE.
Expression:
```text
Compare Expression1 <Bool-Operator> Expression2
```
**Notice**: The `Bool-Operator` only supports the `compare` expressions, which means the result of the left and right expressions should be `Compare Operation Result`.
| Operator | Definition |
|----------|-------------|
| && | logical AND |
| \|\| | logical OR |
For example:
If we want to query the `service_resp_time` metric value greater than 3000 and `service_cpm` less than 1000, we can use the following expression:
```text
service_resp_time > 3000 && service_cpm < 1000
```
## Aggregation Operation
Aggregation Operation takes an expression and performs aggregate calculations on its results.

View File

@ -61,6 +61,7 @@
* Adapt the new Browser API(`/browser/perfData/webVitals`, `/browser/perfData/resources`) protocol.
* Add Circuit Breaking mechanism.
* BanyanDB: Add support for compatibility checks based on the BanyanDB server's API version.
* MQE: Support `&&(and)`, `||(or)` bool operators.
#### UI

View File

@ -20,7 +20,8 @@ Defines the relation between scope and entity name.
An alerting rule is made up of the following elements:
- **Rule name**. A unique name shown in the alarm message. It must end with `_rule`.
- **Expression**. A [MQE](../../api/metrics-query-expression.md) expression that defines the conditions of the rule.
The result type must be `SINGLE_VALUE` and the root operation of the expression must be a [Compare Operation](../../api/metrics-query-expression.md#compare-operation) which provides `1`(true) or `0`(false) result.
The result type must be `SINGLE_VALUE` and the root operation of the expression must be a
[Compare Operation](../../api/metrics-query-expression.md#compare-operation) or [Bool Operation](../../api/metrics-query-expression.md#bool-operation) which provides `1`(true) or `0`(false) result.
When the result is `1`(true), the alarm will be triggered.
For example, `avg(service_resp_time / 1000) > 1` is a valid expression to indicate the request latency is slower than 1s. The typical illegal expressions are
- `avg(service_resp_time > 1000) + 1` expression root doesn't use `Compare Operation`
@ -95,7 +96,7 @@ rules:
- "slack.custom1"
- "pagerduty.custom1"
comp_rule:
expression: (avg(service_sla / 100) > 80) * (avg(service_percentile{p='0'}) > 1000) == 1
expression: (avg(service_sla / 100) > 80) && (avg(service_percentile{p='0'}) > 1000)
period: 10
message: Service {name} avg successful rate is less than 80% and P50 of avg response time is over 1000ms in last 10 minutes.
tags:

View File

@ -46,6 +46,10 @@ LT: '<';
GTE: '>=';
GT: '>';
// Bool operators
AND: '&&';
OR: '||';
// Aggregation operators
AVG: 'avg';
COUNT: 'count';

View File

@ -28,6 +28,7 @@ expression
| expression mulDivMod expression # mulDivModOp
| expression addSub expression # addSubOp
| expression compare expression # compareOp
| expression bool_operator expression #boolOP
| aggregation L_PAREN expression R_PAREN # aggregationOp
| mathematical_operator0 L_PAREN expression R_PAREN #mathematicalOperator0OP
| mathematical_operator1 L_PAREN expression COMMA parameter R_PAREN #mathematicalOperator1OP
@ -82,6 +83,9 @@ topNOf: TOP_N_OF;
logical_operator:
VIEW_AS_SEQ | IS_PRESENT;
bool_operator:
AND | OR;
relabels: RELABELS;
parameter: INTEGER;

View File

@ -29,6 +29,7 @@ import org.apache.skywalking.mqe.rt.grammar.MQEParserBaseVisitor;
import org.apache.skywalking.mqe.rt.operation.AggregateLabelsOp;
import org.apache.skywalking.mqe.rt.operation.AggregationOp;
import org.apache.skywalking.mqe.rt.operation.BinaryOp;
import org.apache.skywalking.mqe.rt.operation.BoolOp;
import org.apache.skywalking.mqe.rt.operation.CompareOp;
import org.apache.skywalking.mqe.rt.operation.LogicalFunctionOp;
import org.apache.skywalking.mqe.rt.operation.MathematicalFunctionOp;
@ -60,7 +61,15 @@ public abstract class MQEVisitorBase extends MQEParserBaseVisitor<ExpressionResu
@Override
public ExpressionResult visitParensOp(MQEParser.ParensOpContext ctx) {
return visit(ctx.expression());
ExpressionResult result = visit(ctx.expression());
if (result.isBoolResult()) {
// The other operation will change the bool result of the expression
if (!(ctx.parent instanceof MQEParser.BoolOPContext
|| ctx.parent instanceof MQEParser.ParensOpContext)) {
result.setBoolResult(false);
}
}
return result;
}
@Override
@ -342,7 +351,10 @@ public abstract class MQEVisitorBase extends MQEParserBaseVisitor<ExpressionResu
int opType = ctx.compare().getStart().getType();
try {
ExpressionResult result = CompareOp.doCompareOP(left, right, opType);
if (ctx.parent == null) {
// The following operation won't change the bool result of the expression
if (ctx.parent == null ||
ctx.parent instanceof MQEParser.ParensOpContext ||
ctx.parent instanceof MQEParser.BoolOPContext) {
result.setBoolResult(true);
}
return result;
@ -425,6 +437,36 @@ public abstract class MQEVisitorBase extends MQEParserBaseVisitor<ExpressionResu
}
}
@Override
public ExpressionResult visitBoolOP(MQEParser.BoolOPContext ctx) {
DebuggingTraceContext traceContext = TRACE_CONTEXT.get();
DebuggingSpan span = traceContext.createSpan("MQE Bool OP: " + ctx.getText());
try {
ExpressionResult left = visit(ctx.expression(0));
if (StringUtil.isNotBlank(left.getError())) {
return left;
}
ExpressionResult right = visit(ctx.expression(1));
if (StringUtil.isNotBlank(right.getError())) {
return right;
}
int opType = ctx.bool_operator().getStart().getType();
try {
ExpressionResult result = BoolOp.doBoolOp(left, right, opType);
if (ctx.parent == null ||
ctx.parent instanceof MQEParser.ParensOpContext ||
ctx.parent instanceof MQEParser.BoolOPContext) {
result.setBoolResult(true);
}
return result;
} catch (IllegalExpressionException e) {
return getErrorResult(e.getMessage());
}
} finally {
traceContext.stopSpan(span);
}
}
@Override
public abstract ExpressionResult visitMetric(MQEParser.MetricContext ctx);

View File

@ -0,0 +1,81 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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
*
* http://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.
*
*/
package org.apache.skywalking.mqe.rt.operation;
import org.apache.skywalking.mqe.rt.exception.IllegalExpressionException;
import org.apache.skywalking.mqe.rt.grammar.MQEParser;
import org.apache.skywalking.oap.server.core.query.mqe.ExpressionResult;
public class BoolOp {
public static ExpressionResult doBoolOp(ExpressionResult left,
ExpressionResult right,
int opType) throws IllegalExpressionException {
if (!checkExpression(left)) {
throw new IllegalExpressionException(
"Bool Operation: The result of the left expression is not a compare result.");
}
if (!checkExpression(right)) {
throw new IllegalExpressionException(
"Bool Operation: The result of the right expression is not a compare result.");
}
try {
return LROp.doLROp(left, right, opType, BoolOp::boolOp);
} catch (IllegalExpressionException e) {
throw new IllegalExpressionException("Unsupported bool operation: " + e.getMessage());
}
}
//bool with bool
private static double boolOp(double leftValue, double rightValue, int opType) throws IllegalExpressionException {
// this should not happen, but just in case
if (!checkBool(leftValue)) {
throw new IllegalExpressionException("Bool Operation: The result of the left expression is not 1 or 0.");
}
if (!checkBool(rightValue)) {
throw new IllegalExpressionException("Bool Operation: The result of the right expression is not 1 or 0.");
}
switch (opType) {
case MQEParser.AND:
if (leftValue == 1 && rightValue == 1) {
return 1;
} else {
return 0;
}
case MQEParser.OR:
if (leftValue == 1 || rightValue == 1) {
return 1;
} else {
return 0;
}
default:
throw new IllegalExpressionException("Unsupported bool operation.");
}
}
private static boolean checkExpression(ExpressionResult result) {
return result.isBoolResult();
}
private static boolean checkBool(double v) {
int i = (int) v;
return i == 0 || i == 1;
}
}

View File

@ -33,7 +33,7 @@ import org.apache.skywalking.oap.server.core.query.type.KeyValue;
@FunctionalInterface
public interface LROp {
double apply(double left, double right, int opType);
double apply(double left, double right, int opType) throws IllegalExpressionException;
static ExpressionResult doLROp(ExpressionResult left,
ExpressionResult right,
@ -162,8 +162,8 @@ public interface LROp {
singleResult.getResults().get(0).getValues().size() != 1) {
throw new IllegalExpressionException("Many to One, single result is empty or has more than one value.");
}
manyResult.getResults().forEach(mqeValues -> {
mqeValues.getValues().forEach(mqeValue -> {
for (MQEValues mqeValues : manyResult.getResults()) {
for (MQEValue mqeValue : mqeValues.getValues()) {
if (!mqeValue.isEmptyValue()) {
double newValue = calculate.apply(
mqeValue.getDoubleValue(), singleResult.getResults()
@ -173,8 +173,8 @@ public interface LROp {
.getDoubleValue(), opType);
mqeValue.setDoubleValue(newValue);
}
});
});
}
}
return manyResult;
}
@ -186,8 +186,8 @@ public interface LROp {
singleResult.getResults().get(0).getValues().size() != 1) {
throw new IllegalExpressionException("One to Many, single result is empty or has more than one value.");
}
manyResult.getResults().forEach(mqeValues -> {
mqeValues.getValues().forEach(mqeValue -> {
for (MQEValues mqeValues : manyResult.getResults()) {
for (MQEValue mqeValue : mqeValues.getValues()) {
if (!mqeValue.isEmptyValue()) {
double newValue = calculate.apply(
singleResult.getResults()
@ -199,8 +199,8 @@ public interface LROp {
);
mqeValue.setDoubleValue(newValue);
}
});
});
}
}
return manyResult;
}

View File

@ -0,0 +1,81 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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
*
* http://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.
*
*/
package org.apache.skywalking.mqe.rt;
import org.apache.skywalking.mqe.rt.exception.IllegalExpressionException;
import org.apache.skywalking.mqe.rt.grammar.MQEParser;
import org.apache.skywalking.mqe.rt.operation.BoolOp;
import org.apache.skywalking.oap.server.core.query.mqe.ExpressionResult;
import org.apache.skywalking.oap.server.core.query.mqe.ExpressionResultType;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
public class BoolOpTest {
private final MockData mockData = new MockData();
@Test
public void boolOpNoLabeledTest() throws IllegalExpressionException {
ExpressionResult left = mockData.newSingleResult(1);
left.setBoolResult(true);
ExpressionResult right = mockData.newSingleResult(0);
right.getResults().get(0).getValues().get(0).setEmptyValue(false);
right.setBoolResult(true);
ExpressionResult andResult = BoolOp.doBoolOp(left, right, MQEParser.AND);
Assertions.assertEquals(ExpressionResultType.SINGLE_VALUE, andResult.getType());
Assertions.assertEquals(0, andResult.getResults().get(0).getValues().get(0).getDoubleValue());
left = mockData.newSingleResult(1);
left.setBoolResult(true);
right = mockData.newSingleResult(0);
right.getResults().get(0).getValues().get(0).setEmptyValue(false);
right.setBoolResult(true);
ExpressionResult orResult = BoolOp.doBoolOp(left, right, MQEParser.OR);
Assertions.assertEquals(1, orResult.getResults().get(0).getValues().get(0).getDoubleValue());
Assertions.assertEquals(ExpressionResultType.SINGLE_VALUE, orResult.getType());
}
@Test
public void boolOpLabeledTest() throws IllegalExpressionException {
ExpressionResult left = mockData.newSingleLabeledResult(1, 0);
left.setBoolResult(true);
left.getResults().get(1).getValues().get(0).setEmptyValue(false);
ExpressionResult right = mockData.newSingleLabeledResult(0, 1);
right.getResults().get(0).getValues().get(0).setEmptyValue(false);
right.setBoolResult(true);
ExpressionResult andResult = BoolOp.doBoolOp(left, right, MQEParser.AND);
Assertions.assertEquals(0, andResult.getResults().get(0).getValues().get(0).getDoubleValue());
Assertions.assertEquals(0, andResult.getResults().get(1).getValues().get(0).getDoubleValue());
Assertions.assertEquals(ExpressionResultType.SINGLE_VALUE, andResult.getType());
left = mockData.newSingleLabeledResult(1, 0);
left.setBoolResult(true);
left.getResults().get(1).getValues().get(0).setEmptyValue(false);
right = mockData.newSingleLabeledResult(0, 1);
right.getResults().get(0).getValues().get(0).setEmptyValue(false);
right.setBoolResult(true);
ExpressionResult orResult = BoolOp.doBoolOp(left, right, MQEParser.OR);
Assertions.assertEquals(1, orResult.getResults().get(0).getValues().get(0).getDoubleValue());
Assertions.assertEquals(1, orResult.getResults().get(1).getValues().get(0).getDoubleValue());
Assertions.assertEquals(ExpressionResultType.SINGLE_VALUE, orResult.getType());
}
}

View File

@ -37,7 +37,7 @@ rules:
hooks:
- webhook.none
comp_rule:
expression: sum((service_resp_time > 10) * (service_sla > 100)) >= 1
expression: sum((service_resp_time > 10) && (service_sla > 100)) >= 1
period: 10
message: Service {name} response time is more than 10ms and sla is more than 1%.
tags:

View File

@ -41,7 +41,7 @@ msgs:
layer: GENERAL
{{- end }}
snapshot:
expression: sum((service_resp_time > 10) * (service_sla > 100)) >= 1
expression: sum((service_resp_time > 10) && (service_sla > 100)) >= 1
metrics:
{{- contains .snapshot.metrics }}
- name: service_resp_time

View File

@ -41,7 +41,7 @@ msgs:
layer: GENERAL
{{- end }}
snapshot:
expression: sum((service_resp_time > 10) * (service_sla > 100)) >= 1
expression: sum((service_resp_time > 10) && (service_sla > 100)) >= 1
metrics:
{{- contains .snapshot.metrics }}
- name: service_resp_time

View File

@ -0,0 +1,26 @@
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You 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
#
# http://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.
type: SINGLE_VALUE
results:
- metric:
labels: []
values:
- id: null
owner: null
value: "1"
traceid: null
error: null
debuggingtrace: null

View File

@ -28,6 +28,14 @@ cases:
- query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression="(service_cpm + 1) > 2" --service-name=e2e-service-provider
expected: expected/compare-OP.yml
# bool-OP
# SINGLE_VALUE
- query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression="avg(service_cpm + 1) > 2 && avg(service_sla/100) > 95" --service-name=e2e-service-provider
expected: expected/bool-OP.yml
# TIME_SERIES_VALUES
- query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression="(service_cpm + 1) > 2 && (service_sla/100) > 95" --service-name=e2e-service-provider
expected: expected/compare-OP.yml
# aggregation-OP
- query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression="avg(service_sla/100)" --service-name=e2e-service-provider
expected: expected/aggregation-OP.yml