工作群里的音讯怕过于宁静,又怕过于频繁

一、业务背景

在开发的过程中会遇到各种各样的开发问题,服务器宕机、网络抖动、代码自身的bug等等。针对代码的bug,咱们能够提前预支,通过发送告警信息来警示咱们去干涉,尽早解决。

二、告警的形式

1、钉钉告警

通过在企业钉钉群,增加群机器人的形式,通过机器人向群内发送报警信息。至于钉钉机器人怎么创立,发送音讯的api等等,请参考官网文档

2、企业微信告警

同样的套路,企业微信也是,在企业微信群中,增加群机器人。通过机器人发送告警信息。具体请看官网文档

3、邮件告警

与上述不同的是,邮件是发送给集体的,当然也能够是批量发送,只实现了发送文本格式的形式,至于markdown格局,有待考查。邮件发送绝对比较简单,这里就不开展赘述。

三、源码解析

1、Alarm自定义注解

@Target({ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)@Documented@Inheritedpublic @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、注解字段解析

  1. title

告警音讯题目:能够定义为业务信息,如导师身份计算

  1. messageType

告警音讯展现类型:目前反对text文本类型,markdown类型

  1. templateId

音讯模板id:与配置文件中配置的模板id统一

  1. 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@RequiredArgsConstructorpublic 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容器
@RequiredArgsConstructorpublic 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还有其余两个子类,别离是MemoryAlarmTemplateProviderJdbcAlarmTemplateProvider。然而这两个子类临时还未实现逻辑,后续能够自行扩大。
@RequiredArgsConstructorpublic 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;    }}
@RequiredArgsConstructorpublic 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办法实现具体的发送逻辑,当然具体发送逻辑还是得由其子类去实现。
@Slf4jpublic 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

次要实现了钉钉发送告警信息的逻辑
@Slf4jpublic 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 + "&timestamp=" + 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

次要实现了发送企业微信告警信息的逻辑
@Slf4jpublic 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

次要实现邮件告警逻辑
@Slf4jpublic 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@Configurationpublic 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