拦截error日志,并发送到钉钉群

本文介绍如何通过自定义Logback Appender拦截error级别日志,结合SpringBoot全局异常处理,将异常详情及请求参数推送至钉钉,提升问题定位效率。

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

       之前公司的错误日志都发送到邮箱中,但是邮箱存在响应延迟,造成问题解决不及时,结合我们正在使用的通信方式,发送到钉钉中效果会更好些。

       一般上,在开发过程中,像log4j2logback日志框架都提供了很多Appender,基本上可以满足大部分的业务需求了。但在一些特殊需求可以自定义Appender。本文主讲利用自定义Appender拦截error级别日志以及springboot的全局异常拦截,并且记录异常的请求参数,全部推送到钉钉群中,研发只需要根据钉钉群异常详情可以轻松定位问题内容。

    利用观察者模式,实现多个监听者可以同时接受到相同的错误信息,例如需要将异常体现在邮箱和钉钉中,本文主要讲错误信息推送到钉钉中。

自定义Appender

import ch.qos.logback.classic.spi.LoggingEvent;
import ch.qos.logback.core.AppenderBase;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;

public class SyncCallbackAppender extends AppenderBase<LoggingEvent> {
    private static List<SyncCallbackAppender.OnAppendListener> onAppendListeners = new ArrayList();

    public SyncCallbackAppender() {
    }

    protected void append(LoggingEvent eventObject) {
        onAppendListeners.forEach((item) -> {
            item.onAppend(eventObject);
        });
    }

    public static void addAppendListenr(SyncCallbackAppender.OnAppendListener listener) {
        if (listener != null && !onAppendListeners.contains(listener)) {
            onAppendListeners.add(listener);
        }

    }

    public interface OnAppendListener {
        void onAppend(LoggingEvent var1);
    }
}

利用springmvc注解@ControllerAdvice,实现请求参数的全局预处理,拦截请求参数当发生异常时同样将请求参数输出到钉钉中

/**
 * @version 1.0
 * @title 获取请求参数信息
 * @description
 * @changeRecord
 */
@ControllerAdvice
public class RequestInfoAdvice implements RequestBodyAdvice {
    private ThreadLocal<String> jsonRequestBody = new ThreadLocal<>();

    @Override
    public boolean supports(MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
        return true;
    }

    @Override
    public Object handleEmptyBody(Object object, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
        return object;
    }

    /**
     * 获取原始的请求参数(json格式的)
     *
     * @param httpInputMessage
     * @param methodParameter
     * @param type
     * @param aClass
     * @return
     * @throws IOException
     */
    @Override
    public HttpInputMessage beforeBodyRead(HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) throws IOException {
        String contentType = RequestData.getRequest().getContentType();
        if (contentType != null && "application/json".equalsIgnoreCase(contentType)) {
            jsonRequestBody.set(null);
            if (httpInputMessage != null && httpInputMessage.getBody() != null) {
                byte[] bodyBytes = IOUtils.toByteArray(httpInputMessage.getBody());
                jsonRequestBody.set(new String(bodyBytes));
                return new HttpInputMessage() {
                    @Override
                    public HttpHeaders getHeaders() {
                        return httpInputMessage.getHeaders();
                    }
                    @Override
                    public InputStream getBody() {
                        return new ByteArrayInputStream(bodyBytes);
                    }
                };
            }
        }
        return httpInputMessage;
    }

    @Override
    public Object afterBodyRead(Object object, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
        return object;
    }

    /**
     * 获取请求信息
     *
     * @return
     */
    public String getRequestInfo() {
        StringBuffer stringBuffer = new StringBuffer("");
        HttpServletRequest req = RequestData.getRequest();
        if (req == null) {
            return "";
        }
        //获取请求方式
        stringBuffer.append("\n");
        stringBuffer.append("请求方式: " + req.getMethod());
        stringBuffer.append("\n");
        //获取完整请求路径
        stringBuffer.append("请求地址: " + req.getRequestURL());
        stringBuffer.append("\n");
        //获取请求参数
        stringBuffer.append("请求参数: " + getRequestParam(req));
        //获取ip来源
        stringBuffer.append("\n");
        stringBuffer.append("IP来源: " + IpUtil.getUserIPString(req));
        stringBuffer.append("\n");
        stringBuffer.append("\n");
        Enumeration<String> headerNames = req.getHeaderNames();
        //获取获取的消息头名称,获取对应的值,并输出
        while (headerNames.hasMoreElements()) {
            String nextElement = headerNames.nextElement();
            stringBuffer.append(nextElement + ":" + req.getHeader(nextElement));
            stringBuffer.append("\n");
        }
        stringBuffer.append("\n");
        return stringBuffer.toString();
    }

