Java 混乱的日志体系

继续后端服务系列:

为服务加日志,这里先梳理一下 Java 混乱的日志体系。

各种日志体系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
SLF4J-JCL
LOG4J-CORE
LOGBACK
SLF4J-SIMPLE
JCL-OVER-SLF4J
LOGBACK-CORE
LOG4J
LOG4J-API
LOG4J-JUL
LOG4J-JCL
LOGBACK-ACCESS
LOGBACK-CLASSIC
SLF4-API
SLF4J-LOGJ12
LOGBACK-CLASSIC
LOG4J-SLF4J-IMPL

来自不知乎的简短踩坑总结:

https://www.zhihu.com/question/40905746

log4j-api log4j-core 让 log4j2 能正常运行
slf4-api log4j-slf4j-impl 让用了 slf4j 的库能转发到 log4j2
jcl-over-slf4j 让用了 commons-logging 的库能转发到 slf4j
log4j-1.2-api 让直接使用 log4j-1.2 的库转发到 log4j2

下面是绝对不能出现的, 在 maven pom 里要一个一个的排除 (添加 exclusion)
commons-logging 被 jcl-over-slf4j 替代了 , 这个在pom里应该有非常多的间接依赖
log4j-1.2 被 log4j-1.2-api 替代了 , 偶尔会有间接依赖

下面也是绝对不能出现的,但是应该不会有间接依赖
log4j-to-slf4j 把 log4j2 转发到 slf4j , 与 log4j-slf4j-impl 形成循环调用
log4j-jcl 把 jcl 转发到 log4j2 , 被 jcl-over-slf4j 替代
这个依赖于 commons-logging , 不好 , 宁愿多绕路用 jcl-over-slf4j + log4j-slf4j-impl

slf4j-jcl 把 slf4j 转发到 commons-logging , 被 log4j-slf4j-impl 替代
slf4j-log4j12把 slf4j 转发到 log4j-1.2 , 被 log4j-slf4j-impl 替代

log4j-over-slf4j log4j-1.2 转发到 slf4j , 被 log4j-1.2-api 替代了
也可以用它而不用 log4j-1.2-api
它稍微多绕路, 但你不用 log4j2 比如用 logback 时 , 就只能用它了

常见的

名称 描述
log4j Log for Java, 早期常用的日志组件
logback 性能优于 log4j,是由log4j创始人涉及的又一个开源的日志组件,logback当前分成三个模块:logback-core,logback- classic和logback-access。logback-core是其它两个模块的基础模块。logback-classic是log4j的一个 改良版本。此外logback-classic完整实现Slf4j API,使我们可以很方便地更换成其它日志系统,如:log4j或JDK。logback-access访问模块与Servlet容器集成提供通过Http来访问日志的功能。
log4j2 log4j升级,性能得到提升的同时,还能够自动装载配置文件,支持参数变量的占位符功能,可以专门指定事件进行过滤,并且支持插件式架构。
Java.util.logging JDK 实现,tomcat 默认实现
Apache Commons Logging (JCL) 也就是Jakarta Commons Logging,提供的是一个日志接口(Interface,是接口不是实现),自身提供一个简单的日志文件系统,但一般和其他日志系统组合使用。如:Commons-Logging + Log4j组合使用。它会通过动态查找机制,在程序运行时自动找出真正使用的日志库。
Slf4j Simple Logging Facade for Java,和Commons-Logging类似,也是对不同日志框架提供的一个门面封装,可以在部署的时候可以通过桥接包接入其他日志方案来组合使用。它支持多个参数并通过”{ }”占位符来进行替换。

  • commons-logging 和 slf4j 都是日志的接口
  • log4j, logback等等才是日志的真正实现

目前较流行的方案:slf4j的异步模式+log4j2,性能好。

日志接口怎么找到日志实现?

  • Common-Logging (JCL) 通过动态查找的机制,在程序运行时自动找出真正使用的日志库。由于它使用了 ClassLoader 寻找和载入底层的日志库,导致了像 OSGI 这样的框架无法正常工作,因为 OSGI 的不同插件使用自己的 ClassLoader。OSGI 的这种机制保证了插件互相独立,然而却使 Common-Logging 无法工作。
  • SLF4J 在编译时静态绑定真正的Log库,因此可以在 OSGI 中使用。
  • 假如你用SLF4J,但需要调用的组件已经使用了 JCL,还有一些可能直接调用了 java.util.logging, 这时就需要一个桥接器(xxx-over-slf4j.jar) 把它们的日志输出重定向到SLF4J。桥接器就是一个假的日志实现工具,比如当你把 jcl-over-slf4j.jar 放到 CLASS_PATH 时,即使某个组件原本是 JCL 输出日志的,现在会被 jcl-over-slf4j.jar “骗到” SLF4J 里,任何 SLF4J 又会根据绑定器把日志交给具体的日志实现工具。
  • log4j 与 log4j2 桥接 https://blog.csdn.net/HarderXin/article/details/80422903

