From 8bcaabb43c4e10d111d79dcf8937db2004d73331 Mon Sep 17 00:00:00 2001 From: Wan Kai Date: Tue, 14 Jan 2025 16:22:29 +0800 Subject: [PATCH] MQE: Support `&&(and)`, `||(or)` bool operators. (#12965) --- docs/en/api/metrics-query-expression.md | 22 +++++ docs/en/changes/changes.md | 1 + docs/en/setup/backend/backend-alarm.md | 5 +- .../skywalking/mqe/rt/grammar/MQELexer.g4 | 4 + .../skywalking/mqe/rt/grammar/MQEParser.g4 | 4 + .../skywalking/mqe/rt/MQEVisitorBase.java | 46 ++++++++++- .../skywalking/mqe/rt/operation/BoolOp.java | 81 +++++++++++++++++++ .../skywalking/mqe/rt/operation/LROp.java | 18 ++--- .../apache/skywalking/mqe/rt/BoolOpTest.java | 81 +++++++++++++++++++ test/e2e-v2/cases/alarm/alarm-settings.yml | 2 +- .../silence-after-graphql-critical.yml | 2 +- .../silence-before-graphql-critical.yml | 2 +- test/e2e-v2/cases/mqe/expected/bool-OP.yml | 26 ++++++ test/e2e-v2/cases/mqe/mqe-cases.yaml | 8 ++ 14 files changed, 286 insertions(+), 16 deletions(-) create mode 100644 oap-server/mqe-rt/src/main/java/org/apache/skywalking/mqe/rt/operation/BoolOp.java create mode 100644 oap-server/mqe-rt/src/test/java/org/apache/skywalking/mqe/rt/BoolOpTest.java create mode 100644 test/e2e-v2/cases/mqe/expected/bool-OP.yml diff --git a/docs/en/api/metrics-query-expression.md b/docs/en/api/metrics-query-expression.md index 1446368fa7..ba7a92974c 100644 --- a/docs/en/api/metrics-query-expression.md +++ b/docs/en/api/metrics-query-expression.md @@ -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 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. diff --git a/docs/en/changes/changes.md b/docs/en/changes/changes.md index 29806bdfca..266d3d7d43 100644 --- a/docs/en/changes/changes.md +++ b/docs/en/changes/changes.md @@ -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 diff --git a/docs/en/setup/backend/backend-alarm.md b/docs/en/setup/backend/backend-alarm.md index 256763be95..a292a33d97 100644 --- a/docs/en/setup/backend/backend-alarm.md +++ b/docs/en/setup/backend/backend-alarm.md @@ -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: diff --git a/oap-server/mqe-grammar/src/main/antlr4/org/apache/skywalking/mqe/rt/grammar/MQELexer.g4 b/oap-server/mqe-grammar/src/main/antlr4/org/apache/skywalking/mqe/rt/grammar/MQELexer.g4 index dbea0ce0bf..fd4dbdeb8e 100644 --- a/oap-server/mqe-grammar/src/main/antlr4/org/apache/skywalking/mqe/rt/grammar/MQELexer.g4 +++ b/oap-server/mqe-grammar/src/main/antlr4/org/apache/skywalking/mqe/rt/grammar/MQELexer.g4 @@ -46,6 +46,10 @@ LT: '<'; GTE: '>='; GT: '>'; +// Bool operators +AND: '&&'; +OR: '||'; + // Aggregation operators AVG: 'avg'; COUNT: 'count'; diff --git a/oap-server/mqe-grammar/src/main/antlr4/org/apache/skywalking/mqe/rt/grammar/MQEParser.g4 b/oap-server/mqe-grammar/src/main/antlr4/org/apache/skywalking/mqe/rt/grammar/MQEParser.g4 index d1a23a2bd5..e120357031 100644 --- a/oap-server/mqe-grammar/src/main/antlr4/org/apache/skywalking/mqe/rt/grammar/MQEParser.g4 +++ b/oap-server/mqe-grammar/src/main/antlr4/org/apache/skywalking/mqe/rt/grammar/MQEParser.g4 @@ -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; diff --git a/oap-server/mqe-rt/src/main/java/org/apache/skywalking/mqe/rt/MQEVisitorBase.java b/oap-server/mqe-rt/src/main/java/org/apache/skywalking/mqe/rt/MQEVisitorBase.java index d15cb11943..ae86ac21e3 100644 --- a/oap-server/mqe-rt/src/main/java/org/apache/skywalking/mqe/rt/MQEVisitorBase.java +++ b/oap-server/mqe-rt/src/main/java/org/apache/skywalking/mqe/rt/MQEVisitorBase.java @@ -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 { - 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; } diff --git a/oap-server/mqe-rt/src/test/java/org/apache/skywalking/mqe/rt/BoolOpTest.java b/oap-server/mqe-rt/src/test/java/org/apache/skywalking/mqe/rt/BoolOpTest.java new file mode 100644 index 0000000000..ba1e1b74d4 --- /dev/null +++ b/oap-server/mqe-rt/src/test/java/org/apache/skywalking/mqe/rt/BoolOpTest.java @@ -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()); + } +} diff --git a/test/e2e-v2/cases/alarm/alarm-settings.yml b/test/e2e-v2/cases/alarm/alarm-settings.yml index aec113e994..04ddbc61cd 100755 --- a/test/e2e-v2/cases/alarm/alarm-settings.yml +++ b/test/e2e-v2/cases/alarm/alarm-settings.yml @@ -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: diff --git a/test/e2e-v2/cases/alarm/expected/silence-after-graphql-critical.yml b/test/e2e-v2/cases/alarm/expected/silence-after-graphql-critical.yml index 304c98e281..0631c162b8 100644 --- a/test/e2e-v2/cases/alarm/expected/silence-after-graphql-critical.yml +++ b/test/e2e-v2/cases/alarm/expected/silence-after-graphql-critical.yml @@ -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 diff --git a/test/e2e-v2/cases/alarm/expected/silence-before-graphql-critical.yml b/test/e2e-v2/cases/alarm/expected/silence-before-graphql-critical.yml index 304c98e281..0631c162b8 100644 --- a/test/e2e-v2/cases/alarm/expected/silence-before-graphql-critical.yml +++ b/test/e2e-v2/cases/alarm/expected/silence-before-graphql-critical.yml @@ -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 diff --git a/test/e2e-v2/cases/mqe/expected/bool-OP.yml b/test/e2e-v2/cases/mqe/expected/bool-OP.yml new file mode 100644 index 0000000000..ef34a7341c --- /dev/null +++ b/test/e2e-v2/cases/mqe/expected/bool-OP.yml @@ -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 diff --git a/test/e2e-v2/cases/mqe/mqe-cases.yaml b/test/e2e-v2/cases/mqe/mqe-cases.yaml index 53a231ad50..4777a8c400 100644 --- a/test/e2e-v2/cases/mqe/mqe-cases.yaml +++ b/test/e2e-v2/cases/mqe/mqe-cases.yaml @@ -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