You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@skywalking.apache.org by wu...@apache.org on 2022/08/14 06:58:50 UTC

[skywalking] branch master updated: feat: support sending alarm to discord (#9469)

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 d1f898419b feat: support sending alarm to discord (#9469)
d1f898419b is described below

commit d1f898419b59619baaab45c68cd84c707e130734
Author: Brandon Fergerson <bf...@apache.org>
AuthorDate: Sun Aug 14 10:58:44 2022 +0400

    feat: support sending alarm to discord (#9469)
---
 docs/en/changes/changes.md                         |   1 +
 docs/en/setup/backend/backend-alarm.md             |  12 ++
 .../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    |  26 +++++
 .../provider/discord/DiscordHookCallback.java      | 121 +++++++++++++++++++++
 .../alarm/provider/discord/DiscordSettings.java    |  60 ++++++++++
 8 files changed, 229 insertions(+)

diff --git a/docs/en/changes/changes.md b/docs/en/changes/changes.md
index ec847494be..09745f13a6 100644
--- a/docs/en/changes/changes.md
+++ b/docs/en/changes/changes.md
@@ -49,6 +49,7 @@
 * Support Elasticsearch column alias for the compatibility between storage logicSharding model and no-logicSharding model.
 * Support MySQL monitoring.
 * Fix query services by serviceId error when Elasticsearch storage `SW_STORAGE_ES_QUERY_MAX_SIZE` > 10000.
+* Support sending alarm messages to Discord.
 
 #### UI
 
diff --git a/docs/en/setup/backend/backend-alarm.md b/docs/en/setup/backend/backend-alarm.md
index 973d817cff..9de365376c 100644
--- a/docs/en/setup/backend/backend-alarm.md
+++ b/docs/en/setup/backend/backend-alarm.md
@@ -329,6 +329,18 @@ pagerDutyHooks:
 
 You can also configure multiple integration keys.
 
+## Discord Hook
+Follow the [Discord Webhooks guide](https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks) and create a new webhook.
+
+Then configure as follows:
+```yml
+discordHooks:
+  textTemplate: "Apache SkyWalking Alarm: \n %s."
+  webhooks:
+    - url: https://discordapp.com/api/webhooks/1008166889777414645/8e0Am4Zb-YGbBqqbiiq0jSHPTEEaHa4j1vIC-zSSm231T8ewGxgY0_XUYpY-k1nN4HBl
+      username: robot
+```
+
 ## Update the settings dynamically
 Since 6.5.0, the alerting settings can be updated dynamically at runtime by [Dynamic Configuration](dynamic-config.md),
 which will override the settings in `alarm-settings.yml`.
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 0ab3e94b65..19938ba33f 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.alarm.AlarmModule;
 import org.apache.skywalking.oap.server.core.alarm.provider.dingtalk.DingtalkSettings;
+import org.apache.skywalking.oap.server.core.alarm.provider.discord.DiscordSettings;
 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;
@@ -147,4 +148,8 @@ public class AlarmRulesWatcher extends ConfigChangeWatcher {
     public PagerDutySettings getPagerDutySettings() {
         return this.rules.getPagerDutySettings();
     }
+
+    public DiscordSettings getDiscordSettings() {
+        return this.rules.getDiscordSettings();
+    }
 }
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 1ec3b9f477..dffe827b85 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
@@ -33,6 +33,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.discord.DiscordHookCallback;
 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.pagerduty.PagerDutyHookCallback;
@@ -187,6 +188,7 @@ public class NotifyHandler implements MetricsNotify {
         allCallbacks.add(new EventHookCallback(this.manager));
         allCallbacks.add(new WeLinkHookCallback(alarmRulesWatcher));
         allCallbacks.add(new PagerDutyHookCallback(alarmRulesWatcher));
+        allCallbacks.add(new DiscordHookCallback(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 f5940bc12f..537bab99c1 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.discord.DiscordSettings;
 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.pagerduty.PagerDutySettings;
@@ -45,6 +46,7 @@ public class Rules {
     private FeishuSettings feishus;
     private WeLinkSettings welinks;
     private PagerDutySettings pagerDutySettings;
+    private DiscordSettings discordSettings;
 
     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 7c28400833..c0bfc8b8fd 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
@@ -28,6 +28,7 @@ import java.util.Map;
 import java.util.Objects;
 import java.util.stream.Collectors;
 
+import org.apache.skywalking.oap.server.core.alarm.provider.discord.DiscordSettings;
 import org.apache.skywalking.oap.server.core.alarm.provider.pagerduty.PagerDutySettings;
 import org.apache.skywalking.oap.server.library.util.StringUtil;
 import org.apache.skywalking.oap.server.core.alarm.provider.dingtalk.DingtalkSettings;
@@ -73,6 +74,7 @@ public class RulesReader {
             readFeishuConfig(rules);
             readWeLinkConfig(rules);
             readPagerDutyConfig(rules);
+            readDiscordConfig(rules);
         }
         return rules;
     }
@@ -303,4 +305,28 @@ public class RulesReader {
             rules.setPagerDutySettings(pagerDutySettings);
         }
     }
+
+    /**
+     * Read Discord hook config into {@link DiscordSettings}
+     */
+    @SuppressWarnings("unchecked")
+    private void readDiscordConfig(Rules rules) {
+        Map<String, Object> discordConfig = (Map<String, Object>) yamlData.getOrDefault(
+                "discordHooks",
+                Collections.EMPTY_MAP
+        );
+        String textTemplate = (String) discordConfig.get("textTemplate");
+        List<Map<String, String>> discordWebHooks = (List<Map<String, String>>) discordConfig.get("webhooks");
+        if (StringUtil.isBlank(textTemplate) || CollectionUtils.isEmpty(discordWebHooks)) {
+            return;
+        }
+        List<DiscordSettings.WebHookUrl> webHookUrls = discordWebHooks.stream().map(
+                DiscordSettings.WebHookUrl::generateFromMap
+        ).collect(Collectors.toList());
+
+        DiscordSettings discordSettings = new DiscordSettings();
+        discordSettings.setTextTemplate(textTemplate);
+        discordSettings.setWebhooks(webHookUrls);
+        rules.setDiscordSettings(discordSettings);
+    }
 }
diff --git a/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/discord/DiscordHookCallback.java b/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/discord/DiscordHookCallback.java
new file mode 100644
index 0000000000..91ee580582
--- /dev/null
+++ b/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/discord/DiscordHookCallback.java
@@ -0,0 +1,121 @@
+/*
+ * 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.discord;
+
+import com.google.gson.JsonObject;
+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.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 java.io.IOException;
+import java.util.List;
+
+/**
+ * Use SkyWalking alarm Discord webhook API.
+ */
+@Slf4j
+public class DiscordHookCallback 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 final AlarmRulesWatcher alarmRulesWatcher;
+    private final RequestConfig requestConfig;
+
+    public DiscordHookCallback(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) {
+        DiscordSettings discordSettings = alarmRulesWatcher.getDiscordSettings();
+        if (discordSettings == null || discordSettings.getWebhooks().isEmpty()) {
+            return;
+        }
+        discordSettings.getWebhooks().forEach(webHookUrl -> {
+            alarmMessages.forEach(alarmMessage -> {
+                String content = String.format(
+                        discordSettings.getTextTemplate(),
+                        alarmMessage.getAlarmMessage()
+                );
+                sendAlarmMessage(webHookUrl, content);
+            });
+        });
+    }
+
+    /**
+     * Send alarm message to remote endpoint
+     */
+    private void sendAlarmMessage(DiscordSettings.WebHookUrl webHookUrl, String content) {
+        JsonObject body = new JsonObject();
+        body.addProperty("username", webHookUrl.getUsername());
+        body.addProperty("content", content);
+        sendPostRequest(webHookUrl.getUrl(), body.toString());
+    }
+
+    /**
+     * Post rest invoke
+     */
+    private void sendPostRequest(String url, String requestBody) {
+        try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
+            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);
+            try (CloseableHttpResponse httpResponse = httpClient.execute(post)) {
+                StatusLine statusLine = httpResponse.getStatusLine();
+                if (statusLine != null) {
+                    if (statusLine.getStatusCode() != HttpStatus.SC_OK && statusLine.getStatusCode() != HttpStatus.SC_NO_CONTENT) {
+                        log.error("send to {} failure. Response code: {}, Response content: {}", url,
+                                statusLine.getStatusCode(),
+                                EntityUtils.toString(httpResponse.getEntity())
+                        );
+                    }
+                }
+            } catch (IOException e) {
+                log.error(e.getMessage(), e);
+            }
+        } catch (IOException e) {
+            log.error(e.getMessage(), e);
+        }
+    }
+}
diff --git a/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/discord/DiscordSettings.java b/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/discord/DiscordSettings.java
new file mode 100644
index 0000000000..1c5b9e2b26
--- /dev/null
+++ b/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/discord/DiscordSettings.java
@@ -0,0 +1,60 @@
+/*
+ * 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.discord;
+
+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;
+import java.util.Map;
+
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@Setter
+@Getter
+@ToString
+public class DiscordSettings {
+
+    private String textTemplate;
+    @Builder.Default
+    private List<WebHookUrl> webhooks = new ArrayList<>();
+
+    @AllArgsConstructor
+    @Setter
+    @Getter
+    @ToString
+    public static class WebHookUrl {
+        // The url to send message
+        private final String url;
+        // Name display in channel
+        private final String username;
+
+        public static WebHookUrl generateFromMap(Map<String, String> params) {
+            String url = params.get("url");
+            String username = params.getOrDefault("username", "robot");
+            return new WebHookUrl(url, username);
+        }
+    }
+}