说在之前的话
本次交流所有的信息都是个人感受,有其他意见大家可以一起交流。正所谓文无第一,武无第二。在没有严格的规范要求下,只要我们实现了我们想要的效果,代码所呈现出来的状态都是每个人的代码习惯和风格的体现。没有本质上的优劣。
我本身是一个实用主义者,我接触一个东西的时候,首先我要知道他能帮我干什么,我能看到什么结果,能为我带来什么方便。
本着这样的思路,今天的交流活动按照这样的思路进行
- 常用的日志框架:我们大致了解一下我们经常用的框架有哪些。
- 日志级别:了解一下日志本身代表的含义。
- 如何打印日志:我理解的什么时候应该输出日志。
- 日志中有几个特殊的方法:isDebugEnabled(或isInfoEnabled)是否要用等
- 日志的配置:配置文件中的参数含义是什么
- 一些我们不常用的,但是我觉得特殊时候也许会有用的方法 MDC
- 以后可以应用到系统中的功能:jmxConfigurator
1.常用的日志框架
我们常用的体制框架:LOG4J、LOGBACK、SLF4J
但是他们之间还是有区别的:SLF4J本身并不是一个完整的日志实现。笼统的讲就是SLF4J是一系列的日志接口(日志门面),而LOG4J、LOGBACK是具体实现了的日志框架。
slf4j官网
The Simple Logging Facade for Java (SLF4J) serves as a simple facade or
abstraction for various logging frameworks, such as java.util.logging, logback
and log4j. SLF4J allows the end-user to plug in the desired logging framework
at deployment time. Note that SLF4J-enabling your library/application implies
the addition of only a single mandatory dependency, namely slf4j-api-1.8.0-beta1.jar.
官方文档的这一段话已经明确描述了三者的关系。slf4j译为简单日志门面,是日志框架的抽象。而log4j和logback是众多日志框架中的几种。
用我的理解来描述:用我们熟悉的MVC结构来解释这三者的关系。SLF4J类似Controller。而LOG4J、LOGBACK类似于Dao,只要2选1就可以。SLF4J本身在没有另外两个日志框架存在的情况下是无法完成日志的输出的。LOGBACK是直接实现了SLF4J的接口,是不消耗内存和计算开销的。而LOG4J不是对SLF4J的原生实现,所以SLF4J的API在调用LOG4J时需要一个我理解的Service层来转换,这个就是适配层。
Logback 与 Log4J
实际上,这两个日志框架都出自同一个开发者之手,Logback 相对于 Log4J 有更多的优点
- 同样的代码路径,Logback 执行更快
- 更充分的测试
- 原生实现了 SLF4J API(Log4J 还需要有一个中间转换层)
- 内容更丰富的文档
- 支持 XML 或者 Groovy 方式配置
- 配置文件自动热加载
- 从 IO 错误中优雅恢复
- 自动删除日志归档
- 自动压缩日志成为归档文件
- 支持 Prudent 模式,使多个 JVM 进程能记录同一个日志文件
- 支持配置文件中加入条件判断来适应不同的环境
- 更强大的过滤器
- 支持 SiftingAppender(可筛选 Appender)
- 异常栈信息带有包信息
其他日志框架:j.u.l (java.util.logging)、commons-logging
代码演示:
1.没有实现的日志,会报错.
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
2.使用不同日志实现,代码可以不用改变.这个在代码中验证.
2.不同日志级别的使用
日志级别提供了一种方式,我们可以用它来根据严重程度对日志进行分类和识别。java.util.logging 按照严重程度从重到轻,提供了以下级别:
SEVERE(最高级别)、WARNING、INFO、CONFIG、FINE、FINER、FINEST(最低级别)
另外, 还有两个日志级别:ALL和OFF。ALL会让Logger输出所有消息,而OFF则会关闭日志功能。
LOG4J、LOGBACK、SLF4J 经常用导的级别:
ERROR、WARN、INFO、DEBUG、TRACE
下面描述的基础的定义,而不是使用时候的强制要求
ERROR:
基本概念
影响到程序正常运行、当前请求正常运行的异常情况:
1.打开配置文件失败
2.所有第三方对接的异常(包括第三方返回错误码)
3.所有影响功能使用的异常,包括:SQLException和除了业务异常之外的所有异常(RuntimeException和Exception)
不应该出现的情况:
比如要使用上传图片,但是服务器未响应
如果有Throwable信息,需要记录完成的堆栈信息:
log.error("获取用户[{}]的用户信息时出错",userName,e);
说明
如果进行了抛出异常操作,请不要记录error日志,由最终处理方进行处理:
反例(不要这么做):
try{
....
}catch(Exception ex){
String errorMessage=String.format("Error while reading information of user [%s]",userName);
logger.error(errorMessage,ex);
throw new UserServiceException(errorMessage,ex);
}
WARN
基本概念
不应该出现但是不影响程序、当前请求正常运行的异常情况:
1.有容错机制的时候出现的错误情况
2.找不到配置文件,但是系统能自动创建配置文件
即将接近临界值的时候,例如:缓存池占用达到警告线
业务异常的记录,比如:当接口抛出业务异常时,应该记录此异常
INFO
基本概念
系统运行信息
1.Service方法中对于系统/业务状态的变更
2.主要逻辑中的分步骤
外部接口部分
1.客户端请求参数(REST/WS)
2.调用第三方时的调用参数和调用结果
说明
1.并不是所有的service都进行出入口打点记录,单一、简单service是没有意义的(job除外,job需要记录开始和结束,)。
反例(不要这么做):
public List listByBaseType(Integer baseTypeId) {
log.info("开始查询基地");
BaseExample ex=new BaseExample();
BaseExample.Criteria ctr = ex.createCriteria();
ctr.andIsDeleteEqualTo(IsDelete.USE.getValue());
Optionals.doIfPresent(baseTypeId, ctr::andBaseTypeIdEqualTo);
log.info("查询基地结束");
return baseRepository.selectByExample(ex);
}
2.对于复杂的业务逻辑,需要进行日志打点,以及埋点记录。
3.对于整个系统的提供出的接口(REST/WS),使用info记录入参
4.如果所有的service为SOA架构,那么可以看成是一个外部接口提供方,那么必须记录入参。
5.调用其他第三方服务时,所有的出参和入参是必须要记录的(因为你很难追溯第三方模块发生的问题)
DEBUG
基本概念
1.可以填写所有的想知道的相关信息(但不代表可以随便写,debug信息要有意义,最好有相关参数)
2.生产环境需要关闭DEBUG信息
3.如果在生产情况下需要开启DEBUG,需要使用开关进行管理,不能一直开启。
TRACE
基本概念
特别详细的系统运行完成信息,业务代码中,不要使用.(除非有特殊用意,否则请使用DEBUG级别替代)
3.什么时候应该打日志(日志里都需要有什么)
1.当你遇到问题的时候,只能通过debug功能来确定问题,你应该考虑打日志,良好的系统,是可以通过日志进行问题定位的。
2.当你碰到if…else 或者 switch这样的分支时,要在分支的首行打印日志,用来确定进入了哪个分支。
3.经常以功能为核心进行开发,应该在提交代码前,可以确定通过日志可以看到整个流程。
4.日志里最好包含哪些信息
一个日志文件中的内容应该易读, 清晰, 可描述. 而不要使用莫名其妙的字符, 日志要给出当前操作做了什么, 使用的什么数据. 好的日志应该被看成文档注释的一部分.
- 操作时间
- 系统设备的IP 地址
- 端口号
- 应用名称
- 类名
- 外部请求ID号
前次应用UUID号 (调用链第一个应用为空)本次应用UUID号- 操作用户:操作员ID
- 授权用户或授权用户组
- 权限资源名称
- 操作方式(如:分配、修改、删除等)
- 操作结果(如:成功、失败)
注
@RequestMapping(value = "notice/{sjbh}/{ywzt}")
5.不要有什么信息
- 切记不要在日志中包含密码和个人隐私信息!
- 文件名
- 类名(全限定名)
- 代码行号
后面三个信息是因为这个会带来严重的性能问题
4.是否使用isDebugEnabled(或isInfoEnabled)
这俩个方法是干什么的?
//以下是isDebugEnabled()的源码(isInfoEnabled:
public boolean isDebugEnabled() {
if(repository.isDisabled( Level.DEBUG_INT))
return false;
return Level.DEBUG.isGreaterOrEqual(this.getEffectiveLevel());
}
从源码中可以看到,这个方法就用来判断日志级别是否时指定的日志界别.
下面这段代码在真正执行logger.debug()之前,进行了logger.isDebugEnabled()的判断。然而log4j.properties已经配置了log level为debug, 为什么还需要这个判断?
if (logger.isDebugEnabled()) {
logger.debug("Student No. : " + i + " is " + stuNo[i].toString());
}
加上这个logger.isDebugEnabled() 后,如果值为false,if里面的代码就不会执行,就节省了字符串的拼接,少生成几个对象,如果不加,总会先拼接里面的字符串(连接字符串最好也不用+)
如果不用logger.isDebugEnabled(), 请使用log4j的其他重载的方法, 比如下面的
logger.debug("Student No. : {} is {}", i, stuNo[i]);
原因是:
//以下是debug()的源码:
public void debug(String msg) {
this.logger.log(FQCN, Level.DEBUG, msg, (Throwable)null);
}
public void debug(String format, Object arg) {
if (this.logger.isDebugEnabled()) {
FormattingTuple ft = MessageFormatter.format(format, arg);
this.logger.log(FQCN, Level.DEBUG, ft.getMessage(), ft.getThrowable());
}
}
日志输出前的判断确实有效,但效果甚微。在日志级别较低,不会输出的情况下,日志的耗时基本可以忽略 。
日志输出大的开销应该在IO上,计算应该不会很多,也不该很多,如果存在大量的运算请自己考虑下是不是有问题;既然是确认要输出的日志,那增加判断其实是种浪费,虽然这种判断的开销可以忽略。
如果debug的参数很简单的话,也可以直接写 logger.debug(message)的。官方的说法,执行一次logger.isDebugEnabled()这样的判断花费的时间大概是写日志时间的万分之一.虽然这个比例很小,但是,程序中的任何地方放到并发的环境下,我们就得重新考虑了。
所以,我觉得,我们开发的时候, 应该视具体的情况选择不同的写法。 我们更应该专注于日志内容, 而将日志的输出的决定权交给日志框架去非处理.
5.配置文件中参数含义(logback)
上面我们了解了 什么是否需要些什么级别的日志,日志中都包含什么样的信息。下面我们要看一下怎么能把我们的这些信息体现在控制台或者日志文件中。
5.1
根节点
5.1.1 scan:
当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true。
5.1.2 scanPeriod:
设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟。
5.1.3 debug:
当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。我们来看一下debug为true时的输出
5.2 的子节点:
5.2.1
每个logger都关联到logger上下文,默认上下文名称为"default"。但可以使用
5.2.2
用来定义变量值的标签,
例如使用
5.2.3
获取时间戳字符串:两个属性 key:标识此
5.3
负责写日志的组件,它有两个必要属性name和class。name指定appender名称,class指定appender的全限定名
5.3.1 ConsoleAppender
把日志输出到控制台,有以下子节点:
5.3.2 FileAppender
把日志添加到文件,有以下子节点:
5.3.3 RollingFileAppender
滚动记录文件,先将日志记录到指定文件,当符合某个条件时,将日志记录到其他文件。有以下子节点:
class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy": 最常用的滚动策略,它根据时间来制定滚动策略,既负责滚动也负责出发滚动。有以下子节点:
:必要节点,包含文件名及“%d”转换符,“%d”可以包含一个java.text.SimpleDateFormat指定的时间格式,如:%d{yyyy-MM}。
如果直接使用 %d,默认格式是 yyyy-MM-dd。RollingFileAppender的file字节点可有可无,通过设置file,可以为活动文件和归档文件指定不同位置,当前日志总是记录到file指定的文件(活动文件),活动文件的名字不会改变;如果没设置file,活动文件的名字会根据fileNamePattern 的值,每隔一段时间改变一次。“/”或者“\”会被当做目录分隔符。
:可选节点,控制保留的归档文件的最大数量,超出数量就删除旧文件。假设设置每个月滚动,且 是6,则只保存最近6个月的文件,删除之前的旧文件。注意,删除旧文件是,那些为了归档而创建的目录也会被删除。
class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy": 查看当前活动文件的大小,如果超过指定大小会告知RollingFileAppender 触发当前活动文件滚动。
5.3.4 还有不常用的,这里就不详解了。
5.4
用来设置某一个包或者具体的某一个类的日志打印级别。
name:用来指定受此logger约束的某一个包或者具体的某一个类。
level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,还有一个特殊值INHERITED或者同义词NULL,代表强制执行上级的级别。如果未设置此属性,那么当前logger将会继承上级的级别。
addtivity:是否向上级logger传递打印信息。默认是true。
5.5
也是
level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,不能设置为INHERITED或者同义词NULL。默认是DEBUG。
5.6 输出代码格式说明如下:
代码 | 含义 |
---|---|
%m | 输出代码中指定的消息 |
%p | 输出优先级,即DEBUG,INFO,WARN,ERROR,FATAL |
%r | 输出自应用启动到输出该log信息耗费的毫秒数 |
%c | 输出所属的类目,通常就是所在类的全名 |
%t | 输出产生该日志事件的线程名 |
%n | 输出一个回车换行符,Windows平台为“\r\n”,Unix平台为“\n” |
%d | 输出日志时间点的日期或时间,默认格式为ISO8601,也可以在其后指定格式,比如:%d{yyy MMM dd HH:mm:ss,SSS},输出类似:2002年10月18日 22:10:28,921 |
%l | 输出日志事件的发生位置,包括类目名、发生的线程,以及在代码中的行数,相当于%C.%M(%F:%L)的组合。举例:Testlog4.main(TestLog4.java:10) |
-X | X信息输出时左对齐 |
%x | 输出和当前线程相关联的NDC(嵌套诊断环境),尤其用到像Java servlets这样的多客户多线程的应用中。 |
%% | 输出一个”%”字符 |
%F | 输出日志消息产生时所在的文件名称 |
%L | 输出代码中的行号 |
6. 映射诊断上下文(MDC)
MDC将值存储在键值对中。这样你可以很容易的在Layout中引用一个单独的键。MDC.put(key,value) 方法将一个新的键值对添加到上下文中,而 MDC.remove(key) 方法会移除指定的键值对。
如果想在日志中同样显示用户名和会话ID,我们需要使用 MDC.put() 方法将这两个变量存储成键值对:
import org.apache.Log4j.Logger;
import org.apache.Log4j.MDC;
...
MDC.put("username","admin");
MDC.put("sessionID","1234");
try {
// tmpFile doesn't exist, causing an exception.
FileReader fr = new FileReader( "tmpFile" );
} catch (Exception ex) {
logger.error( "Unable to open file!" ) ;
}
finally {
MDC.clear();
}
这里再一次强调,在不需要使用Context后,我们需要使用 MDC.clear() 方法将所有的键值对从MDC中移除,这样会降低内存的使用量,并阻止MDC在后面试图调用那些已经过期的数据。
在日志框架中访问MDC的值时,也稍微有些区别。对于存储在上下文中的任何键,我们可以使用%X(键)的方式来访问对应的值。这样,我们可以使用 %X(username) 和 %X(sessionID) 来获取对应的用户名和会话ID:
"admin 1234 ERROR – Unable to open file!"
7.最后:动态控制日志级别
在xml 中 配置下面这个标签
我们可以在系统中动态的修改日志的级别
在logback中可以重新加载配置,此配置意义不大
效率问题
a、日志输出的目的地,输出到控制台的速度比输出到文件系统的速度要慢。
b、日志输出格式不一样对性能也会有影响,如简单输出布局(SimpleLayout)比格式化输出布局(PatternLayout)输出速度要快。可以根据需要尽量采用简单输出布局格式输出日志信息。
c、日志级别越低输出的日志内容就越多,对系统系能影响很大。
d、日志输出方式的不同,对系统系能也是有一定影响的,采用异步输出方式比同步输出方式性能要高。
e、每次接收到日志输出事件就打印一条日志内容比当日志内容达到一定大小时打印系能要低。
参考资料
http://mp.weixin.qq.com/s/X3xtsO6cnoEXUWhoN9Vn0w
https://www.slf4j.org/manual.html
http://www.importnew.com/16331.html
http://www.cnblogs.com/wenbronk/p/6529161.html
http://aub.iteye.com/blog/1103685
https://www.cnblogs.com/davidwang456/p/4448011.html
https://www.cnblogs.com/winner-0715/p/5844119.html
http://blog.csdn.net/tornadowp/article/details/8182496
http://tech.lede.com/2017/06/30/rd/server/loggingHabit/
https://www.jianshu.com/p/1ded57f6c4e3
https://www.cnblogs.com/warking/p/5710303.html
注:
1.本文是内部使用。您看到了,觉得不好呢,请指正,轻喷。
2.如果文字中涉及到了一些利益或其他问题请联系我。