log模块配置

logback配置全解析

作者:muggle
Logback是由log4j创始人设计的另一个开源日志组件,分为三个模块:

  1. logback-core:其它两个模块的基础模块

  2. logback-classic:它是log4j的一个改良版本,同时它完整实现了slf4j API使你可以很方便地更换成其它日志系统如log4j或JDK14 Logging

  3. logback-access:访问模块与Servlet容器集成提供通过Http来访问日志的功能
    在springboot中我们通过xml配置来操作logback

springboot中logback的默认配置文件名称为logback-spring.xml,若需要指定xml名称,需在application.properties(application.yml)中配置logging.config=xxxx.xml
现在贴出一份logback的xml配置,可直接使用,懒得看的小伙伴复制粘贴到你的项目中去体验吧

<?xml version="1.0" encoding="UTF-8" ?>


<configuration scan="true" scanPeriod="60 seconds" debug="false">

    <jmxConfigurator/>

    <property name="log_dir" value="logs"/>
    <property name="maxHistory" value="100"/>

    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>
                %d{yyyy-MM-dd HH:mm:ss.SSS} %highlight([%-5level]) %logger - %msg%n
            </pattern>
        </encoder>
    </appender>
    <appender name="logs" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>
                ${log_dir}/%d{yyyy-MM-dd}-poseidon.log
            </fileNamePattern>
            <maxHistory>${maxHistory}</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>
                %d{yyyy-MM-dd HH:mm:ss.SSS} %highlight([%-5level]) %logger - %msg%n
            </pattern>
        </encoder>
    </appender>

    <appender name="runningTime-file" class="ch.qos.logback.core.rolling.RollingFileAppender">

        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${log_dir}/runningTime/%d{yyyy-MM-dd}-poseidon.log</fileNamePattern>
            <maxHistory>${maxHistory}</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%-5level] %logger - %msg%n</pattern>
        </encoder>
    </appender>
    <appender name="runningTime-console" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>
                %d{yyyy-MM-dd HH:mm:ss.SSS} [%-5level] %logger - %msg%n
            </pattern>
        </encoder>
    </appender>
    <logger name="runningTime" level="info" additivity="false">
        <!--<appender-ref ref="runningTime-file"/>-->
        <appender-ref ref="runningTime-console"/>
    </logger>
<!--  可能会抛出方言异常 两个解决方案 配置方言或者换连接池 换druid不会有这个异常-->
    <appender name="requestLog-db" class="ch.qos.logback.classic.db.DBAppender">
        <connectionSource class="ch.qos.logback.core.db.DataSourceConnectionSource">
            <dataSource class="org.apache.commons.dbcp.BasicDataSource">
                <driverClassName>com.mysql.cj.jdbc.Driver</driverClassName>
                <url>jdbc:mysql://xxx/xxxx?characterEncoding=UTF-8</url>
                <username>xx</username>
                <password>xxxx</password>
            </dataSource>
        </connectionSource>
        <!--<sqlDialect class="ch.qos.logback.core.db.dialect.MySQLDialect" />-->
    </appender>
    <!--异步配置-->
    <!--<appender name="requestLog-file" class="ch.qos.logback.core.rolling.RollingFileAppender">
         <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
             <fileNamePattern>${log_dir}/requestLog/%d{yyyy-MM-dd}-poseidon.log</fileNamePattern>
             <maxHistory>${maxHistory}</maxHistory>
         </rollingPolicy>
         <encoder>
             <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%-5level] %logger - %msg%n</pattern>
         </encoder>
     </appender>-->
    <!-- <appender name="request-asyn" class="ch.qos.logback.classic.AsyncAppender">
        <appender-ref ref="requestLog-file"/>
    </appender> -->
    <appender name="logs-asyn" class="ch.qos.logback.classic.AsyncAppender">
        <appender-ref ref="logs"/>
    </appender>
    <logger name="requestLog" level="info" additivity="false">
        <!--<appender-ref ref="requestLog-file"/>-->
        <!-- DBAppender 查看可知其父类dbappenderbase继承了UnsynchronizedAppenderBase<E> 所以dbappender本身是异步的 无需配置异步-->
        <appender-ref ref="requestLog-db"/>
    </logger>
    <root>
        <level value="info"/>
        <appender-ref ref="console"/>
        <!--<appender-ref ref="logs"/>-->
        <appender-ref ref="logs-asyn"/>
    </root>
</configuration>

我们可以看到xml中有四种节点
appender,logger,root,configuration

节点解读

configuration包含三个属性:

  1. scan: 当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true。
  2. scanPeriod: 设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟。
  3. debug: 当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。

Logger作为日志的记录器,把它关联到应用的对应的context上后,主要用于存放日志对象,也可以定义日志类型、级别。

Appender主要用于指定日志输出的目的地,目的地可以是控制台、文件、远程套接字服务器、 MySQL、PostreSQL、 Oracle和其他数据库、 JMS和远程UNIX Syslog守护进程等。

root 就是最高级别logger,所有不被指定logger的日志都归root管理。

在slf4j框架下我们使用log是这样的:

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

或者

 private static final Logger logger= LoggerFactory.getLogger("xxxx");

可以理解为代码中的getLogger() 方法就是获取xml配置中的logger,如果没有配置相应的logger则为root
比如我配置了:

<logger name="hhh" level="info" additivity="false">
        <!--<appender-ref ref="requestLog-file"/>-->
        <appender-ref ref="xxx"/>
</logger>

那我在获得一个logger时可以这样获得它:

 private static final Logger logger= LoggerFactory.getLogger("hhh");

我所输出的日志将被这个logger所管理
logger 上有三个配置 name level additivity
name就是这个logger的名称,level就是这个日志过滤的级别,低于这个级别的日志不输入到对应的appender中;additivity是否向上级logger传递打印信息,默认是true。logger中可以配置多个appender-ref,也就是可以指定多个输出地点。
而root只是特殊的logger,用法上无差别

appender节点:
appender节点是logback配置的关键,其name属性指定其名称,class属性指定实现类,对应得实现类有

ch.qos.logback.core.ConsoleAppender // 以控制台作为输出
ch.qos.logback.core.rolling.RollingFileAppender//以日志文件作为输出
ch.qos.logback.classic.db.DBAppender//以数据库作为输出
net.logstash.logback.appender.LogstashTcpSocketAppender//以logstash作为输出需要引入如下依赖:
ch.qos.logback.classic.AsyncAppender//异步输出 需要定义appender-ref

// logstash依赖
<dependency>
  <groupId>net.logstash.logback</groupId>
  <artifactId>logstash-logback-encoder</artifactId>
  <version>4.11</version>
</dependency>

所有的appender 实现ch.qos.logback.core.Appender接口或者 ch.qos.logback.core.UnsynchronizedAppenderBase接口(异步),我们也可以自定义appender来指定日志输出;

