log.error()底層到底做了些啥?

今天給大家介紹一下logback日誌,底層是如何實現的。這邊我們打印一下error級別的日誌,看看從log.error到輸出磁盤,這個過程中到底發生了些什麼,並從源碼級別揭祕整個日常的輸出過程。

我們先在代碼中編寫log.error,作爲日誌入口。

log.error("測試日誌輸出:accountId:{},site:{}", accountId, site);

進入error()函數中,我們可以看到,在打印日誌之前,第一件事情是判斷該log日誌是否可以輸出。(比如你配置文件配置的是error級別,你輸出的卻是info級別的日誌,那就會直接被return掉,日誌也不會輸出到控制檯或者文件中)

    @Override
    public void logIfEnabled(final String fqcn, final Level level, final Marker marker, final String message,
            final Object p0, final Object p1) {
        if (isEnabled(level, marker, message, p0, p1)) {
            logMessage(fqcn, level, marker, message, p0, p1);
        }
    }

在logIfEnabled函數中會進行日誌是否可輸出判斷,我們接下來看看logIfEnabled函數中做了哪些事情。

private boolean isEnabledFor(final Level level, final Marker marker) {
        final org.slf4j.Marker slf4jMarker = getMarker(marker);
        switch (level.getStandardLevel()) {
            case DEBUG :
                return logger.isDebugEnabled(slf4jMarker);
            case TRACE :
                return logger.isTraceEnabled(slf4jMarker);
            case INFO :
                return logger.isInfoEnabled(slf4jMarker);
            case WARN :
                return logger.isWarnEnabled(slf4jMarker);
            case ERROR :
                return logger.isErrorEnabled(slf4jMarker);
            default :
                return logger.isErrorEnabled(slf4jMarker);

        }
    }

logIfEnabled函數一直往裏面調用,最後我們可以看到如上一段代碼,這段代碼主要將不同級別的日誌分別調用不用的日誌是否可打印判斷方法,我們這邊的日誌級別的error,所以我們直接看isErrorEnabled函數中的實現。

public boolean isErrorEnabled(Marker marker) {
        FilterReply decision = callTurboFilters(marker, Level.ERROR);
        if (decision == FilterReply.NEUTRAL) {
            return effectiveLevelInt <= Level.ERROR_INT;
        } else if (decision == FilterReply.DENY) {
            return false;
        } else if (decision == FilterReply.ACCEPT) {
            return true;
        } else {
            throw new IllegalStateException("Unknown FilterReply value: " + decision);
        }
    }

在isErrorEnabled函數中主要做了兩件事情,一個是通過callTurboFilters函數獲取當前日誌FilterReply ,然後和配置文件配置日誌等級Level.ERROR_INT進行判斷,最後返回boolean結果。

callTurboFilters函數:

callTurboFilters函數主要是用來過濾配置文件中特殊的配置,如果沒有特殊配置則直接返回FilterReply.NEUTRAL(正常)。

effectiveLevelInt:

現在問題關鍵就是effectiveLevelInt的值是從哪裏賦值的,因爲Level.ERROR_INT的值是固定4000

public static final int ERROR_INT = 40000;

這邊effectiveLevelInt的初始值,是在容器啓動的時候賦值的,具體值和logback配置文件有關。因爲spring容器啓動的時候會設置每一個級別的effectiveLevelInt值,初始值分爲trace、debug、info、warn、error、日誌關閉等幾個值。

image

這邊因爲我配置的日誌級別是異步error(何爲異步,稍後再解釋),所以isErrorEnabled方法返回的是ture。

<appender-ref ref="file_error_async"/>

logIfEnabled函數就介紹到這邊了,下面就是日誌打印的模塊了,日誌打印的起始函數是logMessage函數,我們來看看logMessage函數中又做了哪些事情。

    protected void logMessage(final String fqcn, final Level level, final Marker marker, final String message,
            final Object p0, final Object p1) {
        final Message msg = messageFactory.newMessage(message, p0, p1);
        logMessageSafely(fqcn, level, marker, msg, msg.getThrowable());
    }