    /**
     * 获取请求参数
     *
     * @param request
     * @return
     */
    public String getRequestParam(HttpServletRequest request) {
        StringBuilder stringBuilder = new StringBuilder();
        try {
            if ("get".equalsIgnoreCase(request.getMethod())) {
                stringBuilder.append(request.getQueryString());
            } else if ("post".equalsIgnoreCase(request.getMethod())) {
                String contentType = RequestData.getRequest().getContentType();
                if (contentType != null && "application/json".equalsIgnoreCase(contentType)) {
                    String jsonParm = jsonRequestBody.get();
                    if (StringUtil.isNotEmpty(jsonParm)) {
                        try {
                            //格式化输出
                            return JSON.toJSONString(JSONObject.parse(jsonRequestBody.get()),
                                    SerializerFeature.PrettyFormat, SerializerFeature.WriteMapNullValue, SerializerFeature.WriteDateUseDateFormat);
                        } catch (Exception ex) {
                            return jsonParm;
                        }
                    }
                } else if (request.getParameterNames() != null) {
                    Enumeration<String> enumeration = request.getParameterNames();
                    while (enumeration.hasMoreElements()) {
                        String name = enumeration.nextElement();
                        String[] values = request.getParameterValues(name);
                        for (int i = 0; i < values.length; i++) {
                            String value = values[i];
                            stringBuilder.append(name);
                            stringBuilder.append("=");
                            stringBuilder.append(value);
                            if (i < values.length - 1) {
                                stringBuilder.append("&");
                            }
                        }
                        if (enumeration.hasMoreElements()) {
                            stringBuilder.append("&");
                        }
                    }
                    return URLDecoder.decode(stringBuilder.toString(), "utf-8");
                }
            }
            return stringBuilder.toString();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "";
    }
}

根据拦截的error日志处理堆栈信息,发送到钉钉群中


/**
 * @version 1.0
 * @title 把异常信息发送到钉钉报警群
 * @description
 * @changeRecord
 */
@Service
public class ErrorToDingtalkService implements SyncCallbackAppender.OnAppendListener {

    /**
     * 获取相关请求参数
     */
    @Resource
    private RequestInfoAdvice requestInfoAdvice;
    /**
     * 钉钉通知类
     */
    @Resource
    private DingTalkAlarm dingTalkError;
    /**
     * 将异常信息缓存到redis中
     */
    @Resource
    private ICacheOperation cacheOperation;

    public ErrorToDingtalkService() {
        //注册接收日志需要被通知的通知者
        SyncCallbackAppender.addAppendListenr(this);
    }

    @Override
    public void onAppend(LoggingEvent eventObject) {
        try {
            handLoggingEvent(eventObject);
        } catch (Throwable throwable) {

        }
    }


    /**
     * 钉钉报警详情
     * 记录上次发送时间以及出现次数
     */
    @Data
    static class ReportDetail implements Serializable {
        private String lastSendTime;//最近一次发送时间
        private long count;//出错次数
    }

    /**
     * 处理业务异常转发到钉钉群
     */
    public void handleException(Throwable throwable) {
        try {
            reportToDingTalk(throwable.getStackTrace(), throwable.getClass().getName(), getSimpleDescribe(throwable));
        } catch (Throwable throwable1) {

        }
    }


    /**
     * 把ERROR级别的log转发到钉钉群
     *
     * @param eventObject
     */
    private void handLoggingEvent(LoggingEvent eventObject) {
        if (eventObject.getLevel() != Level.ERROR || "emailLogger".equals(eventObject.getLoggerName())) {
            return;
        }
        String msg;
        try {
            msg = eventObject.getFormattedMessage();
        } catch (Throwable throwable) {
            msg = eventObject.getMessage();
        }
        String className = "";
        if (eventObject.getThrowableProxy() != null) {
            msg = msg + "," + eventObject.getThrowableProxy().getMessage();
            className = eventObject.getThrowableProxy().getClassName();
        }
        reportToDingTalk(eventObject.getCallerData(), className, msg);
    }


