springboot+logback日誌非同步資料庫

flyingkid發表於2018-12-01

logback.xml配置檔案:

<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false">
 
    <!--定義日誌檔案的儲存地址 勿在 LogBack 的配置中使用相對路徑-->
    <springProperty scope="context" name="LOG_HOME" source="logging.path"/>
 
 
    <!-- 控制檯輸出 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!--格式化輸出:%d表示日期,%thread表示執行緒名,%-5level:級別從左顯示5個字元寬度%msg:日誌訊息,%n是換行符-->
            <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n</pattern>
        </encoder>
    </appender>
 
 
    <!-- 按照每天生成日誌檔案 -->
    <appender name="FILE"  class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--日誌檔案輸出的檔名-->
            <FileNamePattern>${LOG_HOME}/%d{yyyy-MM-dd}/MIXPAY_%d{yyyy-MM-s}.log</FileNamePattern>
            <!--日誌檔案保留天數-->
            <MaxHistory>50</MaxHistory>
        </rollingPolicy>
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!--格式化輸出:%d表示日期,%thread表示執行緒名,%-5level:級別從左顯示5個字元寬度%msg:日誌訊息,%n是換行符-->
            <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n</pattern>
        </encoder>
        <!--日誌檔案最大的大小-->
        <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
            <MaxFileSize>50MB</MaxFileSize>
        </triggeringPolicy>
    </appender>
 
 
    <!-- show parameters for hibernate sql 專為 Hibernate 定製 -->
    <logger name="org.hibernate.type.descriptor.sql.BasicBinder"  level="TRACE"/>
    <logger name="org.hibernate.type.descriptor.sql.BasicExtractor"  level="DEBUG"/>
    <logger name="org.hibernate.SQL" level="DEBUG"/>
    <logger name="org.hibernate.engine.QueryParameters" level="DEBUG"/>
    <logger name="org.hibernate.engine.query.HQLQueryPlan" level="DEBUG"/>
 
 
    <!--日誌非同步到資料庫 -->
    <appender name="DB_APPENDER" class="com.config.LogDBAppender">
        <filter class="com.config.LogbackMarkerFilter">
            &emsp;<!-- 自定義標誌 -->
            &emsp;<marker>DB</marker>
            &emsp;<onMatch>ACCEPT</onMatch>
            &emsp;<onMismatch>DENY</onMismatch>
        </filter>
        <connectionSource class="ch.qos.logback.core.db.DataSourceConnectionSource">
            <dataSource class="com.alibaba.druid.pool.DruidDataSource">
                <driverClassName>net.sf.log4jdbc.DriverSpy</driverClassName>
                <url>jdbc:log4jdbc:mysql://127.0.0.1:3306/dbname?characterEncoding=UTF-8</url>
                <username>root</username>
                <password>123456</password>
            </dataSource>
        </connectionSource>
    </appender>
 
 
    <!-- 非同步日誌記錄 -->
    <appender name="ASYNC_APPENDER" class="ch.qos.logback.classic.AsyncAppender">
        &emsp;&emsp;<appender-ref ref="DB_APPENDER" />
        &emsp;&emsp;<includeCallerData>true</includeCallerData>
    </appender>
 
    <!-- 日誌輸出級別 -->
    <root level="INFO">
        <!-- 控制檯輸出 -->
        <appender-ref ref="STDOUT"/>
        <!-- 按照每天生成日誌檔案 -->
        <appender-ref ref="FILE"/>
        <!-- 非同步資料庫-->
        <appender-ref ref="ASYNC_APPENDER"/>
    </root>
 
</configuration>
複製程式碼

非同步日誌的核心配置如下:

