Logback 是一个 Java 领域的日志框架。它被认为是 Log4J 的继承人。
Logback 主要由三个模块组成:
logback-core :
logback-core 是其它模块的基础设施,其它模块基于它构建,显然,logback-core 提供了一些关键的通用机制
logback-classic :
logback-classic 的地位和作用等同于 Log4J,它也被认为是 Log4J 的一个改进版,并且它实现了简单日志门面 SLF4J
logback-access :
logback-access 主要作为一个与 Servlet 容器交互的模块,比如说 tomcat 或者 jetty,提供一些与 HTTP 访问相关的功能
目前 Logback 的使用很广泛,很多知名的开源软件都使用了 Logback作为日志框架,比如说 Akka,Apache Camel 等。
实际上,这两个日志框架都出自同一个开发者之手,Logback 相对于 Log4J 有更多的优点
想在 Java 程序中使用 Logback,需要依赖三个 jar 包,分别是 slf4j-api,logback-core,logback-classic。其中 slf4j-api 并不是 Logback 的一部分,是另外一个项目,但是强烈建议将 slf4j 与 Logback 结合使用。要引用这些 jar 包,在 maven 项目中引入以下3个 dependencies
org.slf4j
slf4j-api
1.7.5
ch.qos.logback
logback-core
1.0.11
ch.qos.logback
logback-classic
1.0.11
SLF4J不同于其他日志类库,与其它有很大的不同。SLF4J(Simple logging Facade for Java)不是一个真正的日志实现,而是一个抽象层( abstraction layer),它允许你在后台使用任意一个日志类库
package io.beansoft.logback.demo.universal;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class SimpleDemo {
private static final Logger logger = LoggerFactory.getLogger(SimpleDemo.class);
public static void main(String[] args) {
logger.info("Hello, this is a line of log message logged by Logback");
}
}
注意到这里,代码里并没有引用任何一个跟 Logback 相关的类,而是引用了 SLF4J 相关的类,这边是使用 SLF4J 的好处,在需要将日志框架切换为其它日志框架时,无需改动已有的代码。
LoggerFactory 的 getLogger() 方法接收一个参数,以这个参数决定 logger 的名字,这里传入了 SimpleDemo 这个类的 Class 实例,那么 logger 的名字便是 SimpleDemo 这个类的全限定类名:io.beansoft.logback.demo.universal.SimpleDemo
package io.beansoft.logback.demo.universal;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.core.util.StatusPrinter;
public class LogInternalStateDemo {
private static final Logger logger = LoggerFactory.getLogger(LogInternalStateDemo.class);
public static void main(String[] args) {
logger.info("Hello world");
//打印 Logback 内部状态
LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
StatusPrinter.print(lc);
}
}
在 logback 里,最重要的三个类分别是
Logger 类位于 logback-classic 模块中, 而 Appender 和 Layout 位于 logback-core 中,这意味着, Appender 和 Layout 并不关心 Logger 的存在,不依赖于 Logger,同时也能看出, Logger 会依赖于 Appender 和 Layout 的协助,日志信息才能被正常打印出来。
为了可以控制哪些信息需要输出,哪些信息不需要输出,logback 中引进了一个 分层 概念。每个 logger 都有一个 name,这个 name 的格式与 Java 语言中的包名格式相同。这也是前面的例子中直接把一个 class 对象传进 LoggerFactory.getLogger() 方法作为参数的原因。
logger 的 name 格式决定了多个 logger 能够组成一个树状的结构,为了维护这个分层的树状结构,每个 logger 都被绑定到一个 logger 上下文中,这个上下文负责厘清各个 logger 之间的关系。
例如, 命名为 io.beansoft 的 logger,是命名为 io.beansoft.logback 的 logger 的父亲,是命名为 io.beansoft.logback.demo 的 logger 的祖先。
在 logger 上下文中,有一个 root logger,作为所有 logger 的祖先,这是 logback 内部维护的一个 logger,并非开发者自定义的 logger。
可通过以下方式获得这个 logger :
Logger rootLogger = LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);
同样,通过 logger 的 name,就能获得对应的其它 logger 实例。
Logger 这个接口主要定义的方法有:
package org.slf4j;
public interface Logger {
// Printing methods:
public void trace(String message);
public void debug(String message);
public void info(String message);
public void warn(String message);
public void error(String message);
}
logger 有日志打印级别,可以为一个 logger 指定它的日志打印级别。
如果不为一个 logger 指定打印级别,那么它将继承离他最近的一个有指定打印级别的祖先的打印级别。这里有一个容易混淆想不清楚的地方,如果 logger 先找它的父亲,而它的父亲没有指定打印级别,那么它会立即忽略它的父亲,往上继续寻找它爷爷,直到它找到 root logger。因此,也能看出来,要使用 logback, 必须为 root logger 指定日志打印级别。
日志打印级别从低级到高级排序的顺序是:
TRACE < DEBUG < INFO < WARN < ERROR
如果一个 logger 允许打印一条具有某个日志级别的信息,那么它也必须允许打印具有比这个日志级别更高级别的信息,而不允许打印具有比这个日志级别更低级别的信息。
举个例子:
package io.beansoft.logback.demo.universal;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ch.qos.logback.classic.Level;
/**
* @date 2017年2月10日 上午12:20:33
* @version 1.0
*/
public class LogLevelDemo {
public static void main(String[] args) {
//这里强制类型转换时为了能设置 logger 的 Level
ch.qos.logback.classic.Logger logger =
(ch.qos.logback.classic.Logger) LoggerFactory.getLogger("com.foo");
logger.setLevel(Level.INFO);
Logger barlogger = LoggerFactory.getLogger("com.foo.Bar");
// 这个语句能打印,因为 WARN > INFO
logger.warn("can be printed because WARN > INFO");
// 这个语句不能打印,因为 DEBUG < INFO.
logger.debug("can not be printed because DEBUG < INFO");
// barlogger 是 logger 的一个子 logger
// 它继承了 logger 的级别 INFO
// 以下语句能打印,因为 INFO >= INFO
barlogger.info("can be printed because INFO >= INFO");
// 以下语句不能打印,因为 DEBUG < INFO
barlogger.debug("can not be printed because DEBUG < INFO");
}
}
在 logback 中,每个 logger 都是一个单例,调用 LoggerFactory.getLogger 方法时,如果传入的 logger name 相同,获取到的 logger 都是同一个实例。
在为 logger 命名时,用类的全限定类名作为 logger name 是最好的策略,这样能够追踪到每一条日志消息的来源。
日志信息不仅仅可以打印至 console,也可以打印至文件,甚至输出到网络流中,日志打印的目的地由 Appender 来决定,不同的 Appender 能将日志信息打印到不同的目的地去。
Appender 是绑定在 logger 上的,同时,一个 logger 可以绑定多个 Appender,意味着一条信息可以同时打印到不同的目的地去。例如,常见的做法是,日志信息既输出到控制台,同时也记录到日志文件中,这就需要为 logger 绑定两个不同的 logger。
Appender 是绑定在 logger 上的,而 logger 又有继承关系,因此一个 logger 打印信息时的目的地 Appender 需要参考它的父亲和祖先。在 logback 中,默认情况下,如果一个 logger 打印一条信息,那么这条信息首先会打印至它自己的 Appender,然后打印至它的父亲和父亲以上的祖先的 Appender,但如果它的父亲设置了 additivity = false,那么这个 logger 除了打印至它自己的 Appender 外,只会打印至其父亲的 Appender,因为它的父亲的 additivity 属性置为了 false,开始变得忘祖忘宗了,所以这个 logger 只认它父亲的 Appender;此外,对于这个 logger 的父亲来说,如果父亲的 logger 打印一条信息,那么它只会打印至自己的 Appender中(如果有的话),因为父亲已经忘记了爷爷及爷爷以上的那些父辈了。
打印的日志除了有打印的目的地外,还有日志信息的展示格式。在 logback 中,用 Layout 来代表日志打印格式。比如说,PatternLayout 能够识别以下这条格式:
%-4relative [%thread] %-5level %logger{32} - %msg%n
然后打印出来的格式效果是:
176 [main] DEBUG manual.architecture.HelloWorld2 - Hello world.
上面这个格式的第一个字段代表从程序启动开始后经过的毫秒数,第二个字段代表打印出这条日志的线程名字,第三个字段代表日志信息的日志打印级别,第四个字段代表 logger name,第五个字段是日志信息,第六个字段仅仅是代表一个换行符。
经常能看到打印日志的时候,使用以下这种方式打印日志:
logger.debug("the message is " + msg + " from " + somebody);
这种打印日志的方式有个缺点,就是无论日志级别是什么,程序总要先执行 “the message is ” + msg + ” from ” + somebody 这段字符串的拼接操作。当 logger 设置的日志级别为比 DEBUG 级别更高级别时,DEBUG 级别的信息不回被打印出来的,显然,字符串拼接的操作是不必要的,当要拼接的字符串很大时,这无疑会带来很大的性能白白损耗
于是,一种改进的打印日志方式被人们发现了:
if(logger.isDebugEnabled()) {
logger.debug("the message is " + msg + " from " + somebody);
}
样的方式确实能避免字符串拼接的不必要损耗,但这也不是最好的方法,当日志级别为 DEBUG 时,那么打印这行消息,需要判断两次日志级别。一次是logger.isDebugEnabled(),另一次是 logger.debug() 方法内部也会做的判断。这样也会带来一点点效率问题,如果能找到更好的方法,谁愿意无视白白消耗的效率
有一种更好的方法,那就是提供占位符的方式,以参数化的方式打印日志,例如上述的语句,可以是这样的写法:
logger.debug("the message {} is from {}", msg, somebody);
当应用程序发起一个记录日志的请求,例如 info() 时,logback 的内部运行流程如上所示:
1 获得过滤器链条
2 检查日志级别以决定是否继续打印
3 创建一个 LoggingEvent 对象
4 调用 Appenders
5 进行日志信息格式化
6 发送 LoggingEvent 到对应的目的地
大量的日志。打印日志中的不当处理,会引发各种性能问题,例如太多的日志记录请求可能使磁盘 IO 成为性能瓶颈,从而影响到应用程序的正常运行。在合适的时候记录日志、以更好的方式发起日志请求、以及合理设置日志级别方面,都有可能造成性能问题。
关于性能问题,以下几个方面需要了解
logback 提供的配置方式有以下几种:
logback 在启动时,根据以下步骤寻找配置文件:
1 在logback-test.xml,则在 classpath 中寻找 logback.groovy 文件
2 如果找不到classpath 中寻找 logback-test.xml文件
3 如果找不到 logback.groovy,则在 classpath 中寻找 logback.xml文件
4 如果上述的文件都找不到,则 logback 会使用 JDK 的 SPI 机制查找 META-INF/services/ ch.qos.logback.classic.spi.Configurator 中的 logback 配置实现类,这个实现类必须实现 Configuration 接口,使用它的实现来进行配置
5 如果上述操作都不成功,logback 就会使用它自带的 BasicConfigurator 来配置,并将日志输出到 console
logback-test.xml 一般用来在测试代码中打日志,如果是 maven 项目,一般把 logback-test.xml 放在 src/test/resources 目录下。maven 打包的时候也不会把这个文件打进 jar 包里。
前面有提到默认的配置,由 BasicConfiguator 类配置而成,这个类的配置可以用如下的配置文件来表示:
%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
如果 logback 在启动时,解析配置文件时,出现了需要警告的信息或者错误信息,那 logback 会自动先打印出自身的状态信息。
如果希望正常情况下也打印出状态信息,则可以使用之前提到的方式,在代码里显式地调用使其输出:
public static void main(String[] args) {
// assume SLF4J is bound to logback in the current environment
LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
// print logback's internal status
StatusPrinter.print(lc);
...
}
也可以在配置文件中,指定 configuration 的 debug 属性为 true
%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
要使配置文件自动重载,需要把 scan 属性设置为 true,默认情况下每分钟才会扫描一次,可以指定扫描间隔:
...
注意扫描间隔要加上单位,可用的单位是 milliseconds,seconds,minutes 和 hours。如果只指定了数字,但没有指定单位,这默认单位为 milliseconds。
在 logback 内部,当设置 scan 属性为 true 后,一个叫做 ReconfigureOnChangeFilter 的过滤器就会被牵扯进来,它负责判断是否到了该扫描的时候,以及是否该重新加载配置。Logger 的任何一个打印日志的方法被调用时,都会触发这个过滤器,所以关于这个过滤器的自身的性能问题,变得十分重要。logback 目前采用这样一种机制,当 logger 的调用次数到达一定次数后,才真正让过滤器去做它要做的事情,这个次数默认是 16,而 logback 会在运行时根据调用的频繁度来动态调整这个数目。
...
也可以通过 LoggerContext 的 setPackagingDataEnabled(boolean) 方法来开启
LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
lc.setPackagingDataEnabled(true);
根节点是 configuration,可包含0个或多个 appender,0个或多个 logger,最多一个 root。
:用来设置某一个包或者具体的某一个类的日志打印级别、以及指定
用来设置某一个包或者具体的某一个类的日志打印级别、以及指定。仅有一个name属性,一个可选的level和一个可选的addtivity属性。
name:用来指定受此loger约束的某一个包或者具体的某一个类。
level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,还有一个特俗值INHERITED或者同义词NULL,代表强制执行上级的级别。
如果未设置此属性,那么当前loger将会继承上级的级别。
addtivity:是否向上级loger传递打印信息。默认是true。
可以包含零个或多个元素,标识这个appender将会添加到这个loger。
也是元素,但是它是根loger。只有一个level属性,应为已经被命名为"root".
level:
用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,不能设置为INHERITED或者同义词NULL。
默认是DEBUG。
可以包含零个或多个元素,标识这个appender将会添加到这个loger。
标签有两个必须填的属性,分别是 name 和 class,class 用来指定具体的实现类。
标签下可以包含至多一个 ,0个或多个 ,0个或多个 ,
除了这些标签外, 下可以包含一些类似于 JavaBean 的配置标签。
包含了一个必须填写的属性 class,用来指定具体的实现类,不过,如果该实现类
的类型是 PatternLayout 时,那么可以不用填写。 也和 一样,可以包含类似于
JavaBean 的配置标签。
标签包含一个必须填写的属性 class,用来指定具体的实现类,如果该类的类型是 PatternLayoutEncoder ,那么 class 属性可以不填。
ConsoleAppender:把日志添加到控制台,
%-4relative [%thread] %-5level %logger{35} - %msg %n
FileAppender:把日志添加到文件
把日志添加到文件,有以下子节点:
:被写入的文件名,可以是相对目录,也可以是绝对目录,如果上级目录不存在会自动创建,没有默认值。
:如果是 true,日志被追加到文件结尾,如果是 false,清空现存文件,默认是true。
:对记录事件进行格式化。
testFile.log
true
%-4relative [%thread] %-5level %logger{35} - %msg%n
RollingFileAppender:滚动记录文件,先将日志记录到指定文件,当符合某个条件时,将日志记录到其他文件。
:被写入的文件名,可以是相对目录,也可以是绝对目录,如果上级目录不存在会自动创建,没有默认值。
:如果是 true,日志被追加到文件结尾,如果是 false,清空现存文件,默认是true。
:对记录事件进行格式化。
:当发生滚动时,决定 RollingFileAppender 的行为,涉及文件移动和重命名。
rollingPolicy:
TimeBasedRollingPolicy: 常用的滚动策略,它根据时间来制定滚动策略,既负责滚动也负责出发滚动。有以下子节点:
:必要节点,包含文件名及“%d”转换符。 “%d”可以包含一个java.text.SimpleDateFormat指定的时间格式,如:%d{yyyy-MM}。
如果直接使用 %d,默认格式是 yyyy-MM-dd。
每天生成一个日志文件,保存30天的日志文件
logFile.%d{yyyy-MM-dd}.log
30
%-4relative [%thread] %-5level %logger{35} - %msg%n
如果想要往一个 logger 上绑定 appender,则使用以下方式:
执行一个过滤器会有返回个枚举值,即DENY,NEUTRAL,ACCEPT其中之一。返回DENY,日志将立即被抛弃不再经过其他过滤器;返回NEUTRAL,有序列表里的下个过滤器过接着处理日志;返回ACCEPT,日志会被立即处理,不再经过剩余过滤器。
过滤器被添加到 中,为 添加一个或多个过滤器后,可以用任意条件对日志进行过滤。 有多个过滤器时,按照配置顺序执行。
LevelFilter: 级别过滤器,根据日志级别进行过滤。如果日志级别等于配置级别,过滤器会根据onMath 和 onMismatch接收或拒绝日志
例如:将过滤器的日志级别配置为INFO,所有INFO级别的日志交给appender处理,非INFO级别的日志,被过滤掉。
INFO
ACCEPT
DENY
%-4relative [%thread] %-5level %logger{30} - %msg%n
ThresholdFilter: 临界值过滤器,过滤掉低于指定临界值的日志。当日志级别等于或高于临界值时,过滤器返回NEUTRAL;当日志级别低于临界值时,日志会被拒绝。
例如:过滤掉所有低于INFO级别的日志。
INFO
%-4relative [%thread] %-5level %logger{30} - %msg%n
每个logger都关联到logger上下文,默认上下文名称为“default”。但可以使用设置成其他名字,用于区分不同应用程序的记录。一旦设置,不能修改。
myAppName
%d %contextName [%t] %level %logger{36} - %msg%n
在 logback 中,支持以 ${varName} 来引用变量
${USER_HOME}/myApp.log
%msg%n
也可以通过外部文件来定义
${USER_HOME}/myApp.log
%msg%n
外部文件也支持 classpath 中的文件
${USER_HOME}/myApp.log
%msg%n
外部文件的格式是 key-value 型。
USER_HOME=/home/sebastien
变量有三个作用域:
local 作用域在配置文件内有效,context 作用域的有效范围延伸至 logger context,system 作用域的范围最广,整个 JVM 内都有效。
logback 在替换变量时,首先搜索 local 变量,然后搜索 context,然后搜索 system。
指定 scope:
/opt/${nodeId}/myApp.log
%msg%n
在引用一个变量时,如果该变量未定义,那么可以为其指定默认值,做法是:
${aName:-golden}
logback 允许在配置文件中定义条件语句,以决定配置的不同行为,具体语法格式如下:
...
...
...
示例:
%d %-5level %logger{35} - %msg %n
${randomOutputDir}/conditional.log
%d %-5level %logger{35} - %msg %n
可以使用 标签在一个配置文件中包含另外一个配置文件,如下图所示:
被包含的文件必须有以下格式:
"%d - %m%n"
支持从多种源头包含:
从文件中包含
从 classpath 中包含
从 URL 中包含
如果包含不成功,那么 logback 会打印出一条警告信息,如果不希望 logback 抱怨,只需这样做:
LoggerContextListener 接口的实例能监听 logger context 上发生的事件,比如说日志级别的变化,添加的方式如下所示:
....