共计 16922 个字符,预计需要花费 43 分钟才能阅读完成。
工作群里的音讯怕过于宁静,又怕过于频繁
一、业务背景
在开发的过程中会遇到各种各样的开发问题,服务器宕机、网络抖动、代码自身的 bug 等等。针对代码的 bug,咱们能够提前预支,通过发送告警信息来警示咱们去干涉,尽早解决。
二、告警的形式
1、钉钉告警
通过在企业钉钉群,增加群机器人的形式,通过机器人向群内发送报警信息。至于钉钉机器人怎么创立,发送音讯的 api 等等,请参考官网文档
2、企业微信告警
同样的套路,企业微信也是,在企业微信群中,增加群机器人。通过机器人发送告警信息。具体请看官网文档
3、邮件告警
与上述不同的是,邮件是发送给集体的,当然也能够是批量发送,只实现了发送文本格式的形式,至于 markdown 格局,有待考查。邮件发送绝对比较简单,这里就不开展赘述。
三、源码解析
1、Alarm 自定义注解
@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、注解应用
@Alarm
标记在办法上应用,被标记的办法产生异样,会依据配置,读取配置信息,发送异样堆栈信息。应用办法如下所示:
@Alarm(title = "某某业务告警", messageType = MessageTye.MARKDOWN, templateId = "errorTemp")
1.2、注解字段解析
- title
告警音讯题目:能够定义为业务信息,如导师身份计算
- messageType
告警音讯展现类型:目前反对 text 文本类型,markdown 类型
- templateId
音讯模板 id:与配置文件中配置的模板 id 统一
- successNotice
失常状况是否也须要发送告警信息,默认值是 fasle,示意不须要发送。当然,有些业务场景失常状况也须要发送,比方:领取出单告诉等。
2、配置文件剖析
2.1、钉钉配置文件
spring:
alarm:
dingtalk:
# 开启钉钉发送告警
enabled: true
# 钉钉群机器人惟一的 token
token: xxxxxx
# 平安设置:加签的密钥
secret: xxxxxxx
2.2、企业微信配置文件
spring:
alarm:
wechat:
# 开启企业微信告警
enabled: true
# 企业微信群机器人惟一 key
key: xxxxxdsf
# 被 @人的手机号
to-user: 1314243
2.3、邮件配置文件
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、自定义模板配置
spring:
alarm:
template:
# 开启通过模板配置
enabled: true
# 配置模板起源为文件
source: FILE
# 配置模板数据
templates:
errorTemp:
templateId: errorTemp
templateName: 服务异样模板
templateContent: 这里是配置模板的内容
spring:alarm:template:enabled
,Boolean 类型,示意开启告警音讯应用模板发送。spring:alarm:template:source
,模板起源,枚举类:JDBC(数据库)、FILE(配置文件)、MEMORY(内存),目前只反对 FILE,其余两种可自行扩大。-
spring:alarm:template:templates
,配置模板内容,是一个 map,errorTemp
是模板 id,须要应用哪种模板,就在@Alarm
中的 templateId 设置为对应配置文件中的 templateId。3、外围 AOP 剖析
3.1、原理剖析
3.2、自定义切面
@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、模板提供器
4.1、AlarmTemplateProvider
定义一个形象接口
AlarmTemplateProvider
,用于被具体的子类实现
public interface AlarmTemplateProvider {
/**
* 加载告警模板
*
* @param templateId 模板 id
* @return AlarmTemplate
*/
AlarmTemplate loadingAlarmTemplate(String templateId);
}
4.2、BaseAlarmTemplateProvider
抽象类
BaseAlarmTemplateProvider
实现该形象接口
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
具体实现类
YamlAlarmTemplateProvider
,实现从配置文件中读取模板,该类在我的项目启动时,会被加载进 spring 的 bean 容器
@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 和 JdbcAlarmTemplateProvider
抽象类
BaseAlarmTemplateProvider
还有其余两个子类,别离是MemoryAlarmTemplateProvider
和JdbcAlarmTemplateProvider
。然而这两个子类临时还未实现逻辑,后续能够自行扩大。
@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;
}
}
两个类中都有 Function<String, AlarmTemplate> 接口,为函数式接口,能够供内部自行去实现逻辑。
5、告警发送
5.1、AlarmFactoryExecute
该类外部保留了一个容器,次要用于缓存真正的发送类
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
形象接口,只提供一个发送的办法
public interface AlarmWarnService {
/**
* 发送信息
*
* @param notifyMessage message
*/
void send(NotifyMessage notifyMessage);
}
5.3、BaseWarnService
与形象的模板提供器
AlarmTemplateProvider
一样的套路,该接口有一个形象的实现类BaseWarnService
, 该类对外裸露 send 办法,用于发送音讯,外部用 doSendMarkdown,doSendText 办法实现具体的发送逻辑,当然具体发送逻辑还是得由其子类去实现。
@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
次要实现了钉钉发送告警信息的逻辑
@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
次要实现了发送企业微信告警信息的逻辑
@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
次要实现邮件告警逻辑
@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 主动拆卸类
使用了 springboot 自定义的 starter,再
META-INF
包下的配置文件spring.factories
下,配置上该类
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.seven.buttemsg.autoconfigure.AlarmAutoConfiguration
主动拆卸类,用于装载自定义的 bean
@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);
}
}
四、总结
次要借助 spring 的切面技术,以及 springboot 的主动拆卸原理,实现了发送告警逻辑。对业务代码无侵入,只须要在业务代码上标记注解,就可实现可插拔的性能,比拟轻量。
五、参考源码
编程文档:https://gitee.com/cicadasmile/butte-java-note
利用仓库:https://gitee.com/cicadasmile/butte-flyer-parent