From 117afff36fc2bbfafeeb9c02f584ef45539c12b5 Mon Sep 17 00:00:00 2001 From: xbkaishui Date: Mon, 5 Oct 2020 20:16:18 +0800 Subject: [PATCH] Support Alarm to dingtalk (#5628) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add dingtalk webhook Co-authored-by: 吴晟 Wu Sheng --- docs/en/setup/backend/backend-alarm.md | 18 ++ .../alarm/provider/AlarmRulesWatcher.java | 6 + .../core/alarm/provider/NotifyHandler.java | 2 + .../oap/server/core/alarm/provider/Rules.java | 2 + .../core/alarm/provider/RulesReader.java | 34 +++- .../dingtalk/DingtalkHookCallback.java | 154 +++++++++++++++ .../provider/dingtalk/DingtalkSettings.java | 51 +++++ .../alarm/provider/AlarmRulesWatcherTest.java | 3 + ...RuleInitTest.java => RulesReaderTest.java} | 47 ++++- .../dingtalk/DingtalkHookCallbackTest.java | 184 ++++++++++++++++++ .../src/test/resources/alarm-settings.yml | 36 ++++ .../src/main/resources/alarm-settings.yml | 12 ++ 12 files changed, 539 insertions(+), 10 deletions(-) create mode 100644 oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/dingtalk/DingtalkHookCallback.java create mode 100644 oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/dingtalk/DingtalkSettings.java rename oap-server/server-alarm-plugin/src/test/java/org/apache/skywalking/oap/server/core/alarm/provider/{AlarmRuleInitTest.java => RulesReaderTest.java} (50%) create mode 100644 oap-server/server-alarm-plugin/src/test/java/org/apache/skywalking/oap/server/core/alarm/provider/dingtalk/DingtalkHookCallbackTest.java diff --git a/docs/en/setup/backend/backend-alarm.md b/docs/en/setup/backend/backend-alarm.md index 345d7cdfa..e3a730d3c 100644 --- a/docs/en/setup/backend/backend-alarm.md +++ b/docs/en/setup/backend/backend-alarm.md @@ -219,6 +219,24 @@ wechatHooks: - https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=dummy_key ``` +## Dingtalk Hook +To do this you need to follow the [Dingtalk Webhooks guide](https://ding-doc.dingtalk.com/doc#/serverapi2/qf2nxq/uKPlK) and create new Webhooks. +For security issue, you can config optional secret for individual webhook url. +The alarm message will send through HTTP post by `application/json` content type if you configured Dingtalk Webhooks as following: +```yml +dingtalkHooks: + textTemplate: |- + { + "msgtype": "text", + "text": { + "content": "Apache SkyWalking Alarm: \n %s." + } + } + webhooks: + - url: https://oapi.dingtalk.com/robot/send?access_token=dummy_token + secret: dummysecret +``` + ## Update the settings dynamically Since 6.5.0, the alarm settings can be updated dynamically at runtime by [Dynamic Configuration](dynamic-config.md), 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 c312b4592..af6364a33 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,7 @@ 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.dingtalk.DingtalkSettings; 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; @@ -128,4 +129,9 @@ public class AlarmRulesWatcher extends ConfigChangeWatcher { public WechatSettings getWechatSettings() { return this.rules.getWecchats(); } + + public DingtalkSettings getDingtalkSettings() { + return this.rules.getDingtalks(); + } + } diff --git a/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/NotifyHandler.java b/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/NotifyHandler.java index 2f95b5cdf..755bc08a7 100644 --- a/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/NotifyHandler.java +++ b/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/NotifyHandler.java @@ -31,6 +31,7 @@ import org.apache.skywalking.oap.server.core.alarm.MetaInAlarm; import org.apache.skywalking.oap.server.core.alarm.MetricsNotify; import org.apache.skywalking.oap.server.core.alarm.ServiceInstanceMetaInAlarm; import org.apache.skywalking.oap.server.core.alarm.ServiceMetaInAlarm; +import org.apache.skywalking.oap.server.core.alarm.provider.dingtalk.DingtalkHookCallback; import org.apache.skywalking.oap.server.core.alarm.provider.grpc.GRPCCallback; import org.apache.skywalking.oap.server.core.alarm.provider.slack.SlackhookCallback; import org.apache.skywalking.oap.server.core.alarm.provider.wechat.WechatHookCallback; @@ -162,6 +163,7 @@ public class NotifyHandler implements MetricsNotify { allCallbacks.add(new GRPCCallback(alarmRulesWatcher)); allCallbacks.add(new SlackhookCallback(alarmRulesWatcher)); allCallbacks.add(new WechatHookCallback(alarmRulesWatcher)); + allCallbacks.add(new DingtalkHookCallback(alarmRulesWatcher)); core.start(allCallbacks); } } 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 04ba1b1ba..7831bd650 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 @@ -23,6 +23,7 @@ import java.util.List; import lombok.Getter; import lombok.Setter; import lombok.ToString; +import org.apache.skywalking.oap.server.core.alarm.provider.dingtalk.DingtalkSettings; 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; @@ -37,6 +38,7 @@ public class Rules { private SlackSettings slacks; private WechatSettings wecchats; private List compositeRules; + private DingtalkSettings dingtalks; public Rules() { this.rules = 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 9290f2fa7..350254e61 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 @@ -18,6 +18,12 @@ package org.apache.skywalking.oap.server.core.alarm.provider; +import org.apache.skywalking.oap.server.core.alarm.provider.dingtalk.DingtalkSettings; +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; +import org.yaml.snakeyaml.Yaml; + import java.io.InputStream; import java.io.Reader; import java.util.ArrayList; @@ -25,11 +31,6 @@ 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; -import org.yaml.snakeyaml.Yaml; - /** * Rule Reader parses the given `alarm-settings.yml` config file, to the target {@link Rules}. */ @@ -59,10 +60,12 @@ public class RulesReader { readSlackConfig(rules); readWechatConfig(rules); readCompositeRuleConfig(rules); + readDingtalkConfig(rules); } return rules; } + /** * Read rule config into {@link AlarmRule} */ @@ -203,4 +206,25 @@ public class RulesReader { } }); } + + /** + * Read dingtalk hook config into {@link DingtalkSettings} + */ + private void readDingtalkConfig(Rules rules) { + Map dingtalkConfig = (Map) yamlData.get("dingtalkHooks"); + if (dingtalkConfig != null) { + DingtalkSettings dingtalkSettings = new DingtalkSettings(); + Object textTemplate = dingtalkConfig.getOrDefault("textTemplate", ""); + dingtalkSettings.setTextTemplate((String) textTemplate); + List> wechatWebhooks = (List>) dingtalkConfig.get("webhooks"); + if (wechatWebhooks != null) { + wechatWebhooks.forEach(wechatWebhook -> { + Object secret = wechatWebhook.getOrDefault("secret", ""); + Object url = wechatWebhook.getOrDefault("url", ""); + dingtalkSettings.getWebhooks().add(new DingtalkSettings.WebHookUrl((String) secret, (String) url)); + }); + } + rules.setDingtalks(dingtalkSettings); + } + } } diff --git a/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/dingtalk/DingtalkHookCallback.java b/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/dingtalk/DingtalkHookCallback.java new file mode 100644 index 000000000..5acc63a80 --- /dev/null +++ b/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/dingtalk/DingtalkHookCallback.java @@ -0,0 +1,154 @@ +/* + * 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.dingtalk; + +import io.netty.handler.codec.http.HttpHeaderValues; +import lombok.extern.slf4j.Slf4j; +import org.apache.http.HttpHeaders; +import org.apache.http.HttpStatus; +import org.apache.http.StatusLine; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.util.EntityUtils; +import org.apache.skywalking.apm.util.StringUtil; +import org.apache.skywalking.oap.server.core.alarm.AlarmCallback; +import org.apache.skywalking.oap.server.core.alarm.AlarmMessage; +import org.apache.skywalking.oap.server.core.alarm.provider.AlarmRulesWatcher; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; +import java.util.List; + +/** + * Use SkyWalking alarm dingtalk webhook API. + */ +@Slf4j +public class DingtalkHookCallback implements AlarmCallback { + + private static final int HTTP_CONNECT_TIMEOUT = 1000; + private static final int HTTP_CONNECTION_REQUEST_TIMEOUT = 1000; + private static final int HTTP_SOCKET_TIMEOUT = 10000; + private AlarmRulesWatcher alarmRulesWatcher; + private RequestConfig requestConfig; + + public DingtalkHookCallback(final AlarmRulesWatcher alarmRulesWatcher) { + this.alarmRulesWatcher = alarmRulesWatcher; + this.requestConfig = RequestConfig.custom() + .setConnectTimeout(HTTP_CONNECT_TIMEOUT) + .setConnectionRequestTimeout(HTTP_CONNECTION_REQUEST_TIMEOUT) + .setSocketTimeout(HTTP_SOCKET_TIMEOUT) + .build(); + } + + /** + * Send alarm message if the settings not empty + */ + @Override + public void doAlarm(List alarmMessages) { + if (this.alarmRulesWatcher.getDingtalkSettings() == null || this.alarmRulesWatcher.getDingtalkSettings().getWebhooks().isEmpty()) { + return; + } + CloseableHttpClient httpClient = HttpClients.custom().build(); + try { + DingtalkSettings dingtalkSettings = this.alarmRulesWatcher.getDingtalkSettings(); + dingtalkSettings.getWebhooks().forEach(webHookUrl -> { + alarmMessages.forEach(alarmMessage -> { + String url = getUrl(webHookUrl); + String requestBody = String.format( + this.alarmRulesWatcher.getDingtalkSettings().getTextTemplate(), alarmMessage.getAlarmMessage() + ); + sendAlarmMessage(httpClient, url, requestBody); + }); + }); + } finally { + try { + httpClient.close(); + } catch (IOException e) { + log.error(e.getMessage(), e); + } + } + } + + /** + * Get webhook url, sign the url when secret is not empty. + */ + private String getUrl(DingtalkSettings.WebHookUrl webHookUrl) { + if (StringUtil.isEmpty(webHookUrl.getSecret())) { + return webHookUrl.getUrl(); + } + return getSignUrl(webHookUrl); + } + + /** + * Sign webhook url using secret and timestamp + */ + private String getSignUrl(DingtalkSettings.WebHookUrl webHookUrl) { + try { + Long timestamp = System.currentTimeMillis(); + return String.format("%s×tamp=%s&sign=%s", webHookUrl.getUrl(), timestamp, sign(timestamp, webHookUrl.getSecret())); + } catch (NoSuchAlgorithmException | UnsupportedEncodingException | InvalidKeyException e) { + throw new RuntimeException(e); + } + } + + /** + * Sign webhook url using HmacSHA256 algorithm + */ + private String sign(final Long timestamp, String secret) throws NoSuchAlgorithmException, UnsupportedEncodingException, InvalidKeyException { + String stringToSign = timestamp + "\n" + secret; + Mac mac = Mac.getInstance("HmacSHA256"); + mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256")); + byte[] signData = mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8)); + return URLEncoder.encode(new String(Base64.getEncoder().encode(signData)), StandardCharsets.UTF_8.name()); + } + + /** + * Send alarm message to remote endpoint + */ + private void sendAlarmMessage(CloseableHttpClient httpClient, String url, String requestBody) { + try { + HttpPost post = new HttpPost(url); + post.setConfig(requestConfig); + post.setHeader(HttpHeaders.ACCEPT, HttpHeaderValues.APPLICATION_JSON.toString()); + post.setHeader(HttpHeaders.CONTENT_TYPE, HttpHeaderValues.APPLICATION_JSON.toString()); + StringEntity entity = new StringEntity(requestBody, ContentType.APPLICATION_JSON); + post.setEntity(entity); + CloseableHttpResponse httpResponse = httpClient.execute(post); + StatusLine statusLine = httpResponse.getStatusLine(); + if (statusLine != null && statusLine.getStatusCode() != HttpStatus.SC_OK) { + log.error("send dingtalk alarm to {} failure. Response code: {}, Response content: {}", url, statusLine.getStatusCode(), + EntityUtils.toString(httpResponse.getEntity())); + } + } catch (Throwable e) { + log.error("send dingtalk alarm to {} failure.", url, e); + } + } +} diff --git a/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/dingtalk/DingtalkSettings.java b/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/dingtalk/DingtalkSettings.java new file mode 100644 index 000000000..ba1d7501f --- /dev/null +++ b/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/dingtalk/DingtalkSettings.java @@ -0,0 +1,51 @@ +/* + * 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.dingtalk; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +import java.util.ArrayList; +import java.util.List; + +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Setter +@Getter +@ToString +public class DingtalkSettings { + + private String textTemplate; + @Builder.Default + private List webhooks = new ArrayList<>(); + + @AllArgsConstructor + @Setter + @Getter + @ToString + public static class WebHookUrl { + private final String secret; + private final String url; + } +} 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 c2ed80700..e9277a6d8 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 @@ -82,6 +82,9 @@ public class AlarmRulesWatcherTest { assertNotNull(alarmRulesWatcher.getGrpchookSetting()); assertEquals(9888, alarmRulesWatcher.getGrpchookSetting().getTargetPort()); assertEquals(2, alarmRulesWatcher.getRunningContext().size()); + assertNotNull(alarmRulesWatcher.getDingtalkSettings()); + assertNotNull(alarmRulesWatcher.getWechatSettings()); + assertNotNull(alarmRulesWatcher.getSlackSettings()); } @Test 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/RulesReaderTest.java similarity index 50% rename from oap-server/server-alarm-plugin/src/test/java/org/apache/skywalking/oap/server/core/alarm/provider/AlarmRuleInitTest.java rename to oap-server/server-alarm-plugin/src/test/java/org/apache/skywalking/oap/server/core/alarm/provider/RulesReaderTest.java index 95414761a..9a81990f8 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/RulesReaderTest.java @@ -18,16 +18,27 @@ package org.apache.skywalking.oap.server.core.alarm.provider; -import java.util.List; +import org.apache.skywalking.oap.server.core.alarm.provider.dingtalk.DingtalkSettings; +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; import org.junit.Assert; import org.junit.Test; -public class AlarmRuleInitTest { +import java.util.List; + +import static org.hamcrest.CoreMatchers.any; +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 RulesReaderTest { @Test - public void testInit() { + public void testReadRules() { RulesReader reader = new RulesReader(this.getClass() - .getClassLoader() - .getResourceAsStream("alarm-settings.yml")); + .getClassLoader() + .getResourceAsStream("alarm-settings.yml")); Rules rules = reader.readRules(); List ruleList = rules.getRules(); @@ -46,8 +57,34 @@ public class AlarmRuleInitTest { Assert.assertEquals(2, rulesWebhooks.size()); Assert.assertEquals("http://127.0.0.1/go-wechat/", rulesWebhooks.get(1)); + GRPCAlarmSetting grpcAlarmSetting = rules.getGrpchookSetting(); + assertNotNull(grpcAlarmSetting); + assertThat(grpcAlarmSetting.getTargetHost(), is("127.0.0.1")); + assertThat(grpcAlarmSetting.getTargetPort(), is(9888)); + + SlackSettings slackSettings = rules.getSlacks(); + assertNotNull(slackSettings); + assertThat(slackSettings.getWebhooks().size(), is(1)); + assertThat(slackSettings.getWebhooks().get(0), is("https://hooks.slack.com/services/x/y/zssss")); + assertThat(slackSettings.getTextTemplate(), any(String.class)); + + WechatSettings wechatSettings = rules.getWecchats(); + assertNotNull(wechatSettings); + assertThat(wechatSettings.getWebhooks().size(), is(1)); + assertThat(wechatSettings.getWebhooks().get(0), is("https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=dummy_key")); + assertThat(slackSettings.getTextTemplate(), any(String.class)); + List compositeRules = rules.getCompositeRules(); Assert.assertEquals(1, compositeRules.size()); Assert.assertEquals("endpoint_percent_more_rule && endpoint_percent_rule", compositeRules.get(0).getExpression()); + + DingtalkSettings dingtalkSettings = rules.getDingtalks(); + assertThat(dingtalkSettings.getTextTemplate(), any(String.class)); + List webHookUrls = dingtalkSettings.getWebhooks(); + assertThat(webHookUrls.size(), is(2)); + assertThat(webHookUrls.get(0).getUrl(), is("https://oapi.dingtalk.com/robot/send?access_token=dummy_token")); + assertThat(webHookUrls.get(0).getSecret(), is("dummysecret")); + assertThat(webHookUrls.get(1).getUrl(), is("https://oapi.dingtalk.com/robot/send?access_token=dummy_token2")); + assertNull(webHookUrls.get(1).getSecret()); } } diff --git a/oap-server/server-alarm-plugin/src/test/java/org/apache/skywalking/oap/server/core/alarm/provider/dingtalk/DingtalkHookCallbackTest.java b/oap-server/server-alarm-plugin/src/test/java/org/apache/skywalking/oap/server/core/alarm/provider/dingtalk/DingtalkHookCallbackTest.java new file mode 100644 index 000000000..e092ed904 --- /dev/null +++ b/oap-server/server-alarm-plugin/src/test/java/org/apache/skywalking/oap/server/core/alarm/provider/dingtalk/DingtalkHookCallbackTest.java @@ -0,0 +1,184 @@ +/* + * 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.dingtalk; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import org.apache.skywalking.apm.util.StringUtil; +import org.apache.skywalking.oap.server.core.alarm.AlarmMessage; +import org.apache.skywalking.oap.server.core.alarm.provider.AlarmRulesWatcher; +import org.apache.skywalking.oap.server.core.alarm.provider.Rules; +import org.apache.skywalking.oap.server.core.source.DefaultScopeDefine; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import javax.servlet.Servlet; +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertTrue; + +public class DingtalkHookCallbackTest implements Servlet { + + private Server server; + private int port; + private volatile boolean isSuccess = false; + private int count; + private volatile boolean checkSign = false; + private final String secret = "dummy-secret"; + + @Before + public void init() throws Exception { + server = new Server(new InetSocketAddress("127.0.0.1", 0)); + ServletContextHandler servletContextHandler = new ServletContextHandler(ServletContextHandler.NO_SESSIONS); + servletContextHandler.setContextPath("/dingtalkhook"); + server.setHandler(servletContextHandler); + ServletHolder servletHolder = new ServletHolder(); + servletHolder.setServlet(this); + servletContextHandler.addServlet(servletHolder, "/receiveAlarm"); + server.start(); + port = server.getURI().getPort(); + assertTrue(port > 0); + } + + @Test + public void testDingtalkWebhookWithoutSign() { + List webHooks = new ArrayList<>(); + webHooks.add(new DingtalkSettings.WebHookUrl("", "http://127.0.0.1:" + port + "/dingtalkhook/receiveAlarm?token=dummy_token")); + Rules rules = new Rules(); + String template = "{\"msgtype\":\"text\",\"text\":{\"content\":\"Skywaling alarm: %s\"}}"; + rules.setDingtalks(DingtalkSettings.builder().webhooks(webHooks).textTemplate(template).build()); + + AlarmRulesWatcher alarmRulesWatcher = new AlarmRulesWatcher(rules, null); + DingtalkHookCallback dingtalkCallBack = new DingtalkHookCallback(alarmRulesWatcher); + List alarmMessages = new ArrayList<>(2); + AlarmMessage alarmMessage = new AlarmMessage(); + alarmMessage.setScopeId(DefaultScopeDefine.ALL); + alarmMessage.setRuleName("service_resp_time_rule"); + alarmMessage.setAlarmMessage("alarmMessage with [DefaultScopeDefine.All]"); + alarmMessages.add(alarmMessage); + AlarmMessage anotherAlarmMessage = new AlarmMessage(); + anotherAlarmMessage.setRuleName("service_resp_time_rule_2"); + anotherAlarmMessage.setScopeId(DefaultScopeDefine.ENDPOINT); + anotherAlarmMessage.setAlarmMessage("anotherAlarmMessage with [DefaultScopeDefine.Endpoint]"); + alarmMessages.add(anotherAlarmMessage); + dingtalkCallBack.doAlarm(alarmMessages); + Assert.assertTrue(isSuccess); + } + + @Test + public void testDingtalkWebhookWithSign() { + checkSign = true; + List webHooks = new ArrayList<>(); + webHooks.add(new DingtalkSettings.WebHookUrl(secret, "http://127.0.0.1:" + port + "/dingtalkhook/receiveAlarm?token=dummy_token")); + Rules rules = new Rules(); + String template = "{\"msgtype\":\"text\",\"text\":{\"content\":\"Skywaling alarm: %s\"}}"; + rules.setDingtalks(DingtalkSettings.builder().webhooks(webHooks).textTemplate(template).build()); + + AlarmRulesWatcher alarmRulesWatcher = new AlarmRulesWatcher(rules, null); + DingtalkHookCallback dingtalkCallBack = new DingtalkHookCallback(alarmRulesWatcher); + List alarmMessages = new ArrayList<>(2); + AlarmMessage alarmMessage = new AlarmMessage(); + alarmMessage.setScopeId(DefaultScopeDefine.ALL); + alarmMessage.setRuleName("service_resp_time_rule"); + alarmMessage.setAlarmMessage("alarmMessage with [DefaultScopeDefine.All]"); + alarmMessages.add(alarmMessage); + AlarmMessage anotherAlarmMessage = new AlarmMessage(); + anotherAlarmMessage.setRuleName("service_resp_time_rule_2"); + anotherAlarmMessage.setScopeId(DefaultScopeDefine.ENDPOINT); + anotherAlarmMessage.setAlarmMessage("anotherAlarmMessage with [DefaultScopeDefine.Endpoint]"); + alarmMessages.add(anotherAlarmMessage); + dingtalkCallBack.doAlarm(alarmMessages); + Assert.assertTrue(isSuccess); + } + + @After + public void stop() throws Exception { + server.stop(); + } + + @Override + public void init(ServletConfig servletConfig) throws ServletException { + } + + @Override + public ServletConfig getServletConfig() { + return null; + } + + @Override + public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException { + HttpServletRequest httpServletRequest = (HttpServletRequest) request; + if (httpServletRequest.getContentType().equals("application/json")) { + InputStream inputStream = request.getInputStream(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] buffer = new byte[2048]; + int readCntOnce; + + while ((readCntOnce = inputStream.read(buffer)) >= 0) { + out.write(buffer, 0, readCntOnce); + } + + JsonObject jsonObject = new Gson().fromJson(new String(out.toByteArray()), JsonObject.class); + String type = jsonObject.get("msgtype").getAsString(); + if (checkSign) { + String timestamp = request.getParameter("timestamp"); + String sign = request.getParameter("sign"); + if (StringUtil.isEmpty(timestamp) || StringUtil.isEmpty(sign)) { + ((HttpServletResponse) response).setStatus(500); + return; + } + } + if (type.equalsIgnoreCase("text")) { + ((HttpServletResponse) response).setStatus(200); + count = count + 1; + if (count == 2) { + isSuccess = true; + } + return; + } + + ((HttpServletResponse) response).setStatus(500); + } + } + + @Override + public String getServletInfo() { + return null; + } + + @Override + public void destroy() { + } +} 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 4679bcc19..9dd339f59 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 @@ -71,3 +71,39 @@ gRPCHook: target_host: 127.0.0.1 target_port: 9888 +slackHooks: + textTemplate: |- + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ":alarm_clock: *Apache Skywalking Alarm* \n **%s**." + } + } + webhooks: + - https://hooks.slack.com/services/x/y/zssss + +wechatHooks: + textTemplate: |- + { + "msgtype": "text", + "text": { + "content": "Apache SkyWalking Alarm: \n %s." + } + } + webhooks: + - https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=dummy_key + +dingtalkHooks: + textTemplate: |- + { + "msgtype": "text", + "text": { + "content": "Apache SkyWalking Alarm: \n %s." + } + } + webhooks: + - url: https://oapi.dingtalk.com/robot/send?access_token=dummy_token + secret: dummysecret + - url: https://oapi.dingtalk.com/robot/send?access_token=dummy_token2 + secret: diff --git a/oap-server/server-bootstrap/src/main/resources/alarm-settings.yml b/oap-server/server-bootstrap/src/main/resources/alarm-settings.yml index 00b7ae955..f78427df2 100755 --- a/oap-server/server-bootstrap/src/main/resources/alarm-settings.yml +++ b/oap-server/server-bootstrap/src/main/resources/alarm-settings.yml @@ -69,3 +69,15 @@ wechatHooks: } webhooks: # - https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=dummy_key + +dingtalkHooks: + textTemplate: |- + { + "msgtype": "text", + "text": { + "content": "Apache SkyWalking Alarm: \n %s." + } + } + webhooks: +# - url: https://oapi.dingtalk.com/robot/send?access_token=dummy_token +# secret: dummysecret