线上log日志输出没有具体堆栈信息问题排查

一、问题描述

    因错误使用了logback的error日志实现中,format、可变参的重载方法,相当然的以为throwable异常作为可变参数也会打印出具体堆栈。

  1. 线上使用logback日志框架

  2. 调用了logback的format、可变参数的重载方法

 public void error(Marker marker, String format, Object... argArray) {
        filterAndLog_0_Or3Plus(FQCN, marker, Level.ERROR, format, argArray, null);
    }


     3.具体的调用代码如下,这样打印的日志,异常堆栈信息是出不来的。

log.error("参数信息{} 描述信息 {} 异常堆栈 {}","参数1","描述1",e);

二、问题定位

  1. 阅读public void error(String format, Object... argArray);方法的源码

  2. 会调用到logback的底层代码filterAndLog_0_Or3Plus方法   

private void filterAndLog_0_Or3Plus(final String localFQCN, final Marker marker, final Level level, final String msg, final Object[] params,
                    final Throwable t) {

        final FilterReply decision = loggerContext.getTurboFilterChainDecision_0_3OrMore(marker, this, level, msg, params, t);

        if (decision == FilterReply.NEUTRAL) {
            if (effectiveLevelInt > level.levelInt) {
                return;
            }
        } else if (decision == FilterReply.DENY) {
            return;
        }
        // 这里会构造异步日志事件,和格式化日志信息
        buildLoggingEventAndAppend(localFQCN, marker, level, msg, params, t);
    }

3.继续跟进buildLoggingEventAndAppend的实现

private void buildLoggingEventAndAppend(final String localFQCN, final Marker marker, final Level level, final String msg, final Object[] params,
                    final Throwable t) {
        //具体构造日志事件,核心地方,这里代表了一条日志消息,异步日志框架会对该消息进行格式化输出
        LoggingEvent le = new LoggingEvent(localFQCN, this, level, msg, t, params);
        le.setMarker(marker);
        callAppenders(le);
    }

4.看一下loggingEvent是如何构造的

 

public LoggingEvent(String fqcn, Logger logger, Level level, String message, Throwable throwable, Object[] argArray) {
        this.fqnOfLoggerClass = fqcn;
        this.loggerName = logger.getName();
        this.loggerContext = logger.getLoggerContext();
        this.loggerContextVO = loggerContext.getLoggerContextRemoteView();
        this.level = level;

        this.message = message;
        // 追踪可变参数列表的调用,赋值给了argumentArray属性
        this.argumentArray = argArray;

        if (throwable == null) {
            throwable = extractThrowableAnRearrangeArguments(argArray);
        }

        if (throwable != null) {
            this.throwableProxy = new ThrowableProxy(throwable);
            LoggerContext lc = logger.getLoggerContext();
            if (lc.isPackagingDataEnabled()) {
                this.throwableProxy.calculatePackagingData();
            }
        }

        timeStamp = System.currentTimeMillis();
    }

5.argumentArray该参数列表如何使用

 public String getFormattedMessage() {
        if (formattedMessage != null) {
            return formattedMessage;
        }
        if (argumentArray != null) {
            //argumentArray 从这个参数里构造出日志的格式化信息
            formattedMessage = MessageFormatter.arrayFormat(message, argumentArray).getMessage();
        } else {
            formattedMessage = message;
        }

        return formattedMessage;
    }

6.继续跟进MessageFormatter.arrayFormat的实现

final public static FormattingTuple arrayFormat(final String messagePattern, final Object[] argArray) {
        Throwable throwableCandidate = getThrowableCandidate(argArray);
        Object[] args = argArray;
        if (throwableCandidate != null) {
            args = trimmedCopy(argArray);
        }
        // 真正从array参数列表格式化日志的入口
        return arrayFormat(messagePattern, args, throwableCandidate);
    }

7.继续追踪arrayFormat的源码实现,并不用被这些冗长的代码吓到,其实里面最核心的部分就在deeplyAppendParameter(..)方法中实现的,从名字中就可以看出是根据参数追加格式化日志