logMessage函數中對日誌打印內容和參數做了處理,並封裝爲Message對象,Message對象中比較重要的三個字段就是messagePattern、parameters、locale。一個是需要打印的日誌、一個是{}中參數、最後一個就是每一個參數在messagePattern對應的參數下標。如下圖所示:

image

logMessageSafely函數一直往下走,會進入logMessage函數,代碼如下所示:

@Override
    public void logMessage(final String fqcn, final Level level, final Marker marker, final Message message, final Throwable t) {
        if (locationAwareLogger != null) {
            if (message instanceof LoggerNameAwareMessage) {
                ((LoggerNameAwareMessage) message).setLoggerName(getName());
            }
            locationAwareLogger.log(getMarker(marker), fqcn, convertLevel(level), message.getFormattedMessage(),
                    message.getParameters(), t);
        } else {
            switch (level.getStandardLevel()) {
                case DEBUG :
                    logger.debug(getMarker(marker), message.getFormattedMessage(), message.getParameters(), t);
                    break;
                case TRACE :
                    logger.trace(getMarker(marker), message.getFormattedMessage(), message.getParameters(), t);
                    break;
                case INFO :
                    logger.info(getMarker(marker), message.getFormattedMessage(), message.getParameters(), t);
                    break;
                case WARN :
                    logger.warn(getMarker(marker), message.getFormattedMessage(), message.getParameters(), t);
                    break;
                case ERROR :
                    logger.error(getMarker(marker), message.getFormattedMessage(), message.getParameters(), t);
                    break;
                default :
                    logger.error(getMarker(marker), message.getFormattedMessage(), message.getParameters(), t);
                    break;
            }
        }
    }

logMessage函數中最核心的就是locationAwareLogger.log函數了,其中message.getFormattedMessage()函數是用來將日誌中{}和參數對應上,返回我們要打印的日誌String,我們先來看看message.getFormattedMessage()函數裏面做了些啥。

locationAwareLogger.log(getMarker(marker), fqcn, convertLevel(level), message.getFormattedMessage(),message.getParameters(), t);

getFormattedMessage函數主要是創建一個StringBuilder對象,然後對日誌字符串進行處理,最後返回要打印的字符串。

 /**
     * Returns the formatted message.
     * @return the formatted message.
     */
    @Override
    public String getFormattedMessage() {
        if (formattedMessage == null) {
            final StringBuilder buffer = getThreadLocalStringBuilder();
            formatTo(buffer);
            formattedMessage = buffer.toString();
            StringBuilders.trimToMaxSize(buffer, Constants.MAX_REUSABLE_MESSAGE_SIZE);
        }
        return formattedMessage;
    }

formatTo函數主要是根據日誌輸出的類型,選擇對應的日誌處理方式。我們的日誌類型會通過ParameterFormatter.formatMessage2函數進行日誌處理。

@Override
    public void formatTo(final StringBuilder buffer) {
        if (formattedMessage != null) {
            buffer.append(formattedMessage);
        } else {
            if (indices[0] < 0) {
                ParameterFormatter.formatMessage(buffer, messagePattern, argArray, usedCount);
            } else {
                ParameterFormatter.formatMessage2(buffer, messagePattern, argArray, usedCount, indices);
            }
        }
    }

formatMessage2函數主要是通過StringBuilder的append方法,對日誌中{}和參數進行拼接,最後返回拼接好的日誌字符串。

 /**
     * Replace placeholders in the given messagePattern with arguments.
     *
     * @param buffer the buffer to write the formatted message into
     * @param messagePattern the message pattern containing placeholders.
     * @param arguments      the arguments to be used to replace placeholders.
     */
    static void formatMessage2(final StringBuilder buffer, final String messagePattern,
            final Object[] arguments, final int argCount, final int[] indices) {
        if (messagePattern == null || arguments == null || argCount == 0) {
            buffer.append(messagePattern);
            return;
        }
        int previous = 0;
        for (int i = 0; i < argCount; i++) {
            buffer.append(messagePattern, previous, indices[i]);
            previous = indices[i] + 2;
            recursiveDeepToString(arguments[i], buffer, null);
        }
        buffer.append(messagePattern, previous, messagePattern.length());
    }

