springboot+logback日志异步数据库

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">
             <!-- 自定义标志 -->
             <marker>DB</marker>
             <onMatch>ACCEPT</onMatch>
             <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">
          <appender-ref ref="DB_APPENDER" />
          <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">
             <!-- 自定义标志 -->
             <marker>DB</marker>
             <onMatch>ACCEPT</onMatch>
             <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;
 
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,923评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,154评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,775评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,960评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,976评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,972评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,893评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,709评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,159评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,400评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,552评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,265评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,876评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,528评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,701评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,552评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,451评论 2 352

推荐阅读更多精彩内容