<!--日誌非同步到資料庫 -->
    <!-- 自定義LogDBAppender 擴充DBAppenderBase -->
    <appender name="DB_APPENDER" class="com.config.LogDBAppender">
        <!-- 自定義LogbackMarkerFilter 擴充AbstractMatcherFilter-->
        <filter class="com.config.LogbackMarkerFilter">
            &emsp;<!-- 自定義標誌 -->
            &emsp;<marker>DB</marker>
            &emsp;<onMatch>ACCEPT</onMatch>
            &emsp;<onMismatch>DENY</onMismatch>
        </filter>
        <connectionSource class="ch.qos.logback.core.db.DataSourceConnectionSource">
            <!-- 本例採用DruidDataSource連線-->
            <dataSource class="com.alibaba.druid.pool.DruidDataSource">
                <!-- 資料庫連線驅動類 -->
                <driverClassName>net.sf.log4jdbc.DriverSpy</driverClassName>
                <!-- 資料庫連線地址 -->
                <url>jdbc:log4jdbc:mysql://127.0.0.1:3306/dbname?characterEncoding=UTF-8</url>
                <!-- 資料庫使用者 -->
                <username>root</username>
                <!-- 資料庫密碼 -->
                <password>123456</password>
            </dataSource>
        </connectionSource>
    </appender>
複製程式碼

自定義 LogDBAppender (Appender是logback框架中最重要的元件之一)

import ch.qos.logback.classic.db.DBHelper;
import ch.qos.logback.classic.db.names.ColumnName;
import ch.qos.logback.classic.db.names.DBNameResolver;
import ch.qos.logback.classic.db.names.DefaultDBNameResolver;
import ch.qos.logback.classic.db.names.TableName;
import ch.qos.logback.classic.spi.CallerData;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.db.DBAppenderBase;
 
import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
 
/**
 * @Title: LogDBAppender.java
 * @Description: TODO(日誌持久化配置)
 * @Author: 愛飄de小子  上午9:43
 */
public class LogDBAppender extends DBAppenderBase<ILoggingEvent> {
 
    protected String insertSQL;
    protected static final Method GET_GENERATED_KEYS_METHOD;
 
    private DBNameResolver dbNameResolver;
 
    static final int TIMESTMP_INDEX = 1;
    static final int FORMATTED_MESSAGE_INDEX = 2;
    static final int LOGGER_NAME_INDEX = 3;
    static final int LEVEL_STRING_INDEX = 4;
    static final int THREAD_NAME_INDEX = 5;
    static final int REFERENCE_FLAG_INDEX = 6;
    static final int ARG0_INDEX = 7;
    static final int ARG1_INDEX = 8;
    static final int ARG2_INDEX = 9;
    static final int ARG3_INDEX = 10;
    static final int CALLER_FILENAME_INDEX = 11;
    static final int CALLER_CLASS_INDEX = 12;
    static final int CALLER_METHOD_INDEX = 13;
    static final int CALLER_LINE_INDEX = 14;
    static final int EVENT_ID_INDEX = 15;
 
    static final StackTraceElement EMPTY_CALLER_DATA = CallerData.naInstance();
 
    static {
        // PreparedStatement.getGeneratedKeys() method was added in JDK 1.4
        Method getGeneratedKeysMethod;
        try {
            // the
            getGeneratedKeysMethod = PreparedStatement.class.getMethod("getGeneratedKeys", (Class[]) null);
        } catch (Exception ex) {
            getGeneratedKeysMethod = null;
        }
        GET_GENERATED_KEYS_METHOD = getGeneratedKeysMethod;
    }
 
    public void setDbNameResolver(DBNameResolver dbNameResolver) {
        this.dbNameResolver = dbNameResolver;
    }
 
    @Override
    public void start() {
        if (dbNameResolver == null)
            dbNameResolver = new DefaultDBNameResolver();
        insertSQL = buildInsertSQL(dbNameResolver);
        super.start();
    }
 
