關(guān)鍵字: logback.xml in jar logback environment variable
最近實(shí)現(xiàn)了一下將logback.xml的配置統(tǒng)一在基礎(chǔ)框架的jar包中,如此所有業(yè)務(wù)應(yīng)用都不用關(guān)心日志的規(guī)范格式或者存放位置,只要做一下簡(jiǎn)單的配置即可。
對(duì)于logback.xml與log4j的兼容或者適配問(wèn)題,本文不做詳細(xì)討論。
參見(jiàn):Java日志框架slf4j、jcl、jul、log4j1、log4j2、logback大總結(jié)
1. logback.xml配置
基本上也是主流配置,幾個(gè)特別說(shuō)明的點(diǎn):
格式化的輸出說(shuō)明
請(qǐng)參照 http://logback.qos.ch/manual/layouts.html
<b>片段</b>:
%20logger false 20 none Left pad with spaces if the logger name is less than 20 characters long.
%-20logger true 20 none Right pad with spaces if the logger name is less than 20 characters long.
%.30logger NA none 30 Truncate from the beginning if the logger name is longer than 30 characters.
%20.30logger false 20 30 Left pad with spaces if the logger name is shorter than 20 characters. However, if logger name is longer than 30 characters, then truncate from the beginning.
%-20.30logger true 20 30 Right pad with spaces if the logger name is shorter than 20 characters. However, if logger name is longer than 30 characters, then truncate from the beginning.
%.-30logger NA none 30 Truncate from the end if the logger name is longer than 30 characters.%d表示日期,
%thread: 表示線(xiàn)程名
%level:日志級(jí)別
%msg:日志消息
%logger: Java類(lèi)名(含包名,這里設(shè)定了36位,若超過(guò)36位,包名會(huì)精簡(jiǎn)為類(lèi)似a.b.c.JavaBean)
%line: Java類(lèi)的行號(hào)
NEUTRAL使用
因?yàn)檎H罩竞湾e(cuò)誤異常日志是拆分成兩個(gè)文件的,所以在appender=FILE中如果使用ThresholdFilter配置的level是最低閥值(一般是INFO),
ERROR也會(huì)進(jìn)入到sys.log中,這時(shí)需要將ERROR過(guò)濾掉,設(shè)置Match Error 直接DENY, Mismatch Error則Neutral中立繼續(xù)走下面的filter。
環(huán)境變量的使用
這里配置了大量的環(huán)境變量,比如log.path(日志的路徑),app.name(應(yīng)用名稱(chēng)),log.root.level(日志root級(jí)別)等,
這些變量若在每個(gè)業(yè)務(wù)系統(tǒng)的本地,可以基于maven的profile filter resources進(jìn)行變量替換即可,
但是當(dāng)前的設(shè)計(jì)是想將logback.xml放在一個(gè)公共組件的jar包內(nèi), 這個(gè)maven是無(wú)法替換jar包內(nèi)文件的內(nèi)容的。引申出我們下面的實(shí)現(xiàn)。
(當(dāng)然你可以使用一些maven plugin進(jìn)行unzip,修改后再zip一把,實(shí)在太麻煩 >_<!)
<configuration>
<!--<statusListener class="ch.qos.logback.core.status.NopStatusListener" />-->
<jmxConfigurator/>
<!-- 控制臺(tái)輸出 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>
[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-36.36thread] [%-5level] [%-36.36logger{36}:%-4.4line] - %msg%n
</pattern>
</encoder>
</appender>
<!-- 按照每天生成日志文件 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/${app.name}/sys.log</file>
<!--拒絕ERROR日志-->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>DENY</onMatch>
<onMisMatch>NEUTRAL</onMisMatch>
</filter>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>${log.lowest.level}</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>${log.path}/${app.name}/sys-%d{yyyy-MM-dd}-%i.log</FileNamePattern>
<MaxHistory>90</MaxHistory>
<TimeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<MaxFileSize>10MB</MaxFileSize>
</TimeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>
[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-36.36thread] [%-5level] [%-36.36logger{36}:%-4.4line] - %msg%n
</pattern>
</encoder>
</appender>
<appender name="FILE-ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/${app.name}/sys-err.log</file>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>${log.path}/${app.name}/sys-err-%d{yyyy-MM-dd}-%i.log</FileNamePattern>
<MaxHistory>90</MaxHistory>
<TimeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<MaxFileSize>10MB</MaxFileSize>
</TimeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-36.36thread] [%-5level] [%-36.36logger{36}:%-4.4line] - %msg%n
</pattern>
</encoder>
</appender>
<!-- show parameters for hibernate sql 專(zhuān)為 Hibernate 定制 -->
<logger name="org.hibernate.type.descriptor.sql.BasicBinder" additivity="true" level="${log.hibernate.level}" />
<logger name="org.hibernate.type.descriptor.sql.BasicExtractor" additivity="true" level="${log.hibernate.level}" />
<logger name="org.hibernate.SQL" additivity="true" level="${log.hibernate.level}" />
<logger name="org.springframework" additivity="true" level="${log.spring.level}"/>
<logger name="com.myown" additivity="true" level="${log.root.level}"/>
<!-- 日志輸出級(jí)別 -->
<root level="${log.root.level}">
<appender-ref ref="STDOUT"/>
<appender-ref ref="FILE"/>
<appender-ref ref="FILE-ERROR"/>
</root>
</configuration>
2. web.xml 監(jiān)聽(tīng)器的實(shí)現(xiàn)
上面logback.xml里設(shè)置了大量的環(huán)境變量,而這些環(huán)境變量都必須在Web應(yīng)用啟動(dòng)之前設(shè)置完畢。
實(shí)現(xiàn)方式其實(shí)比較多:
a. 第一種: 設(shè)置JVM的啟動(dòng)參數(shù) -Dlog.path=/logs/app/ -Dlog.root.level=INFO
b. 第二種:設(shè)置應(yīng)用服務(wù)器的啟動(dòng)參數(shù),比如tomcat,在bin文件夾下新增一個(gè)bat(windows)或者shell文件(linux),命名為setenv.bat/sh,
setenv.bat內(nèi)容:
set log.path=/usr/local/src/logs/test
set log.root.level=INFO
....
c. 第三種:前兩種處理方式都有些不妥的地方就是,開(kāi)發(fā)人員pull代碼之后無(wú)法直接部署tomcat運(yùn)行,還需要添加setenv文件,容易遺漏;
參數(shù)的設(shè)置不靈活:不能根據(jù)OS, 開(kāi)發(fā)/測(cè)試/生產(chǎn)的環(huán)境變量動(dòng)態(tài)調(diào)整log level或者log path。
(其實(shí),在不同環(huán)境下的tomcat只要設(shè)定一次啟動(dòng)參數(shù),業(yè)務(wù)應(yīng)用日后的部署都是一勞永逸的, 不排除以后采用第二種方案 _)
現(xiàn),決定,通過(guò)監(jiān)聽(tīng)器來(lái)實(shí)現(xiàn)。
監(jiān)聽(tīng)器示例代碼如下:
public class LogbackListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent event) {
ServletContext sc = event.getServletContext();
//添加系統(tǒng)屬性示例代碼
if (org.apache.commons.lang3.SystemUtils.IS_OS_WINDOWS) {
System.setProperty("log.path", "${CATALINA_HOME}" + SystemUtils.FILE_SEPARATOR + "logs");
} else {//linux
System.setProperty("log.path", "/logs");
}
if (isProductEnv) {
System.setProperty("log.root.level", "INFO");
} else {//非生產(chǎn)環(huán)境
System.setProperty("log.root.level", "DEBUG");
}
..........................
WebLogbackConfigurer.initLogging(sc);
}
@Override
public void contextDestroyed(ServletContextEvent event){
WebLogbackConfigurer.shutdownLogging(event.getServletContext());
}
}
web.xml添加監(jiān)聽(tīng)器配置,該監(jiān)聽(tīng)器的配置盡量位于第一位,至少保證在其他有可能使用日志打印的監(jiān)聽(tīng)器之前。
············
<!--logback日志環(huán)境變量配置-->
<listener>
<listener-class>com.myown.framework.LogbackListener</listener-class>
</listener>
<!--Spring上下文-->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
············
3. TOMCAT + JNDI + Druid連接池的坑
因?yàn)槟硞€(gè)核心系統(tǒng)期望能監(jiān)控一下數(shù)據(jù)庫(kù)連接方面的性能和請(qǐng)求情況,采用了開(kāi)源連接池Druid, 在context.xml配置的jndi是這樣的:
<!--
1. tomcat lib需添加mysql驅(qū)動(dòng) mysql-connector-java-5.1.38.jar
2. 添加 druid-1.0.16.jar
3. filter若使用log4j,tomcat lib需添加log4j.jar;若使用slf4j基于logback,需添加slf4j-api-1.7.13.jar
-->
<Resource
name="jndi/nicholas"
factory="com.alibaba.druid.pool.DruidDataSourceFactory"
auth="Container"
type="javax.sql.DataSource"
driverClassName="com.mysql.jdbc.Driver"
url="jdbc:mysql://127.0.0.1:3306/nicholas?useUnicode=true&characterEncoding=utf-8"
username="root"
password="root"
initialSize="10"
minIdle="10"
maxActive="50"
maxWait="10000"
timeBetweenEvictionRunsMillis="60000"
minEvictableIdleTimeMillis="300000"
removeabandoned="true"
removeabandonedtimeout="180"
logabandoned="true"
filters="stat,wall,slf4j"/>
具體druid的配置和使用就不詳談,注意最后的filters 配置了slf4j, 它的實(shí)現(xiàn)是基于web應(yīng)用配置的logback,請(qǐng)看下圖首次進(jìn)入LoggerContext的調(diào)用鏈情況。
其實(shí)此時(shí)的Tomcat啟動(dòng)優(yōu)先加載了druid jndi連接池,而druid又需要打印日志,所以在我們的監(jiān)聽(tīng)器初始化之前, logback的配置已經(jīng)加載完畢了,而加載的logback.xml中的一堆變量都是undefined!
問(wèn)題粗線(xiàn)了!現(xiàn)在看起來(lái)好像前面的第一種或者第二種方法才行得通了。。。這尼瑪!

