java SpringBoot系统异常告警推送飞书

本文介绍了如何通过Java编程实现一个自定义机器人,通过Webhook机制将系统异常日志(包括时间、IP、日志消息和CURL请求)推送到飞书群组,便于快速发现和处理问题。作者详细展示了配置步骤和关键代码片段,包括MDCFilter、LogUniqueIdConverter和飞书Appender的实现。
摘要由CSDN通过智能技术生成

先看下效果,我主要做了时间,ip,日志消息和 curl 等内容的推送,方便我们系统出现异常可以及时发现及处理。

第一步:邀请自定义机器人入群

       进入你的目标群组,打开会话设置,找到群机器人,并点击添加机器人,选择自定义机器人加入群聊

修改机器人基本信息

第二步:配置 webhook

你会获取该机器人的 webhook 地址,格式如下:

第三步:调用webhook发送消息

       用任意方式向该 webhook 发起 HTTP POST 请求,即可向这个自定义机器人所在的群聊发送消息。

如请求成功,返回体为:

{
	"Extra": null,
	"StatusCode": 0,
	"StatusMessage": "success"
}

接下来就是 java 代码的编写

这里我的思路是把每条异常的日志加上 id通过 logstash推送到 ES,这样在消息推送到飞书群就可以通过查看详情的按钮请求到完整的异常堆栈,不会显得消息太臃肿。

(1)添加一个 filter,在 filter里面把我们需要的 logId加到每个 request,

@Component
public class MDCFilter implements Filter {

    private static final String LOG_ID = "logId";


    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        MDC.put(LOG_ID, UUID.randomUUID().toString().trim().replaceAll("-", "").substring(0, 16));
        try {
            // 将HttpServletRequest实例存储在ThreadLocal中
            if (servletRequest instanceof HttpServletRequest) {
                FlyBookAppender.setHttpServletRequest((HttpServletRequest) servletRequest);
            }
            chain.doFilter(servletRequest, response);
        } finally {
            FlyBookAppender.clearHttpServletRequest();
            OrderByInterceptor.clearSqlHolder();
            MDC.clear();
        }
    }

    @Override
    public void destroy() {
    }

}

(2)新增一个LogUniqueIdConverter,在ILoggingEvent中让他获取到logId, 这样这个 logId就可以贯穿整个请求,在后面可以为我们使用。

public class LogUniqueIdConverter extends ClassicConverter {

    @Override
    public String convert(ILoggingEvent event) {
        return event.getMDCPropertyMap().get("logId");
    }
}

(3)在 logback-spring.xml中去到这个 logId 并且拼到日志等级为 ERROR 的日志中一起推送到ES

这样带着 logId的错误日志将会被我们推送到 ES

(4)异常消息推送到飞书的具体操作

我的思路是通过继承AppenderBase<ILoggingEvent>对异常日志进行操作,完整代码如下

@Setter
@Component
public class FlyBookAppender extends AppenderBase<ILoggingEvent> {

    // 这个是机器人的webhook地址,我放在 yml 了
    private String alertUrl;

    @Value("${server.ip}")
    private String ip;

    @Value("${server.port}")
    private String port;

    private static String addressId;

    private static String addressPort;

    @PostConstruct
    public void init() {
        addressId = this.ip;
        addressPort = this.port;
    }

    private static ThreadLocal<HttpServletRequest> requestHolder = new ThreadLocal<>();

    public static void setHttpServletRequest(HttpServletRequest request) {
        requestHolder.set(request);
    }

    public static HttpServletRequest getHttpServletRequest() {
        return requestHolder.get();
    }

    public static void clearHttpServletRequest() {
        requestHolder.remove();
    }


    @Override
    protected void append(ILoggingEvent event) {

        String logId = event.getMDCPropertyMap().get("logId");
        String date = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"));
        String message = event.getFormattedMessage();
        Throwable throwable = event.getThrowableProxy() != null ? ((ThrowableProxy) event.getThrowableProxy()).getThrowable() : null;
        String exceptionType = null;
        // 业务异常不推送
        if (throwable != null) {
            if (throwable instanceof BizException) {
                exceptionType = ((BizException) throwable).getCode();
            }
        }
        if ("BIZ_EXCEPTION".equals(exceptionType) || StringUtils.isBlank(logId) || !Level.ERROR.equals(event.getLevel())) {
            return;
        }
        if (event.getLoggerName().contains("aop.LogRecordInterceptor") || (StrUtil.isNotBlank(message) && message.contains("log record execute"))) {
            return;
        }
        String sql = throwable instanceof SQLException ? OrderByInterceptor.getSql() : "/";
        HttpServletRequest request = getHttpServletRequest();
        String curlCommand = getCurlCommand(request);
        String requestId = request != null ? IpUtils.getIP(request) : null;

        // 异步推送错误信息
        CompletableFuture.runAsync(() -> pushToFlyBook(logId, date, message, throwable, sql, requestId, curlCommand));
    }

