头图
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

  1. title

Alert message title: can be defined as business information, such as tutor identity calculation

  1. messageType

Alarm message display type: currently supports text text type, markdown type

  1. templateId

Message template id: consistent with the template id configured in the configuration file

  1. 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 class BaseAlarmTemplateProvider has two other subclasses, namely MemoryAlarmTemplateProvider and JdbcAlarmTemplateProvider . 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 provider AlarmTemplateProvider , this interface has an abstract implementation class BaseWarnService , 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 + "&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

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 package META-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

已注销
479 声望53 粉丝