final public static FormattingTuple arrayFormat(final String messagePattern, final Object[] argArray, Throwable throwable) {

        if (messagePattern == null) {
            return new FormattingTuple(null, argArray, throwable);
        }

        if (argArray == null) {
            return new FormattingTuple(messagePattern);
        }

        int i = 0;
        int j;
        // use string builder for better multicore performance
        StringBuilder sbuf = new StringBuilder(messagePattern.length() + 50);

        int L;
        for (L = 0; L < argArray.length; L++) {

            j = messagePattern.indexOf(DELIM_STR, i);

            if (j == -1) {
                // no more variables
                if (i == 0) { // this is a simple string
                    return new FormattingTuple(messagePattern, argArray, throwable);
                } else { // add the tail string which contains no variables and return
                    // the result.
                    sbuf.append(messagePattern, i, messagePattern.length());
                    return new FormattingTuple(sbuf.toString(), argArray, throwable);
                }
            } else {
                if (isEscapedDelimeter(messagePattern, j)) {
                    if (!isDoubleEscaped(messagePattern, j)) {
                        L--; // DELIM_START was escaped, thus should not be incremented
                        sbuf.append(messagePattern, i, j - 1);
                        sbuf.append(DELIM_START);
                        i = j + 1;
                    } else {
                        // The escape character preceding the delimiter start is
                        // itself escaped: "abc x:\\{}"
                        // we have to consume one backward slash
                        sbuf.append(messagePattern, i, j - 1);
                        deeplyAppendParameter(sbuf, argArray[L], new HashMap());
                        i = j + 2;
                    }
                } else {
                    // normal case
                    sbuf.append(messagePattern, i, j);
                    deeplyAppendParameter(sbuf, argArray[L], new HashMap());
                    i = j + 2;
                }
            }
        }
        // append the characters following the last {} pair.
        sbuf.append(messagePattern, i, messagePattern.length());
        return new FormattingTuple(sbuf.toString(), argArray, throwable);
    }

8.deeplyAppendParameter的实现,

private static void deeplyAppendParameter(StringBuilder sbuf, Object o, Map seenMap) {
        if (o == null) {
            sbuf.append("null");
            return;
        }
        if (!o.getClass().isArray()) {
            //因为调用此方法的为上层的参数列表循环,以上调用会走到这里
            safeObjectAppend(sbuf, o);
        } else {
            // check for primitive array types because they
            // unfortunately cannot be cast to Object[]
            if (o instanceof boolean[]) {
                booleanArrayAppend(sbuf, (boolean[]) o);
            } else if (o instanceof byte[]) {
                byteArrayAppend(sbuf, (byte[]) o);
            } else if (o instanceof char[]) {
                charArrayAppend(sbuf, (char[]) o);
            } else if (o instanceof short[]) {
                shortArrayAppend(sbuf, (short[]) o);
            } else if (o instanceof int[]) {
                intArrayAppend(sbuf, (int[]) o);
            } else if (o instanceof long[]) {
                longArrayAppend(sbuf, (long[]) o);
            } else if (o instanceof float[]) {
                floatArrayAppend(sbuf, (float[]) o);
            } else if (o instanceof double[]) {
                doubleArrayAppend(sbuf, (double[]) o);
            } else {
                objectArrayAppend(sbuf, (Object[]) o, seenMap);
            }
        }
    }

9.safeObjectAppend的实实现

 private static void safeObjectAppend(StringBuilder sbuf, Object o) {
        try {
            // 走到这里,我们不难发现,实际上每个参数都是直接调用了对象自身的toString实现。
            String oAsString = o.toString();
            sbuf.append(oAsString);
        } catch (Throwable t) {
            Util.report("SLF4J: Failed toString() invocation on an object of type [" + o.getClass().getName() + "]", t);
            sbuf.append("[FAILED toString()]");
        }

    }

10.既然最终都是通过调用toString方法做日志追加的,那么可以看下我们自己希望的Throwable的toString方法的实现即可,拿我们常见的TimeOutException他们的toString实现,是在父类中Throwable

//该方法返回异常的一个简述; 
//1.如果自定义好的异常简述存在,则会打印出“当前类名:自定义简述” 
//2.如果自定义的异常简述不存在,则直接返回当前类名 
public String toString() {
        String s = getClass().getName();
        String message = getLocalizedMessage();
        return (message != null) ? (s + ": " + message) : s;
    }

 

11.看到上toString的实现,最终就定位到了原因:public void error(String format, Object... argArray)即使设置了异常作为可变参数,也无法打印出具体的异常信息的

 

三、既能够打印出异常堆栈信息,又能支持可变参数列表该如何实现

1.logback实现的所有error日志输出方法中,都没有throwable和Object... 共同作为参数的重载方法

线上log日志输出没有具体堆栈信息问题排查_第1张图片

 

2.此时需要自己实现一个既能打印异常堆栈,又能输出格式化参数列表的方法。

查看源码发现,我们可以利用log重载方法扩展出自定义,有特殊需求的日志输出实现

   //调用底层Logger实现的这个大而全参数的方法 
   public void log(Marker marker, String fqcn, int levelInt, String message, Object[] argArray, Throwable t) {
        Level level = Level.fromLocationAwareLoggerInteger(levelInt);
        filterAndLog_0_Or3Plus(fqcn, marker, level, message, argArray, t);
    }

 

四、总结

这是一个非常小的问题,但是想当然的以为,竟然最终在线上出现了问题,导致出现异常时非常难定义问题。其实我们只要多看一下所调用方法的具体实现,我们都能找到应该如何依赖现有api做好我们的功能业务;这就需要我们每个开发都能多一些匠心精神。

 

你可能感兴趣的:(问题排查,java,logback,log4j,bug)