Java如何正确地输出日志
日常开发日志打印需要注意哪些地方
我的博客即将同步至腾讯云+社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=2oupi2ih194w8
- 不能面向debug编程,过于依赖debug,应该多依赖日志输出;
- 代码开发测试完成之后不要急着提交,先跑一遍看看日志是否看得懂;
- 日志必须包含哪些信息: – 每次请求的唯一id(便于从海量的日志里区分某次请求); – 每次请求的用户信息(从海量日志里快速找到该用户做了什么);
- 某些地方必须打印日志:
- 分支语句的变量必须打印日志,重要参数必须打印(比如订单code等);
- 修改(包括新增)操作必须打印日志(出问题,做到有证可查);
- 数据量大的时候需要打印数据量,及耗时(用于分析性能。比如查询一个列表,要打印结果列表大小);
使用日志插件优雅解决以上问题
日志插件项目
点我 点我 我是日志插件项目 ,欢迎star
日志插件安装
下载源码,然后mvn install
到自己的maven仓库,最后在项目中添加如下依赖:
<dependency>
<groupId>com.enhancegroupId>
<artifactId>log-pluginartifactId>
<version>1.0-SNAPSHOTversion>
dependency>
项目添加日志配置文件,日志配置文件在日志插件里有案例,详情可参考日志插件的logback.xml文件,主要对日志输出格式做如下配置:
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread %X{X-B3-TraceId}] %-5level[%X{user} %X{logjson} %X{code}]%logger{50} - %msg%n
pattern>
<charset>UTF-8charset>
encoder>
- %X{X-B3-TraceId} :一次请求的唯一traceID
- %X{user} : 一次请求的用户信息,通过实现接口LogService获取用户信息,然后存入MDC
- %X{logjson} : 存放action、itemType和itemIds值
- %X{code}: 方法内一些想存放入MDC的值
日志平台(ELK)安装
下载源码去到nodes-stand-alone目录执行init.sh
脚本即可。这里搭建的ELK是在单台服务器上,不适合生产环境使用。
日志插件功能介绍
打印方法入参出参
方法添加注解@Log
,所有入参、出参都打印
过滤掉不需要打印的入参
- 方法添加
@Log(excludeInParam = {"arg1"})
:排除第二个参数不打印 @Log(excludeInParam = {"arg0", "arg1"})
:多个参数时,忽略掉多个参数不打印@Log(excludeInParam = {"arg2.password"})
:忽略第二个参数的password属性不打印
指定打印某些参数或属性
@Log(includeInParam = {"arg2.name"})
:其他参数正常打印,第三个参数只打印user的name属性@Log(excludeInParam = {"arg2.password"},includeInParam = {"arg2.password"})
:当excludeInParam排除参数的某个属性和includeInParam冲突时,includeInParam生效@Log(excludeInParam = {"arg2"} , includeInParam = {"arg2.password"})
:当excludeInParam排除参数和includeInParam冲突时,excludeInParam生效
日志包含用户信息
实现LogService接口,然后在方法上添加@Log注解即可让日志信息包含用户信息
日志信息包含唯一的key
@LogProfiler(itemIds = "order.orderCode")
:比如订单修改,希望每条日志都包含订单编号@Log(itemIds = {"order.user.userCode","order.orderCode"})
:比如订单修改,希望每条日志都包含订单编号和订单所属用户@Log(itemIds = {"orderEntryDetails[0].orderId"})
: 比如订单修改时,订单行处理时,希望每条日志信息包含订单id
日志信息包含被操作表信息及操作类型
@Log(itemIds = {"orderDetailDTO.orderCode"},itemType = "order表",action = Action.U)
:当有海量日志时,可根据一条日志信息就能知道,具体用户对表的某条记录进行的具体的操作,及具体的操作内容。
日志工具
printCode(String describe, String separator, String... values)
:后续日志信息打印自定义描述和多个code值printCode(String describe, String value)
:后续日志打印自定义描述和code值printCodeByTemplate(String template, Object... values)
:后续日志打印自定义模板日志
日志插件使用
打印方法的入参和出参
经常出现正式环境出问题了,然后发现没有打印方法的入参和出参,然后重新发版就为了添加一条日志,虽然这种做法很low,但是是可能发生的。只需要添加一个@Log注解就能轻松解决。
打印方法入参出参
添加注解@Log,@Log默认是debug级别才会打印入参和出参的,可通过printInfoLog来控制是否打印
- printInfoLog为true,默认会打印INFO级别日志,但是会导致日志里增加
- printInfoLog默认值为false,可通过动态修改某个类的日志级别为DEBUG,达到打印方法入参和出参的目的
过滤掉不需要打印的入参
当一个入参很大,某些数据打印出来没意义,或者属于敏感信息不能打印,可以通过excludeInParam过滤掉不打印。
- 多个参数时,忽略掉某个参数不打印
- 多个参数时,忽略掉多个参数不打印
- 忽略某个参数的某些属性不打印
- 参数时集合类型,忽略掉不打印
指定打印某些参数或属性
当一个方法有多个入参,可以通过includeInParam或param指定打印某些参数,或某些参数的指定属性,从而只打印自己关心部分的日志,减少日志量。
- 打印某个参数的指定属性,其它参数正常打印
- 当excludeInParam排除参数的某个属性和includeInParam冲突时,includeInParam生效
- 当excludeInParam排除参数和includeInParam冲突时,excludeInParam生效
- 当配置了param,就只会打印param配置的参数,此时excludeInParam和includeInParam不会生效
出参只打印数组大小
当方法的出参是一个集合时,可通过printOutParamSize控制是打印集合详情,还是只打印集合大小;在controller层,往往出参是包了一层,真正的数据只是出参对象的一个属性值,此时可通过继承FilterResultService类,实现shouldFilter方法和filter方法,对结果进行处理,已达到只打印集合大小和真正的数据详情。
public abstract class FilterResultService<T, U> {
public abstract boolean shouldFilter(U u);
public Optional<T> filterResult(U u) {
if (shouldFilter(u)) {
log.info("Start dealing with the results");
return Optional.ofNullable(filter(u));
}
return null;
}
public abstract T filter(U u);
}
默认了一个DefaultFilterResultServiceImpl对结果进行处理:
@Component
public class DefaultFilterResultServiceImpl extends FilterResultService {
@Override
public boolean shouldFilter(Object o) {
return false;
}
@Override
public Object filter(Object o) {
return o;
}
}
日志尽可能多的包含有用信息
在尽可能包含更多有效信息的同时,不能增加太多的额外工作;
日志包含用户信息
实现LogService接口,然后再方法上添加@Log注解即可
@Component
public class UserInfoService implements LogService {
/**
* 获取用户信息,用于放入日志框架的MDC里
*
* @return java.lang.String
* @author gongliangjun 2019-12-19 3:33 PM
*/
@Override
public String getUserInfo() {
// 这里为了测试,模拟从请求中获取用户信息,可根据自己实际情况修改
return "admin001";
}
在查询订单列表上添加@Log注解
日志信息包含唯一的key
- 比如订单修改,希望每条日志都包含订单编号
- 比如订单修改,希望每条日志都包含订单编号和订单所属用户
这里的用户在请求pojo的orderDetailDTO.user.userCode里,所以注解是
@Log(itemIds = {"orderDetailDTO.user.userCode"})
- 比如订单修改时,订单行处理时,希望每条日志信息包含订单id
处理订单行方法入参是一个集合,所以注解是
@Log(itemIds = {"orderEntryDetails[0].orderId"})
日志信息包含被操作表信息及操作类型
比如在修改或者删除时,希望日志信息包含被操作的表和具体操作类型,以备出现问题时有证可查,这里需要用@Log的itemType和action,itemType表示表类型,action表示操作类型,操作有6种:
- A 新增操作
- D 删除操作
- U 更新操作
- Q 查询操作
- LQ 查询列表操作
- NULL 其他操作
添加注解@Log(itemIds = {"orderDetailDTO.orderCode"},itemType = "order表",action = Action.U)
,效果如下:
当有海量日志时,我们根据上图一条日志信息就能知道,用户admin001对order表orderCode为MO001的订单进行了更新操作,已经具体的更新内容。
多个方法使用@Log注解
这里我以一个尽可能真实的案例来举例。一个订单详情查询接口,其中包含订单头详情、订单行详情、产品详情和用户详情4个方法,大概调用情况如下:
queryOrderDetail
、conversionOrder
、 conversionOrderEntry
、 conversionSku
和 conversionUser
几个方法都加@Log注解,@Log主要是基于MDC实现的,所以在queryOrderDetail
方法调用方法conversionOrder
,进入conversionOrder
方法时,MDC里存入的就是conversionOrder
方法相关的值,再回到前方法queryOrderDetail
时,MDC中应该切换为方法queryOrderDetail
的值。所以这里存在一个方法调用栈,也需要一个上下文来存储方法相关的MDC值。所以这里我使用了一个LogThreadContext
上下文来存储方法相关的MDC值,和管理方法调用关系的一个stack,然后把LogThreadContext放入到InheritableThreadLocal
中。
具体调用时,日志打印效果如下:
以上每条日志信息都包含一个唯一的tranceID,配合日志框架,比如ELK(项目也有基于docker 快速搭建ELK日志平台的脚本),便可以从海量日志里快速筛选出一次请求的所有日志;也包含了此次请求的用户(实现接口LogService获取用户信息);也包含了每个方法特定的key,方便程序员追溯问题。
日志工具使用
通过LogUtil提供的一下方法,可以在项目开发中使打印的日志信息更有意义,简便了日志打印,从而提高工作效率。LogUtil中简便打印日志的方法,主要使用场景还是在实际项目中,当遇到循环处理逻辑时,循环体逻辑复杂,这时候需要循环体里的日志每条都包含具体处理记录的信息。LogUtil主要提供了如下几个方法:
方法执行耗时统计
实际项目可能需要对一个方法执行耗时进行统计,以便找到效率比较低的方法,后续好做系统优化。通过日志插件的@EnableProfiler
和@LogProfiler
注解,可以轻松实现对一个方法执行时间进行统计。@EnableProfiler
只对方法进行耗时统计,不具备打印日志功能;@LogProfiler
具备日志打印和耗时统计功能,相当于@Log和@@EnableProfiler
。
方法调用执行耗时统计
这里还是以订单详情查询作为例子,在每个需要统计的方法上添加@EnableProfiler
或@LogProfiler
注解
方法内代码块执行耗时统计
这里还是以订单详情查询接口举例,在queryOrderDetail方法内单独统计了查询一个订单所用时长,和模拟一段复杂逻辑执行所用时长。通过LogUtil的startProfiler(name)
方法对方法内的代码块执行耗时进行统计,然后在整个执行耗时统计里打印出来。
@LogProfiler(itemIds = "orderCode")
@Override
public OrderDetailDTO queryOrderDetail(String orderCode) {
log.info("开始处理订单数据");
SleepUtil.threadSleep(2);
Order order1 = new Order();
order1.setOrderCode(orderCode);
// 想统计查询一个订单所用时间
LogThreadContext ltc = LogUtil.startProfiler("findOne");
Optional<Order> orderOptional = orderMapper.findOne(Example.of(order1));
ltc.stopInstantProfiler();
//统计一段复杂逻辑耗时
LogUtil.startProfiler("复杂逻辑统计");
log.info("模拟这里需要处理一大段逻辑");
SleepUtil.threadSleep(4,10);
ltc.stopInstantProfiler();
......
......
log.info("订单数据处理完成");
return detailDTO;
}
日志插件核心类
我的博客即将同步至腾讯云+社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=2oupi2ih194w8
相关文章
- 深入探讨Java中的异常与错误处理
- 研究学习Kotlin的一些方法
- 数据显示Java热度持续下落,日子屈指可数?
- 2017年5月编程语言排行榜:Java与C语言优势正开始缩小
- Java多线程之内置锁与显示锁
- Java线程池的理论与实践
- 白话阿里巴巴Java开发手册(编程规约)
- 关于Java你不知道的十件事
- Java服务化系统线上应急和技术攻关,你必须掌握的Linux命令
- Java实现高斯模糊和图像的空间卷积
- Java阻塞队列实现原理分析
- NPM使用技巧
- Node.js对Java开发者而言是什么?
- Java反射机制应用实践
- 理解RxJava中的Single和Completable
- 2017年你不能错过的Java类库
- 大规模集群下的Hadoop NameNode
- 从源码解密Spark内存管理
- 2017年3月编程语言排行榜:Swift首次进入前十
- JVM热点技术:Java类的加载机制