要打印的日誌有了,接下來就是將日誌輸出到我們配置的文件中或者打印到控制檯上面。日誌的打印有兩種模式,一種是異步的、一種是同步的,我們先來看一下異步的日誌輸出到底是怎麼實現的。

從locationAwareLogger.log函數中一直往下走,我們可以看到callAppenders函數,這個函數的作用就是啓動線程來打印日誌。我們進入callAppenders中看看裏面到底是在幹嘛。

/**
     * Invoke all the appenders of this logger.
     * 
     * @param event
     *          The event to log
     */
    public void callAppenders(ILoggingEvent event) {
        int writes = 0;
        for (Logger l = this; l != null; l = l.parent) {
            writes += l.appendLoopOnAppenders(event);
            if (!l.additive) {
                break;
            }
        }
        // No appenders in hierarchy
        if (writes == 0) {
            loggerContext.noAppenderDefinedWarning(this);
        }
    }

通過循環Logger的parent節點,一直循環到父節點爲null的時候,會進入aai.appendLoopOnAppenders(event)函數中,此函數就是將所有級別的日誌全部輸出指定位置,我們來具體看一下它的實現。

 /**
 * Call the <code>doAppend</code> method on all attached appenders.
*/
public int appendLoopOnAppenders(E e) {
        int size = 0;
        final Appender<E>[] appenderArray = appenderList.asTypedArray();
        final int len = appenderArray.length;
        for (int i = 0; i < len; i++) {
            appenderArray[i].doAppend(e);
            size++;
        }
        return size;
    }

appenderArray數組中保存着我們log配置文件中所有的輸出配置,如下圖所示:

image

appenderArray[i].doAppend(e)中的doAppend就是針對每一種打印類型進行日誌打印。我們進入doAppend函數看看裏面具體做了哪些事情。

public void doAppend(E eventObject) {
        // WARNING: The guard check MUST be the first statement in the
        // doAppend() method.
        // prevent re-entry.
        if (Boolean.TRUE.equals(guard.get())) {
            return;
        }
        try {
            guard.set(Boolean.TRUE);
            if (!this.started) {
                if (statusRepeatCount++ < ALLOWED_REPEATS) {
                    addStatus(new WarnStatus("Attempted to append to non started appender [" + name + "].", this));
                }
                return;
            }
            if (getFilterChainDecision(eventObject) == FilterReply.DENY) {
                return;
            }
            // ok, we now invoke derived class' implementation of append
            this.append(eventObject);
        } catch (Exception e) {
            if (exceptionCount++ < ALLOWED_REPEATS) {
                addError("Appender [" + name + "] failed to append.", e);
            }
        } finally {
            guard.set(Boolean.FALSE);
        }
    }

裏面進行了一系列的判斷,最後會進入this.append(eventObject)中,我們接着跟蹤後續的代碼。

    @Override
    protected void append(E eventObject) {
        if (isQueueBelowDiscardingThreshold() && isDiscardable(eventObject)) {
            return;
        }
        preprocess(eventObject);
        put(eventObject);
    }

我們再進入put函數中看看最後打印的真相是啥。

 private void put(E eventObject) {
        if (neverBlock) {
            blockingQueue.offer(eventObject);
        } else {
            putUninterruptibly(eventObject);
        }
    }

還是沒到頭,我們繼續進入putUninterruptibly函數中。

private void putUninterruptibly(E eventObject) {
        boolean interrupted = false;
        try {
            while (true) {
                try {
                    blockingQueue.put(eventObject);
                    break;
                } catch (InterruptedException e) {
                    interrupted = true;
                }
            }
        } finally {
            if (interrupted) {
                Thread.currentThread().interrupt();
            }
        }
    }

一臉懵逼的發現,最後的結果是將事件放入blockingQueue阻塞隊列中(BlockingQueue<E> blockingQueue)。那最終日誌是在哪裏打印的呢?別急我們全局搜素一下blockingQueue阻塞隊列到底是用來做啥?

class Worker extends Thread {