    @Override
    protected void subAppend(ILoggingEvent event, Connection connection, PreparedStatement insertStatement) throws Throwable {
 
        bindLoggingEventWithInsertStatement(insertStatement, event);
        bindLoggingEventArgumentsWithPreparedStatement(insertStatement, event.getArgumentArray());
 
        // This is expensive... should we do it every time?
        bindCallerDataWithPreparedStatement(insertStatement, event.getCallerData());
 
        int updateCount = insertStatement.executeUpdate();
        if (updateCount != 1) {
            addWarn("Failed to insert loggingEvent");
        }
    }
 
    @Override
    protected void secondarySubAppend(ILoggingEvent event, Connection connection, long eventId) throws Throwable {
        Map<String, String> mergedMap = mergePropertyMaps(event);
        //insertProperties(mergedMap, connection, eventId);
 
//        if (event.getThrowableProxy() != null) {
//            insertThrowable(event.getThrowableProxy(), connection, eventId);
//        }
    }
 
    void bindLoggingEventWithInsertStatement(PreparedStatement stmt, ILoggingEvent event) throws SQLException {
        stmt.setLong(TIMESTMP_INDEX, event.getTimeStamp());
        stmt.setString(FORMATTED_MESSAGE_INDEX, event.getFormattedMessage());
        stmt.setString(LOGGER_NAME_INDEX, event.getLoggerName());
        stmt.setString(LEVEL_STRING_INDEX, event.getLevel().toString());
        stmt.setString(THREAD_NAME_INDEX, event.getThreadName());
        stmt.setShort(REFERENCE_FLAG_INDEX, DBHelper.computeReferenceMask(event));
    }
 
    void bindLoggingEventArgumentsWithPreparedStatement(PreparedStatement stmt, Object[] argArray) throws SQLException {
 
        int arrayLen = argArray != null ? argArray.length : 0;
 
        for (int i = 0; i < arrayLen && i < 4; i++) {
            stmt.setString(ARG0_INDEX + i, asStringTruncatedTo254(argArray[i]));
        }
        if (arrayLen < 4) {
            for (int i = arrayLen; i < 4; i++) {
                stmt.setString(ARG0_INDEX + i, null);
            }
        }
    }
 
    String asStringTruncatedTo254(Object o) {
        String s = null;
        if (o != null) {
            s = o.toString();
        }
 
        if (s == null) {
            return null;
        }
        if (s.length() <= 254) {
            return s;
        } else {
            return s.substring(0, 254);
        }
    }
 
    void bindCallerDataWithPreparedStatement(PreparedStatement stmt, StackTraceElement[] callerDataArray) throws SQLException {
 
        StackTraceElement caller = extractFirstCaller(callerDataArray);
 
        stmt.setString(CALLER_FILENAME_INDEX, caller.getFileName());
        stmt.setString(CALLER_CLASS_INDEX, caller.getClassName());
        stmt.setString(CALLER_METHOD_INDEX, caller.getMethodName());
        stmt.setString(CALLER_LINE_INDEX, Integer.toString(caller.getLineNumber()));
    }
 
    private StackTraceElement extractFirstCaller(StackTraceElement[] callerDataArray) {
        StackTraceElement caller = EMPTY_CALLER_DATA;
        if (hasAtLeastOneNonNullElement(callerDataArray))
            caller = callerDataArray[0];
        return caller;
    }
 
    private boolean hasAtLeastOneNonNullElement(StackTraceElement[] callerDataArray) {
        return callerDataArray != null && callerDataArray.length > 0 && callerDataArray[0] != null;
    }
 
    Map<String, String> mergePropertyMaps(ILoggingEvent event) {
        Map<String, String> mergedMap = new HashMap<String, String>();
        // we add the context properties first, then the event properties, since
        // we consider that event-specific properties should have priority over
        // context-wide properties.
        Map<String, String> loggerContextMap = event.getLoggerContextVO().getPropertyMap();
        Map<String, String> mdcMap = event.getMDCPropertyMap();
        if (loggerContextMap != null) {
            mergedMap.putAll(loggerContextMap);
        }
        if (mdcMap != null) {
            mergedMap.putAll(mdcMap);
        }
 
        return mergedMap;
    }
 