日志规范

  1. 【强制】应用中不可直接使用日志系统(Log4j、Logback)中的 API,而应依赖使用日志框架
    (SLF4J、JCL–Jakarta Commons Logging)中的 API,使用门面模式的日志框架,有利于维护和
    各个类的日志处理方式统一。
    说明:日志框架(SLF4J、JCL–Jakarta Commons Logging)的使用方式(推荐使用 SLF4J)
    使用 SLF4J:
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    private static final Logger logger = LoggerFactory.getLogger(Test.class);
    使用 JCL:
    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    private static final Log log = LogFactory.getLog(Test.class);
  2. 【强制】所有日志文件至少保存 15 天,因为有些异常具备以“周”为频次发生的特点。对于
    当天日志,以“应用名.log”来保存,保存在/home/admin/应用名/logs/目录下,
    过往日志格式为: {logname}.log.{保存日期},日期格式:yyyy-MM-dd
    说明:以 mppserver 应用为例,日志保存在/home/admin/mppserver/logs/mppserver.log,历史日志
    名称为 mppserver.log.2016-08-01
  3. 【强制】应用中的扩展日志(如打点、临时监控、访问日志等)命名方式:
    appName_logType_logName.log。logType:日志类型,如 stats/monitor/access 等;logName:日志描
    述。这种命名的好处:通过文件名就可知道日志文件属于什么应用,什么类型,什么目的,也有利于归类查
    找。
    说明:推荐对日志进行分类,如将错误日志和业务日志分开存放,便于开发人员查看,也便于通过日志对系
    统进行及时监控。
    正例:mppserver 应用中单独监控时区转换异常,如:mppserver_monitor_timeZoneConvert.log
  4. 【强制】在日志输出时,字符串变量之间的拼接使用占位符的方式。
    说明:因为 String 字符串的拼接会使用 StringBuilder 的 append()方式,有一定的性能损耗。使用占位符仅
    是替换动作,可以有效提升性能。
    正例:logger.debug(“Processing trade with id: {} and symbol: {}”, id, symbol);
  5. 【强制】对于 trace/debug/info 级别的日志输出,必须进行日志级别的开关判断。
    说明:虽然在 debug(参数)的方法体内第一行代码 isDisabled(Level.DEBUG_INT)为真时(Slf4j 的常见实现
    Log4j 和 Logback),就直接 return,但是参数可能会进行字符串拼接运算。此外,如果 debug(getName())
    这种参数内有 getName()方法调用,无谓浪费方法调用的开销。
    正例:

    1
    2
    3
    4
    // 如果判断为真,那么可以输出 trace 和 debug 级别的日志
    if (logger.isDebugEnabled()) {
    logger.debug("Current ID is: {} and name is: {}", id, getName());
    }
  6. 【强制】避免重复打印日志,浪费磁盘空间,务必在 log4j.xml 中设置 additivity=false。正例:

    1
    2
    3
    4
    5
    <logger name="com.taobao.dubbo.config" additivity="false">
    case:
    <logger name="com.jd" level="DEBUG">
    <AppenderRef ref="console"/>
    </logger>
  7. 【强制】生产环境禁止直接使用 System.out 或 System.err 输出日志或使用
    e.printStackTrace()打印异常堆栈。
    说明:标准日志输出与标准错误输出文件每次 Jboss 重启时才滚动,如果大量输出送往这两个文件,容易
    造成文件大小超过操作系统大小限制。

  8. 【强制】异常信息应该包括两类信息:案发现场信息和异常堆栈信息。如果不处理,那么通过
    关键字 throws 往上抛出。
    正例:logger.error(各类参数或者对象 toString() + “_” + e.getMessage(), e);
    case:不允许记录日志后又抛出异常,因为这样会多次记录日志,只允许记录一次日志
  9. 【强制】日志打印时禁止直接用 JSON 工具将对象转换成 String。
    说明:如果对象里某些 get 方法被重写,存在抛出异常的情况,则可能会因为打印日志而影响正常业务流
    程的执行。
    正例:打印日志时仅打印出业务相关属性值或者调用其对象的 toString()方法。
    10.【推荐】谨慎地记录日志。生产环境禁止输出 debug 日志;有选择地输出 info 日志;如果使用
    warn 来记录刚上线时的业务行为信息,一定要注意日志输出量的问题,避免把服务器磁盘撑
    爆,并记得及时删除这些观察日志。
    说明:大量地输出无效日志,不利于系统性能提升,也不利于快速定位错误点。记录日志时请思考:这些
    日志真的有人看吗?看到这条日志你能做什么?能不能给问题排查带来好处?
    11.【推荐】可以使用 warn 日志级别来记录用户输入参数错误的情况,避免用户投诉时,无所适
    从。如非必要,请不要在此场景打出 error 级别,避免频繁报警。
    说明:注意日志输出的级别,error 级别只记录系统逻辑出错、异常或者重要的错误信息。
    12.【推荐】尽量用英文来描述日志错误信息,如果日志中的错误信息用英文描述不清楚的话使用
    中文描述即可,否则容易产生歧义。
    说明:国际化团队或海外部署的服务器由于字符集问题,使用全英文来注释和描述日志错误信息。

(日志规范来源:https://hanquan.blog.csdn.net/article/details/106877158


参考文章: