layout | title | category | tags | keywords | description |
---|---|---|---|---|---|
post |
log4j2 日志配置实战 |
技术 |
Logger |
主要介绍Log4J2的日志发邮件、日志写mysql数据库、日志写Mongo等功能。为日志在系统中的高级应用打基础。希望通过通读该文,可以很方便简单的配置日志发邮件、日志写mysql数据库、日志写Mongo的功能。 |
{:toc}
替换log4j,log4j2是log4j的2.x版本但是在log4j上做了比较大的改变,log4j2的性能比log4j好。该文不会具体讲解常用的基础配置,主要介绍日志发邮件
、日志写mysql数据库
、日志写Mongo
功能。
预设环境
-
默认项目为maven管理
-
使用jdk1.8及以上
-
idea开发环境
-
mongo
添加依赖
<properties>
<druid.version>1.0.18</druid.version>
<fastjson.version>1.2.9</fastjson.version>
<log4j2.version>2.8</log4j2.version>
<mail.version>1.4.7</mail.version>
<mongo.version>3.4.2</mongo.version>
<mysql.driver.version>5.1.36</mysql.driver.version>
<slf4j.version>1.7.24</slf4j.version>
<spring-mongo.version>1.10.3.RELEASE</spring-mongo.version>
</properties>
<dependencies>
<!-- mysql驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.driver.version}</version>
</dependency>
<!--邮箱-->
<dependency>
<groupId>javax.mail</groupId>
<artifactId>mail</artifactId>
<version>${mail.version}</version>
</dependency>
<!-- mongoDB 驱动 -->
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongo-java-driver</artifactId>
<version>${mongo.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb</artifactId>
<version>${spring-mongo.version}</version>
</dependency>
<!--json解析器-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<!--slf4j 接口-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<!--java slf4j的桥接器-->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>${log4j2.version}</version>
</dependency>
<!--log4j2 实现及接口-->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>${log4j2.version}</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>${log4j2.version}</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-nosql</artifactId>
<version>${log4j2.version}</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-web</artifactId>
<version>${log4j2.version}</version>
</dependency>
</dependencies>
对以上依赖包的说明:
-
slf4j
和log4j*
包是日志门面接口和实际日志发送者其中
log4j-web
是为了在代码中使用Spring管理log4j2,通过Spring进行初始化,并应用Druid的数据源。log4j-nosql
是为了使得log4j2支持非关系型数据库(如Mongo,CouchDb)。log4j-slf4j-impl
是用于log4j2桥接slf4j,便于使用slf4j声明,log4j2输出日志。 -
fastjson
:是将java类格式化为json格式,用于发送日志时将日志转为json。 -
mongo-java-driver
:是mongo的java驱动,便于java对mongo的调用 -
spring-data-mongodb
: 是Spring对Mongo的集成,方便以Spring的方式操作Mongo。 -
mail
:发送邮件时需要。 -
mysql-connector-java
:java操作mysql数据库的驱动。
这些只是当前讲解日志相关功能使用的包,而Spring的核心包以及其他如Mybatis等不在此文关心范围内。
log4j2.xml是作为log4j2使用时指定的文件名称之一,其他的还有(log4j.configurationFile
,log4j2-test.properties
,log4j2-test.yaml
,log4j2-test.yml
,log4j2-test.json
,log4j2-test.jsn
,log4j2-test.xml
,log4j2.properties
,log4j2.yaml
,log4j2.yml
,log4j2.json
,log4j2.jsn
,log4j2.xml
,默认配置),log42会以名称出现的顺序查找上面的文件依次是否存在,而log4j2.xml
是优先级最低的,log4j.configurationFile
是指用户自定义指定的文件名,优先级最高。
贴出所有的log4j2.xml代码,但不会全部介绍
<?xml version="1.0" encoding="UTF-8"?>
<!--monitorInterval:指定间隔监控配置文件是否更改,更改后自动重新配置,不需要重启服务。-->
<configuration name="esn-palmyy-plugin" status="info" monitorInterval="5" strict="false">
<properties>
<property name="receipients">to-mail@mail.com</property>
<property name="from">from-mail@mail.com</property>
<property name="smtpHost">mail.host.com</property>
<property name="smtpPort">25</property>
<property name="smtpProtocol">smtp</property>
<property name="smtpUser">from-mail@mail.com</property>
<property name="smtpPassword">password</property>
<property name="smtpSubject">日志主题信息</property>
</properties>
<appenders>
<!--打印日志到控制台-->
<Console name="stdout" target="SYSTEM_OUT">
<!--<ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY"/>-->
<PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level %class{36} %L %M - %msg%xEx%n"/>
</Console>
<!--将info级别的信息追加到info.log日志文件-->
<RollingFile name="infoLog"
fileName="${catalina.home}/logs/esn-palmyy-plugin/info.log"
append="true"
filePattern="${catalina.home}/logs/esn-palmyy-plugin/info.log.%d{yyyy-MM-dd}">
<PatternLayout
pattern="%d{yyyy.MM.dd 'at' HH:mm:ss.SSS z} %-5level %class{36} %L %M - %msg%xEx%n"/>
<ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/>
<Policies>
<TimeBasedTriggeringPolicy modulate="true" interval="24"/>
</Policies>
</RollingFile>
<!--将error级别的信息追加到error.log日志文件-->
<RollingFile name="errorLog"
fileName="${catalina.home}/logs/esn-palmyy-plugin/error.log"
append="true"
filePattern="${catalina.home}/logs/esn-palmyy-plugin/error.log.%d{yyyy-MM-dd}">
<PatternLayout
pattern="%d{yyyy.MM.dd 'at' HH:mm:ss.SSS z} %-5level %class{36} %L %M - %msg%xEx%n"/>
<ThresholdFilter level="error" onMatch="ACCEPT" onMismatch="DENY"/>
<Policies>
<TimeBasedTriggeringPolicy modulate="true" interval="24"/>
</Policies>
</RollingFile>
<!--将特定的信息追加到error-esn.log日志文件-->
<RollingFile name="errorEsnLog"
fileName="${catalina.home}/logs/esn-palmyy-plugin/error-esn.log"
append="true"
filePattern="${catalina.home}/logs/esn-palmyy-plugin/error-esn.log.%d{yyyy-MM-dd}">
<PatternLayout pattern="%d %-5p %t - %c %l - %m%n"/>
<ThresholdFilter level="error" onMatch="ACCEPT" onMismatch="DENY"/>
<Policies>
<TimeBasedTriggeringPolicy modulate="true" interval="24"/>
</Policies>
</RollingFile>
<!-- 配置日志发送邮件的标签,其中“${}”引用的配置都在最上面定义好了-->
<SMTP name="Mailer"
subject="${smtpSubject}" to="${receipients}" from="${from}"
smtpHost="${smtpHost}" smtpPort="${smtpPort}"
smtpProtocol="${smtpProtocol}" smtpUsername="${smtpUser}"
smtpPassword="${smtpPassword}" smtpDebug="false" bufferSize="1024">
<customHtmlLayout/>
</SMTP>
<!-- 异步发送邮件日志 -->
<Async name="AsyncMailer">
<appender-ref ref="Mailer"/>
</Async>
<!--配置日志写入mysql数据库 -->
<JDBC name="databaseAppender" tableName="logger_info">
<ConnectionFactory class="com.yonyou.esn.palmyy.common.LoggerConnectionFactory"
method="getDatabaseConnection"/>
<Column name="thread_name" pattern="%t"/>
<Column name="class_name" pattern="%C"/>
<Column name="method_name" pattern="%method"/>
<Column name="beginTime" pattern="%d{yyyy-MM-dd HH:mm:ss.SSS}"/>
<Column name="logger_level" pattern="%level"/>
<Column name="logger_name" pattern="%c"/>
<Column name="logger_message" pattern="%m"/>
<Column name="logger_throwable" pattern="%throwable{3}"/>
<Column name="createTime" pattern="%d{yyyy-MM-dd HH:mm:ss.SSS}"/>
</JDBC>
<!--异步写入数据库-->
<Async name="AsyncDatabaseAppender">
<appender-ref ref="databaseAppender"/>
</Async>
<!--异步写入nosql中,如mongo-->
<Async name="asyncNoSqlDbLogger">
<appender-ref ref="noSqlDbAppender"/>
</Async>
<!--日志输出到Nosql数据库-->
<NoSql name="noSqlDbAppender">
<MongoDb databaseName="esn-palmyy" collectionName="log_info" server="172.20.9.41"
port="27017"/>
</NoSql>
</appenders>
<!--然后定义logger,只有定义了logger并引入的appender,appender才会生效
<logger>标签可以在<Root>标签中引用(全局使用)也可以在类调用时指定引用的名称,定向调用
如:private static final Logger LOGGER_MAIL= LoggerFactory.getLogger("AsyncMailerLogger");
LOGGER_MAIL的日志都只会发送邮件,不写到文件中
-->
<loggers>
<logger name="AsyncMailerLogger" level="error" additivity="false">
<appender-ref ref="Mailer"/>
</logger>
<logger name="noSqlDbLogger" level="info" additivity="false">
<appender-ref ref="noSqlDbAppender"/>
</logger>
<logger name="AsyncDBLogger" level="error" additivity="false">
<appender-ref ref="databaseAppender"/>
</logger>
<Logger name="errorEsnLogger" level="error" additivity="true">
<AppenderRef ref="errorEsnLog"/>
</Logger>
<Root level="debug">
<AppenderRef ref="stdout"/>
<AppenderRef ref="errorLog"/>
<AppenderRef ref="infoLog"/>
</Root>
</loggers>
</configuration>
具体配置的各个属性名称不做具体解释了,自解释了。
节选部分配置
<!--配置日志写入mysql数据库 -->
<JDBC name="databaseAppender" tableName="logger_info">
<ConnectionFactory class="com.xxx.aaa.palmyy.common.LoggerConnectionFactory"
method="getDatabaseConnection"/>
<Column name="thread_name" pattern="%t"/>
<Column name="class_name" pattern="%C"/>
<Column name="method_name" pattern="%method"/>
<Column name="beginTime" pattern="%d{yyyy-MM-dd HH:mm:ss.SSS}"/>
<Column name="logger_level" pattern="%level"/>
<Column name="logger_name" pattern="%c"/>
<Column name="logger_message" pattern="%m"/>
<Column name="logger_throwable" pattern="%throwable{3}"/>
<Column name="createTime" pattern="%d{yyyy-MM-dd HH:mm:ss.SSS}"/>
</JDBC>
-
tableName是数据库中的表名,
-
<Column>
的name属性是指定数据库表中的字段名,pattern是匹配日志的内容格式,会将对应的内容写入到指定的字段下。 -
<ConnectionFactory>
需要自定义数据库连接工厂类,并提供获取DataSource或者Connection的静态方法。
LoggerConnectionFactory
类的代码如下:
package com.yonyou.esn.palmyy.common;
import com.alibaba.druid.pool.DruidDataSourceFactory;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;
import javax.annotation.PostConstruct;
import javax.sql.DataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* 日志配置所使用的数据库连接工厂类
* <p>Created by followtry on 2017/4/20.
*/
@Component
public class LoggerConnectionFactory {
private static final Logger LOGGER = LoggerFactory.getLogger(LoggerConnectionFactory.class);
private static final LoggerConnectionFactory loggerConnectionFactory = new LoggerConnectionFactory();
@Autowired
private DataSource dataSource;
//通过内部单例接口实例化当前工厂类并提供Connection连接
private interface Singleton {
LoggerConnectionFactory INSTANCE = new LoggerConnectionFactory();
}
@PostConstruct
void init(){
Properties prop = new Properties() {
{
put("username","abc");
put("password","abc");
put("url","jdbc:mysql://172.20.19.200:3306/xx_test?useUnicode=true&characterEncoding=UTF-8");
put("driverClassName","com.mysql.jdbc.Driver");
}
};
try {
dataSource = DruidDataSourceFactory.createDataSource(prop);
} catch (Exception e) {
LOGGER.error("获取数据源出错",e);
}
}
private LoggerConnectionFactory() {}
public static Connection getDatabaseConnection() throws SQLException {
return Singleton.INSTANCE.dataSource.getConnection();
}
}
这样的方式是可以获取到Mysql数据库的Connection
并且也可以将日志写入到数据库中,但是很明显这样有两个很大的缺点:
- 自己控制对数据库的链接,没有应用业界比较好的数据源库也没有使用线程池。
- 没有应用到Spring的IOC和DI。
有人会问:
对于连接属性的配置直接调用Spring管理的数据源不行吗?答案是现在这样的配置不行,因为log4j2等日志是在Spring初始化前加载并初始化的,因此log4j2在当前配置下是拿不到Spring的bean信息的。
那这个问题就无解了吗?当然不是,要不然干嘛要引入log4j-web
包呢。引入该包就是为了在web应用中使用Log4j2的,具体可以参考http://logging.apache.org/log4j/2.x/manual/webapp.html
要说明的是:Log4j2仅支持Servlet3.0及以上版本,Tomcat7.0及以上
Log4j2会在WEB容器启动和销毁时自动启动和关闭。是通过Log4jServletContainerInitializer
继承自ServletContainerInitializer
达到自动启动的目的。在Tomcat7.0.43之前,基于性能原因,web容器会忽略名为log4j*.jar包,阻止其获取随web容器启动的特性。但在之后的版本中已经修复。
如果禁止Log4j2的自动初始化,那么需要在web.xml中加入配置:
<context-param>
<param-name>isLog4jAutoInitializationDisabled</param-name>
<param-value>true</param-value>
</context-param>
但是在Servlet2.5需要在其他应用代码执行前初始化Log4j。
如果使用的是Servlet2.5,那么需要在web.xml文件中加入配置:
<listener>
<listener-class>org.apache.logging.log4j.web.Log4jServletContextListener</listener-class>
</listener>
<filter>
<filter-name>log4jServletFilter</filter-name>
<filter-class>org.apache.logging.log4j.web.Log4jServletFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>log4jServletFilter</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>REQUEST</dispatcher>
<dispatcher>FORWARD</dispatcher>
<dispatcher>INCLUDE</dispatcher>
<dispatcher>ERROR</dispatcher>
</filter-mapping>
详情请看http://logging.apache.org/log4j/2.x/manual/webapp.html,接下来就说明下怎样通过在Log4j2中使用Spring管理的bean。
这次配置是基于Spring的,使用了Spring容器的功能,对配置文件中Log4j2的配置进行增加。
代码如下:
package com.yonyou.esn.palmyy.common;
import java.sql.Connection;
import java.sql.SQLException;
import javax.annotation.PostConstruct;
import javax.sql.DataSource;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.LoggerContext;
import org.apache.logging.log4j.core.appender.AsyncAppender;
import org.apache.logging.log4j.core.appender.db.ColumnMapping;
import org.apache.logging.log4j.core.appender.db.jdbc.ColumnConfig;
import org.apache.logging.log4j.core.appender.db.jdbc.ConnectionSource;
import org.apache.logging.log4j.core.appender.db.jdbc.JdbcAppender;
import org.apache.logging.log4j.core.config.AppenderRef;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.config.LoggerConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* 配置log4j2的JDBC写数据库的配置类
*
* <p>Created by followtry on 2017/4/20.
*/
@Component
public class LoggerBean {
@Autowired
private DataSource dataSource;
/**
* 初始化日志写数据库配置,在构造方法后执行
*/
@PostConstruct
void init(){
LoggerContext context = (LoggerContext)LogManager.getContext(false);
//<configuration></configuration>
Configuration cfg = context.getConfiguration();
/**
* 该配置对应columnConfig
*
* <JDBC name="databaseAppender" tableName="logger_info">
<ConnectionFactory class="com.yonyou.esn.palmyy.common.LoggerConnectionFactory" method="getDatabaseConnection" />
<Column name="thread_name" pattern="%t" />
<Column name="class_name" pattern="%C"/>
<Column name="method_name" pattern="%method" />
<Column name="beginTime" pattern="%d{yyyy-MM-dd HH:mm:ss.SSS}" />
<Column name="logger_level" pattern="%level" />
<Column name="logger_name" pattern="%c" />
<Column name="logger_message" pattern="%m" />
<Column name="logger_throwable" pattern="%throwable{3}" />
<Column name="createTime" pattern="%d{yyyy-MM-dd HH:mm:ss.SSS}" />
</JDBC>
*/
ColumnConfig[] columnConfig = {
ColumnConfig.newBuilder().setConfiguration(cfg).setName("thread_name")
.setPattern("%t").build(),
ColumnConfig.newBuilder().setConfiguration(cfg).setName("class_name")
.setPattern("%C").build(),
ColumnConfig.newBuilder().setConfiguration(cfg).setName("method_name")
.setPattern("%method").build(),
ColumnConfig.newBuilder().setConfiguration(cfg).setName("beginTime")
.setEventTimestamp(true).build(),
ColumnConfig.newBuilder().setConfiguration(cfg).setName("logger_level")
.setPattern("%level").build(),
ColumnConfig.newBuilder().setConfiguration(cfg).setName("logger_name")
.setPattern("%c").build(),
ColumnConfig.newBuilder().setConfiguration(cfg).setName("logger_message")
.setPattern("%m").build(),
ColumnConfig.newBuilder().setConfiguration(cfg).setName("logger_throwable")
.setPattern("%throwable{3}").build(),
ColumnConfig.newBuilder().setConfiguration(cfg).setName("createTime")
.setEventTimestamp(true).build(),
};
JdbcAppender jdbcAppender = JdbcAppender.newBuilder()
.setConfiguration(cfg)
.withName("databaseAppender")
.withIgnoreExceptions(false)
.setConnectionSource(new Connect(dataSource))
.setTableName("logger_info")
.setColumnConfigs(columnConfig)
//ColumnMapping设置为空,是因为如果不设置或设置为null会报空指针异常
.setColumnMappings(new ColumnMapping[]{}).build();
jdbcAppender.start();
//将<JDBC></JDBC>添加到<appenders></appenders>
cfg.addAppender(jdbcAppender);
/**
* <Async name="AsyncDatabaseAppender">
<appender-ref ref="databaseAppender"/>
</Async>
*/
AsyncAppender asyncDbAppender = AsyncAppender.newBuilder().setConfiguration(cfg).setName
("asyncDatabaseAppender").setAppenderRefs(new AppenderRef[] { AppenderRef
.createAppenderRef(jdbcAppender.getName(),Level.ERROR,null) }).build();
asyncDbAppender.start();
//将<Async></Async>添加到<appenders></appenders>
cfg.addAppender(asyncDbAppender);
//将<logger></logger>添加到<loggers></loggers>
LoggerConfig loggerConfig = new LoggerConfig();
loggerConfig.setAdditive(false);
//设置为jdbcAppender为同步写库,配置为asyncDbAppender为异步写库,但是异步写库会导致类名和方法名找不到
loggerConfig.addAppender(jdbcAppender,Level.ERROR,null);
cfg.addLogger("asyncDBLogger",loggerConfig);
context.updateLoggers();
}
//内部类
class Connect implements ConnectionSource {
private DataSource dsource;
public Connect(DataSource dsource) {
this.dsource = dsource;
}
@Override
public Connection getConnection() throws SQLException {
return this.dsource.getConnection();
}
}
private LoggerBean() {}
public DataSource getDataSource() {
return dataSource;
}
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
}
以上代码中的注释已经比较详细了,概括来说就是在当前LoggerBean被IOC实例化之后获取到已经从配置文件中读取的Log4j2的配置,并在init()方法中用代码将JDBC部分的逻辑补充上,更新下Log4j2的上下文即可。一个类内就把Log4j2的JDBC功能实现了,不需要在任何地方做log4j2对JBDC支持的配置。
log4j2.xml中</SMTP>
内的配置就是控制日志发送邮件的功能了,默认使用的Lauout是<HtmlLayout>
,对应类org.apache.logging.log4j.core.layout.HtmlLayout
,比较蛋疼的是该类是final型的,不能被继承也就是说不能通过继承该类实现自定义Html布局。唉没办法,使用自定义的Layout吧!
自定义的layout需要继承org.apache.logging.log4j.core.layout.AbstractStringLayout
,并且要在类上加上注解:
@Plugin(
name = "HtmlLayout",
category = "Core",
elementType = "layout",
printObject = true
)
其中name处value是配置文件log4j2.xml
中使用的标签,如<HtmlLayout />
,写者没有对其原理进行分析,直接强行复制其代码新建了个类,并修改了关键的位置@Plugin
的name作为自己的布局模板。
自定义的布局模板关键代码如下:
@Plugin(
//此处name值即在Log4j2.xml中配置的标签名,如<customHtmlLayout/>
name = "customHtmlLayout",
category = "Core",
elementType = "layout",
printObject = true)
/**
* Created by followtry on 2017/4/26.
*/
public class CustomHtmlLayout extends AbstractStringLayout {
public byte[] getHeader() {
//实现布局Header部分的逻辑
}
public String toSerializable(LogEvent event){
//内部实现布局body的逻辑
}
public byte[] getFooter() {
//实现布局footer的逻辑
}
}
通过将上面解释的逻辑调整为自己想要的就实现了自定义布局。
此处不涉及Mongo的安装和使用
对于NoSql的配置就比较简单了,以Mongo为例:
<NoSql name="noSqlDbAppender">
<MongoDb databaseName="esn-palmyy" collectionName="log_info" server="172.20.9.41"
port="27017"/>
</NoSql>
相信Mongo配置的属性不用解释也能看明白了,就这点配置,只要能连上Mongo就可以将日志写入到Mongo中,当然前提是要在<Logger>
或者<Root>
标签中引用noSqlDbAppender
。
Mongo存储结构如下:
log4j2 中配置<Async>
标签异步存储日志会使得写Mysql库和Mongo库时,调用Logger的ClassName和MethodName都为空,写者当前在项目中暂时没有使用异步写日志操作。