    @Override
    protected Method getGeneratedKeysMethod() {
        return GET_GENERATED_KEYS_METHOD;
    }
 
    @Override
    protected String getInsertSQL() {
        return insertSQL;
    }
 
 
    static String buildInsertSQL(DBNameResolver dbNameResolver) {
        StringBuilder sqlBuilder = new StringBuilder("INSERT INTO ");
        sqlBuilder.append(dbNameResolver.getTableName(TableName.LOGGING_EVENT)).append(" (");
        sqlBuilder.append(dbNameResolver.getColumnName(ColumnName.TIMESTMP)).append(", ");
        sqlBuilder.append(dbNameResolver.getColumnName(ColumnName.FORMATTED_MESSAGE)).append(", ");
        sqlBuilder.append(dbNameResolver.getColumnName(ColumnName.LOGGER_NAME)).append(", ");
        sqlBuilder.append(dbNameResolver.getColumnName(ColumnName.LEVEL_STRING)).append(", ");
        sqlBuilder.append(dbNameResolver.getColumnName(ColumnName.THREAD_NAME)).append(", ");
        sqlBuilder.append(dbNameResolver.getColumnName(ColumnName.REFERENCE_FLAG)).append(", ");
        sqlBuilder.append(dbNameResolver.getColumnName(ColumnName.ARG0)).append(", ");
        sqlBuilder.append(dbNameResolver.getColumnName(ColumnName.ARG1)).append(", ");
        sqlBuilder.append(dbNameResolver.getColumnName(ColumnName.ARG2)).append(", ");
        sqlBuilder.append(dbNameResolver.getColumnName(ColumnName.ARG3)).append(", ");
        sqlBuilder.append(dbNameResolver.getColumnName(ColumnName.CALLER_FILENAME)).append(", ");
        sqlBuilder.append(dbNameResolver.getColumnName(ColumnName.CALLER_CLASS)).append(", ");
        sqlBuilder.append(dbNameResolver.getColumnName(ColumnName.CALLER_METHOD)).append(", ");
        sqlBuilder.append(dbNameResolver.getColumnName(ColumnName.CALLER_LINE)).append(") ");
        sqlBuilder.append("VALUES (?, ?, ? ,?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
        return sqlBuilder.toString();
    }
 
}
複製程式碼

日誌攔截  新建LogbackMarkerFilter類

import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.filter.AbstractMatcherFilter;
import ch.qos.logback.core.spi.FilterReply;
import org.slf4j.Marker;
import org.slf4j.MarkerFactory;
 
/**
 * @Title: LogbackMarkerFilter.java
 * @Description: TODO(日誌攔截)
 * @Author: 愛飄de小子  上午10:15
 */
public class LogbackMarkerFilter extends AbstractMatcherFilter<ILoggingEvent> {
 
    private Marker markerToMatch = null;
 
    @Override
    public void start() {
        if (null != this.markerToMatch) {
            super.start();
        } else {
            addError(" no MARKER yet !");
        }
    }
 
    @Override
    public FilterReply decide(ILoggingEvent event) {
        Marker marker = event.getMarker();
        if (!isStarted()) {
            return FilterReply.NEUTRAL;
        }
        if (null == marker) {
            return onMismatch;
        }
        if (markerToMatch.contains(marker)) {
            return onMatch;
        }
        return onMismatch;
    }
 
    public void setMarker(String markerStr) {
        if (null != markerStr) {
            markerToMatch = MarkerFactory.getMarker(markerStr);
        }
    }
 
}
複製程式碼

資料庫指令碼:

BEGIN;
 DROP TABLE IF EXISTS logging_event_property;
 DROP TABLE IF EXISTS logging_event_exception;
 DROP TABLE IF EXISTS logging_event;
 COMMIT;
 
