From 2c729b5d69b095962e7be5b8570eedc374659ec5 Mon Sep 17 00:00:00 2001 From: xbkaishui Date: Sat, 3 Oct 2020 23:26:29 +0800 Subject: [PATCH] Support group concept in the alarm core (#5615) --- dist-material/release-docs/LICENSE | 1 + docs/en/setup/backend/backend-alarm.md | 23 ++ oap-server/pom.xml | 6 + oap-server/server-alarm-plugin/pom.xml | 4 + .../server/core/alarm/provider/AlarmCore.java | 13 +- .../server/core/alarm/provider/AlarmRule.java | 1 + .../alarm/provider/AlarmRulesWatcher.java | 11 +- .../alarm/provider/CompositeAlarmRule.java | 38 +++ .../provider/CompositeRuleEvaluator.java | 130 ++++++++++ .../oap/server/core/alarm/provider/Rules.java | 2 + .../core/alarm/provider/RulesReader.java | 237 +++++++++++------- .../core/alarm/provider/RunningRule.java | 17 +- .../alarm/provider/expression/Expression.java | 80 ++++++ .../expression/ExpressionContext.java | 58 +++++ .../alarm/provider/AlarmRuleInitTest.java | 5 +- .../alarm/provider/AlarmRulesWatcherTest.java | 2 +- .../provider/CompositeRuleEvaluatorTest.java | 166 ++++++++++++ .../expression/ExpressionContextTest.java | 55 ++++ .../provider/expression/ExpressionTest.java | 88 +++++++ .../src/test/resources/alarm-settings.yml | 23 ++ .../oap/server/core/alarm/AlarmMessage.java | 8 +- .../known-oap-backend-dependencies-es7.txt | 3 +- .../known-oap-backend-dependencies.txt | 1 + 23 files changed, 865 insertions(+), 107 deletions(-) create mode 100644 oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/CompositeAlarmRule.java create mode 100644 oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/CompositeRuleEvaluator.java create mode 100644 oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/expression/Expression.java create mode 100644 oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/expression/ExpressionContext.java create mode 100644 oap-server/server-alarm-plugin/src/test/java/org/apache/skywalking/oap/server/core/alarm/provider/CompositeRuleEvaluatorTest.java create mode 100644 oap-server/server-alarm-plugin/src/test/java/org/apache/skywalking/oap/server/core/alarm/provider/expression/ExpressionContextTest.java create mode 100644 oap-server/server-alarm-plugin/src/test/java/org/apache/skywalking/oap/server/core/alarm/provider/expression/ExpressionTest.java diff --git a/dist-material/release-docs/LICENSE b/dist-material/release-docs/LICENSE index e3212baad..a2628e6d1 100755 --- a/dist-material/release-docs/LICENSE +++ b/dist-material/release-docs/LICENSE @@ -337,6 +337,7 @@ The text of each license is the standard Apache 2.0 license. lz4-java 1.6.0: https://github.com/jpountz/lz4-java, Apache 2.0 snappy-java 1.1.7.3: https://github.com/xerial/snappy-java, Apache 2.0 slf4j-api 1.7.28: http://www.slf4j.org, Apache 2.0 + mvel 2.4.8: https://github.com/mvel/mvel, Apache 2.0 ======================================================================== MIT licenses diff --git a/docs/en/setup/backend/backend-alarm.md b/docs/en/setup/backend/backend-alarm.md index 704b22fbd..345d7cdfa 100644 --- a/docs/en/setup/backend/backend-alarm.md +++ b/docs/en/setup/backend/backend-alarm.md @@ -16,6 +16,8 @@ Define the relation between scope and entity name. - **Endpoint Relation**: {Source endpoint name} in {Source Service name} to {Dest endpoint name} in {Dest service name} ## Rules +**There are two types of rules, individual rule and composite rule, composite rule is the combination of individual rules** +### Individual rules Alarm rule is constituted by following keys - **Rule name**. Unique name, show in alarm message. Must end with `_rule`. - **Metrics name**. A.K.A. metrics name in oal script. Only long, double, int types are supported. See @@ -41,10 +43,20 @@ Such as in **percentile**, `value1` is threshold of P50, and `-, -, value3, valu backend deployment env time. - **Count**. In the period window, if the number of **value**s over threshold(by OP), reaches count, alarm should send. +- **Only as condition**. Specify if the rule can send notification or just as an condition of composite rule. - **Silence period**. After alarm is triggered in Time-N, then keep silence in the **TN -> TN + period**. By default, it is as same as **Period**, which means in a period, same alarm(same ID in same metrics name) will be trigger once. +### Composite rules +**NOTE**. Composite rules only work for alarm rules targeting the same entity level, such as alarm rules of the service level. +For example, `service_percent_rule && service_resp_time_percentile_rule`. You shouldn't compose alarm rules of different entity levels. +such as one alarm rule of the service metrics with another rule of the endpoint metrics. + +Composite rule is constituted by the following keys +- **Rule name**. Unique name, show in alarm message. Must end with `_rule`. +- **Expression**. Specify how to compose rules, support `&&`, `||`, `()`. +- **Message**. Specify the notification message when rule triggered. ```yaml rules: @@ -60,6 +72,8 @@ rules: count: 3 # How many times of checks, the alarm keeps silence after alarm triggered, default as same as period. silence-period: 10 + # Specify if the rule can send notification or just as an condition of composite rule + only-as-condition: false service_percent_rule: metrics-name: service_percent # [Optional] Default, match all services in this metrics @@ -73,6 +87,7 @@ rules: op: < period: 10 count: 4 + only-as-condition: false service_resp_time_percentile_rule: # Metrics value need to be long, double or int metrics-name: service_percentile @@ -83,6 +98,7 @@ rules: count: 3 silence-period: 5 message: Percentile response time of service {name} alarm in 3 minutes of last 10 minutes, due to more than one condition of p50 > 1000, p75 > 1000, p90 > 1000, p95 > 1000, p99 > 1000 + only-as-condition: false meter_service_status_code_rule: metrics-name: meter_status_code exclude-labels: @@ -93,8 +109,15 @@ rules: count: 3 silence-period: 5 message: The request number of entity {name} non-200 status is more than expected. + only-as-condition: false +composite-rules: + comp_rule: + # Must satisfied percent rule and resp time rule + expression: service_percent_rule && service_resp_time_percentile_rule + message: Service {name} successful rate is less than 80% and P50 of response time is over 1000ms ``` + ### Default alarm rules We provided a default `alarm-setting.yml` in our distribution only for convenience, which including following rules 1. Service average response time over 1s in last 3 minutes. diff --git a/oap-server/pom.xml b/oap-server/pom.xml index e07643ef9..70440f1da 100755 --- a/oap-server/pom.xml +++ b/oap-server/pom.xml @@ -94,6 +94,7 @@ 3.25.0-GA 0.10.3 3.0.3 + 2.4.8.Final 3.5 2.4.1 @@ -531,6 +532,11 @@ groovy ${groovy.version} + + org.mvel + mvel2 + ${mvel.version} + diff --git a/oap-server/server-alarm-plugin/pom.xml b/oap-server/server-alarm-plugin/pom.xml index 83ac1cdd1..a914c8a24 100644 --- a/oap-server/server-alarm-plugin/pom.xml +++ b/oap-server/server-alarm-plugin/pom.xml @@ -42,6 +42,10 @@ io.grpc grpc-testing + + org.mvel + mvel2 + diff --git a/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/AlarmCore.java b/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/AlarmCore.java index 03afbf680..675aa1fb4 100644 --- a/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/AlarmCore.java +++ b/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/AlarmCore.java @@ -22,6 +22,8 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + import org.apache.skywalking.oap.server.core.alarm.AlarmCallback; import org.apache.skywalking.oap.server.core.alarm.AlarmMessage; import org.joda.time.LocalDateTime; @@ -52,10 +54,10 @@ public class AlarmCore { lastExecuteTime = now; Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(() -> { try { - List alarmMessageList = new ArrayList<>(30); + final List alarmMessageList = new ArrayList<>(30); LocalDateTime checkTime = LocalDateTime.now(); int minutes = Minutes.minutesBetween(lastExecuteTime, checkTime).getMinutes(); - boolean[] hasExecute = new boolean[] {false}; + boolean[] hasExecute = new boolean[]{false}; alarmRulesWatcher.getRunningContext().values().forEach(ruleList -> ruleList.forEach(runningRule -> { if (minutes > 0) { runningRule.moveTo(checkTime); @@ -74,7 +76,12 @@ public class AlarmCore { } if (alarmMessageList.size() > 0) { - allCallbacks.forEach(callback -> callback.doAlarm(alarmMessageList)); + if (alarmRulesWatcher.getCompositeRules().size() > 0) { + List messages = alarmRulesWatcher.getCompositeRuleEvaluator().evaluate(alarmRulesWatcher.getCompositeRules(), alarmMessageList); + alarmMessageList.addAll(messages); + } + List filteredMessages = alarmMessageList.stream().filter(msg -> !msg.isOnlyAsCondition()).collect(Collectors.toList()); + allCallbacks.forEach(callback -> callback.doAlarm(filteredMessages)); } } catch (Exception e) { LOGGER.error(e.getMessage(), e); diff --git a/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/AlarmRule.java b/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/AlarmRule.java index 9667ba794..31f38e716 100644 --- a/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/AlarmRule.java +++ b/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/AlarmRule.java @@ -51,6 +51,7 @@ public class AlarmRule { private int count; private int silencePeriod; private String message; + private boolean onlyAsCondition; @Override public boolean equals(final Object o) { diff --git a/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/AlarmRulesWatcher.java b/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/AlarmRulesWatcher.java index 880aab1cf..c312b4592 100644 --- a/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/AlarmRulesWatcher.java +++ b/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/AlarmRulesWatcher.java @@ -28,6 +28,8 @@ import lombok.extern.slf4j.Slf4j; import org.apache.skywalking.oap.server.configuration.api.ConfigChangeWatcher; import org.apache.skywalking.oap.server.core.Const; import org.apache.skywalking.oap.server.core.alarm.AlarmModule; +import org.apache.skywalking.oap.server.core.alarm.provider.expression.Expression; +import org.apache.skywalking.oap.server.core.alarm.provider.expression.ExpressionContext; import org.apache.skywalking.oap.server.core.alarm.provider.grpc.GRPCAlarmSetting; import org.apache.skywalking.oap.server.core.alarm.provider.slack.SlackSettings; import org.apache.skywalking.oap.server.core.alarm.provider.wechat.WechatSettings; @@ -46,13 +48,16 @@ public class AlarmRulesWatcher extends ConfigChangeWatcher { private volatile Map alarmRuleRunningRuleMap; private volatile Rules rules; private volatile String settingsString; + @Getter + private final CompositeRuleEvaluator compositeRuleEvaluator; public AlarmRulesWatcher(Rules defaultRules, ModuleProvider provider) { super(AlarmModule.NAME, provider, "alarm-settings"); this.runningContext = new HashMap<>(); this.alarmRuleRunningRuleMap = new HashMap<>(); this.settingsString = Const.EMPTY_STRING; - + Expression expression = new Expression(new ExpressionContext()); + this.compositeRuleEvaluator = new CompositeRuleEvaluator(expression); notify(defaultRules); } @@ -104,6 +109,10 @@ public class AlarmRulesWatcher extends ConfigChangeWatcher { return this.rules.getRules(); } + public List getCompositeRules() { + return this.rules.getCompositeRules(); + } + public List getWebHooks() { return this.rules.getWebhooks(); } diff --git a/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/CompositeAlarmRule.java b/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/CompositeAlarmRule.java new file mode 100644 index 000000000..28aedc17c --- /dev/null +++ b/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/CompositeAlarmRule.java @@ -0,0 +1,38 @@ +/* + * 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.oap.server.core.alarm.provider; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Setter +@Getter +@ToString +public class CompositeAlarmRule { + private String alarmRuleName; + private String expression; + private String message; +} diff --git a/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/CompositeRuleEvaluator.java b/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/CompositeRuleEvaluator.java new file mode 100644 index 000000000..be3cb6a46 --- /dev/null +++ b/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/CompositeRuleEvaluator.java @@ -0,0 +1,130 @@ +/* + * 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.oap.server.core.alarm.provider; + +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableListMultimap; +import com.google.common.collect.Multimaps; +import org.apache.skywalking.oap.server.core.Const; +import org.apache.skywalking.oap.server.core.alarm.AlarmMessage; +import org.apache.skywalking.oap.server.core.alarm.MetaInAlarm; +import org.apache.skywalking.oap.server.core.alarm.provider.expression.Expression; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Evaluate composite rule using expression eval + * + * @since 8.2.0 + */ +public class CompositeRuleEvaluator { + + private Expression expression; + private Map messageFormatterCache; + + public CompositeRuleEvaluator(Expression expression) { + this.expression = expression; + this.messageFormatterCache = new ConcurrentHashMap<>(); + } + + /** + * Evaluate composite rule + * + * @param compositeAlarmRules compositeRules + * @param alarmMessages triggered alarm messages + * @return + */ + public List evaluate(List compositeAlarmRules, List alarmMessages) { + final List compositeRuleMessages = new ArrayList<>(); + ImmutableListMultimap messageMap = Multimaps.index(alarmMessages, alarmMessage -> + Joiner.on(Const.ID_CONNECTOR).useForNull(Const.EMPTY_STRING).join(alarmMessage.getId0(), alarmMessage.getId1())); + for (CompositeAlarmRule compositeAlarmRule : compositeAlarmRules) { + String expr = compositeAlarmRule.getExpression(); + Set dependencyRules = expression.analysisInputs(expr); + Map dataContext = new HashMap<>(); + messageMap.asMap().forEach((key, alarmMessageList) -> { + dependencyRules.forEach(ruleName -> dataContext.put(ruleName, false)); + alarmMessageList.forEach(alarmMessage -> { + if (dependencyRules.contains(alarmMessage.getRuleName())) { + dataContext.put(alarmMessage.getRuleName(), true); + } + }); + Object matched = expression.eval(expr, dataContext); + if (matched instanceof Boolean && (Boolean) matched) { + AlarmMessage headMsg = alarmMessageList.iterator().next(); + AlarmMessage message = new AlarmMessage(); + message.setOnlyAsCondition(false); + message.setScopeId(headMsg.getScopeId()); + message.setScope(headMsg.getScope()); + message.setName(headMsg.getName()); + message.setId0(headMsg.getId0()); + message.setId1(headMsg.getId1()); + message.setStartTime(System.currentTimeMillis()); + message.setRuleName(compositeAlarmRule.getAlarmRuleName()); + String alarmMessage = formatMessage(message, compositeAlarmRule.getMessage(), compositeAlarmRule.getExpression()); + message.setAlarmMessage(alarmMessage); + compositeRuleMessages.add(message); + } + }); + } + return compositeRuleMessages; + } + + /** + * Format alarm message using {@link AlarmMessageFormatter}, only support name and id0 meta + */ + private String formatMessage(AlarmMessage alarmMessage, String message, String metricName) { + return messageFormatterCache.computeIfAbsent(message, AlarmMessageFormatter::new).format(new MetaInAlarm() { + @Override + public String getScope() { + return alarmMessage.getScope(); + } + + @Override + public int getScopeId() { + return alarmMessage.getScopeId(); + } + + @Override + public String getName() { + return alarmMessage.getName(); + } + + @Override + public String getMetricsName() { + return metricName; + } + + @Override + public String getId0() { + return alarmMessage.getId0(); + } + + @Override + public String getId1() { + return alarmMessage.getId1(); + } + }); + } +} diff --git a/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/Rules.java b/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/Rules.java index 0b67ead2a..04ba1b1ba 100644 --- a/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/Rules.java +++ b/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/Rules.java @@ -36,9 +36,11 @@ public class Rules { private GRPCAlarmSetting grpchookSetting; private SlackSettings slacks; private WechatSettings wecchats; + private List compositeRules; public Rules() { this.rules = new ArrayList<>(); this.webhooks = new ArrayList<>(); + this.compositeRules = new ArrayList<>(); } } diff --git a/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/RulesReader.java b/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/RulesReader.java index 1a5d5dc19..9290f2fa7 100644 --- a/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/RulesReader.java +++ b/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/RulesReader.java @@ -24,6 +24,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Objects; + import org.apache.skywalking.oap.server.core.alarm.provider.grpc.GRPCAlarmSetting; import org.apache.skywalking.oap.server.core.alarm.provider.slack.SlackSettings; import org.apache.skywalking.oap.server.core.alarm.provider.wechat.WechatSettings; @@ -45,97 +46,161 @@ public class RulesReader { yamlData = yaml.loadAs(io, Map.class); } + /** + * Read rule config file to {@link Rules} + */ public Rules readRules() { Rules rules = new Rules(); if (Objects.nonNull(yamlData)) { - Map rulesData = (Map) yamlData.get("rules"); - if (rulesData != null) { - rules.setRules(new ArrayList<>()); - rulesData.forEach((k, v) -> { - if (((String) k).endsWith("_rule")) { - AlarmRule alarmRule = new AlarmRule(); - alarmRule.setAlarmRuleName((String) k); - Map settings = (Map) v; - Object metricsName = settings.get("metrics-name"); - if (metricsName == null) { - throw new IllegalArgumentException("metrics-name can't be null"); - } - - alarmRule.setMetricsName((String) metricsName); - alarmRule.setIncludeNames((ArrayList) settings.getOrDefault("include-names", new ArrayList(0))); - alarmRule.setExcludeNames((ArrayList) settings.getOrDefault("exclude-names", new ArrayList(0))); - alarmRule.setIncludeNamesRegex((String) settings.getOrDefault("include-names-regex", "")); - alarmRule.setExcludeNamesRegex((String) settings.getOrDefault("exclude-names-regex", "")); - alarmRule.setIncludeLabels( - (ArrayList) settings.getOrDefault("include-labels", new ArrayList(0))); - alarmRule.setExcludeLabels( - (ArrayList) settings.getOrDefault("exclude-labels", new ArrayList(0))); - alarmRule.setIncludeLabelsRegex((String) settings.getOrDefault("include-labels-regex", "")); - alarmRule.setExcludeLabelsRegex((String) settings.getOrDefault("exclude-labels-regex", "")); - alarmRule.setThreshold(settings.get("threshold").toString()); - alarmRule.setOp((String) settings.get("op")); - alarmRule.setPeriod((Integer) settings.getOrDefault("period", 1)); - alarmRule.setCount((Integer) settings.getOrDefault("count", 1)); - // How many times of checks, the alarm keeps silence after alarm triggered, default as same as period. - alarmRule.setSilencePeriod((Integer) settings.getOrDefault("silence-period", alarmRule.getPeriod())); - alarmRule.setMessage( - (String) settings.getOrDefault("message", "Alarm caused by Rule " + alarmRule - .getAlarmRuleName())); - - rules.getRules().add(alarmRule); - } - }); - } - List webhooks = (List) yamlData.get("webhooks"); - if (webhooks != null) { - rules.setWebhooks(new ArrayList<>()); - webhooks.forEach(url -> { - rules.getWebhooks().add((String) url); - }); - } - - Map grpchooks = (Map) yamlData.get("gRPCHook"); - if (grpchooks != null) { - GRPCAlarmSetting grpcAlarmSetting = new GRPCAlarmSetting(); - Object targetHost = grpchooks.get("target_host"); - if (targetHost != null) { - grpcAlarmSetting.setTargetHost((String) targetHost); - } - - Object targetPort = grpchooks.get("target_port"); - if (targetPort != null) { - grpcAlarmSetting.setTargetPort((Integer) targetPort); - } - - rules.setGrpchookSetting(grpcAlarmSetting); - } - - Map slacks = (Map) yamlData.get("slackHooks"); - if (slacks != null) { - SlackSettings slackSettings = new SlackSettings(); - Object textTemplate = slacks.getOrDefault("textTemplate", ""); - slackSettings.setTextTemplate((String) textTemplate); - - List slackWebhooks = (List) slacks.get("webhooks"); - if (slackWebhooks != null) { - slackSettings.getWebhooks().addAll(slackWebhooks); - } - rules.setSlacks(slackSettings); - } - - Map wechatConfig = (Map) yamlData.get("wechatHooks"); - if (wechatConfig != null) { - WechatSettings wechatSettings = new WechatSettings(); - Object textTemplate = wechatConfig.getOrDefault("textTemplate", ""); - wechatSettings.setTextTemplate((String) textTemplate); - List wechatWebhooks = (List) wechatConfig.get("webhooks"); - if (wechatWebhooks != null) { - wechatSettings.getWebhooks().addAll(wechatWebhooks); - } - rules.setWecchats(wechatSettings); - } + readRulesConfig(rules); + readWebHookConfig(rules); + readGrpcConfig(rules); + readSlackConfig(rules); + readWechatConfig(rules); + readCompositeRuleConfig(rules); } return rules; } + + /** + * Read rule config into {@link AlarmRule} + */ + private void readRulesConfig(Rules rules) { + Map rulesData = (Map) yamlData.get("rules"); + if (rulesData == null) { + return; + } + rules.setRules(new ArrayList<>()); + rulesData.forEach((k, v) -> { + if (((String) k).endsWith("_rule")) { + AlarmRule alarmRule = new AlarmRule(); + alarmRule.setAlarmRuleName((String) k); + Map settings = (Map) v; + Object metricsName = settings.get("metrics-name"); + if (metricsName == null) { + throw new IllegalArgumentException("metrics-name can't be null"); + } + + alarmRule.setMetricsName((String) metricsName); + alarmRule.setIncludeNames((ArrayList) settings.getOrDefault("include-names", new ArrayList(0))); + alarmRule.setExcludeNames((ArrayList) settings.getOrDefault("exclude-names", new ArrayList(0))); + alarmRule.setIncludeNamesRegex((String) settings.getOrDefault("include-names-regex", "")); + alarmRule.setExcludeNamesRegex((String) settings.getOrDefault("exclude-names-regex", "")); + alarmRule.setIncludeLabels( + (ArrayList) settings.getOrDefault("include-labels", new ArrayList(0))); + alarmRule.setExcludeLabels( + (ArrayList) settings.getOrDefault("exclude-labels", new ArrayList(0))); + alarmRule.setIncludeLabelsRegex((String) settings.getOrDefault("include-labels-regex", "")); + alarmRule.setExcludeLabelsRegex((String) settings.getOrDefault("exclude-labels-regex", "")); + alarmRule.setThreshold(settings.get("threshold").toString()); + alarmRule.setOp((String) settings.get("op")); + alarmRule.setPeriod((Integer) settings.getOrDefault("period", 1)); + alarmRule.setCount((Integer) settings.getOrDefault("count", 1)); + // How many times of checks, the alarm keeps silence after alarm triggered, default as same as period. + alarmRule.setSilencePeriod((Integer) settings.getOrDefault("silence-period", alarmRule.getPeriod())); + alarmRule.setOnlyAsCondition((Boolean) settings.getOrDefault("only-as-condition", false)); + alarmRule.setMessage( + (String) settings.getOrDefault("message", "Alarm caused by Rule " + alarmRule + .getAlarmRuleName())); + + rules.getRules().add(alarmRule); + } + }); + } + + /** + * Read web hook config + */ + private void readWebHookConfig(Rules rules) { + List webhooks = (List) yamlData.get("webhooks"); + if (webhooks != null) { + rules.setWebhooks(new ArrayList<>()); + webhooks.forEach(url -> { + rules.getWebhooks().add((String) url); + }); + } + } + + /** + * Read grpc hook config into {@link GRPCAlarmSetting} + */ + private void readGrpcConfig(Rules rules) { + Map grpchooks = (Map) yamlData.get("gRPCHook"); + if (grpchooks != null) { + GRPCAlarmSetting grpcAlarmSetting = new GRPCAlarmSetting(); + Object targetHost = grpchooks.get("target_host"); + if (targetHost != null) { + grpcAlarmSetting.setTargetHost((String) targetHost); + } + + Object targetPort = grpchooks.get("target_port"); + if (targetPort != null) { + grpcAlarmSetting.setTargetPort((Integer) targetPort); + } + + rules.setGrpchookSetting(grpcAlarmSetting); + } + } + + /** + * Read slack hook config into {@link SlackSettings} + */ + private void readSlackConfig(Rules rules) { + Map slacks = (Map) yamlData.get("slackHooks"); + if (slacks != null) { + SlackSettings slackSettings = new SlackSettings(); + Object textTemplate = slacks.getOrDefault("textTemplate", ""); + slackSettings.setTextTemplate((String) textTemplate); + + List slackWebhooks = (List) slacks.get("webhooks"); + if (slackWebhooks != null) { + slackSettings.getWebhooks().addAll(slackWebhooks); + } + rules.setSlacks(slackSettings); + } + } + + /** + * Read wechat hook config into {@link WechatSettings} + */ + private void readWechatConfig(Rules rules) { + Map wechatConfig = (Map) yamlData.get("wechatHooks"); + if (wechatConfig != null) { + WechatSettings wechatSettings = new WechatSettings(); + Object textTemplate = wechatConfig.getOrDefault("textTemplate", ""); + wechatSettings.setTextTemplate((String) textTemplate); + List wechatWebhooks = (List) wechatConfig.get("webhooks"); + if (wechatWebhooks != null) { + wechatSettings.getWebhooks().addAll(wechatWebhooks); + } + rules.setWecchats(wechatSettings); + } + } + + /** + * Read composite rule config into {@link CompositeAlarmRule} + */ + private void readCompositeRuleConfig(Rules rules) { + Map compositeRulesData = (Map) yamlData.get("composite-rules"); + if (compositeRulesData == null) { + return; + } + compositeRulesData.forEach((k, v) -> { + String ruleName = (String) k; + if (ruleName.endsWith("_rule")) { + Map settings = (Map) v; + CompositeAlarmRule compositeAlarmRule = new CompositeAlarmRule(); + compositeAlarmRule.setAlarmRuleName(ruleName); + String expression = (String) settings.get("expression"); + if (expression == null) { + throw new IllegalArgumentException("expression can't be null"); + } + compositeAlarmRule.setExpression(expression); + compositeAlarmRule.setMessage( + (String) settings.getOrDefault("message", "Alarm caused by Rule " + ruleName)); + rules.getCompositeRules().add(compositeAlarmRule); + } + }); + } } diff --git a/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/RunningRule.java b/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/RunningRule.java index 34f5839d2..2a3eca7d9 100644 --- a/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/RunningRule.java +++ b/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/RunningRule.java @@ -24,6 +24,7 @@ import java.util.Comparator; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.ReentrantLock; import java.util.regex.Pattern; @@ -72,6 +73,7 @@ public class RunningRule { private final Pattern includeLabelsRegex; private final Pattern excludeLabelsRegex; private final AlarmMessageFormatter formatter; + private final boolean onlyAsCondition; public RunningRule(AlarmRule alarmRule) { metricsName = alarmRule.getMetricsName(); @@ -101,6 +103,7 @@ public class RunningRule { this.excludeLabelsRegex = StringUtil.isNotEmpty(alarmRule.getExcludeLabelsRegex()) ? Pattern.compile(alarmRule.getExcludeLabelsRegex()) : null; this.formatter = new AlarmMessageFormatter(alarmRule.getMessage()); + this.onlyAsCondition = alarmRule.isOnlyAsCondition(); } /** @@ -221,8 +224,9 @@ public class RunningRule { List alarmMessageList = new ArrayList<>(30); windows.forEach((meta, window) -> { - AlarmMessage alarmMessage = window.checkAlarm(); - if (alarmMessage != AlarmMessage.NONE) { + Optional alarmMessageOptional = window.checkAlarm(); + if (alarmMessageOptional.isPresent()) { + AlarmMessage alarmMessage = alarmMessageOptional.get(); alarmMessage.setScopeId(meta.getScopeId()); alarmMessage.setScope(meta.getScope()); alarmMessage.setName(meta.getName()); @@ -230,6 +234,7 @@ public class RunningRule { alarmMessage.setId1(meta.getId1()); alarmMessage.setRuleName(this.ruleName); alarmMessage.setAlarmMessage(formatter.format(meta)); + alarmMessage.setOnlyAsCondition(this.onlyAsCondition); alarmMessage.setStartTime(System.currentTimeMillis()); alarmMessageList.add(alarmMessage); } @@ -323,7 +328,7 @@ public class RunningRule { } } - public AlarmMessage checkAlarm() { + public Optional checkAlarm() { if (isMatch()) { /* * When @@ -334,9 +339,7 @@ public class RunningRule { counter++; if (counter >= countThreshold && silenceCountdown < 1) { silenceCountdown = silencePeriod; - - // set empty message, but new message - return new AlarmMessage(); + return Optional.of(new AlarmMessage()); } else { silenceCountdown--; } @@ -346,7 +349,7 @@ public class RunningRule { counter--; } } - return AlarmMessage.NONE; + return Optional.empty(); } private boolean isMatch() { diff --git a/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/expression/Expression.java b/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/expression/Expression.java new file mode 100644 index 000000000..956f3e3d7 --- /dev/null +++ b/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/expression/Expression.java @@ -0,0 +1,80 @@ +/* + * 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.oap.server.core.alarm.provider.expression; + +import lombok.extern.slf4j.Slf4j; +import org.mvel2.MVEL; +import org.mvel2.ParserContext; + +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Expression support eval java basic expressions, just like groovy script + * The internal detail is it first compile the expression to a parseTree then execute the parseTree with data + * It caches the compiled expression for sake of performance + */ +@Slf4j +public class Expression { + private final Map expressionCache; + private final ExpressionContext context; + + public Expression(ExpressionContext context) { + this.context = context; + this.expressionCache = new ConcurrentHashMap<>(); + } + + /** + * Eval the given expression using empty data context + */ + public Object eval(String expression) { + return eval(expression, null); + } + + /** + * Eval the given expression with data context + */ + public Object eval(String expression, Map vars) { + try { + Object obj = compile(expression, context); + return MVEL.executeExpression(obj, vars); + } catch (Throwable e) { + log.error("eval expression {} error", expression, e); + return null; + } + } + + /** + * Compile the given expression to a parseTree + */ + public Object compile(String expression, ExpressionContext pctx) { + return expressionCache.computeIfAbsent(expression, s -> MVEL.compileExpression(expression, pctx.getContext())); + } + + /** + * Analysis expression dependencies + */ + public Set analysisInputs(String expression) { + ParserContext pCtx = ParserContext.create(); + MVEL.analysisCompile(expression, pCtx); + Map inputsMap = pCtx.getInputs(); + return inputsMap.keySet(); + } +} diff --git a/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/expression/ExpressionContext.java b/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/expression/ExpressionContext.java new file mode 100644 index 000000000..049853726 --- /dev/null +++ b/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/expression/ExpressionContext.java @@ -0,0 +1,58 @@ +/* + * 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.oap.server.core.alarm.provider.expression; + +import lombok.Getter; +import org.mvel2.ParserContext; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; + +/*** + * Expression context can support custom function in expression, + * for example `md5(a) == '111111'`, the md5 function add register in the context + */ +public class ExpressionContext { + + @Getter + private ParserContext context; + + public ExpressionContext() { + context = new ParserContext(); + } + + /** + * Register a single method in the context + */ + public void registerFunc(String func, Method method) { + context.addImport(func, method); + } + + /** + * Register hole class public static methods in the context + */ + public void registerFunc(Class clz) { + Method[] methods = clz.getDeclaredMethods(); + for (Method method : methods) { + int mod = method.getModifiers(); + if (Modifier.isStatic(mod) && Modifier.isPublic(mod)) { + registerFunc(method.getName(), method); + } + } + } +} diff --git a/oap-server/server-alarm-plugin/src/test/java/org/apache/skywalking/oap/server/core/alarm/provider/AlarmRuleInitTest.java b/oap-server/server-alarm-plugin/src/test/java/org/apache/skywalking/oap/server/core/alarm/provider/AlarmRuleInitTest.java index a86f57443..95414761a 100644 --- a/oap-server/server-alarm-plugin/src/test/java/org/apache/skywalking/oap/server/core/alarm/provider/AlarmRuleInitTest.java +++ b/oap-server/server-alarm-plugin/src/test/java/org/apache/skywalking/oap/server/core/alarm/provider/AlarmRuleInitTest.java @@ -31,7 +31,7 @@ public class AlarmRuleInitTest { Rules rules = reader.readRules(); List ruleList = rules.getRules(); - Assert.assertEquals(2, ruleList.size()); + Assert.assertEquals(3, ruleList.size()); Assert.assertEquals("85", ruleList.get(1).getThreshold()); Assert.assertEquals("endpoint_percent_rule", ruleList.get(0).getAlarmRuleName()); Assert.assertEquals(0, ruleList.get(0).getIncludeNames().size()); @@ -46,5 +46,8 @@ public class AlarmRuleInitTest { Assert.assertEquals(2, rulesWebhooks.size()); Assert.assertEquals("http://127.0.0.1/go-wechat/", rulesWebhooks.get(1)); + List compositeRules = rules.getCompositeRules(); + Assert.assertEquals(1, compositeRules.size()); + Assert.assertEquals("endpoint_percent_more_rule && endpoint_percent_rule", compositeRules.get(0).getExpression()); } } diff --git a/oap-server/server-alarm-plugin/src/test/java/org/apache/skywalking/oap/server/core/alarm/provider/AlarmRulesWatcherTest.java b/oap-server/server-alarm-plugin/src/test/java/org/apache/skywalking/oap/server/core/alarm/provider/AlarmRulesWatcherTest.java index fe78c1609..c2ed80700 100644 --- a/oap-server/server-alarm-plugin/src/test/java/org/apache/skywalking/oap/server/core/alarm/provider/AlarmRulesWatcherTest.java +++ b/oap-server/server-alarm-plugin/src/test/java/org/apache/skywalking/oap/server/core/alarm/provider/AlarmRulesWatcherTest.java @@ -77,7 +77,7 @@ public class AlarmRulesWatcherTest { alarmRulesWatcher.notify(new ConfigChangeWatcher.ConfigChangeEvent(new String(chars, 0, length), ConfigChangeWatcher.EventType.MODIFY)); - assertEquals(2, alarmRulesWatcher.getRules().size()); + assertEquals(3, alarmRulesWatcher.getRules().size()); assertEquals(2, alarmRulesWatcher.getWebHooks().size()); assertNotNull(alarmRulesWatcher.getGrpchookSetting()); assertEquals(9888, alarmRulesWatcher.getGrpchookSetting().getTargetPort()); diff --git a/oap-server/server-alarm-plugin/src/test/java/org/apache/skywalking/oap/server/core/alarm/provider/CompositeRuleEvaluatorTest.java b/oap-server/server-alarm-plugin/src/test/java/org/apache/skywalking/oap/server/core/alarm/provider/CompositeRuleEvaluatorTest.java new file mode 100644 index 000000000..500873160 --- /dev/null +++ b/oap-server/server-alarm-plugin/src/test/java/org/apache/skywalking/oap/server/core/alarm/provider/CompositeRuleEvaluatorTest.java @@ -0,0 +1,166 @@ +/* + * 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.oap.server.core.alarm.provider; + +import org.apache.skywalking.oap.server.core.alarm.AlarmMessage; +import org.apache.skywalking.oap.server.core.alarm.provider.expression.Expression; +import org.apache.skywalking.oap.server.core.alarm.provider.expression.ExpressionContext; +import org.junit.Before; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +public class CompositeRuleEvaluatorTest { + + private CompositeRuleEvaluator ruleEvaluate; + + @Before + public void init() { + Expression expression = new Expression(new ExpressionContext()); + ruleEvaluate = new CompositeRuleEvaluator(expression); + } + + @Test + public void testEvaluateMessageWithAndOp() { + List compositeAlarmRules = new ArrayList<>(); + CompositeAlarmRule compositeAlarmRule = new CompositeAlarmRule("dummy", "a_rule && b_rule", "composite rule {name},{id} triggered!"); + compositeAlarmRules.add(compositeAlarmRule); + List alarmMessages = getAlarmMessages(); + List compositeMsgs = ruleEvaluate.evaluate(compositeAlarmRules, alarmMessages); + assertThat(compositeMsgs.size(), is(1)); + assertThat(compositeMsgs.get(0).getAlarmMessage(), is("composite rule demo service,id0 triggered!")); + assertThat(compositeMsgs.get(0).getRuleName(), is("dummy")); + assertThat(compositeMsgs.get(0).getId0(), is("id0")); + assertThat(compositeMsgs.get(0).getId1(), is("id1")); + assertThat(compositeMsgs.get(0).isOnlyAsCondition(), is(false)); + } + + @Test + public void testEvaluateMessageWithFormatMessage() { + List compositeAlarmRules = new ArrayList<>(); + CompositeAlarmRule compositeAlarmRule = new CompositeAlarmRule("dummy", "a_rule && b_rule", "composite rule {name} triggered!"); + compositeAlarmRules.add(compositeAlarmRule); + List alarmMessages = getAlarmMessages(); + List compositeMsgs = ruleEvaluate.evaluate(compositeAlarmRules, alarmMessages); + assertThat(compositeMsgs.size(), is(1)); + assertThat(compositeMsgs.get(0).getAlarmMessage(), is("composite rule demo service triggered!")); + assertThat(compositeMsgs.get(0).getRuleName(), is("dummy")); + assertThat(compositeMsgs.get(0).getId0(), is("id0")); + assertThat(compositeMsgs.get(0).getId1(), is("id1")); + assertThat(compositeMsgs.get(0).isOnlyAsCondition(), is(false)); + } + + @Test + public void testEvaluateMessageWithNotExistsRule() { + List compositeAlarmRules = new ArrayList<>(); + CompositeAlarmRule compositeAlarmRule = new CompositeAlarmRule("dummy", "a_rule && not_exist_rule", "composite rule triggered!"); + compositeAlarmRules.add(compositeAlarmRule); + List alarmMessages = getAlarmMessages(); + List compositeMsgs = ruleEvaluate.evaluate(compositeAlarmRules, alarmMessages); + assertThat(compositeMsgs.size(), is(0)); + } + + @Test + public void testEvaluateMessageWithException() { + List compositeAlarmRules = new ArrayList<>(); + CompositeAlarmRule compositeAlarmRule = new CompositeAlarmRule("dummy", "a_rule + b_rule", "composite rule triggered!"); + compositeAlarmRules.add(compositeAlarmRule); + List alarmMessages = getAlarmMessages(); + List compositeMsgs = ruleEvaluate.evaluate(compositeAlarmRules, alarmMessages); + assertThat(compositeMsgs.size(), is(0)); + } + + private List getAlarmMessages() { + List alarmMessages = new ArrayList<>(); + AlarmMessage alarmMessage = new AlarmMessage(); + alarmMessage.setRuleName("a_rule"); + alarmMessage.setOnlyAsCondition(true); + alarmMessage.setId0("id0"); + alarmMessage.setId1("id1"); + alarmMessage.setName("demo service"); + alarmMessage.setScope(""); + alarmMessage.setScopeId(1); + alarmMessages.add(alarmMessage); + alarmMessage = new AlarmMessage(); + alarmMessage.setRuleName("b_rule"); + alarmMessage.setOnlyAsCondition(true); + alarmMessage.setId0("id0"); + alarmMessage.setId1("id1"); + alarmMessage.setName("demo service"); + alarmMessage.setScope(""); + alarmMessage.setScopeId(1); + alarmMessages.add(alarmMessage); + alarmMessage = new AlarmMessage(); + alarmMessage.setRuleName("c_rule"); + alarmMessage.setOnlyAsCondition(true); + alarmMessage.setId0("id0"); + alarmMessage.setId1("id1"); + alarmMessage.setName("demo service"); + alarmMessage.setScope(""); + alarmMessage.setScopeId(1); + alarmMessages.add(alarmMessage); + return alarmMessages; + } + + @Test + public void testEvaluateMessageWithOrOp() { + List compositeAlarmRules = new ArrayList<>(); + CompositeAlarmRule compositeAlarmRule = new CompositeAlarmRule("dummy", "a_rule || b_rule", "composite rule triggered!"); + compositeAlarmRules.add(compositeAlarmRule); + List alarmMessages = getAlarmMessages(); + alarmMessages.remove(0); + List compositeMsgs = ruleEvaluate.evaluate(compositeAlarmRules, alarmMessages); + assertThat(compositeMsgs.size(), is(1)); + assertThat(compositeMsgs.get(0).getAlarmMessage(), is("composite rule triggered!")); + assertThat(compositeMsgs.get(0).getRuleName(), is("dummy")); + assertThat(compositeMsgs.get(0).getId0(), is("id0")); + assertThat(compositeMsgs.get(0).getId1(), is("id1")); + assertThat(compositeMsgs.get(0).isOnlyAsCondition(), is(false)); + } + + @Test + public void testEvaluateMessageWithParenthesisAndOp() { + List compositeAlarmRules = new ArrayList<>(); + CompositeAlarmRule compositeAlarmRule = new CompositeAlarmRule("dummy", "(a_rule || b_rule) && c_rule", "composite rule triggered!"); + compositeAlarmRules.add(compositeAlarmRule); + List alarmMessages = getAlarmMessages(); + alarmMessages.remove(alarmMessages.size() - 1); + List compositeMsgs = ruleEvaluate.evaluate(compositeAlarmRules, alarmMessages); + assertThat(compositeMsgs.size(), is(0)); + } + + @Test + public void testEvaluateMessageWithParenthesisAndOrOp() { + List compositeAlarmRules = new ArrayList<>(); + CompositeAlarmRule compositeAlarmRule = new CompositeAlarmRule("dummy", "(a_rule && b_rule) || c_rule", "composite rule triggered!"); + compositeAlarmRules.add(compositeAlarmRule); + List alarmMessages = getAlarmMessages(); + List compositeMsgs = ruleEvaluate.evaluate(compositeAlarmRules, alarmMessages); + assertThat(compositeMsgs.size(), is(1)); + assertThat(compositeMsgs.get(0).getAlarmMessage(), is("composite rule triggered!")); + assertThat(compositeMsgs.get(0).getRuleName(), is("dummy")); + assertThat(compositeMsgs.get(0).getId0(), is("id0")); + assertThat(compositeMsgs.get(0).getId1(), is("id1")); + assertThat(compositeMsgs.get(0).isOnlyAsCondition(), is(false)); + } +} diff --git a/oap-server/server-alarm-plugin/src/test/java/org/apache/skywalking/oap/server/core/alarm/provider/expression/ExpressionContextTest.java b/oap-server/server-alarm-plugin/src/test/java/org/apache/skywalking/oap/server/core/alarm/provider/expression/ExpressionContextTest.java new file mode 100644 index 000000000..86deb8bdf --- /dev/null +++ b/oap-server/server-alarm-plugin/src/test/java/org/apache/skywalking/oap/server/core/alarm/provider/expression/ExpressionContextTest.java @@ -0,0 +1,55 @@ +/* + * 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.oap.server.core.alarm.provider.expression; + +import org.junit.Test; + +import java.lang.reflect.Method; +import java.util.Arrays; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +public class ExpressionContextTest { + + @Test + public void testRegisterFuncWithMethod() throws NoSuchMethodException { + ExpressionContext expressionContext = new ExpressionContext(); + Method[] methods = Math.class.getMethods(); + Arrays.stream(methods).forEach(method -> { + if (method.getName().equalsIgnoreCase("sqrt")) { + expressionContext.registerFunc("sqrt", method); + } + }); + Expression expression = new Expression(expressionContext); + Number number = (Number) expression.eval("sqrt(16)"); + assertThat(number, is(4.0)); + } + + @Test + public void testRegisterFuncWithClazz() throws NoSuchMethodException { + ExpressionContext expressionContext = new ExpressionContext(); + expressionContext.registerFunc(Math.class); + Expression expression = new Expression(expressionContext); + Number number = (Number) expression.eval("sqrt(16)"); + assertThat(number, is(4.0)); + number = (Number) expression.eval("abs(-12)"); + assertThat(number, is(12)); + } +} diff --git a/oap-server/server-alarm-plugin/src/test/java/org/apache/skywalking/oap/server/core/alarm/provider/expression/ExpressionTest.java b/oap-server/server-alarm-plugin/src/test/java/org/apache/skywalking/oap/server/core/alarm/provider/expression/ExpressionTest.java new file mode 100644 index 000000000..604dc2cbd --- /dev/null +++ b/oap-server/server-alarm-plugin/src/test/java/org/apache/skywalking/oap/server/core/alarm/provider/expression/ExpressionTest.java @@ -0,0 +1,88 @@ +/* + * 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.oap.server.core.alarm.provider.expression; + +import com.google.common.collect.Sets; +import org.junit.Before; +import org.junit.Test; +import org.mvel2.CompileException; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; + +public class ExpressionTest { + private Expression expression; + + @Before + public void init() { + expression = new Expression(new ExpressionContext()); + } + + @Test + public void testEval() { + String expr = " a && b "; + Map dataMap = new HashMap(); + dataMap.put("a", Boolean.TRUE); + Object flag = expression.eval(expr, dataMap); + assertNull(flag); + dataMap.put("b", Boolean.TRUE); + flag = expression.eval(expr, dataMap); + assertThat(flag, is(true)); + } + + @Test + public void testAnalysisInputs() { + String expr = " a && b "; + Set inputs = expression.analysisInputs(expr); + assertThat(inputs.size(), is(2)); + assertThat(inputs, is(Sets.newHashSet("a", "b"))); + } + + @Test + public void testEvalWithEmptyContext() { + String expr = " a && b "; + Object flag = expression.eval(expr); + assertNull(flag); + flag = expression.eval(" 1 > 0"); + assertThat(flag, is(true)); + } + + @Test + public void testCompile() { + String expr = " a && b "; + ExpressionContext context = new ExpressionContext(); + Object compiledExpression = expression.compile(expr, context); + assertNotNull(compiledExpression); + Object sameExpression = expression.compile(expr, context); + assertThat(compiledExpression, is(sameExpression)); + } + + @Test(expected = CompileException.class) + public void testCompileWithException() { + String expr = " a && * b "; + ExpressionContext context = new ExpressionContext(); + expression.compile(expr, context); + } +} diff --git a/oap-server/server-alarm-plugin/src/test/resources/alarm-settings.yml b/oap-server/server-alarm-plugin/src/test/resources/alarm-settings.yml index 144e94d32..4679bcc19 100755 --- a/oap-server/server-alarm-plugin/src/test/resources/alarm-settings.yml +++ b/oap-server/server-alarm-plugin/src/test/resources/alarm-settings.yml @@ -26,7 +26,9 @@ rules: count: 3 # How many times of checks, the alarm keeps silence after alarm triggered, default as same as period. silence-period: 10 + only-as-condition: false message: Successful rate of endpoint {name} is lower than 75% + service_percent_rule: metrics-name: service_percent # [Optional] Default, match all services in this metrics @@ -39,6 +41,27 @@ rules: op: < period: 10 count: 4 + only-as-condition: false + + endpoint_percent_more_rule: + # Metrics value need to be long, double or int + metrics-name: endpoint_percent + threshold: 60 + op: ">" + # The length of time to evaluate the metrics + period: 10 + # How many times after the metrics match the condition, will trigger alarm + count: 3 + # How many times of checks, the alarm keeps silence after alarm triggered, default as same as period. + silence-period: 10 + # Control if this rule is just as a composite rule condition. do not notification alone + only-as-condition: false + message: Successful rate of endpoint {name} is higher than 60% + +composite-rules: + comp1_rule: + expression: endpoint_percent_more_rule && endpoint_percent_rule + message: xxxxx webhooks: - http://127.0.0.1/notify/ diff --git a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/alarm/AlarmMessage.java b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/alarm/AlarmMessage.java index 401e02334..02fd26333 100644 --- a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/alarm/AlarmMessage.java +++ b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/alarm/AlarmMessage.java @@ -27,9 +27,6 @@ import lombok.Setter; @Setter @Getter public class AlarmMessage { - - public static AlarmMessage NONE = new NoAlarm(); - private int scopeId; private String scope; private String name; @@ -38,8 +35,5 @@ public class AlarmMessage { private String ruleName; private String alarmMessage; private long startTime; - - private static class NoAlarm extends AlarmMessage { - - } + private transient boolean onlyAsCondition; } diff --git a/tools/dependencies/known-oap-backend-dependencies-es7.txt b/tools/dependencies/known-oap-backend-dependencies-es7.txt index 98640ae30..1a7b918d9 100755 --- a/tools/dependencies/known-oap-backend-dependencies-es7.txt +++ b/tools/dependencies/known-oap-backend-dependencies-es7.txt @@ -167,4 +167,5 @@ zookeeper-3.4.10.jar kafka-clients-2.4.1.jar lz4-java-1.6.0.jar snappy-java-1.1.7.3.jar -zstd-jni-1.4.3-1.jar \ No newline at end of file +zstd-jni-1.4.3-1.jar +mvel2-2.4.8.Final.jar diff --git a/tools/dependencies/known-oap-backend-dependencies.txt b/tools/dependencies/known-oap-backend-dependencies.txt index 4dd8f35d1..6b0470f9b 100755 --- a/tools/dependencies/known-oap-backend-dependencies.txt +++ b/tools/dependencies/known-oap-backend-dependencies.txt @@ -167,3 +167,4 @@ kafka-clients-2.4.1.jar lz4-java-1.6.0.jar snappy-java-1.1.7.3.jar zstd-jni-1.4.3-1.jar +mvel2-2.4.8.Final.jar