The news in the work group is afraid of being too quiet and too frequent
1. Business Background
In the process of development, various development problems will be encountered, such as server downtime, network jitter, bugs in the code itself, and so on. For bugs in the code, we can advance in advance, and send alerts to warn us to intervene and deal with them as soon as possible.
2. The way of warning
1. DingTalk alarm
By adding group robots in the enterprise DingTalk group, the robot sends alarm information to the group. As for how to create a DingTalk robot, the api for sending messages, etc., please refer to the official documentation
2. Enterprise WeChat alarm
The same routine, enterprise WeChat is also, in the enterprise WeChat group, add group robots. Send alert information through the robot. Please see the official documentation for details
3. Email alert
The difference from the above is that the email is sent to individuals, of course, it can also be sent in batches, and only the way of sending text format is realized. As for the markdown format, it needs to be investigated. Sending emails is relatively simple, so I won't go into details here.
3. Source code analysis
1. Alarm custom annotation
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface Alarm {
/**
* 报警标题
*
* @return String
*/
String title() default "";
/**
* 发送报警格式:目前支持text,markdown
* @return
*/
MessageTye messageType() default MessageTye.TEXT;
/**
* 告警模板id
* @return
*/
String templateId() default "";
/**
* 成功是否通知:true-通知,false-不通知
* @return
*/
boolean successNotice() default false;
}
1.1. Use of annotations
@Alarm
mark is used on the method. If the marked method is abnormal, it will read the configuration information and send the exception stack information according to the configuration. The usage method is as follows:
@Alarm(title = "某某业务告警", messageType = MessageTye.MARKDOWN, templateId = "errorTemp")
1.2, Annotation field analysis
- title
Alert message title: can be defined as business information, such as tutor identity calculation
- messageType
Alarm message display type: currently supports text text type, markdown type
- templateId
Message template id: consistent with the template id configured in the configuration file
- successNotice
Whether the alarm information needs to be sent under normal conditions, the default value is fasle, which means that it does not need to be sent. Of course, some business scenarios also need to be sent under normal circumstances, such as: payment notification, etc.
2. Configuration file analysis
2.1, DingTalk configuration file
spring:
alarm:
dingtalk:
# 开启钉钉发送告警
enabled: true
# 钉钉群机器人唯一的token
token: xxxxxx
# 安全设置:加签的密钥
secret: xxxxxxx
2.2. Enterprise WeChat configuration file
spring:
alarm:
wechat:
# 开启企业微信告警
enabled: true
# 企业微信群机器人唯一key
key: xxxxxdsf
# 被@人的手机号
to-user: 1314243
2.3, mail configuration file
spring:
alarm:
mail:
enabled: true
smtpHost: xxx@qq.com
smtpPort: 22
to: xxx@qq.com
from: 132@qq.com
username: wsrf
password: xxx
2.4, custom template configuration
spring:
alarm:
template:
# 开启通过模板配置
enabled: true
# 配置模板来源为文件
source: FILE
# 配置模板数据
templates:
errorTemp:
templateId: errorTemp
templateName: 服务异常模板
templateContent: 这里是配置模板的内容
-
spring:alarm:template:enabled
, Boolean type, indicating that alarm messages are enabled to be sent using templates. -
spring:alarm:template:source
, template source, enumeration class: JDBC (database), FILE (configuration file), MEMORY (memory), currently only FILE is supported, the other two can be extended by themselves. spring:alarm:template:templates
, the content of the configuration template, is a map,errorTemp
is the template id, which template needs to be used, just set the templateId in@Alarm
to the corresponding configuration file templateId in .3. Core AOP Analysis
3.1. Principle analysis
3.2, custom facets
@Aspect @Slf4j @RequiredArgsConstructor public class AlarmAspect { private final AlarmTemplateProvider alarmTemplateProvider; private final static String ERROR_TEMPLATE = "\n\n<font color=\"#F37335\">异常信息:</font>\n" + "```java\n" + "#{[exception]}\n" + "```\n"; private final static String TEXT_ERROR_TEMPLATE = "\n异常信息:\n" + "#{[exception]}"; private final static String MARKDOWN_TITLE_TEMPLATE = "# 【#{[title]}】\n" + "\n请求状态:<font color=\"#{[stateColor]}\">#{[state]}</font>\n\n"; private final static String TEXT_TITLE_TEMPLATE = "【#{[title]}】\n" + "请求状态:#{[state]}\n"; @Pointcut("@annotation(alarm)") public void alarmPointcut(Alarm alarm) { } @Around(value = "alarmPointcut(alarm)", argNames = "joinPoint,alarm") public Object around(ProceedingJoinPoint joinPoint, Alarm alarm) throws Throwable { Object result = joinPoint.proceed(); if (alarm.successNotice()) { String templateId = alarm.templateId(); String fileTemplateContent = ""; if (Objects.nonNull(alarmTemplateProvider)) { AlarmTemplate alarmTemplate = alarmTemplateProvider.loadingAlarmTemplate(templateId); fileTemplateContent = alarmTemplate.getTemplateContent(); } String templateContent = ""; MessageTye messageTye = alarm.messageType(); if (messageTye.equals(MessageTye.TEXT)) { templateContent = TEXT_TITLE_TEMPLATE.concat(fileTemplateContent); } else if (messageTye.equals(MessageTye.MARKDOWN)) { templateContent = MARKDOWN_TITLE_TEMPLATE.concat(fileTemplateContent); } Map<String, Object> alarmParamMap = new HashMap<>(); alarmParamMap.put("title", alarm.title()); alarmParamMap.put("stateColor", "#45B649"); alarmParamMap.put("state", "成功"); sendAlarm(alarm, templateContent, alarmParamMap); } return result; } @AfterThrowing(pointcut = "alarmPointcut(alarm)", argNames = "joinPoint,alarm,e", throwing = "e") public void doAfterThrow(JoinPoint joinPoint, Alarm alarm, Exception e) { log.info("请求接口发生异常 : [{}]", e.getMessage()); String templateId = alarm.templateId(); // 加载模板中配置的内容,若有 String templateContent = ""; String fileTemplateContent = ""; if (Objects.nonNull(alarmTemplateProvider)) { AlarmTemplate alarmTemplate = alarmTemplateProvider.loadingAlarmTemplate(templateId); fileTemplateContent = alarmTemplate.getTemplateContent(); } MessageTye messageTye = alarm.messageType(); if (messageTye.equals(MessageTye.TEXT)) { templateContent = TEXT_TITLE_TEMPLATE.concat(fileTemplateContent).concat(TEXT_ERROR_TEMPLATE); } else if (messageTye.equals(MessageTye.MARKDOWN)) { templateContent = MARKDOWN_TITLE_TEMPLATE.concat(fileTemplateContent).concat(ERROR_TEMPLATE); } Map<String, Object> alarmParamMap = new HashMap<>(); alarmParamMap.put("title", alarm.title()); alarmParamMap.put("stateColor", "#FF4B2B"); alarmParamMap.put("state", "失败"); alarmParamMap.put("exception", ExceptionUtil.stacktraceToString(e)); sendAlarm(alarm, templateContent, alarmParamMap); } private void sendAlarm(Alarm alarm, String templateContent, Map<String, Object> alarmParamMap) { ExpressionParser parser = new SpelExpressionParser(); TemplateParserContext parserContext = new TemplateParserContext(); String message = parser.parseExpression(templateContent, parserContext).getValue(alarmParamMap, String.class); MessageTye messageTye = alarm.messageType(); NotifyMessage notifyMessage = new NotifyMessage(); notifyMessage.setTitle(alarm.title()); notifyMessage.setMessageTye(messageTye); notifyMessage.setMessage(message); AlarmFactoryExecute.execute(notifyMessage); } }
4. Template provider
4.1. AlarmTemplateProvider
Define an abstract interface
AlarmTemplateProvider
for implementation by concrete subclasses
public interface AlarmTemplateProvider {
/**
* 加载告警模板
*
* @param templateId 模板id
* @return AlarmTemplate
*/
AlarmTemplate loadingAlarmTemplate(String templateId);
}
4.2, BaseAlarmTemplateProvider
Abstract class BaseAlarmTemplateProvider
implements this abstract interface
public abstract class BaseAlarmTemplateProvider implements AlarmTemplateProvider {
@Override
public AlarmTemplate loadingAlarmTemplate(String templateId) {
if (StringUtils.isEmpty(templateId)) {
throw new AlarmException(400, "告警模板配置id不能为空");
}
return getAlarmTemplate(templateId);
}
/**
* 查询告警模板
*
* @param templateId 模板id
* @return AlarmTemplate
*/
abstract AlarmTemplate getAlarmTemplate(String templateId);
}
4.3. YamlAlarmTemplateProvider
The specific implementation class YamlAlarmTemplateProvider
realizes reading the template from the configuration file. This class will be loaded into the spring bean container when the project starts.
@RequiredArgsConstructor
public class YamlAlarmTemplateProvider extends BaseAlarmTemplateProvider {
private final TemplateConfig templateConfig;
@Override
AlarmTemplate getAlarmTemplate(String templateId) {
Map<String, AlarmTemplate> configTemplates = templateConfig.getTemplates();
AlarmTemplate alarmTemplate = configTemplates.get(templateId);
if (ObjectUtils.isEmpty(alarmTemplate)) {
throw new AlarmException(400, "未发现告警配置模板");
}
return alarmTemplate;
}
}
4.4, MemoryAlarmTemplateProvider and JdbcAlarmTemplateProvider
The abstract classBaseAlarmTemplateProvider
has two other subclasses, namelyMemoryAlarmTemplateProvider
andJdbcAlarmTemplateProvider
. However, these two subclasses have not implemented the logic for the time being, and can be extended by themselves in the future.
@RequiredArgsConstructor
public class MemoryAlarmTemplateProvider extends BaseAlarmTemplateProvider {
private final Function<String, AlarmTemplate> function;
@Override
AlarmTemplate getAlarmTemplate(String templateId) {
AlarmTemplate alarmTemplate = function.apply(templateId);
if (ObjectUtils.isEmpty(alarmTemplate)) {
throw new AlarmException(400, "未发现告警配置模板");
}
return alarmTemplate;
}
}
@RequiredArgsConstructor
public class JdbcAlarmTemplateProvider extends BaseAlarmTemplateProvider {
private final Function<String, AlarmTemplate> function;
@Override
AlarmTemplate getAlarmTemplate(String templateId) {
AlarmTemplate alarmTemplate = function.apply(templateId);
if (ObjectUtils.isEmpty(alarmTemplate)) {
throw new AlarmException(400, "未发现告警配置模板");
}
return alarmTemplate;
}
}
Both classes have the Function<String, AlarmTemplate> interface, which is a functional interface, which can be used by the outside to implement logic.
5. Alarm sending
5.1. AlarmFactoryExecute
This class holds a container inside, which is mainly used to cache the real sending class
public class AlarmFactoryExecute {
private static List<AlarmWarnService> serviceList = new ArrayList<>();
public AlarmFactoryExecute(List<AlarmWarnService> alarmLogWarnServices) {
serviceList = alarmLogWarnServices;
}
public static void addAlarmLogWarnService(AlarmWarnService alarmLogWarnService) {
serviceList.add(alarmLogWarnService);
}
public static List<AlarmWarnService> getServiceList() {
return serviceList;
}
public static void execute(NotifyMessage notifyMessage) {
for (AlarmWarnService alarmWarnService : getServiceList()) {
alarmWarnService.send(notifyMessage);
}
}
}
5.2. AlarmWarnService
Abstract interface that only provides a method to send
public interface AlarmWarnService {
/**
* 发送信息
*
* @param notifyMessage message
*/
void send(NotifyMessage notifyMessage);
}
5.3. BaseWarnService
The same routine as the abstract template providerAlarmTemplateProvider
, this interface has an abstract implementation classBaseWarnService
, which exposes the send method externally for sending messages, and uses doSendMarkdown and doSendText internally. The method implements the specific sending logic. Of course, the specific sending logic must be implemented by its subclasses.
@Slf4j
public abstract class BaseWarnService implements AlarmWarnService {
@Override
public void send(NotifyMessage notifyMessage) {
if (notifyMessage.getMessageTye().equals(MessageTye.TEXT)) {
CompletableFuture.runAsync(() -> {
try {
doSendText(notifyMessage.getMessage());
} catch (Exception e) {
log.error("send text warn message error", e);
}
});
} else if (notifyMessage.getMessageTye().equals(MessageTye.MARKDOWN)) {
CompletableFuture.runAsync(() -> {
try {
doSendMarkdown(notifyMessage.getTitle(), notifyMessage.getMessage());
} catch (Exception e) {
log.error("send markdown warn message error", e);
}
});
}
}
/**
* 发送Markdown消息
*
* @param title Markdown标题
* @param message Markdown消息
* @throws Exception 异常
*/
protected abstract void doSendMarkdown(String title, String message) throws Exception;
/**
* 发送文本消息
*
* @param message 文本消息
* @throws Exception 异常
*/
protected abstract void doSendText(String message) throws Exception;
}
5.4. DingTalkWarnService
Mainly implements the logic of Dingding sending alarm information
@Slf4j
public class DingTalkWarnService extends BaseWarnService {
private static final String ROBOT_SEND_URL = "https://oapi.dingtalk.com/robot/send?access_token=";
private final String token;
private final String secret;
public DingTalkWarnService(String token, String secret) {
this.token = token;
this.secret = secret;
}
public void sendRobotMessage(DingTalkSendRequest dingTalkSendRequest) throws Exception {
String json = JSONUtil.toJsonStr(dingTalkSendRequest);
String sign = getSign();
String body = HttpRequest.post(sign).contentType(ContentType.JSON.getValue()).body(json).execute().body();
log.info("钉钉机器人通知结果:{}", body);
}
/**
* 获取签名
*
* @return 返回签名
*/
private String getSign() throws Exception {
long timestamp = System.currentTimeMillis();
String stringToSign = timestamp + "\n" + secret;
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
byte[] signData = mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8));
return ROBOT_SEND_URL + token + "×tamp=" + timestamp + "&sign=" + URLEncoder.encode(new String(Base64.getEncoder().encode(signData)), StandardCharsets.UTF_8.toString());
}
@Override
protected void doSendText(String message) throws Exception {
DingTalkSendRequest param = new DingTalkSendRequest();
param.setMsgtype(DingTalkSendMsgTypeEnum.TEXT.getType());
param.setText(new DingTalkSendRequest.Text(message));
sendRobotMessage(param);
}
@Override
protected void doSendMarkdown(String title, String message) throws Exception {
DingTalkSendRequest param = new DingTalkSendRequest();
param.setMsgtype(DingTalkSendMsgTypeEnum.MARKDOWN.getType());
DingTalkSendRequest.Markdown markdown = new DingTalkSendRequest.Markdown(title, message);
param.setMarkdown(markdown);
sendRobotMessage(param);
}
}
5.5. WorkWeXinWarnService
Mainly realizes the logic of sending enterprise WeChat alarm information
@Slf4j
public class WorkWeXinWarnService extends BaseWarnService {
private static final String SEND_MESSAGE_URL = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=%s";
private final String key;
private final String toUser;
public WorkWeXinWarnService(String key, String toUser) {
this.key = key;
this.toUser = toUser;
}
private String createPostData(WorkWeXinSendMsgTypeEnum messageTye, String contentValue) {
WorkWeXinSendRequest wcd = new WorkWeXinSendRequest();
wcd.setMsgtype(messageTye.getType());
List<String> toUsers = Arrays.asList("@all");
if (StringUtils.isNotEmpty(toUser)) {
String[] split = toUser.split("\\|");
toUsers = Arrays.asList(split);
}
if (messageTye.equals(WorkWeXinSendMsgTypeEnum.TEXT)) {
WorkWeXinSendRequest.Text text = new WorkWeXinSendRequest.Text(contentValue, toUsers);
wcd.setText(text);
} else if (messageTye.equals(WorkWeXinSendMsgTypeEnum.MARKDOWN)) {
WorkWeXinSendRequest.Markdown markdown = new WorkWeXinSendRequest.Markdown(contentValue, toUsers);
wcd.setMarkdown(markdown);
}
return JSONUtil.toJsonStr(wcd);
}
@Override
protected void doSendText(String message) {
String data = createPostData(WorkWeXinSendMsgTypeEnum.TEXT, message);
String url = String.format(SEND_MESSAGE_URL, key);
String resp = HttpRequest.post(url).body(data).execute().body();
log.info("send work weixin message call [{}], param:{}, resp:{}", url, data, resp);
}
@Override
protected void doSendMarkdown(String title, String message) {
String data = createPostData(WorkWeXinSendMsgTypeEnum.MARKDOWN, message);
String url = String.format(SEND_MESSAGE_URL, key);
String resp = HttpRequest.post(url).body(data).execute().body();
log.info("send work weixin message call [{}], param:{}, resp:{}", url, data, resp);
}
}
5.6. MailWarnService
Mainly implements email alert logic
@Slf4j
public class MailWarnService extends BaseWarnService {
private final String smtpHost;
private final String smtpPort;
private final String to;
private final String from;
private final String username;
private final String password;
private Boolean ssl = true;
private Boolean debug = false;
public MailWarnService(String smtpHost, String smtpPort, String to, String from, String username, String password) {
this.smtpHost = smtpHost;
this.smtpPort = smtpPort;
this.to = to;
this.from = from;
this.username = username;
this.password = password;
}
public void setSsl(Boolean ssl) {
this.ssl = ssl;
}
public void setDebug(Boolean debug) {
this.debug = debug;
}
@Override
protected void doSendText(String message) throws Exception {
Properties props = new Properties();
props.setProperty("mail.smtp.auth", "true");
props.setProperty("mail.transport.protocol", "smtp");
props.setProperty("mail.smtp.host", smtpHost);
props.setProperty("mail.smtp.port", smtpPort);
props.put("mail.smtp.ssl.enable", true);
Session session = Session.getInstance(props);
session.setDebug(false);
MimeMessage msg = new MimeMessage(session);
msg.setFrom(new InternetAddress(from));
for (String toUser : to.split(",")) {
msg.setRecipient(MimeMessage.RecipientType.TO, new InternetAddress(toUser));
}
Map<String, String> map = JSONUtil.toBean(message, Map.class);
msg.setSubject(map.get("subject"), "UTF-8");
msg.setContent(map.get("content"), "text/html;charset=UTF-8");
msg.setSentDate(new Date());
Transport transport = session.getTransport();
transport.connect(username, password);
transport.sendMessage(msg, msg.getAllRecipients());
transport.close();
}
@Override
protected void doSendMarkdown(String title, String message) throws Exception {
log.warn("暂不支持发送Markdown邮件");
}
}
6, AlarmAutoConfiguration automatic assembly class
Use the springboot custom starter, and then configure the class under the configuration file under the packageMETA-INF
spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.seven.buttemsg.autoconfigure.AlarmAutoConfiguration
Autowiring class for loading custom beans
@Slf4j
@Configuration
public class AlarmAutoConfiguration {
// 邮件相关配置装载
@Configuration
@ConditionalOnProperty(prefix = MailConfig.PREFIX, name = "enabled", havingValue = "true")
@EnableConfigurationProperties(MailConfig.class)
static class MailWarnServiceMethod {
@Bean
@ConditionalOnMissingBean(MailWarnService.class)
public MailWarnService mailWarnService(final MailConfig mailConfig) {
MailWarnService mailWarnService = new MailWarnService(mailConfig.getSmtpHost(), mailConfig.getSmtpPort(), mailConfig.getTo(), mailConfig.getFrom(), mailConfig.getUsername(), mailConfig.getPassword());
mailWarnService.setSsl(mailConfig.getSsl());
mailWarnService.setDebug(mailConfig.getDebug());
AlarmFactoryExecute.addAlarmLogWarnService(mailWarnService);
return mailWarnService;
}
}
// 企业微信相关配置装载
@Configuration
@ConditionalOnProperty(prefix = WorkWeXinConfig.PREFIX, name = "enabled", havingValue = "true")
@EnableConfigurationProperties(WorkWeXinConfig.class)
static class WorkWechatWarnServiceMethod {
@Bean
@ConditionalOnMissingBean(MailWarnService.class)
public WorkWeXinWarnService workWechatWarnService(final WorkWeXinConfig workWeXinConfig) {
return new WorkWeXinWarnService(workWeXinConfig.getKey(), workWeXinConfig.getToUser());
}
@Autowired
void setDataChangedListener(WorkWeXinWarnService workWeXinWarnService) {
AlarmFactoryExecute.addAlarmLogWarnService(workWeXinWarnService);
}
}
// 钉钉相关配置装载
@Configuration
@ConditionalOnProperty(prefix = DingTalkConfig.PREFIX, name = "enabled", havingValue = "true")
@EnableConfigurationProperties(DingTalkConfig.class)
static class DingTalkWarnServiceMethod {
@Bean
@ConditionalOnMissingBean(DingTalkWarnService.class)
public DingTalkWarnService dingTalkWarnService(final DingTalkConfig dingtalkConfig) {
DingTalkWarnService dingTalkWarnService = new DingTalkWarnService(dingtalkConfig.getToken(), dingtalkConfig.getSecret());
AlarmFactoryExecute.addAlarmLogWarnService(dingTalkWarnService);
return dingTalkWarnService;
}
}
// 消息模板配置装载
@Configuration
@ConditionalOnProperty(prefix = TemplateConfig.PREFIX, name = "enabled", havingValue = "true")
@EnableConfigurationProperties(TemplateConfig.class)
static class TemplateConfigServiceMethod {
@Bean
@ConditionalOnMissingBean
public AlarmTemplateProvider alarmTemplateProvider(TemplateConfig templateConfig) {
if (TemplateSource.FILE == templateConfig.getSource()) {
return new YamlAlarmTemplateProvider(templateConfig);
} else if (TemplateSource.JDBC == templateConfig.getSource()) {
// 数据库(如mysql)读取文件,未实现,可自行扩展
return new JdbcAlarmTemplateProvider(templateId -> null);
} else if (TemplateSource.MEMORY == templateConfig.getSource()) {
// 内存(如redis,本地内存)读取文件,未实现,可自行扩展
return new MemoryAlarmTemplateProvider(templateId -> null);
}
return new YamlAlarmTemplateProvider(templateConfig);
}
}
@Bean
public AlarmAspect alarmAspect(@Autowired(required = false) AlarmTemplateProvider alarmTemplateProvider) {
return new AlarmAspect(alarmTemplateProvider);
}
}
4. Summary
Mainly rely on the aspect technology of spring and the automatic assembly principle of springboot to realize the sending alarm logic. There is no intrusion to the business code, and the pluggable function can be realized only by marking the annotation on the business code, which is relatively lightweight.
5. Reference source code
编程文档:
https://gitee.com/cicadasmile/butte-java-note
应用仓库:
https://gitee.com/cicadasmile/butte-flyer-parent
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。