        public void run() {
            AsyncAppenderBase<E> parent = AsyncAppenderBase.this;
            AppenderAttachableImpl<E> aai = parent.aai;

            // loop while the parent is started
            while (parent.isStarted()) {
                try {
                    E e = parent.blockingQueue.take();
                    aai.appendLoopOnAppenders(e);
                } catch (InterruptedException ie) {
                    break;
                }
            }

            addInfo("Worker thread will flush remaining events before exiting. ");

            for (E e : parent.blockingQueue) {
                aai.appendLoopOnAppenders(e);
                parent.blockingQueue.remove(e);
            }

            aai.detachAndStopAllAppenders();
        }
    }

我們可以看到在一個線程中,會取出blockingQueue中的事件對象,然後調用aai.appendLoopOnAppenders(e)函數,通過io流將字符串輸出到配置文件指定位置中。aai.appendLoopOnAppenders(e)裏面的實現其實就是同步日誌的實現,我們將配置文件修改爲同步,然後一起看看aai.appendLoopOnAppenders(e)裏面到底做了些什麼?

<!-- 日誌輸出級別 -->
    <root level="INFO">
        <!--異步輸出-->
        <!--<appender-ref ref="file_info_async"/>-->
        <!--<appender-ref ref="file_debug_async"/>-->
        <!--<appender-ref ref="file_error_async"/>-->
        <!--<appender-ref ref="console_async"/>-->
        <!--同步輸出-->
        <appender-ref ref="console"/>
        <!--<appender-ref ref="file-debug"/>-->
        <!--<appender-ref ref="file-info"/>-->
        <appender-ref ref="file-error"/>
    </root>

this.append(eventObject)函數和之前異步日誌輸出的調用有所不同,同步日誌會走如下所示的代碼:

 @Override
    protected void append(E eventObject) {
        if (!isStarted()) {
            return;
        }

        subAppend(eventObject);
    }

我們進入subAppend(eventObject)函數看看裏面具體做了什麼。

/**
     * Actual writing occurs here.
     * <p>
     * Most subclasses of <code>WriterAppender</code> will need to override this
     * method.
     * 
     * @since 0.9.0
     */
    protected void subAppend(E event) {
        if (!isStarted()) {
            return;
        }
        try {
            // this step avoids LBCLASSIC-139
            if (event instanceof DeferredProcessingAware) {
                ((DeferredProcessingAware) event).prepareForDeferredProcessing();
            }
            // the synchronization prevents the OutputStream from being closed while we
            // are writing. It also prevents multiple threads from entering the same
            // converter. Converters assume that they are in a synchronized block.
            // lock.lock();

            byte[] byteArray = this.encoder.encode(event);
            writeBytes(byteArray);

        } catch (IOException ioe) {
            // as soon as an exception occurs, move to non-started state
            // and add a single ErrorStatus to the SM.
            this.started = false;
            addStatus(new ErrorStatus("IO failure in appender", this, ioe));
        }
    }

我們可以很明顯的看到這個函數中就是將字符串輸出到磁盤中,我們進入writeBytes(byteArray)看一下,此函數是否就是通過io流將日誌打印到磁盤中。

 private void writeBytes(byte[] byteArray) throws IOException {
        if(byteArray == null || byteArray.length == 0)
            return;
        
        lock.lock();
        try {
            this.outputStream.write(byteArray);
            if (immediateFlush) {
                this.outputStream.flush();
            }
        } finally {
            lock.unlock();
        }
    }

this.outputStream.write(byteArray)函數就是將字符串對應的字節數組輸出到指定位置。如圖所示:

image

這邊的this.outputStream對象也是在spring啓動的時候初始化的,對象具體值會根據log配置文件所設置。

總結:

logback的日誌打印原理就介紹到這邊了,別看我們只是通過log.error()輸出日誌,但是內部卻做了非常多的處理。開發雖然會用就可以了,但是如果不瞭解其內部實現原理,在出現百度不到bug的時候,就非常棘手了。好了今天林老師logback源碼課程就介紹到這邊了,謝謝童鞋們的觀看。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章