    private void pushToFlyBook(String logId, String date, String message, Throwable throwable, String sql, String requestId, String curlCommand) {
        String hostIp = IpUtils.getHostIp();
        String imgKey = getFlyBookImgKey();
        String ipAddr = AddressUtils.getRealAddressByIP(requestId);
        String stack = "无";
        StringWriter stringWriter = new StringWriter();
        throwable.printStackTrace(new PrintWriter(stringWriter, true));
        stack = stringWriter.toString();

        FlyBookCardMessage cardMessage = new FlyBookCardMessage(new FlyBookCardMessageContent(
                FlyBookCardMessageConfig.DEFAULT,
                new FlyBookCardMessageHeader(
                        new FlyBookCardMessageHeaderTitle("错误日志告警"),
                        FlyBookCardMessageHeader.ERROR
                ),
                Lists.newArrayList(
                        new FlyBookCardMessageImgElement(
                                new FlyBookCardMessageTextElement("", "plain_text"), imgKey, "img"
                        ),
                        new FlyBookCardMessageTextElement(
                                String.format("%s             [%s]", date, logId), "markdown"
                        ),
                        new FlyBookCardMessageTextElement(
                                String.format("%s", ipAddr), "markdown"
                        ),
                        new FlyBookCardMessageTextElement(
                                String.format("%s", hostIp), "markdown"
                        ),
                        new FlyBookCardMessageTextElement(
                                "**日志消息:**" + message, "markdown"
                        ),
                        new FlyBookCardMessageTextElement(
                                "**sql:**" + sql, "markdown"
                        ),
                        new FlyBookCardMessageTextElement(
                                "**异常堆栈:**" + stack.substring(0, 100), "markdown"
                        ),
                        new FlyBookCardMessageTextElement(
                                "**curl:**" + curlCommand, "markdown"
                        ),
                        new FlyBookCardMessageButtonElement(
                                "action", logId)
                )
        ));
        HttpRequest.post(this.alertUrl).body(JSONUtil.parseObj(cardMessage).toString(), "application/json;charset=UTF-8").execute().body();
    }

    public static String getFlyBookImgKey() {
        ILbpAttachmentService lbpAttachmentService = SpringContextUtil.getBean(ILbpAttachmentService.class);
        LbpAttachmentVO attachmentVO = lbpAttachmentService.getRandomDataByBizName("flyBookImg");
        if (attachmentVO == null) {
            return null;
        }
        return attachmentVO.getBizId();
    }

    @Data
    static abstract class FlyBookBootMessage implements Serializable {
        private final String msg_type;
    }

    @EqualsAndHashCode(callSuper = true)
    @Getter
    static class FlyBookCardMessage extends FlyBookBootMessage {
        private final FlyBookCardMessageContent card;

        public FlyBookCardMessage(FlyBookCardMessageContent card) {
            super("interactive");
            this.card = card;
        }
    }

    @Data
    static class FlyBookCardMessageContent implements Serializable {
        private final FlyBookCardMessageConfig config;
        private final FlyBookCardMessageHeader header;
        private final List<FlyBookCardMessageElement> elements;
    }

    @Data
    @AllArgsConstructor
    static class FlyBookCardMessageConfig implements Serializable {
        public static final FlyBookCardMessageConfig DEFAULT = new FlyBookCardMessageConfig(true, true);
        private boolean wideScreenMode;
        private boolean enableForward;
    }

    @Data
    static class FlyBookCardMessageHeader implements Serializable {
        public static final String ERROR = "red";
        public static final String WARNING = "orange";
        public static final String SUCCESS = "green";
        public static final String PRIMARY = "blue";
        public static final String GREY = "grey";
        private final FlyBookCardMessageHeaderTitle title;
        private final String template;
    }

    @Data
    static class FlyBookCardMessageHeaderTitle implements Serializable {
        private final String tag = "plain_text";
        private final String content;
    }

    @Data
    static abstract class FlyBookCardMessageElement implements Serializable {
        private final String tag;
    }


    @Getter
    @EqualsAndHashCode(callSuper = true)
    static class FlyBookCardMessageTextElement extends FlyBookCardMessageElement {

