利用AOP实现系统告警的方法详解
2022-09-19 09:41:21 来源:易采站长站 作者:
目录一、业务背景二、告警的方式1、钉钉告警2、企业微信告警3、邮件告警三、源码解析1、Alarm自定义注解2、配置文件分析3、核心AOP分析4、模板提供器5、告警发送6、AlarmAutoConfig...
目录
一、业务背景二、告警的方式
1、钉钉告警
2、企业微信告警
3、邮件告警
三、源码解析
1、Alarm自定义注解
2、配置文件分析
3、核心AOP分析
4、模板提供器
5、告警发送
6、AlarmAutoConfiguration自动装配类
四、总结
一、业务背景
在开发的过程中会遇到各种各样的开发问题,服务器宕机、网络抖动、代码本身的bug等等。针对代码的bug,我们可以提前预支,通过发送告警信息来警示我们去干预,尽早处理。
二、告警的方式
1、钉钉告警
通过在企业钉钉群,添加群机器人的方式,通过机器人向群内发送报警信息。至于钉钉机器人怎么创建,发送消息的api等等,请参考官方文档
2、企业微信告警
同样的套路,企业微信也是,在企业微信群中,添加群机器人。通过机器人发送告警信息。具体请看官方文档
3、邮件告警
与上述不同的是,邮件是发送给个人的,当然也可以是批量发送,只实现了发送文本格式的方式,至于markdown格式,有待考察。邮件发送相对比较简单,这里就不展开赘述。
三、源码解析
1、Alarm自定义注解
@Target({Elemen编程tType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public@interfaceAlarm{
/**
*报警标题
*
*@returnString
*/
Stringtitle()default"";
/**
*发送报警格式:目前支持text,markdown
*@return
*/
MessageTyemessageType()defaultMessageTye.TEXT;
/**
*告警模板id
*@return
*/
StringtemplateId()default"";
/**
*成功是否通知:true-通知,false-不通知
*@return
*/
booleansuccessNotice()defaultfalse;
}
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
publicclassAlarmAspect{
privatefinalAlarmTemplateProvideralarmTemplateProvider;
privatefinalstaticStringERROR_TEMPLATE="\n\n<fontcolor=\"#F37335\">异常信息:</font>\n"+
"```Java\n"+
"#{[exception]}\n"+
"```\n";
privatefinalstaticStringTEXT_ERROR_TEMPLATE="\n异常信息:\n"+
"#{[exception]}";
privatefinalstaticStringMARKDOWN_TITLE_TEMPLATE="#【#{[title]}】\n"+
"\n请求状态:<font color=\"#{[stateColor]}\">#{[state]}</font>\n\n";
privatefinalstaticStringTEXT_TITLE_TEMPLATE="【#{[title]}】\n"+
"请求状态:#{[state]}\n";
@Pointcut("@annotation(alarm)")
publicvoidalarmPointcut(Alarmalarm){
}
@Around(value="alarmPointcut(alarm)",argNames="joinPoint,alarm")
publicObjectaround(ProceedingJoinPointjoinPoint,Alarmalarm)throwsThrowable{
Objectresult=joinPoint.proceed();
if(alarm.successNotice()){
StringtemplateId=alarm.templateId();
StringfileTemplateContent="";
if(Objects.nonNull(alarmTemplateProvider)){
AlarmTemplatealarmTemplate=alarmTemplateProvider.loadingAlarmTemplate(templateId);
fileTemplateContent=alarmTemplate.getTemplateContent();
}
StringtemplateContent="";
MessageTyemessageTye=alarm.messageType();
if(messageTye.equals(MessageTye.TEXT)){
templateContent=TEXT_TITLE_TEMPLATE.concat(fileTemplateContent);
}elseif(messageTye.equals(MessageTye.MARKDOWN)){
templateContent=MARKDOWN_TITLE_TEMPLATE.concat(fileTemplateContent);
}
Map<String,Object>alarmParamMap=newHashMap<>();
alarmParamMap.put("title",alarm.title());
alarmParamMap.put("stateColor","#45B649");
alarmParamMap.put("state","成功");
sendAlarm(alarm,templateContent,alarmParamMap);
}
returnresult;
}
@AfterThrowing(pointcut="alarmPointcut(alarm)",argNames="joinPoint,alarm,e",throwing="e")
publicvoiddoAfterThrow(JoinPointjoinPoint,Alarmalarm,Exceptione){
log.info("请求接口发生异常:[{}]",e.getMessage());
StringtemplateId=alarm.templateId();
//加载模板中配置的内容,若有
StringtemplateContent="";
StringfileTemplateContent="";
if(Objects.nonNull(alarmTemplateProvider)){
AlarmTemplatealarmTemplate=alarmTemplateProvider.loadingAlarmTemplate(templateId);
fileTemplateContent=alarmTemplate.getTemplateContent();
}
MessageTyemessageTye=alarm.messageType();
if(messageTye.equals(MessageTye.TEXT)){
templateContent=TEXT_TITLE_TEMPLATE.concat(fileTemplateContent).concat(TEXT_ERROR_TEMPLATE);
}elseif(messageTye.equals(MessageTye.MARKDOWN)){
templateContent=MARKDOWN_TITLE_TEMPLATE.concat(fileTemplateContent).concat(ERROR_TEMPLATE);
}
Map<String,Object>alarmParamMap=newHashMap<>();
alarmParamMap.put("title",alarm.title());
alarmParamMap.put("stateColor","#FF4B2B");
alarmParamMap.put("state","失败");
alarmParamMap.put("exception",ExceptionUtil.stacktraceToString(e));
sendAlarm(alarm,templateContent,alarmParamMap);
}
privatevoidsendAlarm(Alarmalarm,StringtemplateContent,Map<String,Object>alarmParamMap){
ExpressionParserparser=newSpelExpressionParser();
TemplateParserContextparserContext=newTemplateParserContext();
Stringmessage=parser.parseExpression(templateContent,parserContext).getValue(alarmParamMap,String.class);
Mess编程ageTyemessageTye=alarm.messageType();
NotifyMessagenotifyMessage=newNotifyMessage();
notifyMessage.setTitle(alarm.title());
notifyMessage.setMessageTye(messageTye);
notifyMessage.setMessage(message);
AlarmFactoryExecute.execute(notifyMessage);
}
}
4、模板提供器
4.1、AlarmTemplateProvider
定义一个抽象接口AlarmTemplateProvider,用于被具体的子类实现
publicinterfaceAlarmTemplateProvider{
/**
*加载告警模板
*
*@paramtemplateId模板id
*@returnAlarmTemplate
*/
AlarmTemplateloadingAlarmTemplate(StringtemplateId);
}
4.2、BaseAlarmTemplateProvider
抽象类BaseAlarmTemplateProvider实现该抽象接口
publicabstractclassBaseAlarmTemplateProviderimplementsAlarmTemplateProvider{
@Override
publicAlarmTemplateloadingAlarmTemplate(StringtemplateId){
if(StringUtils.isEmpty(templateId)){
thrownewAlarmException(400,"告警模板配置id不能为空");
}
returngetAlarmTemplate(templateId);
}
/**
*查询告警模板
*
*@paramtemplateId模板id
*@returnAlarmTemplate
*/
abstractAlarmTemplategetAlarmTemplate(StringtemplateId);
}
4.3、YamlAlarmTemplateProvider
具体实现类YamlAlarmTemplateProvider,实现从配置文件中读取模板,该类在项目启动时,会被加载进spring的bean容器
@RequiredArgsConstructor
publicclassYamlAlarmTemplateProviderextendsBaseAlarmTemplateProvider{
privatefinalTemplateConfigtemplateConfig;
@Override
AlarmTemplategetAlarmTemplate(StringtemplateId){
Map<String,AlarmTemplate>configTemplates=templateConfig.getTemplates();
AlarmTemplatealarmTemplate=configTemplates.get(templateId);
if(ObjectUtils.isEmpty(alarmTemplate)){
thrownewAlarmException(400,"未发现告警配置模板");
}
returnalarmTemplate;
}
}
4.4、MemoryAlarmTemplateProvider和JdbcAlarmTemplateProvider
抽象类BaseAlarmTemplateProvider还有其他两个子类,分别是MemoryAlarmTemplateProvider和JdbcAlarmTemplateProvider。但是这两个子类暂时还未实现逻辑,后续可以自行扩展。
@RequiredArgsConstructor
publicclassMemoryAlarmTemplateProviderextendsBaseAlarmTemplateProvider{
privatefinalFunction<String,AlarmTemplate>function;
@Override
AlarmTemplategetAlarmTemplate(StringtemplateId){
AlarmTemplatealarmTemplate=function.apply(templateId);
if(ObjectUtils.isEmpty(alarmTemplate)){
thrownewAlarmException(400,"未发现告警配置模板");
}
returnalarmTemplate;
}
}
@RequiredArgsConstructor
publicclassJdbcAlarmTemplateProviderextendsBaseAlarmTemplateProvider{
privatefinalFunction<String,AlarmTemplate>function;
@Override
AlarmTemplategetAlarmTemplate(StringtemplateId){
AlarmTemplatealarmTemplate=function.apply(templateId);
if(ObjectUtils.isEmpty(alarmTemplate)){
thrownewAlarmException(400,"未发现告警配置模板");
}
returnalarmTemplate;
}
}
两个类中都有Function<String, AlarmTemplate>接口,为函数式接口,可以供外部自行去实现逻辑。
5、告警发送
5.1、AlarmFactoryExecute
该类内部保存了一个容器,主要用于缓存真正的发送类
publicclassAlarmFactoryExecute{
privatestaticList<AlarmWarnService>serviceList=newArrayList<>();
publicAlarmFactoryExecute(List<AlarmWarnService>alarmLogWarnServices){
serviceList=alarmLogWarnServices;
}
publicstaticvoidaddAlarmLogWarnService(AlarmWarnServicealarmLogWarnService){
serviceList.add(alarmLogWarnService);
}
publicstaticList<AlarmWarnService>getServiceList(){
returnserviceList;
}
publicstaticvoidexecute(NotifyMessagenotifyMessage){
for(AlarmWarnServicealarmWarnService:getServiceList()){
alarmWarnService.send(notifyMessage);
}
}
}
5.2、AlarmWarnService
抽象接口,只提供一个发送的方法
publicinterfaceAlarmWarnService{
/**
*发送信息
*
*@paramnotifyMessagemessage
*/
voidsend(NotifyMessagenotifyMessage);
}
5.3、BaseWarnService
与抽象的模板提供器AlarmTemplateProvider一样的套路,该接口有一个抽象的实现类BaseWarnService,该类对外暴露send方法,用于发送消息,内部用DOSendMarkdown,doSendText方法实现具体的发送逻辑,当然具体发送逻辑还是得由其子类去实现。
@Slf4j
publicabstractclassBaseWarnServiceimplementsAlarmWarnService{
@Override
publicvoidsend(NotifyMessagenotifyMessage){
if(notifyMessage.getMessageTye().equals(MessageTye.TEXT)){
CompletableFuture.runAsync(()->{
try{
doSendText(notifyMessage.getMessage());
}catch(Exceptione){
log.error("sendtextwarnmessageerror",e);
}
});
}elseif(notifyMessage.getMessageTye().equals(MessageTye.MARKDOWN)){
CompletableFuture.runAsync(()->{
try{
doSendMarkdown(notifyMessage.getTitle(),notifyMessage.getMessage());
}catch(Exceptione){
log.error("sendmarkdownwarnmessageerror",e);
}
});
}
}
/**
*发送Markdown消息
*
*@paramtitleMarkdown标题
*@parammessageMarkdown消息
*@throwsException异常
*/
protectedabstractvoiddoSendMarkdown(Stringtitle,Stringmessage)throwsException;
/**
*发送文本消息
*
*@parammessage文本消息
*@throwsException异常
*/
protectedabstractvoiddoSendText(Stringmessage)throwsException;
}
5.4、DingTalkWarnService
主要实现了钉钉发送告警信息的逻辑
@Slf4j
publicclassDingTalkWarnServiceextendsBaseWarnService{
privatestaticfinalStringROBOT_SEND_URL="https://oapi.dingtalk.com/robot/send?Access_token=";
privatefinalStringtoken;
privatefinalStringsecret;
publicDingTalkWarnService(Stringtoken,Stringsecret){
this.token=token;
this.secret=secret;
}
publicvoidsendRobotMessage(DingTalkSendRequestdingTalkSendRequest)throwsException{
Stringjson=JSONUtil.toJsonStr(dingTalkSendRequest);
Stringsign=getSign();
Stringbody=HttpRequest.post(sign).contentType(ContentType.JSON.getValue()).body(json).execute().body();
log.info("钉钉机器人通知结果:{}",body);
}
/**
*获取签名
*
*@return返回签名
*/
privateStringgetSign()throwsException{
longtimestamp=System.currentTimeMillis();
StringstringToSign=timestamp+"\n"+secret;
MACmac=Mac.getInstance("HmacSHA256");
mac.init(newSecretKeySpec(secret.getBytes(StandardCharsets.UTF_8),"HmacSHA256"));
byte[]signData=mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8));
returnROBOT_SEND_URL+token+"×tamp="+timestamp+"&sign="+URLEncoder.encode(newString(Base64.getEncoder().encode(signData)),StandardCharsets.UTF_8.toString());
}
@Override
protectedvoiddoSendText(Stringmessage)throwsException{
DingTalkSendRequestparam=newDingTalkSendRequest();
param.setMsgtype(DingTalkSendMsgTypeEnum.TEXT.getType());
param.setText(newDingTalkSendRequest.Text(message));
sendRobotMessage(param);
}
@Override
protectedvoiddoSendMarkdown(Stringtitle,Stringmessage)throwsException{
DingTalkSendRequestparam=newDingTalkSendRequest();
param.setMsgtype(DingTalkSendMsgTypeEnum.MARKDOWN.getType());
DingTalkSendRequest.Markdownmarkdown=newDingTalkSendRequest.Markdown(title,message);
param.setMarkdown(markdown);
sendRobotMessage(param);
}
}
5.5、WorkWeXinWarnService
主要实现了发送企业微信告警信息的逻辑
@Slf4j
publicclassWorkWeXinWarnServiceextendsBaseWarnService{
privatestaticfinalStringSEND_MESSAGE_URL="https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=%s";
privatefinalStringkey;
privatefinalStringtoUser;
publicWorkWeXinWarnService(Stringkey,StringtoUser){
this.key=key;
this.toUser=toUser;
}
privateStringcreatePostData(WorkWeXinSendMsgTypeEnummessageTye,StringcontentValue){
WorkWeXinSendRequestwcd=newWorkWeXinSendRequest();
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.Texttext=newWorkWeXinSendRequest.Text(contentValue,toUsers);
wcd.setText(text);
}elseif(messageTye.equals(WorkWeXinSendMsgTypeEnum.MARKDOWN)){
WorkWeXinSendRequest.Markdownmarkdown=newWorkWeXinSendRequest.Markdown(contentValue,toUsers);
wcd.setMarkdown(markdown);
}
returnJSONUtil.toJsonStr(wcd);
}
@Override
protectedvoiddoSendText(Stringmessage){
Stringdata=createPostData(WorkWeXinSendMsgTypeEnum.TEXT,message);
Stringurl=String.format(SEND_MESSAGE_URL,key);
Stringresp=HttpRequest.post(url).body(data).execute().body();
log.info("sendworkweixinmessagecall[{}],param:{},resp:{}",url,data,resp);
}
@Override
protectedvoiddoSendMarkdown(Stringtitle,Stringmessage){
Stringdata=createPostData(WorkWeXinSendMsgTypeEnum.MARKDOWN,message);
Stringurl=String.format(SEND_MESSAGE_URL,key);
Stringresp=HttpRequest.post(url).body(data).execute().body();
log.info("sendworkweixinmessagecall[{}],param:{},resp:{}",url,data,resp);
}
}
5.6、MailWarnService
主要实现邮件告警逻辑
@Slf4j
publicclassMailWarnServiceextendsBaseWarnService{
privatefinalStringsmtpHost;
privatefinalStringsmtpPort;
privatefinalStringto;
privatefinalStringfrom;
privatefinalStringusername;
privatefinalStringpassword;
privateBooleanssl=true;
privateBooleandebug=false;
publicMailWarnService(StringsmtpHost,StringsmtpPort,Stringto,Stringfrom,Stringusername,Stringpassword){
this.smtpHost=smtpHost;
this.smtpPort=smtpPort;
this.to=to;
this.from=from;
this.username=username;
this.password=password;
}
publicvoidsetSsl(Booleanssl){
this.ssl=ssl;
}
publicvoidsetDebug(Booleandebug){
this.debug=debug;
}
@Override
protectedvoiddoSendText(Stringmessage)throwsException{
Propertiesprops=newproperties();
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);
Sessionsession=Session.getInstance(props);
session.setDebug(false);
MimeMessagemsg=newMimeMessage(session);
msg.setFrom(newInternetAddress(from));
for(StringtoUser:to.split(",")){
msg.setRecipient(MimeMessage.RecipientType.TO,newInternetAddress(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(newDate());
Transporttransport=session.getTransport();
transport.connect(username,password);
transport.sendMessage(msg,msg.getAllRecipients());
transport.close();
}
@Override
protectedvoiddoSendMarkdown(Stringtitle,Stringmessage)throwsException{
log.warn("暂不支持发送Markdown邮件");
}
}
6、AlarmAutoConfiguration自动装配类
运用了springboot自定义的starter,再META-INF包下的配置文件spring.factories下,配置上该类
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.seven.buttemsg.autoconfigure.AlarmAutoConfiguration
自动装配类,用于装载自定义的bean
@Slf4j
@Configuration
publicclassAlarmAutoConfiguration{
//邮件相关配置装载
@Configuration
@ConditionalOnProperty(prefix=MailConfig.PREFIX,name="enabled",havingValue="true")
@EnableConfigurationProperties(MailConfig.class)
staticclassMailWarnServiceMethod{
@Bean
@ConditionalOnMissingBean(MailWarnService.class)
publicMailWarnServicemailWarnService(finalMailConfigmailConfig){
MailWarnServicemailWarnService=newMailWarnService(mailConfig.getSmtpHost(),mailConfig.getSmtpPort(),mailConfig.getTo(),mailConfig.getFrom(),mailConfig.getUsername(),mailConfig.getPassword());
mailWarnService.setSsl(mailConfig.getSsl());
mailWarnService.setDebug(mailConfig.getDebug());
AlarmFactoryExecute.addAlarmLogWarnService(mailWarnService);
returnmailWarnService;
}
}
//企业微信相关配置装载
@Configuration
@ConditionalOnProperty(prefix=WorkWeXinConfig.PREFIX,name="enabled",havingValue="true")
@EnableConfigurationProperties(WorkWeXinConfig.class)
staticclassWorkWechatWarnServiceMethod{
@Bean
@ConditionalOnMissingBean(MailWarnService.class)
publicWorkWeXinWarnServiceworkWechatWarnService(finalWorkWeXinConfigworkWeXinConfig){
returnnewWorkWejavascriptXinWarnService(workWeXinConfig.getKey(),workWeXinConfig.getToUser());
}
@Autowired
voidsetDataChangedListener(WorkWeXinWarnServiceworkWeXinWarnService){
AlarmFactoryExecute.addAlarmLogWarnService(workWeXinWarnService);
}
}
//钉钉相关配置装载
@Configuration
@ConditionalOnProperty(prefix=DingTalkConfig.PREFIX,name="enabled",havingValue="true")
@EnableConfigurationProperties(DingTalkConfig.class)
staticclassDingTalkWarnServiceMethod{
@Bean
@ConditionalOnMissingBean(DingTalkWarnService.class)
publicDingTalkWarnServicedingTalkWarnService(finalDingTalkConfigdingtalkConfig){
DingTalkWarnServicedingTalkWarnService=newDingTalkWarnService(dingtalkConfig.getToken(),dingtalkConfig.getSecret());
AlarmFactoryExecute.addAlarmLogWarnService(dingTalkWarnService);
returndingTalkWarnService;
}
}
//消息模板配置装载
@Configuration
@ConditionalOnProperty(prefix=TemplateConfig.PREFIX,name="enabled",havingValue="true")
@EnableConfigurationProperties(TemplateConfig.class)
staticclassTemplateConfigServiceMethod{
@Bean
@ConditionalOnMissingBean
publicAlarmTemplateProvideralarmTemplateProvider(TemplateConfigtemplateConfig){
if(TemplateSource.FILE==templateConfig.getSource()){
returnnewYamlAlarmTemplateProvider(templateConfig);
}elseif(TemplateSource.JDBC==templateConfig.getSource()){
//数据库(如mysql)读取文件,未实现,可自行扩展
returnnewJdbcAlarmTemplateProvider(templateId->null);
}elseif(TemplateSource.MEMORY==templateConfig.getSource()){
//内存(如Redis,本地内存)读取文件,未实现,可自行扩展
returnnewMemoryAlarmTemplateProvider(templateId->null);
}
returnnewYamlAlarmTemplateProvider(templateConfig);
}
}
@Bean
publicAlarmAspectalarmAspect(@Autowired(required=false)AlarmTemplateProvideralarmTemplateProvider){
returnnewAlarmAspect(alarmTemplateProvider);
}
}
四、总结
主要借助spring的切面技术,以及springboot的自动装配原理,实现了发送告警逻辑。对业务代码无侵入,只需要在业务代码上标记注解,就可实现可插拔的功能,比较轻量。
以上就是利用AOP实现系统告警的方法详解的详细内容,更多关于AOP系统告警的资料请关注我们其它相关文章!













闽公网安备 35020302000061号