    /**
     * @param stackTraceElements 上下文堆栈
     * @param className          异常类型
     * @param message            异常描述
     * @return
     */
    private void reportToDingTalk(StackTraceElement[] stackTraceElements, String className, String message) {
        if (stackTraceElements == null || stackTraceElements.length == 0) {
            return;
        }
        String msg = "service_error_md5_" + stackTraceElements[0].toString() + "_" + stackTraceElements.length;
        String errorMd5 = Md5Util.md5(msg);
        ReportDetail detail = cacheOperation.load(errorMd5);
        if (detail == null) {
            detail = new ReportDetail();
            detail.setCount(0);
            detail.setLastSendTime(DateUtil.dateTime2String(LocalDateTime.now()));
        }
        try {
            java.time.Duration duration = java.time.Duration.between(DateUtil.string2DateTime(detail.getLastSendTime()), LocalDateTime.now());
            detail.setCount(detail.getCount() + 1);
            //不是首次出现异常或者距离上次发送少于1分钟 ,利用配置中心Apollo,实现动态配置相同异常出现间隔
            Config dingtalk = ConfigService.getConfig("dingtalk");
            Integer reportInterval = dingtalk.getIntProperty("reportInterval", 60);
            if (detail.getCount() > 1 && duration.toMillis() < reportInterval * 1000) {
                return;
            } else {
                TokenUser user = RequestData.getTokenUser();
                HttpServletRequest httpServletRequest = RequestData.getRequest();
                StringBuffer stringBuffer = new StringBuffer();
                stringBuffer.append("\n发送时间: " + DateUtil.dateTime2String(LocalDateTime.now()));
                stringBuffer.append("\n请求编号: ");
                stringBuffer.append(RequestData.getRequestId());
                stringBuffer.append("\n主机名称: ");
                stringBuffer.append(IpUtil.getHostName());
                stringBuffer.append("\n城市编码: ");
                stringBuffer.append(user == null ? "未知" : user.getAreaCode());
                stringBuffer.append("\n操作人员: ");
                stringBuffer.append(user == null ? "未知" : user.getUserName() + "(" + user.getUserId() + ")");
                if (httpServletRequest != null) {
                    stringBuffer.append("\n异常路径: ");
                    stringBuffer.append(httpServletRequest.getRequestURI());
                }
                stringBuffer.append("\n异常预览: ");
                if (StringUtil.isNotEmpty(className)) {
                    stringBuffer.append("\n>" + className);
                }
                stringBuffer.append("\n>" + message);
                stringBuffer.append("\n\n[查看详情]");
                String errorId = RequestData.getRequestId();
                if (StringUtil.isEmpty(errorId) || "unknown-requestId".equals(errorId)) {
                    errorId = BusinessIdUtil.generateBizId();
                }
                cacheOperation.save(errorId, requestInfoAdvice.getRequestInfo() + "\n" + message + "\n" + stackTraceElementToString(stackTraceElements), 3600 * 24 * 3);
                String detailUrl = "https://****.com/call/api/debug/errorDetail?errorId=" + errorId;
                if (StringUtil.isNotEmpty(Util.runEvn) && !"master".equals(Util.runEvn) ) {
                    detailUrl = "https://" + Util.runEvn + "-**.com/call/api/debug/errorDetail?errorId=" + errorId;
                } 
                //开发本机调试
                if (DeveloperUtil.isLocalDebug()) {
                    detailUrl = "http://localhost:8065/**/errorDetail?errorId=" + errorId;
                }
                stringBuffer.append("(" + detailUrl + ")");
                if (detail.getCount() > 1) {
                    stringBuffer.append("\n###### <font color=#ff0000>");
                    stringBuffer.append(formatDateTime(DateUtil.string2DateTime(detail.getLastSendTime())));
                    stringBuffer.append("也出现过,");
                    stringBuffer.append(" 共出现了" + detail.getCount() + "次");
                    stringBuffer.append("</font>");
                }
                boolean result = dingTalkError.sendMarkDownMsg(stringBuffer.toString().replace("\n", "\n\n"), null);
                //记录最后出现次数
                if (result) {
                    detail.setLastSendTime(DateUtil.dateTime2String(LocalDateTime.now()));
                }
            }
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        } finally {
            try {
                //缓存异常的堆栈信息
                cacheOperation.save(errorMd5, detail);
            } catch (Throwable throwable) {
                throwable.printStackTrace();
            }
        }

    }
    /**
     * 堆栈信息转成字符串
     *
     * @param stackTraceElements
     * @return
     */
    private String stackTraceElementToString(StackTraceElement[] stackTraceElements) {
        StringBuffer stringBuffer = new StringBuffer();
        if (stackTraceElements != null && stackTraceElements.length > 0) {
            for (int i = 0; i < stackTraceElements.length; i++) {
                stringBuffer.append(stackTraceElements[i] + "\n");
            }
        }
        return stringBuffer.toString();
    }


