This is an automated email from the ASF dual-hosted git repository.

wusheng pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/skywalking.git


The following commit(s) were added to refs/heads/master by this push:
     new 4e43f22  Support Alarm to feishu (#6193)
4e43f22 is described below

commit 4e43f227b4a579b7bd48996974b5872d30c191f1
Author: HendSame <[email protected]>
AuthorDate: Mon Jan 18 09:24:20 2021 +0800

    Support Alarm to feishu (#6193)
---
 CHANGES.md                                         |   1 +
 docs/en/setup/backend/backend-alarm.md             |  19 ++
 .../core/alarm/provider/AlarmRulesWatcher.java     |   5 +
 .../server/core/alarm/provider/NotifyHandler.java  |   2 +
 .../oap/server/core/alarm/provider/Rules.java      |   2 +
 .../server/core/alarm/provider/RulesReader.java    |  33 +++-
 .../alarm/provider/feishu/FeishuHookCallback.java  | 173 +++++++++++++++++
 .../{Rules.java => feishu/FeishuSettings.java}     |  40 ++--
 .../core/alarm/provider/RulesReaderTest.java       |  10 +
 .../provider/feishu/FeishuHookCallbackTest.java    | 211 +++++++++++++++++++++
 .../src/test/resources/alarm-settings.yml          |  14 ++
 .../src/main/resources/alarm-settings.yml          |  14 ++
 12 files changed, 500 insertions(+), 24 deletions(-)

diff --git a/CHANGES.md b/CHANGES.md
index 51d722c..694be60 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 e3a730d..978abc4 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 af6364a..6884905 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 755bc08..3b295dd 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 7831bd6..1e93acd 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<CompositeAlarmRule> 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 196e7f7..fea2e8c 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<Map<String, Object>> wechatWebhooks = (List<Map<String, 
Object>>) 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 0000000..bff3a55
--- /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<AlarmMessage> 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<String, Object> 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<String, Object> buildContent(JsonObject jsonObject) {
+        Map<String, Object> 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<String> collect = Arrays.stream(ats.split(","))
+                    .map(String::trim).collect(Collectors.toList());
+            for (String userId : collect) {
+                text += "<at user_id=\"" + userId + "\"></at>";
+            }
+            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/Rules.java
 
b/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/feishu/FeishuSettings.java
similarity index 53%
copy from 
oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/Rules.java
copy to 
oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/feishu/FeishuSettings.java
index 7831bd6..fa66b95 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/feishu/FeishuSettings.java
@@ -16,33 +16,35 @@
  *
  */
 
-package org.apache.skywalking.oap.server.core.alarm.provider;
+package org.apache.skywalking.oap.server.core.alarm.provider.feishu;
 
-import java.util.ArrayList;
-import java.util.List;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
 import lombok.Getter;
+import lombok.NoArgsConstructor;
 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;
+import java.util.ArrayList;
+import java.util.List;
 
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
 @Setter
 @Getter
 @ToString
-public class Rules {
-    private List<AlarmRule> rules;
-    private List<String> webhooks;
-    private GRPCAlarmSetting grpchookSetting;
-    private SlackSettings slacks;
-    private WechatSettings wecchats;
-    private List<CompositeAlarmRule> compositeRules;
-    private DingtalkSettings dingtalks;
+public class FeishuSettings {
+
+    private String textTemplate;
+    @Builder.Default
+    private List<WebHookUrl> webhooks = new ArrayList<>();
 
-    public Rules() {
-        this.rules = new ArrayList<>();
-        this.webhooks = new ArrayList<>();
-        this.compositeRules = 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 9a81990..a53850b 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<FeishuSettings.WebHookUrl> 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 0000000..41b336b
--- /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<FeishuSettings.WebHookUrl> 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<AlarmMessage> 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<FeishuSettings.WebHookUrl> 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<AlarmMessage> 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<FeishuSettings.WebHookUrl> 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<AlarmMessage> 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 9dd339f..0b31ddc 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 f78427d..b7fa979 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

Reply via email to