一個(gè)開(kāi)源組件的依賴(lài)會(huì)帶來(lái)如此不堪忍受的問(wèn)題,這個(gè)事件的教訓(xùn)就是設(shè)計(jì)一個(gè)越基礎(chǔ)的組件越要減少對(duì)其他組件的依賴(lài),不然會(huì)有各種升級(jí)或者兼容層面的潛在隱患
目前本人的解決方案是在剛才的 LogbackListener 監(jiān)聽(tīng)器中重置logback上下文!
@Override
public void contextInitialized(ServletContextEvent event) {
//系統(tǒng)環(huán)境變量設(shè)置 System.setProperty()
..............
LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
//進(jìn)入Web之前判斷是否已加載了Logger,若已加載需要重置!
if (loggerContext != null && loggerContext.getLoggerList().size() > 0) {
//必須清空一下,否則之前加載的logger堆棧信息還保留著StatusPrinter.print會(huì)打印出之前的狀態(tài)
loggerContext.getStatusManager().clear();
loggerContext.reset();
ContextInitializer ci = new ContextInitializer(loggerContext);
try {
ci.autoConfig();
} catch (JoranException e) {
sc.log("-=-=-= Reset Logback status Failed =-=-=- \n" + ExceptionUtil.getStackTrace(e));
}
}
WebLogbackConfigurer.initLogging(sc);
StatusPrinter.print(loggerContext);
}
4. 默認(rèn)賦值問(wèn)題
某些情況下需要變量設(shè)置個(gè)默認(rèn)值,以防出現(xiàn)比較惡心的 _IS_UNDEFINED 后綴( log4j不存在的變量會(huì)留空)
只要使用" :- " 操作符即可(冒號(hào)+減號(hào))。
比如 log.path 沒(méi)有定義, 使用該變量的地方就會(huì)變成** log.path_IS_UNDEFINED**, 給他一個(gè)默認(rèn)值
${log.path:-/var/logs/myapp}
5. 注意事項(xiàng)
Spring的WARN日志無(wú)法打印出來(lái)
檢查spring是否exclude自帶的commons-logging 即 jcl jar包,并同時(shí)添加 jcl-over-slf4j的jar包,將其適配給slf4j
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
</dependency>
Zookeeper/ZkClient等默認(rèn)使用log4j的組件無(wú)法打印日志
需要exclude log4j包,并添加log4j-over-slf4j依賴(lài) (注意 slf4j-log4j12 與 log4j-over-slf4j的環(huán)形依賴(lài)會(huì)導(dǎo)致異常)
<dependency>
<groupId>com.github.sgroschupf</groupId>
<artifactId>zkclient</artifactId>
<version>0.1</version>
<exclusions>
<exclusion>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--zkClient和disconf都會(huì)依賴(lài)zookeeper,而zookeeper會(huì)依賴(lài)slf4j-log4j12和log4j,
在我們使用logback的情況下,需要將log4j轉(zhuǎn)為logback,則需依賴(lài)log4j-over-slf4j。
slf4j-log4j12會(huì)與log4j-over-slf4j循環(huán)依賴(lài)導(dǎo)致沖突,
所以所有依賴(lài)zookeeper的jar必須exclude掉slf4j-log4j12-->
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>${zookeeper.version}</version>
<exclusions>
<exclusion>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</exclusion>
<!--
https://issues.apache.org/jira/browse/ZOOKEEPER-1371
zookeeper Remove dependency on log4j in the source code.
slf4j-log4j12會(huì)與log4j-over-slf4j循環(huán)依賴(lài)導(dǎo)致沖突
-->
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>log4j-over-slf4j</artifactId>
</dependency>