diff --git a/docs/en/setup/backend/backend-alarm.md b/docs/en/setup/backend/backend-alarm.md index 90189be37..704b22fbd 100644 --- a/docs/en/setup/backend/backend-alarm.md +++ b/docs/en/setup/backend/backend-alarm.md @@ -180,6 +180,23 @@ slackHooks: - https://hooks.slack.com/services/x/y/z ``` +## WeChat Hook +Note, only WeCom(WeChat Company Edition) supports webhook. To use the WeChat webhook you need to follow the [Wechat Webhooks guide](https://work.weixin.qq.com/help?doc_id=13376). +The alarm message would send through HTTP post by `application/json` content type after you set up Wechat Webhooks as following: +```yml +wechatHooks: + textTemplate: |- + { + "msgtype": "text", + "text": { + "content": "Apache SkyWalking Alarm: \n %s." + } + } + webhooks: + - https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=dummy_key +``` + + ## Update the settings dynamically Since 6.5.0, the alarm 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 51a535387..880aab1cf 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 @@ -30,6 +30,7 @@ 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.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.apache.skywalking.oap.server.library.module.ModuleProvider; /** @@ -114,4 +115,8 @@ public class AlarmRulesWatcher extends ConfigChangeWatcher { public SlackSettings getSlackSettings() { return this.rules.getSlacks(); } + + public WechatSettings getWechatSettings() { + return this.rules.getWecchats(); + } } 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 2209d19d2..2f95b5cdf 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.ServiceInstanceMetaInAlarm; import org.apache.skywalking.oap.server.core.alarm.ServiceMetaInAlarm; 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; import org.apache.skywalking.oap.server.core.analysis.IDManager; import org.apache.skywalking.oap.server.core.analysis.metrics.Metrics; import org.apache.skywalking.oap.server.core.analysis.metrics.MetricsMetaInfo; @@ -160,6 +161,7 @@ public class NotifyHandler implements MetricsNotify { allCallbacks.add(new WebhookCallback(alarmRulesWatcher)); allCallbacks.add(new GRPCCallback(alarmRulesWatcher)); allCallbacks.add(new SlackhookCallback(alarmRulesWatcher)); + allCallbacks.add(new WechatHookCallback(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 0f95820bc..0b67ead2a 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 @@ -25,6 +25,7 @@ import lombok.Setter; import lombok.ToString; 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; @Setter @Getter @@ -34,6 +35,7 @@ public class Rules { private List webhooks; private GRPCAlarmSetting grpchookSetting; private SlackSettings slacks; + private WechatSettings wecchats; 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 e0909dea7..1a5d5dc19 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 @@ -26,6 +26,7 @@ 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; /** @@ -118,15 +119,23 @@ public class RulesReader { List slackWebhooks = (List) slacks.get("webhooks"); if (slackWebhooks != null) { - slackWebhooks.forEach( - url -> slackSettings.getWebhooks().add(url) - ); + slackSettings.getWebhooks().addAll(slackWebhooks); } - rules.setSlacks(slackSettings); } - } + Map wechatConfig = (Map) yamlData.get("wechatHooks"); + if (wechatConfig != null) { + WechatSettings wechatSettings = new WechatSettings(); + Object textTemplate = wechatConfig.getOrDefault("textTemplate", ""); + wechatSettings.setTextTemplate((String) textTemplate); + List wechatWebhooks = (List) wechatConfig.get("webhooks"); + if (wechatWebhooks != null) { + wechatSettings.getWebhooks().addAll(wechatWebhooks); + } + rules.setWecchats(wechatSettings); + } + } return rules; } } diff --git a/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/wechat/WechatHookCallback.java b/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/wechat/WechatHookCallback.java new file mode 100644 index 000000000..9ac151778 --- /dev/null +++ b/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/wechat/WechatHookCallback.java @@ -0,0 +1,101 @@ +/* + * 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.wechat; + +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.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 wechat webhook API. + */ +@Slf4j +public class WechatHookCallback 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 WechatHookCallback(final AlarmRulesWatcher alarmRulesWatcher) { + this.alarmRulesWatcher = alarmRulesWatcher; + this.requestConfig = RequestConfig.custom() + .setConnectTimeout(HTTP_CONNECT_TIMEOUT) + .setConnectionRequestTimeout(HTTP_CONNECTION_REQUEST_TIMEOUT) + .setSocketTimeout(HTTP_SOCKET_TIMEOUT) + .build(); + } + + @Override + public void doAlarm(List alarmMessages) { + if (this.alarmRulesWatcher.getWechatSettings() == null || this.alarmRulesWatcher.getWechatSettings().getWebhooks().isEmpty()) { + return; + } + CloseableHttpClient httpClient = HttpClients.custom().build(); + try { + this.alarmRulesWatcher.getWechatSettings().getWebhooks().forEach(url -> { + alarmMessages.forEach(alarmMessage -> { + String requestBody = String.format( + this.alarmRulesWatcher.getWechatSettings().getTextTemplate(), alarmMessage.getAlarmMessage() + ); + sendAlarmMessage(httpClient, url, requestBody); + }); + }); + } finally { + try { + httpClient.close(); + } catch (IOException e) { + log.error(e.getMessage(), e); + } + } + } + + 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 wechat alarm to {} failure. Response code: {} ", url, statusLine.getStatusCode()); + } + } catch (Throwable e) { + log.error("send wechat alarm to {} failure.", url, e); + } + } +} diff --git a/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/wechat/WechatSettings.java b/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/wechat/WechatSettings.java new file mode 100644 index 000000000..03063534b --- /dev/null +++ b/oap-server/server-alarm-plugin/src/main/java/org/apache/skywalking/oap/server/core/alarm/provider/wechat/WechatSettings.java @@ -0,0 +1,42 @@ +/* + * 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.wechat; + +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; + +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Setter +@Getter +@ToString +public class WechatSettings { + + private String textTemplate; + + @Builder.Default + private List webhooks = new ArrayList<>(); +} diff --git a/oap-server/server-alarm-plugin/src/test/java/org/apache/skywalking/oap/server/core/alarm/provider/wechat/WechatHookCallbackTest.java b/oap-server/server-alarm-plugin/src/test/java/org/apache/skywalking/oap/server/core/alarm/provider/wechat/WechatHookCallbackTest.java new file mode 100644 index 000000000..23683ecd7 --- /dev/null +++ b/oap-server/server-alarm-plugin/src/test/java/org/apache/skywalking/oap/server/core/alarm/provider/wechat/WechatHookCallbackTest.java @@ -0,0 +1,147 @@ +/* + * 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.wechat; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +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 WechatHookCallbackTest implements Servlet { + private Server server; + private int port; + private volatile boolean isSuccess = false; + private int count; + + @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("/wechathook"); + 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 testWechatWebhook() { + List remoteEndpoints = new ArrayList<>(); + remoteEndpoints.add("http://127.0.0.1:" + port + "/wechathook/receiveAlarm"); + Rules rules = new Rules(); + String template = "{\"msgtype\":\"text\",\"text\":{\"content\":\"Skywaling alarm: %s\"}}"; + rules.setWecchats(WechatSettings.builder().webhooks(remoteEndpoints).textTemplate(template).build()); + AlarmRulesWatcher alarmRulesWatcher = new AlarmRulesWatcher(rules, null); + WechatHookCallback wechatHookCallback = new WechatHookCallback(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); + wechatHookCallback.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 (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-bootstrap/src/main/resources/alarm-settings.yml b/oap-server/server-bootstrap/src/main/resources/alarm-settings.yml index 771bc2af4..32eee4baa 100755 --- a/oap-server/server-bootstrap/src/main/resources/alarm-settings.yml +++ b/oap-server/server-bootstrap/src/main/resources/alarm-settings.yml @@ -59,3 +59,13 @@ slackHooks: 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