MQE: Support `&&(and)`, `||(or)` bool operators. (#12965)
This commit is contained in:
parent
1dde137f8e
commit
8bcaabb43c
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -46,6 +46,10 @@ LT: '<';
|
|||
GTE: '>=';
|
||||
GT: '>';
|
||||
|
||||
// Bool operators
|
||||
AND: '&&';
|
||||
OR: '||';
|
||||
|
||||
// Aggregation operators
|
||||
AVG: 'avg';
|
||||
COUNT: 'count';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue