From 4e43f227b4a579b7bd48996974b5872d30c191f1 Mon Sep 17 00:00:00 2001 From: HendSame Date: Mon, 18 Jan 2021 09:24:20 +0800 Subject: [PATCH] Support Alarm to feishu (#6193) --- CHANGES.md | 1 + docs/en/setup/backend/backend-alarm.md | 19 ++ .../alarm/provider/AlarmRulesWatcher.java | 5 + .../core/alarm/provider/NotifyHandler.java | 2 + .../oap/server/core/alarm/provider/Rules.java | 2 + .../core/alarm/provider/RulesReader.java | 33 ++- .../provider/feishu/FeishuHookCallback.java | 173 ++++++++++++++ .../alarm/provider/feishu/FeishuSettings.java | 50 +++++ .../core/alarm/provider/RulesReaderTest.java | 10 + .../feishu/FeishuHookCallbackTest.java | 211 ++++++++++++++++++ .../src/test/resources/alarm-settings.yml | 14 ++ .../src/main/resources/alarm-settings.yml | 14 ++ 12 files changed, 529 insertions(+), 5 deletions(-) create mode 100644 oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/feishu/FeishuHookCallback.java create mode 100644 oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/feishu/FeishuSettings.java create mode 100644 oap-server/server-alarm-plugin/src/test/java/org/apache/skywalking/oap/server/core/alarm/provider/feishu/FeishuHookCallbackTest.java diff --git a/CHANGES.md b/CHANGES.md index 51d722c84c..694be60133 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -67,6 +67,7 @@ Release Notes. * Fix bug endpoint name grouping not work due to setting service name and endpoint name out of order. * Fix receiver analysis error count metrics * Log collecting and query implementation +* Support Alarm to feishu #### UI * Fix un-removed tags in trace query. diff --git a/docs/en/setup/backend/backend-alarm.md b/docs/en/setup/backend/backend-alarm.md index e3a730d3ce..978abc4d49 100644 --- a/docs/en/setup/backend/backend-alarm.md +++ b/docs/en/setup/backend/backend-alarm.md @@ -237,6 +237,25 @@ dingtalkHooks: secret: dummysecret ``` +## Feishu Hook +To do this you need to follow the [Feishu Webhooks guide](https://www.feishu.cn/hc/zh-cn/articles/360024984973) and create new Webhooks. +For security issue, you can config optional secret for individual webhook url. +if you want to at someone, you can config `ats` which is the feishu's user_id and separated by "," . +The alarm message will send through HTTP post by `application/json` content type if you configured Feishu Webhooks as following: +```yml +feishuHooks: + textTemplate: |- + { + "msg_type": "text", + "content": { + "text": "Apache SkyWalking Alarm: \n %s." + }, + "ats":"feishu_user_id_1,feishu_user_id_2" + } + webhooks: + - url: https://open.feishu.cn/open-apis/bot/v2/hook/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 af6364a338..6884905fa6 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 @@ -31,6 +31,7 @@ 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.feishu.FeishuSettings; 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; @@ -134,4 +135,8 @@ public class AlarmRulesWatcher extends ConfigChangeWatcher { return this.rules.getDingtalks(); } + public FeishuSettings getFeishuSettings() { + return this.rules.getFeishus(); + } + } 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 755bc08a7f..3b295dd8e8 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 @@ -32,6 +32,7 @@ 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.feishu.FeishuHookCallback; 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; @@ -164,6 +165,7 @@ public class NotifyHandler implements MetricsNotify { allCallbacks.add(new SlackhookCallback(alarmRulesWatcher)); allCallbacks.add(new WechatHookCallback(alarmRulesWatcher)); allCallbacks.add(new DingtalkHookCallback(alarmRulesWatcher)); + allCallbacks.add(new FeishuHookCallback(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 7831bd6502..1e93acd826 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 @@ -24,6 +24,7 @@ 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.feishu.FeishuSettings; 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; @@ -39,6 +40,7 @@ public class Rules { private WechatSettings wecchats; private List compositeRules; private DingtalkSettings dingtalks; + private FeishuSettings feishus; 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 196e7f7229..fea2e8c8a4 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 @@ -25,6 +25,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; import org.apache.skywalking.oap.server.core.alarm.provider.dingtalk.DingtalkSettings; +import org.apache.skywalking.oap.server.core.alarm.provider.feishu.FeishuSettings; 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; @@ -61,6 +62,7 @@ public class RulesReader { readWechatConfig(rules); readCompositeRuleConfig(rules); readDingtalkConfig(rules); + readFeishuConfig(rules); } return rules; } @@ -90,9 +92,9 @@ public class RulesReader { 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))); + (ArrayList) settings.getOrDefault("include-labels", new ArrayList(0))); alarmRule.setExcludeLabels( - (ArrayList) settings.getOrDefault("exclude-labels", new ArrayList(0))); + (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()); @@ -103,8 +105,8 @@ public class RulesReader { 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())); + (String) settings.getOrDefault("message", "Alarm caused by Rule " + alarmRule + .getAlarmRuleName())); rules.getRules().add(alarmRule); } @@ -200,7 +202,7 @@ public class RulesReader { } compositeAlarmRule.setExpression(expression); compositeAlarmRule.setMessage( - (String) settings.getOrDefault("message", "Alarm caused by Rule " + ruleName)); + (String) settings.getOrDefault("message", "Alarm caused by Rule " + ruleName)); rules.getCompositeRules().add(compositeAlarmRule); } }); @@ -226,4 +228,25 @@ public class RulesReader { rules.setDingtalks(dingtalkSettings); } } + + /** + * Read feishu hook config into {@link FeishuSettings} + */ + private void readFeishuConfig(Rules rules) { + Map feishuConfig = (Map) yamlData.get("feishuHooks"); + if (feishuConfig != null) { + FeishuSettings feishuSettings = new FeishuSettings(); + Object textTemplate = feishuConfig.getOrDefault("textTemplate", ""); + feishuSettings.setTextTemplate((String) textTemplate); + List> wechatWebhooks = (List>) feishuConfig.get("webhooks"); + if (wechatWebhooks != null) { + wechatWebhooks.forEach(wechatWebhook -> { + Object secret = wechatWebhook.getOrDefault("secret", ""); + Object url = wechatWebhook.getOrDefault("url", ""); + feishuSettings.getWebhooks().add(new FeishuSettings.WebHookUrl((String) secret, (String) url)); + }); + } + rules.setFeishus(feishuSettings); + } + } } diff --git a/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/feishu/FeishuHookCallback.java b/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/feishu/FeishuHookCallback.java new file mode 100644 index 0000000000..bff3a558fa --- /dev/null +++ b/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/feishu/FeishuHookCallback.java @@ -0,0 +1,173 @@ +/* + * 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.feishu; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import io.netty.handler.codec.http.HttpHeaderValues; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.codec.binary.Base64; +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.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * Use SkyWalking alarm feishu webhook API. + */ +@Slf4j +public class FeishuHookCallback 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 FeishuHookCallback(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.getFeishuSettings() == null || this.alarmRulesWatcher.getFeishuSettings().getWebhooks().isEmpty()) { + return; + } + CloseableHttpClient httpClient = HttpClients.custom().build(); + try { + FeishuSettings feishuSettings = this.alarmRulesWatcher.getFeishuSettings(); + feishuSettings.getWebhooks().forEach(webHookUrl -> { + alarmMessages.forEach(alarmMessage -> { + String requestBody = getRequestBody(webHookUrl, alarmMessage); + sendAlarmMessage(httpClient, webHookUrl.getUrl(), requestBody); + }); + }); + } finally { + try { + httpClient.close(); + } catch (IOException e) { + log.error(e.getMessage(), e); + } + } + } + + /** + * deal requestBody,if has sign set the sign + */ + private String getRequestBody(FeishuSettings.WebHookUrl webHookUrl, AlarmMessage alarmMessage) { + String requestBody = String.format( + this.alarmRulesWatcher.getFeishuSettings().getTextTemplate(), alarmMessage.getAlarmMessage() + ); + Gson gson = new Gson(); + JsonObject jsonObject = gson.fromJson(requestBody, JsonObject.class); + Map content = buildContent(jsonObject); + if (!StringUtil.isBlank(webHookUrl.getSecret())) { + Long timestamp = System.currentTimeMillis() / 1000; + content.put("timestamp", timestamp); + try { + content.put("sign", sign(timestamp, webHookUrl.getSecret())); + } catch (NoSuchAlgorithmException | InvalidKeyException e) { + throw new RuntimeException(e); + } + } + return gson.toJson(content); + } + + /** + * build content,if has ats someone set the ats + */ + private Map buildContent(JsonObject jsonObject) { + Map content = new HashMap<>(); + content.put("msg_type", jsonObject.get("msg_type").getAsString()); + if (jsonObject.get("ats") != null) { + String ats = jsonObject.get("ats").getAsString(); + String text = jsonObject.get("content").getAsJsonObject().get("text").getAsString(); + List collect = Arrays.stream(ats.split(",")) + .map(String::trim).collect(Collectors.toList()); + for (String userId : collect) { + text += ""; + } + jsonObject.get("content").getAsJsonObject().addProperty("text", text); + } + content.put("content", jsonObject.get("content").getAsJsonObject()); + return content; + } + + /** + * Sign webhook url using HmacSHA256 algorithm + */ + private String sign(final Long timestamp, String secret) throws NoSuchAlgorithmException, InvalidKeyException { + String stringToSign = timestamp + "\n" + secret; + Mac mac = Mac.getInstance("HmacSHA256"); + mac.init(new SecretKeySpec(stringToSign.getBytes(), "HmacSHA256")); + byte[] signData = mac.doFinal(); + return Base64.encodeBase64String(signData); + } + + /** + * 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 feishu alarm to {} failure. Response code: {}, Response content: {}", url, statusLine.getStatusCode(), + EntityUtils.toString(httpResponse.getEntity())); + } + } catch (Throwable e) { + log.error("send feishu alarm to {} failure.", url, e); + } + } +} diff --git a/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/feishu/FeishuSettings.java b/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/feishu/FeishuSettings.java new file mode 100644 index 0000000000..fa66b9560e --- /dev/null +++ b/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/feishu/FeishuSettings.java @@ -0,0 +1,50 @@ +/* + * 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.feishu; + +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 FeishuSettings { + + 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/RulesReaderTest.java b/oap-server/server-alarm-plugin/src/test/java/org/apache/skywalking/oap/server/core/alarm/provider/RulesReaderTest.java index 9a81990f80..a53850b775 100644 --- a/oap-server/server-alarm-plugin/src/test/java/org/apache/skywalking/oap/server/core/alarm/provider/RulesReaderTest.java +++ b/oap-server/server-alarm-plugin/src/test/java/org/apache/skywalking/oap/server/core/alarm/provider/RulesReaderTest.java @@ -19,6 +19,7 @@ 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.feishu.FeishuSettings; 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; @@ -86,5 +87,14 @@ public class RulesReaderTest { 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()); + + FeishuSettings feishuSettings = rules.getFeishus(); + assertThat(feishuSettings.getTextTemplate(), any(String.class)); + List feishuSettingsWebhooks = feishuSettings.getWebhooks(); + assertThat(feishuSettingsWebhooks.size(), is(2)); + assertThat(feishuSettingsWebhooks.get(0).getUrl(), is("https://open.feishu.cn/open-apis/bot/v2/hook/dummy_token")); + assertThat(feishuSettingsWebhooks.get(0).getSecret(), is("dummysecret")); + assertThat(feishuSettingsWebhooks.get(1).getUrl(), is("https://open.feishu.cn/open-apis/bot/v2/hook/dummy_token2")); + assertNull(feishuSettingsWebhooks.get(1).getSecret()); } } diff --git a/oap-server/server-alarm-plugin/src/test/java/org/apache/skywalking/oap/server/core/alarm/provider/feishu/FeishuHookCallbackTest.java b/oap-server/server-alarm-plugin/src/test/java/org/apache/skywalking/oap/server/core/alarm/provider/feishu/FeishuHookCallbackTest.java new file mode 100644 index 0000000000..41b336b1d3 --- /dev/null +++ b/oap-server/server-alarm-plugin/src/test/java/org/apache/skywalking/oap/server/core/alarm/provider/feishu/FeishuHookCallbackTest.java @@ -0,0 +1,211 @@ +/* + * 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.feishu; + +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 FeishuHookCallbackTest 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("/feishuhook"); + 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 testFeishuWebhookWithoutSign() { + List webHooks = new ArrayList<>(); + webHooks.add(new FeishuSettings.WebHookUrl("", "http://127.0.0.1:" + port + "/feishuhook/receiveAlarm?token=dummy_token")); + Rules rules = new Rules(); + String template = "{\"msg_type\":\"text\",\"content\":{\"text\":\"Skywaling alarm: %s\"}}"; + rules.setFeishus(FeishuSettings.builder().webhooks(webHooks).textTemplate(template).build()); + + AlarmRulesWatcher alarmRulesWatcher = new AlarmRulesWatcher(rules, null); + FeishuHookCallback feishuHookCallback = new FeishuHookCallback(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); + feishuHookCallback.doAlarm(alarmMessages); + Assert.assertTrue(isSuccess); + } + + @Test + public void testFeishuWebhookWithSign() { + checkSign = true; + List webHooks = new ArrayList<>(); + webHooks.add(new FeishuSettings.WebHookUrl(secret, "http://127.0.0.1:" + port + "/feishuhook/receiveAlarm?token=dummy_token")); + Rules rules = new Rules(); + String template = "{\"msg_type\":\"text\",\"content\":{\"text\":\"Skywaling alarm: %s\"}}"; + rules.setFeishus(FeishuSettings.builder().webhooks(webHooks).textTemplate(template).build()); + + AlarmRulesWatcher alarmRulesWatcher = new AlarmRulesWatcher(rules, null); + FeishuHookCallback feishuHookCallback = new FeishuHookCallback(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); + feishuHookCallback.doAlarm(alarmMessages); + Assert.assertTrue(isSuccess); + } + + @Test + public void testFeishuWebhookWithSignAndAt() { + checkSign = true; + List webHooks = new ArrayList<>(); + webHooks.add(new FeishuSettings.WebHookUrl(secret, "http://127.0.0.1:" + port + "/feishuhook/receiveAlarm?token=dummy_token")); + Rules rules = new Rules(); + String template = "{\"msg_type\":\"text\",\"content\":{\"text\":\"Skywaling alarm: %s\"},\"ats\":\"123\"}"; + rules.setFeishus(FeishuSettings.builder().webhooks(webHooks).textTemplate(template).build()); + + AlarmRulesWatcher alarmRulesWatcher = new AlarmRulesWatcher(rules, null); + FeishuHookCallback feishuHookCallback = new FeishuHookCallback(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); + feishuHookCallback.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("msg_type").getAsString(); + if (checkSign) { + String timestamp = jsonObject.get("timestamp").getAsString(); + String sign = jsonObject.get("sign").getAsString(); + 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 9dd339f59b..0b31ddc2a7 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 @@ -107,3 +107,17 @@ dingtalkHooks: secret: dummysecret - url: https://oapi.dingtalk.com/robot/send?access_token=dummy_token2 secret: + +feishuHooks: + textTemplate: |- + { + "msg_type": "text", + "content": { + "text": "Apache SkyWalking Alarm: \n %s." + } + } + webhooks: + - url: https://open.feishu.cn/open-apis/bot/v2/hook/dummy_token + secret: dummysecret + - url: https://open.feishu.cn/open-apis/bot/v2/hook/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 f78427df2f..b7fa97952c 100755 --- a/oap-server/server-bootstrap/src/main/resources/alarm-settings.yml +++ b/oap-server/server-bootstrap/src/main/resources/alarm-settings.yml @@ -81,3 +81,17 @@ dingtalkHooks: webhooks: # - url: https://oapi.dingtalk.com/robot/send?access_token=dummy_token # secret: dummysecret + +feishuHooks: + textTemplate: |- + { + "msg_type": "text", + # at someone with feishu_user_ids + # "ats": "feishu_user_id_1,feishu_user_id_2", + "content": { + "text": "Apache SkyWalking Alarm: \n %s." + } + } + webhooks: +# - url: https://open.feishu.cn/open-apis/bot/v2/hook/dummy_token +# secret: dummysecret -- GitLab