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

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

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

先看下效果,我主要做了时间,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);
    }
}

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

<think>嗯,用户想了解Excel中的VLOOKUP函数的用法和参数含义。首先,我需要回忆一下VLOOKUP的基本结构。VLOOKUP是垂直查找函数,用于在表格的首列查找指定的值,然后返回该行中指定列的数据。它的四个参数分别是查找值、表格范围、列索引号和匹配模式。 接下来,我得确认每个参数的具体作用。第一个参数是lookup_value,也就是用户要查找的值,比如产品ID或者姓名。第二个参数是table_array,即查找的范围,这里要注意的是,查找值必须位于这个范围的第一列,否则函数无法正确工作。第三个参数是col_index_num,也就是用户想要返回的数据在表格范围中的第几列,这个列数是从表格范围的第一列开始算起的,不是整个工作表的列。第四个参数是range_lookup,决定是精确匹配还是近似匹配,通常用FALSE或0来指定精确匹配,避免错误。 然后,用户可能需要一个具体的例子来理解如何使用。比如,假设有一个产品价格表,用户想根据产品ID查找对应的价格。这时候,VLOOKUP就可以在ID列查找,然后返回价格列的数据。同时,要提醒用户注意表格范围最好使用绝对引用,比如$A$2:$C$100,这样在拖动公式时范围不会变化。 可能遇到的问题也需要考虑。比如,如果查找值不在表格的第一列,或者列索引号超过了表格的列数,函数会返回错误。此外,数据格式不一致也可能导致问题,比如查找值是数字,而表格中是文本格式的数字,这时候需要统一格式。还有,当出现#N/A错误时,可能是查找值不存在,或者匹配模式设置不正确,需要检查参数是否正确。 用户可能还会问到如何避免常见的错误,比如部分数据匹配出错的情况。这时候需要检查是否有多余的空格,或者数据格式不一致,或者表格范围没有锁定导致拖动时偏移。此外,XLOOKUP作为VLOOKUP的升级版,可能在某些情况下更方便,比如可以向左查找或者返回多列数据,这也是可以提及的内容。 最后,整理步骤,从基础语法到参数说明,再到具体案例,逐步引导用户理解如何使用VLOOKUP函数。同时,提供常见错误和解决方法,帮助用户在实际应用中避免问题。如果有必要,还可以比较VLOOKUP和其他查找函数的区别,比如HLOOKUP或XLOOKUP,但根据用户的问题,主要集中在VLOOKUP本身即可。</think>### Excel VLOOKUP函数详解 #### 一、基础语法与参数说明 VLOOKUP函数的语法为: $$=VLOOKUP(lookup\_value, table\_array, col\_index\_num, [range\_lookup])$$ 包含4个参数: 1. **lookup_value**(必填):要查找的值(如单元格引用或具体值) 2. **table_array**(必填):包含数据的表格范围(必须包含查找列和返回列) 3. **col_index_num**(必填):返回值所在列的序号(从table_array第一列开始计数) 4. **range_lookup**(可选):匹配类型 - `TRUE`/`1`:近似匹配(默认值,需数据升序排列) - `FALSE`/`0`:精确匹配(常用选项) [^1][^2] #### 二、使用步骤演示(工资表查询案例) 假设需要根据员工编号查询工资: 1. 建立查询单元格(如`B12`) 2. 输入公式: ```excel =VLOOKUP(A12, $A$2:$D$100, 4, 0) ``` - `A12`:待查询的员工编号 - `$A$2:$D$100`:锁定数据区域(绝对引用) - `4`:返回第4列(工资列) - `0`:精确匹配 [^2][^3] #### 三、常见错误与解决方法 | 错误现象 | 原因 | 解决方案 | |---------|------|---------| | #N/A | 查找值不存在 | 检查数据源或改用`IFERROR`容错 | | #REF! | 列序号超出范围 | 确认col_index_num ≤ 表格列数 | | 部分匹配失败 | 数据格式不一致 | 统一数值/文本格式 | | 结果错位 | 表格未锁定 | 使用`$`符号固定区域引用 | [^3][^4] #### 四、进阶技巧 1. **多条件查询**: 使用辅助列合并多个条件字段 ```excel =VLOOKUP(A2&B2, $D$2:$F$100, 3, 0) ``` 2. **通配符匹配**: `"*"`匹配任意字符,`"?"`匹配单个字符 ```excel =VLOOKUP("张*", $A$2:$C$100, 3, 0) ``` 3. **跨表查询**: 引用其他工作表数据 ```excel =VLOOKUP(A2, Sheet2!$A$2:$D$100, 4, 0) ``` [^1][^4]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值