    /**
     * 获取异常简短描述
     *
     * @param throwable
     * @return
     */
    public String getSimpleDescribe(Throwable throwable) {
        if (throwable instanceof RuntimeException) {
            String className = throwable.getClass().getName();
            String message = throwable.getMessage();
            if (StringUtil.isNotEmpty(message) &&
                    message.contains("command denied to user")) {
                //需要刷数据的时候为了避免停服 DBA把帐号权限改成只读的 等刷之后再改正常权限
                return "系统维护中,当前只能进行查看操作,请稍等再试!";
            } else if (StringUtil.isNotEmpty(message) && message.contains("BadSqlGrammarException")) {
                return "sql异常!";
            } else if (StringUtil.isNotEmpty(message) && className.startsWith("com.izk")) {
                //只转发业务异常
                int startIndex = message.indexOf(":", 0);
                int endIndex = message.indexOf('\n', 0);
                if (startIndex != -1 && endIndex != -1 && endIndex > startIndex) {
                    return message.substring(startIndex + 1, endIndex).trim();
                } else {
                    return message;
                }
            }
        }
        return throwable.getMessage();
    }

    /**
     * 格式化显示时间
     *
     * @param localDateTime
     * @return
     */
    public static String formatDateTime(LocalDateTime localDateTime) {
        java.time.Duration duration = java.time.Duration.between(localDateTime, LocalDateTime.now());
        if (duration.toMillis() < 60 * 1000) {
            return duration.getSeconds() + "秒前";
        } else if (duration.toMinutes() < 60) {
            return duration.toMinutes() + "分钟前";
        } else if (duration.toHours() < 24) {
            return duration.toHours() + "小时前";
        } else if (duration.toDays() < 28) {
            return duration.toDays() + "天前";
        } else if (duration.toDays() < 365) {
            return duration.toDays() / 30 + "月前";
        } else {
            return duration.toDays() / 365 + "年前";
        }
    }
}

拦截全局异常,调用error异常的处理类

@ControllerAdvice
public class CallControllerExceptionHandler {

    private static Logger logger = LoggerFactory.getLogger(CallControllerExceptionHandler.class);

    @Resource
    private ErrorToDingtalkService errorToDingtalkService;

    @Resource
    private RequestInfoAdvice requestInfoAdvice;

    @ExceptionHandler(value = Exception.class)
    @ResponseBody
    public Object exceptionHandler(Exception exception) {
        //此处的error级别会自动触发ErrorToDingtalkService中对error日志的处理
        logger.error("system error:{}", requestInfoAdvice.getRequestInfo(), exception);
        if (Strings.isNullOrEmpty(exception.getMessage())
                || !(exception instanceof ClientAbortException
                || exception instanceof MethodArgumentTypeMismatchException
                || exception instanceof HttpMediaTypeNotAcceptableException
                || exception instanceof HttpRequestMethodNotSupportedException)) {
            errorToDingtalkService.handleException(exception);
        }
        return JsonData.error("系统异常,请联系管理员。");
    }
}

在项目中添加错误信息查看的controller,用于查看错误详细

@Slf4j
@RestController
@RequestMapping(value = {"/debug"})
public class DebugController {
    @Resource
    private ICacheOperation cacheOperation;

    @GetMapping(value = "/errorDetail")
    public void changeTime(String errorId, Writer writer) {
        long reslut = cacheOperation.setnx(("/errorDetail").getBytes(), Constants.ACCESS_LIMIT_WAIT_TIME, "1".getBytes());
        if (reslut <= 0) {
            return;
        }
        String errorDetail = cacheOperation.load(errorId);
        StringBuilder sbHtml = new StringBuilder();
        sbHtml.append("<!doctype html><html><head><meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">");
        sbHtml.append("<title>异常详情</title></head><body>");
        if (StringUtil.isNotEmpty(errorDetail)) {
            String[] strs = errorDetail.split("\n");
            for (String line : strs) {
                //将业务异常的显示加红
                line = line.replace("\t", "&nbsp;&nbsp;&nbsp;&nbsp;");
                if (line.contains("com.**")) {
                    sbHtml.append("<font color ='red'>");
                    sbHtml.append(line);
                    sbHtml.append("</font>");
                } else {
                    sbHtml.append(line);
                }
                sbHtml.append("<br>");
            }
        } else {
            sbHtml.append("<font color ='red'>");
            sbHtml.append("异常信息获取异常,异常编号:" + errorId + ",请查看日志");
            sbHtml.append("</font>");
        }
        sbHtml.append("<br><br><br>");
        sbHtml.append("</body></html>");
        try {
            writer.write(sbHtml.toString());
        } catch (IOException e) {
            log.error(e.getMessage());
        }
    }
}

效果展示

点击上方标红查看详情即展示 

 

总结

      关于钉钉发送消息的工具类封装,之后会在博客中展示。其实这个问题挺好解决的,但是最重要的不去想,相比程序的实现,最难的是能有思路。除了提升变成技能,更重要的要多看,多想,真正的做到解决痛点问题。 

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Mandy_i

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值