 BEGIN;
 CREATE TABLE logging_event
 (
 timestmp         BIGINT NOT NULL,
 formatted_message  TEXT NOT NULL,
 logger_name       VARCHAR(254) NOT NULL,
 level_string      VARCHAR(254) NOT NULL,
 thread_name       VARCHAR(254),
 reference_flag    SMALLINT,
 arg0              VARCHAR(254),
 arg1              VARCHAR(254),
 arg2              VARCHAR(254),
 arg3              VARCHAR(254),
 caller_filename   VARCHAR(254) NOT NULL,
 caller_class      VARCHAR(254) NOT NULL,
 caller_method     VARCHAR(254) NOT NULL,
 caller_line       CHAR(4) NOT NULL,
 event_id          BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY
 );
 COMMIT;
 
 BEGIN;
 CREATE TABLE logging_event_property
 (
 event_id          BIGINT NOT NULL,
 mapped_key        VARCHAR(254) NOT NULL,
 mapped_value      TEXT,
 PRIMARY KEY(event_id, mapped_key),
 FOREIGN KEY (event_id) REFERENCES logging_event(event_id)
 );
 COMMIT;
 
 BEGIN;
 CREATE TABLE logging_event_exception
 (
 event_id         BIGINT NOT NULL,
 i                SMALLINT NOT NULL,
 trace_line       VARCHAR(254) NOT NULL,
 PRIMARY KEY(event_id, i),
 FOREIGN KEY (event_id) REFERENCES logging_event(event_id)
 );
 COMMIT;
複製程式碼

主要用到logging_event表,logging_event_property和logging_event_exception可以刪除。

使用:

//指定配置的Marker 
log.info(MarkerFactory.getMarker("DB"),"hello,logback!");
複製程式碼

資料庫顯示如下

深度截圖_選擇區域_20181201175331.png

如果專案整合spring-data-jpa,可以不執行資料庫指令碼,配置開啟自動更新表,並新建LoggingEvent類:

import lombok.Data;
import javax.persistence.*;
 
/**
 * @Title: LoggingEvent.java
 * @Description: TODO(日誌持久實體)
 * @Author: 愛飄de小子  上午10:57
 */
@Data
@Entity
@Table(name="logging_event")
public class LoggingEvent {
 
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Basic(optional = false)
    @Column(name = "event_id", nullable = false, columnDefinition = "BIGINT UNSIGNED")
    private Long eventId;
 
    @Column(name = "timestmp", nullable = false, columnDefinition = "BIGINT")
    private Long timestmp;
 
    @Column(name = "formatted_message", nullable = false, columnDefinition = "text")
    private String formattedMessage;
 
    @Column(name = "logger_name", nullable = false, columnDefinition = "varchar(254)")
    private String loggerName;
 
    @Column(name = "level_string", nullable = false, columnDefinition = "varchar(254)")
    private String levelString;
 
    @Column(name = "thread_name", columnDefinition = "varchar(254) DEFAULT NULL")
    private String threadName;
 
    @Column(name = "reference_flag", columnDefinition = "smallint(6)")
    private Integer referenceFlag;
 
    @Column(name = "arg0", columnDefinition = "varchar(254) DEFAULT NULL")
    private String arg0;
 
    @Column(name = "arg1", columnDefinition = "varchar(254) DEFAULT NULL")
    private String arg1;
 
    @Column(name = "arg2", columnDefinition = "varchar(254) DEFAULT NULL")
    private String arg2;
 
    @Column(name = "arg3", columnDefinition = "varchar(254) DEFAULT NULL")
    private String arg3;
 
    @Column(name = "caller_filename", nullable = false, columnDefinition = "varchar(254)")
    private String callerFilename;
 
    @Column(name = "caller_class", nullable = false, columnDefinition = "varchar(254)")
    private String callerClass;
 
    @Column(name = "caller_method", nullable = false, columnDefinition = "varchar(254)")
    private String callerMethod;
 
    @Column(name = "caller_line", nullable = false, columnDefinition = "varchar(5)")
    private String callerLine;
 
}
複製程式碼

相關文章