未验证 提交 1aa5f268 编写于 作者: 高云峰 提交者: GitHub

feature:Support alarm to WeLink (#6794)

上级 883bd151
......@@ -23,6 +23,7 @@ Release Notes.
* Fix K8s monitoring the incorrect metrics calculate.
* Loop alarm into event system.
* Support alarm tags.
* Support WeLink as a channel of alarm notification.
#### UI
* Add logo for kong plugin.
......
......@@ -281,6 +281,24 @@ feishuHooks:
secret: dummysecret
```
## WeLink Hook
To do this you need to follow the [WeLink Webhooks guide](https://open.welink.huaweicloud.com/apiexplorer/#/apiexplorer?type=internal&method=POST&path=/welinkim/v1/im-service/chat/group-chat) and create new Webhooks.
The alarm message will send through HTTP post by `application/json` content type if you configured WeLink Webhooks as following:
```yml
welinkHooks:
textTemplate: "Apache SkyWalking Alarm: \n %s."
webhooks:
# you may find your own client_id and client_secret in your app, below are dummy, need to change.
- client_id: "dummy_client_id"
client_secret: dummy_secret_key
access_token_url: https://open.welink.huaweicloud.com/api/auth/v2/tickets
message_url: https://open.welink.huaweicloud.com/api/welinkim/v1/im-service/chat/group-chat
# if you send to multi group at a time, separate group_ids with commas, e.g. "123xx","456xx"
group_ids: "dummy_group_id"
# make a name you like for the robot, it will display in group
robot_name: robot
```
## 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`.
......
......@@ -35,6 +35,7 @@ import org.apache.skywalking.oap.server.core.alarm.provider.feishu.FeishuSetting
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.core.alarm.provider.welink.WeLinkSettings;
import org.apache.skywalking.oap.server.library.module.ModuleProvider;
/**
......@@ -139,4 +140,7 @@ public class AlarmRulesWatcher extends ConfigChangeWatcher {
return this.rules.getFeishus();
}
public WeLinkSettings getWeLinkSettings() {
return this.rules.getWelinks();
}
}
......@@ -36,6 +36,7 @@ import org.apache.skywalking.oap.server.core.alarm.provider.feishu.FeishuHookCal
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.alarm.provider.welink.WeLinkHookCallback;
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;
......@@ -170,6 +171,7 @@ public class NotifyHandler implements MetricsNotify {
allCallbacks.add(new DingtalkHookCallback(alarmRulesWatcher));
allCallbacks.add(new FeishuHookCallback(alarmRulesWatcher));
allCallbacks.add(new EventHookCallback(this.manager));
allCallbacks.add(new WeLinkHookCallback(alarmRulesWatcher));
core.start(allCallbacks);
}
}
......@@ -28,6 +28,7 @@ import org.apache.skywalking.oap.server.core.alarm.provider.feishu.FeishuSetting
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.core.alarm.provider.welink.WeLinkSettings;
@Setter
@Getter
......@@ -41,6 +42,7 @@ public class Rules {
private List<CompositeAlarmRule> compositeRules;
private DingtalkSettings dingtalks;
private FeishuSettings feishus;
private WeLinkSettings welinks;
public Rules() {
this.rules = new ArrayList<>();
......
......@@ -30,6 +30,7 @@ import org.apache.skywalking.oap.server.core.alarm.provider.feishu.FeishuSetting
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.core.alarm.provider.welink.WeLinkSettings;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.SafeConstructor;
......@@ -64,6 +65,7 @@ public class RulesReader {
readCompositeRuleConfig(rules);
readDingtalkConfig(rules);
readFeishuConfig(rules);
readWeLinkConfig(rules);
}
return rules;
}
......@@ -251,4 +253,32 @@ public class RulesReader {
rules.setFeishus(feishuSettings);
}
}
/**
* Read WeLink hook config into {@link WeLinkSettings}
*/
private void readWeLinkConfig(Rules rules) {
Map welinkConfig = (Map) yamlData.get("welinkHooks");
if (welinkConfig != null) {
WeLinkSettings welinkSettings = new WeLinkSettings();
Object textTemplate = welinkConfig.getOrDefault("textTemplate", "");
welinkSettings.setTextTemplate((String) textTemplate);
List<Map<String, Object>> welinkWebHooks = (List<Map<String, Object>>) welinkConfig.get("webhooks");
if (welinkWebHooks != null) {
welinkWebHooks.forEach(welinkWebhook -> {
String clientId = (String) welinkWebhook.getOrDefault("client_id", "");
String clientSecret = (String) welinkWebhook.getOrDefault("client_secret", "");
String accessTokenUrl = (String) welinkWebhook.getOrDefault("access_token_url", "");
String messageUrl = (String) welinkWebhook.getOrDefault("message_url", "");
String groupIds = (String) welinkWebhook.getOrDefault("group_ids", "");
String rebootName = (String) welinkWebhook.getOrDefault("robot_name", "reboot");
welinkSettings.getWebhooks()
.add(new WeLinkSettings.WebHookUrl(clientId, clientSecret, accessTokenUrl, messageUrl,
rebootName, groupIds
));
});
}
rules.setWelinks(welinkSettings);
}
}
}
/*
* 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.welink;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import io.netty.handler.codec.http.HttpHeaderValues;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
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;
/**
* Use SkyWalking alarm WeLink webhook API.
*/
@Slf4j
public class WeLinkHookCallback 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 WeLinkHookCallback(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.getWeLinkSettings() == null || this.alarmRulesWatcher.getWeLinkSettings()
.getWebhooks()
.isEmpty()) {
return;
}
WeLinkSettings welinkSettings = this.alarmRulesWatcher.getWeLinkSettings();
welinkSettings.getWebhooks().forEach(webHookUrl -> {
String accessToken = getAccessToken(webHookUrl);
alarmMessages.forEach(alarmMessage -> {
String content = String.format(
Locale.US,
this.alarmRulesWatcher.getWeLinkSettings().getTextTemplate(),
alarmMessage.getAlarmMessage()
);
sendAlarmMessage(webHookUrl, accessToken, content);
});
});
}
/**
* Send alarm message to remote endpoint
*/
private void sendAlarmMessage(WeLinkSettings.WebHookUrl webHookUrl, String accessToken, String content) {
JsonObject appServiceInfo = new JsonObject();
appServiceInfo.addProperty("app_service_id", "1");
appServiceInfo.addProperty("app_service_name", webHookUrl.getRobotName());
JsonArray groupIds = new JsonArray();
Arrays.stream(webHookUrl.getGroupIds().split(",")).forEach(groupIds::add);
JsonObject body = new JsonObject();
body.add("app_service_info", appServiceInfo);
body.addProperty("app_msg_id", UUID.randomUUID().toString());
body.add("group_id", groupIds);
body.addProperty("content", String.format(
Locale.US, "<r><n></n><g>0</g><c>&lt;imbody&gt;&lt;imagelist/&gt;" +
"&lt;html&gt;&lt;![CDATA[&lt;DIV&gt;%s&lt;/DIV&gt;]]&gt;&lt;/html&gt;&lt;content&gt;&lt;![CDATA[%s]]&gt;&lt;/content&gt;&lt;/imbody&gt;</c></r>",
content, content
));
body.addProperty("content_type", 0);
body.addProperty("client_app_id", "1");
sendPostRequest(
webHookUrl.getMessageUrl(), Collections.singletonMap("x-wlk-Authorization", accessToken), body.toString());
}
/**
* Get access token from remote endpoint
*/
private String getAccessToken(WeLinkSettings.WebHookUrl webHookUrl) {
String accessTokenUrl = webHookUrl.getAccessTokenUrl();
String clientId = webHookUrl.getClientId();
String clientSecret = webHookUrl.getClientSecret();
String response = sendPostRequest(
accessTokenUrl, Collections.emptyMap(),
String.format(Locale.US, "{\"client_id\":%s,\"client_secret\":%s}", clientId, clientSecret)
);
Gson gson = new Gson();
JsonObject responseJson = gson.fromJson(response, JsonObject.class);
return Optional.ofNullable(responseJson)
.map(r -> r.get("access_token"))
.map(JsonElement::getAsString)
.orElse("");
}
/**
* Post rest invoke
*/
private String sendPostRequest(String url, Map<String, String> headers, String requestBody) {
String response = "";
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());
headers.forEach(post::setHeader);
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) {
log.error("send to {} failure. Response code: {}, Response content: {}", url,
statusLine.getStatusCode(),
EntityUtils.toString(httpResponse.getEntity())
);
} else {
response = EntityUtils.toString(httpResponse.getEntity(), StandardCharsets.UTF_8);
}
}
} catch (IOException e) {
log.error(e.getMessage(), e);
}
} catch (IOException e) {
log.error(e.getMessage(), e);
}
return response;
}
}
/*
* 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.welink;
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 WeLinkSettings {
private String textTemplate;
@Builder.Default
private List<WebHookUrl> webhooks = new ArrayList<>();
@AllArgsConstructor
@Setter
@Getter
@ToString
public static class WebHookUrl {
// The unique identity of the application, used for interface authentication to obtain access_token
private final String clientId;
// The application key is used for interface authentication to obtain access_token
private final String clientSecret;
// The url get access token
private final String accessTokenUrl;
// The url to send message
private final String messageUrl;
// Name display in group
private final String robotName;
// The groupIds message to send
private final String groupIds;
}
}
......@@ -90,6 +90,7 @@ public class AlarmRulesWatcherTest {
assertNotNull(alarmRulesWatcher.getDingtalkSettings());
assertNotNull(alarmRulesWatcher.getWechatSettings());
assertNotNull(alarmRulesWatcher.getSlackSettings());
assertNotNull(alarmRulesWatcher.getWeLinkSettings());
}
@Test
......
/*
* 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.welink;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
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 javax.servlet.Servlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
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 static org.junit.Assert.assertTrue;
public class WeLinkHookCallbackTest 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("/welinkhook");
server.setHandler(servletContextHandler);
ServletHolder servletHolder = new ServletHolder();
servletHolder.setServlet(this);
servletContextHandler.addServlet(servletHolder, "/api/auth/v2/tickets");
servletContextHandler.addServlet(servletHolder, "/api/welinkim/v1/im-service/chat/group-chat");
server.start();
port = server.getURI().getPort();
assertTrue(port > 0);
}
@Test
public void testWeLinkDoAlarm() {
List<WeLinkSettings.WebHookUrl> webHooks = new ArrayList<>();
webHooks.add(new WeLinkSettings.WebHookUrl("clientId", "clientSecret",
"http://127.0.0.1:" + port + "/welinkhook/api/auth/v2/tickets",
"http://127.0.0.1:" + port + "/welinkhook/api/welinkim/v1/im-service/chat/group-chat",
"rebootName", "1,2,3"
));
Rules rules = new Rules();
String template = "Apache SkyWalking Alarm: \n %s.";
rules.setWelinks(WeLinkSettings.builder().webhooks(webHooks).textTemplate(template).build());
AlarmRulesWatcher alarmRulesWatcher = new AlarmRulesWatcher(rules, null);
WeLinkHookCallback welinkHookCallback = new WeLinkHookCallback(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);
welinkHookCallback.doAlarm(alarmMessages);
Assert.assertTrue(isSuccess);
}
@After
public void stop() throws Exception {
server.stop();
}
@Override
public void init(ServletConfig servletConfig) {
}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public void service(ServletRequest request, ServletResponse response) throws 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);
if (count == 0) {
String clientId = jsonObject.get("client_id").getAsString();
count = clientId == null ? count : count + 1;
((HttpServletResponse) response).setStatus(200);
} else if (count >= 1) {
String appMsgId = jsonObject.get("app_msg_id").getAsString();
count = appMsgId == null ? count : count + 1;
((HttpServletResponse) response).setStatus(200);
} else {
((HttpServletResponse) response).setStatus(500);
}
if (count == 2) {
isSuccess = true;
}
}
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {
}
}
......@@ -121,3 +121,16 @@ feishuHooks:
secret: dummysecret
- url: https://open.feishu.cn/open-apis/bot/v2/hook/dummy_token2
secret:
welinkHooks:
textTemplate: "Apache SkyWalking Alarm: \n %s."
webhooks:
# you may find your own client_id and client_secret in your app, below are dummy, need to change.
- client_id: "dummy_client_id"
client_secret: dummy_secret_key
access_token_url: https://open.welink.huaweicloud.com/api/auth/v2/tickets
message_url: https://open.welink.huaweicloud.com/api/welinkim/v1/im-service/chat/group-chat
# if you send to multi group at a time, separate group_ids with commas, e.g. "123xx","456xx"
group_ids: "dummy_group_id"
# make a name you like for the robot, it will display in group
robot_name: robot
\ No newline at end of file
......@@ -97,3 +97,16 @@ feishuHooks:
webhooks:
# - url: https://open.feishu.cn/open-apis/bot/v2/hook/dummy_token
# secret: dummysecret
welinkHooks:
textTemplate: "Apache SkyWalking Alarm: \n %s."
webhooks:
# # you may find your own client_id and client_secret in your app, below are dummy, need to change.
# - client_id: "dummy_client_id"
# client_secret: dummy_secret_key
# access_token_url: https://open.welink.huaweicloud.com/api/auth/v2/tickets
# message_url: https://open.welink.huaweicloud.com/api/welinkim/v1/im-service/chat/group-chat
# # if you send to multi group at a time, separate group_ids with commas, e.g. "123xx","456xx"
# group_ids: "dummy_group_id"
# # make a name you like for the robot, it will display in group
# robot_name: robot
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册