        private final String content;

        public FlyBookCardMessageTextElement(String content, String tag) {
            super(tag);
            this.content = content;
        }
    }

    /**
     * 图片
     */
    @Getter
    @EqualsAndHashCode(callSuper = true)
    static class FlyBookCardMessageImgElement extends FlyBookCardMessageElement {

        private final String img_key;

        private final FlyBookCardMessageTextElement alt;

        public FlyBookCardMessageImgElement(FlyBookCardMessageTextElement alt, String imgKey, String tag) {
            super(tag);
            this.img_key = imgKey;
            this.alt = alt;
        }
    }

    /**
     * 按钮
     */
    @Getter
    @EqualsAndHashCode(callSuper = true)
    static class FlyBookCardMessageButtonElement extends FlyBookCardMessageElement {

        private final List<FlyBookCardMessageActionsElement> actions;

        public FlyBookCardMessageButtonElement(String tag, String logId) {
            super(tag);
            String ipAddr = String.format("%s:%s", addressId, addressPort);
            if ("xx.xx.xx.xx".equals(addressId) || "xx.xx.xx.xx".equals(addressId)) {
                ipAddr = addressId;
            }
            this.actions = new ArrayList<>();
            actions.add(new FlyBookCardMessageActionsElement("button",
                    new FlyBookCardMessageTextElement("查看详情", "plain_text"),
                    "primary", String.format("http://%s/xx/xx/xx/getLogFromEs?logId=%s", ipAddr, logId)));
        }
    }

    @Data
    static class FlyBookCardMessageActionsElement {
        private final String tag;
        private final FlyBookCardMessageTextElement text;
        private final String type;
        private final String url;

        public FlyBookCardMessageActionsElement(String tag, FlyBookCardMessageTextElement text, String type, String url) {
            this.tag = tag;
            this.text = text;
            this.type = type;
            this.url = url;
        }
    }

    private String getCurlCommand(HttpServletRequest request) {
        String requestBody = null;
        StringBuilder stringBuilder = new StringBuilder();
        String line;
        try {
            BufferedReader reader = request.getReader();
            while ((line = reader.readLine()) != null) {
                stringBuilder.append(line);
            }
            requestBody = stringBuilder.toString();
        } catch (Exception e) {
            return null;
        }

        List<String> paramsList = new ArrayList<>();
        for (Map.Entry<String, String[]> entry : request.getParameterMap().entrySet()) {
            String k = entry.getKey();
            paramsList.add(k + "=" + request.getParameterMap().get(k)[0]);
        }
        String method = request.getMethod() + " ";

        String params = String.join("&", paramsList);

        String url = request.getRequestURL().toString();

        String headers = Collections.list(request.getHeaderNames())
                .stream()
                .map(name -> "-H '" + name + ": " + request.getHeader(name) + "'" + "\n")
                .reduce("", (acc, item) -> acc + " " + item);
        String dataRaw = "--data-raw $'" + requestBody + "'";
        if (StrUtil.isNotBlank(params)) {
            url = url + "?" + params;
        }
        String curlCommand = "curl -X " + method + "'"  + url + "'" + "\n" + headers;
        if (StrUtil.isNotBlank(requestBody)) {
            curlCommand = curlCommand + dataRaw;
        }
        return curlCommand;
    }

}

(5)实现ApplicationListener重写onApplicationEvent把我们刚刚的FlyBookAppender添加到 Logger 里面

@Component
@Slf4j
@RequiredArgsConstructor
@EnableConfigurationProperties(FlyBookLoggerAppenderConfigProperties.class)
public class FlyBookLoggerAppenderInitializer implements ApplicationListener<ApplicationReadyEvent> {

    private final FlyBookLoggerAppenderConfigProperties properties;

    /**
     * Handle an application event.
     *
     * @param event the event to respond to
     */
    @Override
    public void onApplicationEvent(@NonNull ApplicationReadyEvent event) {
        if (!properties.getEnabled()) {
            return;
        }
        // 添加飞书日志Appender
        LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
        FlyBookAppender flyBookAppender = new FlyBookAppender();
        flyBookAppender.setName("FLY_BOOK");
        flyBookAppender.setAlertUrl(this.properties.getAlertUrl());
        flyBookAppender.setContext(loggerContext);
        flyBookAppender.start();

        Logger logger = loggerContext.getLogger(Logger.ROOT_LOGGER_NAME);
        logger.addAppender(flyBookAppender);
    }
}

这样就可以实现往飞书群机器人推送我们系统的异常消息

  • 11
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值