在Appender中可以定义哪些节点我们一个个来看:

第一种: ConsoleAppender
如同它的名字一样,这个Appender将日志输出到console,更准确的说是System.out 或者System.err。
它包含的参数如下:

Property Name Type Description
encoder Encoder 通常在其pattern里指定日志格式 如: %d{yyyy-MM-dd HH:mm:ss.SSS} %highlight([%-5level]) %logger - %msg%n表示 日期格式 日志级别(高亮)logger的名称 logger的message
target String 指定输出目标。可选值:System.out 或 System.err。默认值:System.out
withJansi boolean 是否支持ANSI color codes(类似linux中的shell脚本的输出字符串颜色控制代码)。默认为false。如果设置为true。例如:[31m 代表将前景色设置成红色。在windows中,需要提供"org.fusesource.jansi:jansi:1.9",而在linux,mac os x中默认支持。

第二种: FileAppender
将日志输出到文件当中,目标文件取决于file属性。是否追加输出,取决于append属性。

Property Name Type Description
append boolean 是否以追加方式输出。默认为true。
encoder Encoder See OutputStreamAppender properties.
file String 指定文件名。注意在windows当中,反斜杠 \ 需要转义,或直接使用 / 也可以。例如 c:/temp/test.logor 或 c:\temp\test.log 都可以。没有默认值,如果上层目录不存在,FileAppender会自动创建。
prudent boolean 是否工作在谨慎模式下。在谨慎模式下,FileAppender将会安全写入日志到指定文件,即时在不同的虚拟机jvm中有另一个相同的FileAppender实例。默认值:fales;设置为true,意味着append会被自动设置成true。prudent依赖于文件排它锁。实验表明,使用文件锁,会增加3倍的日志写入消耗。比如说,当prudent模式为off,写入一条日志到文件只要10毫秒,但是prudent为真,则会接近30毫秒。prudent 模式实际上是将I/O请求序列化,因此在I/O数量较大,比如说100次/s或更多的时候,带来的延迟也会显而易见,所以应该避免。在networked file system(远程文件系统)中,这种消耗将会更大,可能导致死锁。

第三个: RollingFileAppender

RollingFileAppender继承自FileAppender,提供日志目标文件自动切换的功能。例如可以用日期作为日志分割的条件。
RollingFileAppender有两个重要属性,RollingPolicy负责怎么切换日志,TriggeringPolicy负责何时切换。为了使RollingFileAppender起作用,这两个属性必须设置,但是如果RollingPolicy的实现类同样实现了TriggeringPolicy接口,则也可以只设置RollingPolicy这个属性。
下面是它的参数:

Property Name Type Description
file String 指定文件名。注意在windows当中,反斜杠 \ 需要转义,或直接使用 / 也可以。例如 c:/temp/test.logor 或 c:\temp\test.log 都可以。没有默认值,如果上层目录不存在,FileAppender会自动创建。
append boolean 是否以追加方式输出。默认为true。
encoder Encoder See OutputStreamAppender properties.
rollingPolicy RollingPolicy 当发生日志切换时,RollingFileAppender的切换行为。例如日志文件名的修改
triggeringPolicy TriggeringPolicy 决定什么时候发生日志切换,例如日期,日志文件大小到达一定值
prudent boolean FixedWindowRollingPolicy 不支持prudent模式。TimeBasedRollingPolicy 支持prudent模式,但是需要满足一下两条约束:在prudent模式中,日志文件的压缩是不被允许,不被支持的。不能设置file属性。

第四个:SocketAppender及SSLSocketAppender(未尝试过)

到目前为止我们讲的appender都只能将日志输出到本地资源。与之相对的,SocketAppender就是被设计用来输出日志到远程实例中的。SocketAppender输出日志采用明文方式,SSLSocketAppender则采用加密方式传输日志。
被序列化的日志事件的类型是 LoggingEventVO 继承ILoggingEvent接口。远程日志记录并非是侵入式的。在反序列化接收后,日志事件就可以好像在本地生成的日志一样处理了。多个SockerAppender可以向同一台日志服务器发送日志。SocketAppender并不需要关联一个Layout,因为它只是发送序列化的日志事件给远程日志服务器。SocketAppender的发送操作是基于TCP协议的。因此如果远程服务器是可到达的,则日志会被其处理,如果远程服务器宕机或不可到达,那么日志将会被丢弃。等到远程服务器复活,日志发送将会透明的重新开始。这种透明式的重连,是通过一个“连接“线程周期性的尝试连接远程服务器实现的。
Logging events会由TCP协议实现自动缓冲。这意味着,如果网络速度比日志请求产生速度快,则网络速度并不会影响应用。但如果网络速度过慢,则网络速度则会变成限制,在极端情况下,如果远程日志服务器不可到达,则会导致应用最终阻塞。不过,如果服务器可到达,但是服务器宕机了,这种情况,应用不会阻塞,而只是丢失一些日志事件而已。
需要注意的是,即使SocketAppender没有被logger链接,它也不会被gc回收,因为他在connector thread中任然存在引用。一个connector thread 只有在网络不可达的情况下,才会退出。为了防止这个垃圾回收的问题,我们应该显示声明关闭SocketAppender。长久存活并创建/销毁大量的SocketAppender实例的应用,更应该注意这个问题。不过大多数应用可以忽略这个问题。如果JVM在SocketAppender关闭之前将其退出,又或者是被垃圾回收,这样子可能导致丢失一些还未被传输,在管道中等待的日志数据。为了防止避免日志丢失,经常可靠的办法就是调用SocketAppender的close方法,或者调用LoggerContext的stop方法,在退出应用之前。

下面我们来看看SocketAppender的属性:

Property Name Type Description
includeCallerData boolean 是否包含调用者的信息如果为true,则以下日志输出的 ?:? 会替换成调用者的文件名跟行号,为false,则为问号。2019-01-06 17:37:30,968 DEBUG [Thread-0] [?:?] chapters.appenders.socket.SocketClient2 - Hi
port int 端口号
reconnectionDelay Duration 重连延时,如果设置成“10 seconds”,就会在连接u武器失败后,等待10秒,再连接。默认值:“30 seconds”。如果设置成0,则关闭重连功能。
queueSize int 设置缓冲日志数,如果设置成0,日志发送是同步的,如果设置成大于0的值,会将日志放入队列,队列长度到达指定值,在统一发送。可以加大服务吞吐量。
eventDelayLimit Duration 设置日志超时丢弃时间。当设置“10 seconds”类似的值,如果日志队列已满,而服务器长时间来不及接收,当滞留时间超过10 seconds,日志就会被丢弃。默认值: 100 milliseconds
remoteHost String 远程日志服务器的IP
ssl SSLConfiguration 只在SSLSocketAppender包含该属性节点。提供SSL配置,详情见 Using SSL.

标准的Logback Classic包含四个可供使用的Receiver用来接收来自SocketAppender的logging evnets。

第五个: SMTPAppender

SMTPAppender 可以将logging event存放在一个或多个固定大小的缓冲区中,然后在用户指定的event到来之时,将适当的大小的logging event以邮件方式发送给运维人员。
详细属性如下:

Property Name Type Description
smtpHost String SMTP server的地址,必需指定。如网易的SMTP服务器地址是: smtp.163.com
smtpPort int SMTP server的端口地址。默认值:25
to String 指定发送到那个邮箱,可设置多个<to>属性,指定多个目的邮箱
from String 指定发件人名称。如果设置成“muggle <hh@moral.org> ”,则邮件发件人将会是“muggle hh@moral.org
subject String 指定emial的标题,它需要满足PatternLayout中的格式要求。如果设置成“Log: %logger - %msg”,就案例来讲,则发送邮件时,标题为“Log: com.foo.Bar - Hello World ”。 默认值:"%logger{20} - %m".
discriminator Discriminator 通过Discriminator, SMTPAppender可以根据Discriminator的返回值,将到来的logging event分发到不同的缓冲区中。默认情况下,总是返回相同的值来达到使用一个缓冲区的目的。
evaluator IEvaluator 指定触发日志发送的条件。通过<evaluator class=... />指定EventEvaluator接口的实现类。默认情况下SMTPAppeender使用的是OnErrorEvaluator,表示当发送ERROR或更高级别的日志请求时,发送邮件。Logback提供了几个evaluators:OnErrorEvaluator、OnMarkerEvaluator、JaninoEventEvaluator、GEventEvaluator(功能强大)
cyclicBufferTracker CyclicBufferTracker 指定一个cyclicBufferTracker跟踪cyclic buffer。它是基于discriminator的实现的。如果你不指定,默认会创建一个CyclicBufferTracker ,默认设置cyclic buffer大小为256。你也可以手动指定使用默认的CyclicBufferTracker,并且通过<bufferSize>属性修改默认的缓冲区接收多少条logging event。
username String 发送邮件账号,默认为null
password String 发送邮件密码,默认为null
STARTTLS boolean 如果设置为true,appender会尝试使用STARTTLS命令,如果服务端支持,则会将明文连接转换成加密连接。需要注意的是,与日志服务器连接一开始是未加密的。默认值:false
SSL boolean 如果设置为true,appender将会使用SSL连接到日志服务器。 默认值:false
charsetEncoding String 指定邮件信息的编码格式 默认值:UTF-8
localhost String 如果smtpHost没有正确配置,比如说不是完整的地址。这时候就需要localhost这个属性提供服务器的完整路径(如同java中的完全限定名 ),详情参考com.sun.mail.smtp 中的mail.smtp.localhost属性
asynchronousSending boolean 这个属性决定email的发送是否是异步。默认:true,异步发送但是在某些情况下,需要以同步方式发送错误日志的邮件给管理人员,防止不能及时维护应用。
includeCallerData boolean 默认:false 指定是否包含callerData在日志中
sessionViaJNDI boolean SMTPAppender依赖javax.mail.Session来发送邮件。默认情况下,sessionViaJNDI为false。javax.mail.Session实例的创建依赖于SMTPAppender本身的配置信息。如果设置为true,则Session的创建时通过JNDI获取引用。这样做的好处可以让你的代码复用更好,让配置更简洁。需要注意的是,如果使用JNDI获取Session对象,需要保证移除mail.jar以及activation.jar这两个jar包
jndiLocation String 如果sessionViaJNDI设置为true,则jndiLocation指定JNDI的资源名,默认值为:"java:comp/env/mail/Session"

SMTPAppender只保留最近的256条logging events 在循环缓冲区中,当缓冲区慢,就会开始丢弃最老的logging event。因此不管什么时候,SMTPAppender一封邮件最多传递256条日志事件。SMTPAppender依赖于JavaMail API。而JavaMail API又依赖于IOC框架(依赖注入)。

第六个:DBAppender

DBAppender 可以将日志事件插入到3张数据表中。它们分别是logging_event,logging_event_property,logging_event_exception。这三张数据表必须在DBAppender工作之前存在。它们的sql脚本可以在 logback-classic/src/main/java/ch/qos/logback/classic/db/script folder 这个目录下找到。这个脚本对大部分SQL数据库都是有效的,除了少部分,少数语法有差异需要调整。
下面是logback与常见数据库的支持信息:

RDBMS tested version(s) tested JDBC driver version(s) supports getGeneratedKeys() method is a dialect provided by logback
DB2 untested untested unknown NO
H2 -- - unknown YES
HSQL -- - NO YES
Microsoft SQL Server -- -- YES YES
MySQL 5.7 YES YES
PostgreSQL -- -- NO YES
Oracle -- -- YES YES
SQLLite -- - unknown YES
Sybase -- - unknown YES

下面给出三张表的sql语句:

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;

第七个: AsyncAppender

AsyncAppender记录ILoggingEvents的方式是异步的。它仅仅相当于一个event分配器,因此需要配合其他appender才能有所作为。

需要注意的是:AsyncAppender将event缓存在 BlockingQueue ,一个由AsyncAppender创建的工作线程,会一直从这个队列的头部获取events,然后将它们分配给与AsyncAppender唯一关联的Appender中。默认情况下,如果这个队列80%已经被占满,则AsyncAppender会丢弃等级为 TRACE,DEBUG,INFO这三个等级的日志事件。
在应用关闭或重新部署的时候,AsyncAppender一定要被关闭,目的是为了停止,回收再利用worker thread,和刷新缓冲队列中logging events。那如果关闭AsyncAppender呢?可以通过关闭LoggerContext来关闭所有appender,当然也包括AsyncAppender了。AsyncAppender会在maxFlushTime属性设置的时间内等待Worker thread刷新全部日志event。如果你发现缓冲的event在关闭LoggerContext的时候被丢弃,这时候你就也许需要增加等待的时间。将maxFlushTime设置成0,就是AsyncAppender一直等待直到工作线程将所有被缓冲的events全部刷新出去才执行才结束。
根据JVM退出的模式,工作线程worker thread处理被缓冲的events的工作是可以被中断的,这样就导致了剩余未处理的events被搁浅。这种现象通常的原因是当LoggerContext没有完全关闭,或者当JVM终止那些非典型的控制流(不明觉厉)。为了避免工作线程的因为这些情况而发生中断,一个shutdown hook(关闭钩子)可以被插入到JVM运行的时候,这个钩子的作用是在JVM开始shutdown刚开始的时候执行关闭 LoggerContext的任务。

下面是AsyncAppender的属性表

Property Name Type Description
queueSize int 设置blocking queue的最大容量,默认是256条events
discardingThreshold int 默认,当blocking queue被占用80%以上,AsyncAppender就会丢弃level为 TRACE,DEBUG,INFO的日志事件,如果要保留所有等级的日志,需要设置成0
includeCallerData boolean 提取CallerData代价比较昂贵,为了提高性能,caller data默认不提供。只有一些获取代价较低的数据,如线程名称,MDC值才会被保留。如果设置为true,就会包含caller data
maxFlushTime int 设置最大等待刷新事件,单位为miliseconds(毫秒)。当LoggerContext关闭的时候,AsyncAppender会在这个时间内等待工作线程完成events的flush工作,超时未处理的events将会被抛弃。
neverBlock boolean 默认为false,如果队列被填满,为了处理所有日志,就会阻塞的应用。如果为true,为了不阻塞你的应用,也会选择抛弃一些message。

默认情况下,event queue最大的容量是256。如果队列被填充满那么就会阻塞你的应用,直到队列能够容纳新的logging event。所以当AsyncAppender工作在队列满的情况下,可以称作伪同步。
在以下四种情况下容易导致AsyncAppender伪同步状态的出现:

  1. 应用中存在大量线程
  2. 每秒产生大量的logging events
  3. 每一个logging event都存在大量的数据
  4. 子appender中存在很高的延迟

为了避免伪同步的出现,提高queueSizes普遍有效,但是就消耗了应用的可用内存。

下面列出一些 appender配置示例:


<configuration>
  <appender name="FILE" class="ch.qos.logback.core.FileAppender">
    <file>myapp.log</file>
    <encoder>
      <pattern>%logger{35} - %msg%n</pattern>
    </encoder>
  </appender>

  <appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
    <appender-ref ref="FILE" />
  </appender>

  <root level="DEBUG">
    <appender-ref ref="ASYNC" />
  </root>
</configuration>

<configuration>

  <appender name="DB" class="ch.qos.logback.classic.db.DBAppender">
    <connectionSource
      class="ch.qos.logback.core.db.DataSourceConnectionSource">
      <dataSource
        class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <driverClass>com.mysql.jdbc.Driver</driverClass>
        <jdbcUrl>jdbc:mysql://${serverName}:${port}/${dbName}</jdbcUrl>
        <user>${user}</user>
        <password>${password}</password>
      </dataSource>
    </connectionSource>
  </appender>

  <root level="DEBUG">
    <appender-ref ref="DB" />
  </root>
</configuration>

<configuration>
  <appender name="EMAIL" class="ch.qos.logback.classic.net.SMTPAppender">
    <smtpHost>smtp.gmail.com</smtpHost>
    <smtpPort>465</smtpPort>
    <SSL>true</SSL>
    <username>YOUR_USERNAME@gmail.com</username>
    <password>YOUR_GMAIL_PASSWORD</password>

    <to>EMAIL-DESTINATION</to>
    <to>ANOTHER_EMAIL_DESTINATION</to> <!-- additional destinations are possible -->
    <from>YOUR_USERNAME@gmail.com</from>
    <subject>TESTING: %logger{20} - %m</subject>
    <layout class="ch.qos.logback.classic.PatternLayout">
      <pattern>%date %-5level %logger{35} - %message%n</pattern>
    </layout>
  </appender>

  <root level="DEBUG">
    <appender-ref ref="EMAIL" />
  </root>
</configuration>

<configuration>
  <appender name="EMAIL" class="ch.qos.logback.classic.net.SMTPAppender">
    <smtpHost>smtp.gmail.com</smtpHost>
    <smtpPort>587</smtpPort>
    <STARTTLS>true</STARTTLS>
    <username>YOUR_USERNAME@gmail.com</username>
    <password>YOUR_GMAIL_xPASSWORD</password>

    <to>EMAIL-DESTINATION</to>
    <to>ANOTHER_EMAIL_DESTINATION</to> <!-- additional destinations are possible -->
    <from>YOUR_USERNAME@gmail.com</from>
    <subject>TESTING: %logger{20} - %m</subject>
    <layout class="ch.qos.logback.classic.PatternLayout">
      <pattern>%date %-5level %logger - %message%n</pattern>
    </layout>
  </appender>

  <root level="DEBUG">
    <appender-ref ref="EMAIL" />
  </root>
</configuration>

SimpleSocketServer需要两个命令行参数,port 和 configFile路径。(该方法待验证)
java ch.qos.logback.classic.net.SimpleSocketServer 6000 \ src/main/java/chapters/appenders/socket/server1.xml

客户端的SocketAppender的简单配置例子:
<configuration>

  <appender name="SOCKET" class="ch.qos.logback.classic.net.SocketAppender">
    <remoteHost>192.168.0.101</remoteHost>
    <port>8888</port>
    <reconnectionDelay>10000</reconnectionDelay>
    <includeCallerData>true</includeCallerData>
  </appender>

  <root level="DEBUG">
    <appender-ref ref="SOCKET" />
  </root>

</configuration>

在服务端使用SimpleSSLSocketServer
java -Djavax.net.ssl.keyStore=src/main/java/chapters/appenders/socket/ssl/keystore.jks \ -Djavax.net.ssl.keyStorePassword=changeit \ ch.qos.logback.classic.net.SimpleSSLSocketServer 6000 \ src/main/java/chapters/appenders/socket/ssl/server.xml

SSLSocketAppender配置
<configuration debug="true">

  <appender name="SOCKET" class="ch.qos.logback.classic.net.SSLSocketAppender">
    <remoteHost>${host}</remoteHost>
    <port>${port}</port>
    <reconnectionDelay>10000</reconnectionDelay>
    <ssl>
      <trustStore>
        <location>${truststore}</location>
        <password>${password}</password>
      </trustStore>
    </ssl>
  </appender>

  <root level="DEBUG">
    <appender-ref ref="SOCKET" />
  </root>

</configuration>

<configuration>

  <appender name="FILE" class="ch.qos.logback.core.FileAppender">
    <file>testFile.log</file>
    <append>true</append>
    <!-- encoders are assigned the type
        ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
    <encoder>
      <pattern>%-4relative [%thread] %-5level %logger{35} - %msg%n</pattern>
    </encoder>
  </appender>

  <root level="DEBUG">
    <appender-ref ref="FILE" />
  </root>
</configuration>

参考:https://blog.csdn.net/tianyaleixiaowu/article/details/73327752

下面基于logback配置做一个请求日志的的封装

功能:记录每次请求的参数和用户ID存入数据库或者elk
问题:javaee规范中request输入输出流都只能被读取一次,所以如果用过滤器或者拦截器读取request中的流都会导致后面的controller无法接受到数据。
所以我们要用原生的aop获得请求参数,切点为controller,这就很好的避开了以上问题。

package com.muggle.poseidon.core.aspect;

import com.muggle.poseidon.manager.UserInfoManager;
import com.muggle.poseidon.utils.RequestUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.Serializable;

/**
 * @program: hiram_erp
 * @description: 日志信息切面
 * @author: muggle
 * @create: 2019-02-21
 **/
@Aspect
@Component
public class LogMessageAspect {

    private final static Logger logger = LoggerFactory.getLogger("requestLog");
//    private final static Logger timeLog = LoggerFactory.getLogger(LogMessageAspect.class);
    private static final ThreadLocal<Long> threadLocal = new ThreadLocal<>();
    @Pointcut("execution(public * com.hiram.erp.controller.*.*(..))")
    public void webLog() {}

    /**
     * 在切点之前织入
     * @param joinPoint
     * @throws Throwable
     */
    @Before("webLog()")
    public void doBefore(JoinPoint joinPoint) throws Throwable {
//        System.out.println("sssssssssssssssssssssssssssssssssssssssssssssssssssss");
       /* // 开始打印请求日志
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();

        // 打印请求相关参数
        // 打印请求 url
        // 请求id
        Long userId=null;
        if (user!=null){
            userId=user.getUserInfo().getUserId();
        }
        logger.info("URL : {}, 登录id: {} ,HTTP Method: {},ip :{},Request Args : {}", request.getRequestURL().toString(),userId, request.getMethod(),request.getRemoteAddr());
*/    }

    /**
     * 在切点之后织入
     * @throws Throwable
     */
    @After("webLog()")
    public void doAfter(JoinPoint joinPoint) throws Throwable {



    }

    /**
     * 环绕
     * @param joinPoint
     * @return
     * @throws Throwable
     */
    @Around("webLog()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();
        Object result = joinPoint.proceed();
        // 打印出参
//        logger.info("Response Args  : {},", JSONObject.toJSONString(result),new Date());
        // 执行耗时
        // 开始打印请求日志
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        HttpServletResponse response = attributes.getResponse();
        String requestURL = request.getRequestURL().toString();
        if (requestURL.contains("/sys/log_info/")){
            return result;
        }
        // 打印请求相关参数
        // 打印请求 url
        // 请求id
        String userId = UserInfoManager.getUserId();

        String url = request.getRequestURL().toString();
        String method = request.getMethod();
        String remoteAddr = RequestUtils.getIpAddr(request);
        Object[] args = joinPoint.getArgs();
//        List<Object> objects=new ArrayList<>();
        StringBuilder stringBuilder = new StringBuilder();
        for (int i=0;i<args.length;i++){
            if (args[i] instanceof Serializable||args[i] instanceof Number ||args[i] instanceof String){
                stringBuilder.append( args[i].toString());
//                objects.add(args[i]);
            }
        }
        logger.info("{\"startTime\":\"{}\",\"url\":\"{}\",\"userId\":\"{}\" ,\"httpMethod\":\"{}\",\"ip\":\"{}\",\"requestArgs\":\"{}\",\"status\":{}}",startTime,url,userId,method,remoteAddr,stringBuilder.toString(),response.getStatus());
        return result;
    }


}

对于数据库存储,如果我们希望log存在另外一个数据库中不存在项目里的数据库中,并且可以通过持久化框架查询数据库内信息。我们则可以配置多数据源,如果将日志放在同一个数据库中则直接配置appender就行了,很方便。
多数据源配置mybatis版:

其原理是配置多个sessionfactory,然后根据不同的mapperscan来区分不同mapper对应的数据库

以druid连接池为例

application.yml

log:
  datasource:
    druid:
      url: ${mysql_url}/log?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowMultiQueries=true&serverTimezone=GMT%2B8&nullCatalogMeansCurrent=true
      username:
      password:
      driver-class-name: com.mysql.cj.jdbc.Driver
      connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
      filters: stat,wall
      initialSize: 5
      maxActive: 20
      maxPoolPreparedStatementPerConnectionSize: 20
      maxWait: 60000
      minIdle: 5
      poolPreparedStatements: true
      testOnBorrow: false
      testOnReturn: false
      testWhileIdle: true
      timeBetweenEvictionRunsMillis: 60000
      validationQuery: SELECT 1


spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      url: ${mysql_url}/hiram_erp?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowMultiQueries=true&serverTimezone=GMT%2B8&nullCatalogMeansCurrent=true
      username:
      password:
      driver-class-name: com.mysql.cj.jdbc.Driver
      connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
      filters: stat,wall
      initialSize: 5
      maxActive: 20
      maxPoolPreparedStatementPerConnectionSize: 20
      maxWait: 60000
      minIdle: 5
      poolPreparedStatements: true
      testOnBorrow: false
      testOnReturn: false
      testWhileIdle: true
      timeBetweenEvictionRunsMillis: 60000
      validationQuery: SELECT 1

@Configuration
// 主数据库配置 指定mapper位置
@MapperScan(basePackages = {"com.muggle.poseidon.mapper"}, sqlSessionTemplateRef = "sqlSessionTemplate")
public class ManySourceDBConfig {

    @Bean(name = "dataSource")
    // 读取application的配置信息
   @ConfigurationProperties(prefix = "spring.datasource.druid")
   // 最高优先级,表示系统默认使用该配置
    @Primary
    public DataSource dataSource() {
        DruidDataSource druidDataSource = new DruidDataSource();

        List filterList = new ArrayList<>();

        filterList.add(wallFilter());

        druidDataSource.setProxyFilters(filterList);

        return druidDataSource;
    }

    @Bean(name = "sqlSessionFactory")
    @Primary
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(this.dataSource());

        Properties props = new Properties();
        props.setProperty("localCacheScope", "true");
        props.setProperty("lazyLoadingEnabled", "true");
        props.setProperty("aggressiveLazyLoading", "false");
        props.setProperty("jdbcTypeForNull", "NULL");
        sqlSessionFactoryBean.setConfigurationProperties(props);
        sqlSessionFactoryBean.setVfs(SpringBootVFS.class);
        //pageHelper
        Properties properties = new Properties();
        properties.setProperty("reasonable", "true");
        properties.setProperty("supportMethodsArguments", "true");
        properties.setProperty("params", "count=countSql");
        properties.setProperty("pageSizeZero", "true");
        PageInterceptor interceptor = new PageInterceptor();
        interceptor.setProperties(properties);
        sqlSessionFactoryBean.setPlugins(new Interceptor[]{interceptor});
        sqlSessionFactoryBean.setTypeAliasesPackage("com.muggle.poseidon.model");
        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        sqlSessionFactoryBean.setMapperLocations(resolver.getResources("classpath*:/mapper/*.xml"));
        return sqlSessionFactoryBean.getObject();
    }

    @Bean(name = "transactionManager")
    @Primary
    public PlatformTransactionManager transactionManager() {
        return new DataSourceTransactionManager(this.dataSource());
    }

    @Bean(name = "sqlSessionTemplate")
    public SqlSessionTemplate testSqlSessionTemplate(@Qualifier("sqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception {
        return new SqlSessionTemplate(sqlSessionFactory);
    }

    @Bean
    public ServletRegistrationBean statViewServlet() {
        ServletRegistrationBean druid = new ServletRegistrationBean();
        druid.setServlet(new StatViewServlet());
        druid.setUrlMappings(Collections.singletonList("/druid/*"));
        Map<String, String> params = new HashMap<>();
        params.put("loginUsername", "");
        params.put("loginPassword", "");
        druid.setInitParameters(params);
        return druid;
    }

    @Bean
    public FilterRegistrationBean webStatFilter() {
        FilterRegistrationBean fitler = new FilterRegistrationBean();
        fitler.setFilter(new WebStatFilter());
        fitler.setUrlPatterns(Collections.singletonList("/*"));
        fitler.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");
        return fitler;
    }

    @Bean
    public WallFilter wallFilter() {

        WallFilter wallFilter = new WallFilter();

        wallFilter.setConfig(wallConfig());

        return wallFilter;

    }

    @Bean
    public WallConfig wallConfig() {

        WallConfig config = new WallConfig();

        config.setMultiStatementAllow(true);//允许一次执行多条语句

        config.setNoneBaseStatementAllow(true);//允许非基本语句的其他语句

        return config;

    }

    @Bean
    public ProcessEngineConfiguration processEngineConfiguration() {
        ProcessEngineConfiguration pec = StandaloneProcessEngineConfiguration.createStandaloneProcessEngineConfiguration();
        pec.setDataSource(dataSource());
        //如果表不存在,自动创建表
        pec.setDatabaseSchemaUpdate(ProcessEngineConfiguration.DB_SCHEMA_UPDATE_TRUE);
        //属性asyncExecutorActivate定义为true,工作流引擎在启动时就建立启动async executor线程池
        pec.setAsyncExecutorActivate(false);
        return pec;
    }


    @Bean
    public ProcessEngine processEngine() {
        return processEngineConfiguration().buildProcessEngine();
    }

}

log数据库配置




/**
 * @program:
 * @description:
 * @author: muggle
 * @create: 2019-02-23
 **/
@Configuration
// 注意确保主配置无法扫描到这个包
@MapperScan(basePackages = "com.muggle.poseidon.logmapper", sqlSessionTemplateRef  = "test1SqlSessionTemplate")

public class LogDBConfig  {
    @Bean(name = "test1DataSource")
    @ConfigurationProperties(prefix = "log.datasource.druid")
    public DataSource dataSource() {
        DruidDataSource druidDataSource = new DruidDataSource();

        List filterList = new ArrayList<>();

        filterList.add(wallFilter());

        druidDataSource.setProxyFilters(filterList);

        return druidDataSource;
    }

    @Bean(name = "test1SqlSessionFactory")
    public SqlSessionFactory testSqlSessionFactory(@Qualifier("test1DataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dataSource);
        bean.setMapperLocations(new
        // mapper位置,不要和主配置的mapper放到一起
         PathMatchingResourcePatternResolver().getResources("classpath*:/mapper/log/*.xml"));
        return bean.getObject();
    }

    @Bean(name = "test1TransactionManager")
    public DataSourceTransactionManager testTransactionManager(@Qualifier("test1DataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

    @Bean(name = "test1SqlSessionTemplate")
    public SqlSessionTemplate testSqlSessionTemplate(@Qualifier("test1SqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception {
        return new SqlSessionTemplate(sqlSessionFactory);
    }

    @Bean
    public WallFilter wallFilter() {

        WallFilter wallFilter = new WallFilter();

        wallFilter.setConfig(wallConfig());

        return wallFilter;

    }
    @Bean
    public WallConfig wallConfig() {

        WallConfig config = new WallConfig();

        config.setMultiStatementAllow(true);//允许一次执行多条语句

        config.setNoneBaseStatementAllow(true);//允许非基本语句的其他语句

        return config;

    }
}

多数据源jpa版

package com.muggle.poseidon.config;


import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.core.JdbcTemplate;

import javax.sql.DataSource;

@Configuration
public class DataSourceConfig {

    /**
     * 扫描spring.datasource.primary开头的配置信息
     *
     * @return 数据源配置信息
     */
    @Primary
    @Bean(name = "primaryDataSourceProperties")
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSourceProperties dataSourceProperties() {
        return new DataSourceProperties();
    }

    /**
     * 获取主库数据源对象
     *
     * @param properties 注入名为primaryDataSourceProperties的bean
     * @return 数据源对象
     */
    @Primary
    @Bean(name = "primaryDataSource")
    public DataSource dataSource(@Qualifier("primaryDataSourceProperties") DataSourceProperties properties) {
        return properties.initializeDataSourceBuilder().build();
    }

    /**
     * 该方法仅在需要使用JdbcTemplate对象时选用
     *
     * @param dataSource 注入名为primaryDataSource的bean
     * @return 数据源JdbcTemplate对象
     */
    @Primary
    @Bean(name = "primaryJdbcTemplate")
    public JdbcTemplate jdbcTemplate(@Qualifier("primaryDataSource") DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }




}

package com.muggle.poseidon.config;


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateSettings;
import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import java.util.Map;


@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
        // repository包名
        basePackages = "com.muggle.poseidon.repos",
        // 实体管理bean名称
        entityManagerFactoryRef = "primaryEntityManagerFactory",
        // 事务管理bean名称
        transactionManagerRef = "primaryTransactionManager"
)
public class MainDataBaseConfig {

    /**
     * 扫描spring.jpa.primary开头的配置信息
     *
     * @return jpa配置信息
     */
    @Primary
    @Bean(name = "primaryJpaProperties")
    @ConfigurationProperties(prefix = "spring.jpa")
    public JpaProperties jpaProperties() {
        return new JpaProperties();
    }

    /**
     * 获取主库实体管理工厂对象
     *
     * @param primaryDataSource 注入名为primaryDataSource的数据源
     * @param jpaProperties     注入名为primaryJpaProperties的jpa配置信息
     * @param builder           注入EntityManagerFactoryBuilder
     * @return 实体管理工厂对象
     */
    @Primary
    @Bean(name = "primaryEntityManagerFactory")
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(@Qualifier("primaryDataSource") DataSource primaryDataSource
            , @Qualifier("primaryJpaProperties") JpaProperties jpaProperties, EntityManagerFactoryBuilder builder) {
        return builder
                // 设置数据源
                .dataSource(primaryDataSource)
                // 设置jpa配置
                .properties(jpaProperties.getProperties())
                // 设置hibernate配置
                .properties(jpaProperties.getHibernateProperties(new HibernateSettings()))
                // 设置实体包名
                .packages("com.muggle.poseidon.model")
                // 设置持久化单元名,用于@PersistenceContext注解获取EntityManager时指定数据源
                .persistenceUnit("primaryPersistenceUnit")
                .build();
    }

    /**
     * 获取实体管理对象
     *
     * @param factory 注入名为primaryEntityManagerFactory的bean
     * @return 实体管理对象
     */
    @Primary
    @Bean(name = "primaryEntityManager")
    public EntityManager entityManager(@Qualifier("primaryEntityManagerFactory") EntityManagerFactory factory) {
        return factory.createEntityManager();
    }

    /**
     * 获取主库事务管理对象
     *
     * @param factory 注入名为primaryEntityManagerFactory的bean
     * @return 事务管理对象
     */
    @Primary
    @Bean(name = "primaryTransactionManager")
    public PlatformTransactionManager transactionManager(@Qualifier("primaryEntityManagerFactory") EntityManagerFactory factory) {
        return new JpaTransactionManager(factory);
    }
}

```java
package com.muggle.poseidon.core.config;


import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateSettings;
import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
        // repository包名
        basePackages = "com.muggle.poseidon.logrep",
        // 实体管理bean名称
        entityManagerFactoryRef = "secondEntityManagerFactory",
        // 事务管理bean名称
        transactionManagerRef = "secondTransactionManager"
)
public class LogDataBaseConfig {

    /**
     * 扫描spring.jpa.second开头的配置信息
     *
     * @return jpa配置信息
     */
    @Bean(name = "secondJpaProperties")
    @ConfigurationProperties(prefix = "spring.aa")
    public JpaProperties jpaProperties() {
        return new JpaProperties();
    }

    /**
     * 获取从库实体管理工厂对象
     *
     * @param secondDataSource 注入名为secondDataSource的数据源
     * @param jpaProperties    注入名为secondJpaProperties的jpa配置信息
     * @param builder          注入EntityManagerFactoryBuilder
     * @return 实体管理工厂对象
     */
    @Bean(name = "secondEntityManagerFactory")
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(@Qualifier("secondDataSource") DataSource secondDataSource
            , @Qualifier("secondJpaProperties") JpaProperties jpaProperties, EntityManagerFactoryBuilder builder) {
        return builder
                // 设置数据源
                .dataSource(secondDataSource)
                // 设置jpa配置
                .properties(jpaProperties.getProperties())
                // 设置hibernate配置
                .properties(jpaProperties.getHibernateProperties(new HibernateSettings()))
                // 设置实体包名
                .packages("com.muggle.poseidon.entity")
                // 设置持久化单元名,用于@PersistenceContext注解获取EntityManager时指定数据源
                .persistenceUnit("secondPersistenceUnit")
                .build();
    }

    /**
     * 获取实体管理对象
     *
     * @param factory 注入名为secondEntityManagerFactory的bean
     * @return 实体管理对象
     */
    @Bean(name = "secondEntityManager")
    public EntityManager entityManager(@Qualifier("secondEntityManagerFactory") EntityManagerFactory factory) {
        return factory.createEntityManager();
    }

    /**
     * 获取从库事务管理对象
     *
     * @param factory 注入名为secondEntityManagerFactory的bean
     * @return 事务管理对象
     */
    @Bean(name = "secondTransactionManager")
    public PlatformTransactionManager transactionManager(@Qualifier("secondEntityManagerFactory") EntityManagerFactory factory) {
        return new JpaTransactionManager(factory);
    }
}


package com.muggle.poseidon.core.config;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;

import javax.sql.DataSource;

@Configuration
public class LogDataConfig {
    /**
     * 扫描spring.datasource.second开头的配置信息
     *
     * @return 数据源配置信息
     */
    @Bean(name = "secondDataSourceProperties")
    @ConfigurationProperties(prefix = "spring.ss")
    public DataSourceProperties dataSourceProperties() {
        return new DataSourceProperties();
    }

    /**
     * 获取从库数据源对象
     *
     * @param properties 注入名为secondDataSourceProperties的beanf
     * @return 数据源对象
     */
    @Bean(name = "secondDataSource")
    public DataSource dataSource(@Qualifier("secondDataSourceProperties") DataSourceProperties properties) {
        return properties.initializeDataSourceBuilder().build();
    }

    /**
     * 该方法仅在需要使用JdbcTemplate对象时选用
     *
     * @param dataSource 注入名为secondDataSource的bean
     * @return 数据源JdbcTemplate对象
     */
    @Bean(name = "secondJdbcTemplate")
    public JdbcTemplate jdbcTemplate(@Qualifier("secondDataSource") DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }
}

application.properties

server.port=8080
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
#spring.datasource.url = jdbc:mysql://localhost:3306/test
spring.datasource.driverClassName = com.mysql.cj.jdbc.Driver
spring.datasource.url = jdbc:mysql://119.23.75.58:3306/poseidon?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowMultiQueries=true
spring.datasource.username =
spring.datasource.password =
spring.datasource.max-active=20
spring.datasource.max-idle=8
spring.datasource.min-idle=8
spring.datasource.initial-size=10

spring.jpa.database=mysql
spring.jpa.show-sql = true
#配置方言
spring.jpa.database-platform=org.hibernate.dialect.MySQL5Dialect

spring.ss.type=com.alibaba.druid.pool.DruidDataSource
#spring.datasource.url = jdbc:mysql://localhost:3306/test
spring.ss.driverClassName = com.mysql.cj.jdbc.Driver
spring.ss.url = jdbc:mysql://zzzzz/log?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowMultiQueries=true
spring.ss.username =
spring.ss.password =
spring.ss.max-active=20
spring.ss.max-idle=8
spring.ss.min-idle=8
spring.ss.initial-size=10


spring.aa.database=mysql
spring.aa.show-sql = true
#配置方言
spring.aa.database-platform=org.hibernate.dialect.MySQL5Dialect

以数据库作为输出配置就算完成了,接下来整合elk系统到我们日志系统中:

先整合logstash

logstash安装和配置:
https://www.elastic.co/cn/downloads/logstash 选择zip包下载

解压,进入bin目录 创建logstash.conf 并配置:

input {
    tcp {
    ##host:port就是上面appender中的 destination,这里其实把logstash作为服务,开启9250端口接收logback发出的消息
    host => "127.0.0.1"
    port => 9100
    mode => "server"
    tags => ["tags"]
    codec => json_lines
    }
}
output {
    stdout { codec => rubydebug }
    #输出到es
    #elasticsearch { hosts => "127.0.0.1:9200" }
        #输出到一个文件中
    file {
       path => "D:\logs\test.log"
       codec => line
    }
}

我这里先配置输出到文件,后面再修改,创建文件:D:\logs\test.log

启动:

打开cmd(不要使用powershell),进入bin:

D:\exe\logstash-6.6.1\logstash-6.6.1\bin>logstash -f logstash.conf

然后在我们的项目中进行相应的配置:
按这个来:https://github.com/logstash/logstash-logback-encoder

加入pom并指定logback版本:

<!-- 父pom中 -->
<ch.qos.logback.version>1.2.3</ch.qos.logback.version>

<!--  日志模块-->
<dependency>
  <groupId>net.logstash.logback</groupId>
  <artifactId>logstash-logback-encoder</artifactId>
  <version>5.3</version>
  </dependency>
        <!-- Your project must also directly depend on either logback-classic or logback-access.  For example: -->
  <dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.3</version>
  </dependency>

配置apppender和logger

<appender name="stash" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
    <destination>127.0.0.1:9100</destination>
    <includeCallerData>true</includeCallerData>

    <encoder class="net.logstash.logback.encoder.LogstashEncoder">
        <includeCallerData>true</includeCallerData>
    </encoder>
  </appender>
  <logger name="logstash" level="info">
      <appender-ref ref="stash"/>
  </logger>

测试:

RestController
@RequestMapping("/public/log")
public class LogTestController {
    private static final Logger log = LoggerFactory.getLogger("logstash");
    @Autowired
    LoggingEventRepository repository;

    @GetMapping("/")
    public String test(){
        log.info("sssssssssssssss");
        Iterable<LoggingEvent> all = repository.findAll();

        return "sss";

    }
}

访问接口,logstash打印信息:

[2019-03-09T11:32:56,358][INFO ][logstash.outputs.file    ] Opening file {:path=>"D:/logs/test.log"}
{
                  "host" => "www.xmind.net",
                 "level" => "INFO",
     "caller_class_name" => "com.muggle.poseidon.controller.LogTestController",
            "@timestamp" => 2019-03-09T03:33:03.413Z,
           "logger_name" => "logstash",
              "@version" => "1",
           "thread_name" => "http-nio-8080-exec-9",
               "message" => "sssssssssssssss",
    "caller_line_number" => 22,
                  "port" => 58368,
           "level_value" => 20000,
      "caller_file_name" => "LogTestController.java",
                  "tags" => [
        [0] "tags"
    ],
    "caller_method_name" => "test"
}

test.log输出了文件:

2019-03-09T03:33:03.413Z www.xmind.net sssssssssssssss

接下来只要把输出路径换成ES就可以了,这属于logstash和es的整合,这里先不讲解;重新回归到我们的请求模块:

我希望我的模块,对每次请求都能记录下来(请求日志),并将记录存到数据库或者ES,同时我要对所有接口都进行一个幂等性的保障;保障接口的幂等性有多种方法,比较简单的是数据库做唯一索引或者加拦截器,我这里加了一个拦截器来保障接口幂等和拦截前端数据的重复提交(关于接口幂等性在其他文档中介绍):

@Slf4j
public class RequestLockInterceptor implements HandlerInterceptor {
    RedisLock redisTool;
    private int expireTime;

    public RequestLockInterceptor(int expireTime, RedislockImpl redisTool) {
        this.expireTime = expireTime;
        this.redisTool = redisTool;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        if("post".equalsIgnoreCase(request.getMethod())){
            String token = request.getParameter("request_key");
            if (token==null||"".equals(token)){
                log.error("请求非法");
//            throw new PoseidonException("请求太频繁",PoseidonProperties.TOO_NUMBER_REQUEST);
                response.setContentType("application/json;charset=UTF-8");
                PrintWriter writer = response.getWriter();
                writer.write("{\"code\":\"5001\",\"msg\":\"请求非法\"}");
                writer.close();
                return false;
            }
            String ipAddr = RequestUtils.getIpAddr(request);
            String lockKey = request.getRequestURI() + "_"  + "_" + token;
            boolean lock = redisTool.lock(lockKey, ipAddr, expireTime);
            if (!lock) {//
                log.error("拦截表单重复提交");
//            throw new PoseidonException("请求太频繁",PoseidonProperties.TOO_NUMBER_REQUEST);
                response.setContentType("application/json;charset=UTF-8");
                PrintWriter writer = response.getWriter();
                writer.write("{\"code\":\"5001\",\"msg\":\"请求太频繁\"}");
                writer.close();
                return false;
            }
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//        String requestURI = request.getRequestURI();
//        String lockKey = request.getRequestURI() + "_" + RequestUtils.getIpAddr(request);
//        redisTool.unlock(lockKey,getIpAddr(request));
    }


}

项目使用了redis锁(redis锁原理和使用在其他文档中介绍)

对于系统异常,如果是业务的异常,正常处理,如果是系统发生的异常比如空指针,数据库异常等我希望系统能马上通知,以便排查问题,所以我配置邮件异常通知(关于springboot邮件配置其他文档介绍):


@RestControllerAdvice
@Slf4j
public class RestExceptionHandlerController {
    @Autowired
    EmailService emailService;
    @Value("${admin.email}")
    private String adminEmail;

    @ExceptionHandler(value = {PoseidonException.class})
    public ResultBean poseidonExceptionHandler(PoseidonException e, HttpServletRequest req) {
        return new ResultBean().setMsg(e.getMsg()).setCode(e.getCode());
    }
    @ExceptionHandler(value = {MethodArgumentNotValidException.class})
    public ResultBean MethodArgumentNotValidException(MethodArgumentNotValidException e, HttpServletRequest req) {
        System.out.println(e.getMessage());
        return new ResultBean().setMsg("数据未通过校验").setCode(PoseidonProperties.COMMIT_DATA_ERROR);
    }

    @ExceptionHandler(value = {Exception.class})
    public ResultBean exceptionHandler(Exception e, HttpServletRequest req) {
        log.error("系统异常:" + req.getMethod() + req.getRequestURI(), e);
        try {
//
            EmailBean emailBean = new EmailBean();
            emailBean.setRecipient(adminEmail);
            emailBean.setSubject("poseidon---系统异常");
            emailBean.setContent("系统异常:" + req.getMethod() + req.getRequestURI()+"----"+e.getMessage());
//            改良
            emailService.sendSimpleMail(emailBean);
        } finally {
            return new ResultBean().setMsg("系统异常,请联系管理员").setCode("500");
        }
    }

    @ExceptionHandler(value = {HttpRequestMethodNotSupportedException.class})
    public ResultBean notsupported(Exception e, HttpServletRequest req) {
        return new ResultBean().setMsg("不支持的请求方式").setCode(PoseidonProperties.NOT_SUPPORT_METHOD);
    }
    @ExceptionHandler(value = {NoHandlerFoundException.class})
    public ResultBean notFoundUrl(Exception e, HttpServletRequest req) {
        return new ResultBean().setMsg("请求路径不存在").setCode("404");
    }
}

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

推荐阅读更多精彩内容