Java日志规范
前言
写好程序的日志可以帮助我们大大减轻后期维护压力,开发人员应在一开始就养成良好的日志撰写习惯
日志可以帮我们解决以下问题:
①、程序是不是按预期执行
②、程序哪里出现了BUG
③、用户在系统上干了什么
④、问题是谁造成的,是依赖的业务系统还是自身系统
一、日志框架选型
logback、log4j2
logback:推荐,Springboot默认的日志框架
log4j2:版本大于2.15.0
二、日志规约
【强制】应用中不可直接使用日志系统(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);
【强制】所有日志文件至少保存15天,因为有些异常具备以“周”为频次发生的特点。对于 当天日志,以“info.log/error.log”来保存,保存在/home/work/logs/应用名 目录下,过往日志 格式为: {logname}.log.{保存日期},日期格式:yyyy-MM-dd
示例: 以 aap 应用为例,日志保存在/home/work/logs/aap/info.log,历史日志名称为 info.log.2022-05-20
【强制】在日志输出时,字符串变量之间的拼接使用占位符的方式
说明:因为 String 字符串的拼接会使用 StringBuilder 的 append()方式,有一定的性能损耗。使用占位符仅 是替换动作,可以有效提升性能
示例:logger.debug("Processing trade with id: {} and symbol: {}", id, symbol);
【强制】对于 trace/debug/info 级别的日志输出,必须进行日志级别的开关判断
说明:虽然在 debug(参数)的方法体内第一行代码 isDisabled(Level.DEBUG_INT)为真时(Slf4j 的常见实现Log4j 和 Logback),就直接 return,但是参数可能会进行字符串拼接运算。此外,如果 debug(getName())这种参数内有 getName()方法调用,无谓浪费方法调用的开销
// 如果判断为真,那么可以输出 trace 和 debug 级别的日志 if (logger.isDebugEnabled()) { logger.debug("Current ID is: {} and name is: {}", id, getName()); }
【强制】避免重复打印日志,浪费磁盘空间,务必在日志配置文件中设置 additivity=false
<logger name="com.taobao.dubbo.config" additivity="false">
【强制】生产环境禁止直接使用 System.out 或 System.err 输出日志或使用 e.printStackTrace()打印异常堆栈
说明:标准日志输出与标准错误输出文件每次 Jboss 重启时才滚动,如果大量输出送往这两个文件,容易 造成文件大小超过操作系统大小限制
【强制】异常信息应该包括两类信息:案发现场信息和异常堆栈信息。如果不处理,那么通过 关键字 throws 往上抛出
logger.error("inputParams:{} and errorMessage:{}", 各类参数或者对象 toString(), e.getMessage(), e);
【强制】日志打印时禁止直接用 JSON 工具将对象转换成 String
说明:如果对象里某些 get 方法被覆写,存在抛出异常的情况,则可能会因为打印日志而影响正常业务流 程的执行
正例:打印日志时仅打印出业务相关属性值或者调用其对象的 toString()方法
【强制】我们在写日志的时候,需要注意输出适当的内容。首先,尽量使用业务相关的描述
说明:我们的程序是实现某种业务的,那么就最好能描述清楚这个时候走到了业务过程的哪一步
其次,避免在日志中输出一些敏感信息,例如用户名和密码。以及,要保持编码的一致。如果不能保证就尽量使用英文而不是中文。这样当我们拿到日志之后就不会因为看到一堆乱码而不知所云了
【强制】控制日志输出的长度在一定范围内,比如请求参数,避免打印大量的业务日志
如日志layout中 msg配置为 %.-2048msg,输出日志长度最长为2048字节,多余的将从后面截断不打印
三、打日志的正确方式
1、日志需要打印哪些信息
开发人员只需关注日志内容和异常堆栈即可,其余信息在日志配置文件中会自动输出
①、日志时间:yyyy-MM-dd HH:mm:ss.SSS
②、日志级别:DEBUG、INFO、WARN、ERROR
③、记录器名称(类名)
日志的记录器名称一般是声明日志记录器实例的类名,通过记录器名称可以快速定位到日志输出的类是哪个
④、方法名,打印日志的方法
⑤、产生行数
即产生日志的所在类的源代码行号
⑥、日志内容
开发人员输出业务相关的日志
⑦、tracing 标识
通过 AOP 切面,日志框架 MDC 等技术结合,在日志上添加一些链路追踪的扩展元素,将会很大程度方便通过日志进行程序请求调用的链路追踪,这在分布式系统中尤其重要
⑧、异常堆栈
堆栈异常信息有助于程序异常的排查定位,ERROR级别必须打印异常堆栈,其他日志级别不必打印
2、什么时候应该打印日志
①、当你遇到问题时,只能通过debug功能来确定问题,你应该考虑打日志,良好的系统,是可以通过日志进行问题定位的
②、当你使用if...else或者swith分支时,要在分支的首行打印日志,用来确定进入了哪个分支
③、经常以功能为核心进行开发,应该在提交代码前,可以确定通过日志可以看到整个流程
3、日志基本格式
①、必须使用参数化信息的方式:logger.debug("Processing trade with id:[{}] and symbol : [{}] ", id, symbol);
不要进行字符串拼接,那样会产生很多String对象,占用空间,影响性能
②、使用[] 进行参数变量隔离
logger.debug("Processing trade with id:[{}] and symbol : [{}] ", id, symbol);这样的格式写法,可读性更好,对于排查问题更有帮助
四、日志追踪
1、为什么要进行日志追踪?
日志追踪可以根据traceId追踪到一个请求链路中的所有日志信息,便于排查问题
2、如何进行日志追踪?
单体服务:
通过 AOP 切面,日志框架 MDC 等技术结合,在日志中打印traceId,而后根据traceId可检索整个请求链路的日志
1)AOP切面结合MDC方式
①、日志配置文件中配置traceId占位符
<pattern>%date{yyyy-MM-dd HH:mm:ss.SSS}|%t|%-5level|%X{traceId}|%C{0}#%M:%L|%msg%n</pattern>
②、AOP切入要增强traceId的方法
③、在增强类中往MDC中添加traceId字段,并赋值,并在finally块移除MDC中traceId字段
@Aspect @Component public class LogAspect { private static final Logger LOGGER = LoggerFactory.getLogger(LogAspect.class); @Pointcut(value = "@annotation(org.springframework.web.bind.annotation.RequestMapping)||@annotation(org.springframework.web.bind.annotation.PostMapping)||@annotation(org.springframework.web.bind.annotation.GetMapping)") private void webPointcut() { // doNothing } /** * 为所有的HTTP请求添加traceId * * @param joinPoint * @throws Throwable */ @Around(value = "webPointcut()") public Object around(ProceedingJoinPoint joinPoint) throws IllegalAccessException, InstantiationException { // 方法执行前加上线程号,并将线程号放到线程本地变量中 String uuid = StringUtil.getUUID(); MDC.put("traceId", uuid); //...此处省略其他业务处理代码 // 执行切点方法 Object result = joinPoint.proceed(); } finally { // 方法执行结束移除线程号,并移除线程本地变量,防止内存泄漏 MDC.remove("traceId"); } return result; } }
2)直接使用MDC方式
①、日志配置文件中配置traceId占位符,同上
②、在方法执行业务逻辑前往MDC中添加traceId字段,并赋值
③、在方法返回前移除MDC中的traceId字段
MDC.put("traceId", uuid); try { // 业务逻辑处理 } finally { MDC.remove("traceId"); }
分布式服务: TODO
五、推荐日志配置
- ERROR级别单独打印在error.log中,INFO及以下级别日志打印在info.log中
- 日志文件按天滚动输出
- 日志异步打印,提高系统性能
logback.xml:
<?xml version="1.0" encoding="UTF-8"?>
<!--默认每隔一分钟扫描此配置文件的修改并重新加载-->
<configuration>
<!--定义日志文件的存储地址 勿在LogBack的配置中使用相对路径-->
<property name="LOG_HOME" value="/home/work/log/${artifactId}"/>
<!--输出日志到文件中-->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/info.log</file>
<!--不输出ERROR级别的日志-->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>DENY</onMatch>
<onMismatch>ACCEPT</onMismatch>
</filter>
<!--根据日期滚动输出日志策略-->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/info.log.%d{yyyy-MM-dd}</fileNamePattern>
<!--保存的存档日志文件的数量-->
<maxHistory>15</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%date{yyyy-MM-dd HH:mm:ss.SSS}|%t|%-5level|%X{traceId}|%C{0}#%M:%L|%.-2048msg%n</pattern>
</encoder>
</appender>
<!--错误日志输出文件-->
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/error.log</file>
<!--只输出ERROR级别的日志-->
<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_HOME}/error.log.%d{yyyy-MM-dd}</fileNamePattern>
<!--保存的存档日志文件的数量-->
<maxHistory>15</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%date{yyyy-MM-dd HH:mm:ss.SSS}|%t|%-5level|%X{traceId}|%C{0}#%M:%L|%msg%n</pattern>
</encoder>
</appender>
<!--异步打印日志,任务放在阻塞队列中,如果队列达到80%,将会丢弃TRACE,DEBUG,INFO级别的日志任务,对性能要求不是太高的话不用启用-->
<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
<!--队列的深度,该值会影响性能,默认256-->
<queueSize>512</queueSize>
<!--设为0表示队列达到80%,也不丢弃任务-->
<discardingThreshold>0</discardingThreshold>
<!--日志上下文关闭后,AsyncAppender继续执行写任务的时间,单位毫秒-->
<maxFlushTime>1000</maxFlushTime>
<!--队列满了直接丢弃要写的消息-->
<neverBlock>true</neverBlock>
<!--是否包含调用方的信息,false则无法打印类名方法名行号等-->
<includeCallerData>true</includeCallerData>
<!--One and only one appender may be attached to AsyncAppender,添加多个的话后面的会被忽略-->
<appender-ref ref="FILE"/>
</appender>
<appender name="ERROR_ASYNC" class="ch.qos.logback.classic.AsyncAppender">
<queueSize>256</queueSize>
<!--设为0表示队列达到80%,也不丢弃任务-->
<discardingThreshold>0</discardingThreshold>
<!--日志上下文关闭后,AsyncAppender继续执行写任务的时间,单位毫秒-->
<maxFlushTime>1000</maxFlushTime>
<!--队列满了直接丢弃要写的消息,不阻塞写入队列-->
<neverBlock>true</neverBlock>
<!--是否包含调用方的信息,false则无法打印类名方法名行号等-->
<includeCallerData>true</includeCallerData>
<!--One and only one appender may be attached to AsyncAppender,添加多个的话后面的会被忽略-->
<appender-ref ref="ERROR_FILE"/>
</appender>
<!--指定一些依赖包的日志输出级别,所有的logger会继承root,为了避免日志重复打印,需指定additivity="false",将不会继承root的append-ref-->
<!-- <logger name="com.xiaomi.mitv.outgoing" level="ERROR" additivity="false">
<appender-ref ref="STDOUT"/>
<!–<appender-ref ref="ERROR_FILE"/>–>
</logger>-->
<root level="INFO">
<!--使用异步打印日志-->
<appender-ref ref="ASYNC"/>
<appender-ref ref="ERROR_ASYNC"/>
</root>
</configuration>
END.