微信公众号:橘松Java技术窝
文章首发掘金平台,后续同步更新公众号,关注后回复 “加群” 可加入互联网技术交流&内推群,和一群大厂大佬讨论面试问题。回复 “666” 可获取一线互联网具备所有资料包(包括开发软件、开发规范、经典电子pdf、以及一些精品学习课程)。
前言
想必大家在开发中都会碰到过这么几种情况,接口日志很难查啊,服务器日志太多看的眼花缭乱。这还好我可以通过前面讲的熟悉这些Java排查工具就够了一些工具完成,关键是服务器日志堆积告警,尤其是qa
环境(quality assurance), 然后你收到这条短信报警连忙登录到机器,开始了一顿rm
操作解决了美滋滋,过了半小时,你又收到一条短信告警,很熟悉,又是这个…
于是你开始寻思这哪里天天来的这么多日志打印,出于好奇的你打开了温顺的IDEA,看着满屏的代码,时不时轻微右键看看git master
提交者,知道真相的你都想砸电脑了。
循环体里一堆日志,日志相似度很高,每个接口的参数和结果都打日志,debug日志没删除,居然还有带有’xxxTest’字样日志,从头到尾没看到一条Warn日志,除了Info日志就是Error日志,实在看不下去了要合电脑了,突然看到某个类以xxxAspect结尾的文件?哦?难道这是业务切面? 出于好奇点进去一看,尼玛 日志切面,切入点是所有biz业务代码方法…
那么,针对日志不规范,服务器日志太多,日志治理这块我们有什么解决方案呢,于是灵机一动,万物且可配置,是不是这个东西可以做成配置的呢,于是你就开始着手设计了起来,
这么多类,这么多方法,我是不是每个接口的日志打印如何能做成开关控制就好了?搞定!
开关控制做好了,那么打印日志的内容呢,我需要打印哪些内容合适呢? 入参?返回值?
日志打印的内容为接口的入参、响应结果,响应耗时..可扩展参数等等。搞定!
日志打印内容定好了,那么日志打印级别呢?每个方法每个场景可能不一样啊,不同环境我也想不一样怎么办呢?
懂了,不是万物皆配置么,我做成配置不就好啦,配置几种不同的策略即可,搞定!
*配置?我用什么做配置呢,我这个项目是分布式项目,于是经过balabala一顿百度 你可能看到有可选的diamond
、apollo
、disconf
等,于是你丢骰子最后选择了apollo
其实都可以哈哈哈,理解理解
通过控制接口的黑白名单日志打印实现动态日志打印,搞定!
好了,思路有了,开始coding尝试实现起来
apollo动态配置实现接口日志打印
首先,我们定义一个控制开关策略的枚举LogSwitch
/**
* @创建人 : 掘金账号 "橘松Java"
* @创建时间 2021/7/9
* @描述 : 需要源文件加QQ群[572411121]免费索取
*/
public enum LogSwitch {
/**
* 完全关闭
*/
OFF(1,"off"),
/**
* 部分开启
*/
PART_ON(2,"part on"),
/**
* 部分关闭
*/
PART_OFF(3,"part off"),
/**
* 完全开启
*/
ON(4,"on");
LogSwitch(int code ,String name){
this.code = code;
this.name = name;
}
public static LogSwitch of(int code) {
for(LogSwitch logSwitch : LogSwitch.values()) {
if(logSwitch.getCode() == code) {
return logSwitch;
}
}
return null;
}
@Getter
@Setter
private int code;
@Getter
@Setter
private String name;
}
复制代码
其次,日志打印的内容我们也定义一个枚举LogDimension
/**
* @创建人 : 掘金账号 "橘松Java"
* @创建时间 2021/7/9
* @描述 : 需要源文件加QQ群[572411121]免费索取
*/
public enum LogDimension {
NONE(1,"none"),
TIME_ELAPSE(2,"time elapse"),
PARAM(3,"param"),
RESULT(4,"result"),
ALL(5,"all");
LogDimension(int code,String name) {
this.code = code;
this.name = name;
}
public static LogDimension of(int code) {
for(LogDimension logDimension : values()) {
if(logDimension.getCode() == code) {
return logDimension;
}
}
return null;
}
@Getter
@Setter
private int code;
@Getter
@Setter
private String name;
}
复制代码
好了,接下来打印的日志关键要素已经形成,我们需要定义一个切面类来完成。
既然有切面,那我们就可以一个切入点注解来完成我们的切面,定义日志打印注解LogAnnotation
不需要任何属性,对我们来说只是一个切面标识。
/**
* @创建人 : 掘金账号 "橘松Java"
* @创建时间 2021/7/9
* @描述 : 需要源文件加QQ群[572411121]免费索取
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD,ElementType.TYPE})
@Documented
public @interface LogAnnotation {
}
复制代码
接下来可以写我们的切面类了,我们定义LogAspect
。
/**
* @创建人 : 掘金账号 "橘松Java"
* @创建时间 2021/7/9
* @描述 : 需要源文件加QQ群[572411121]免费索取
*/
@Component
@Aspect
public class LogAspect {
/**
* 日志开关级别
* 1.OFF 2.PART_ON 3.PART_OFF 4.ON
* @see LogSwitch
*/
@Value("${log_aspect_switch_level:1}")
private int logSwitch;
/**
* 日志内容控制
* 1.NONE 2.TIME_ELAPSE 3.PARAM 4.RESULT
* @see LogDimension
*/
@Value("${log_aspect_dimension_level:1}")
private int logDimension;
/**
* 当logSwitch为2时 需处理的方法签名 逗号分隔
* eg. "com.orange.application.xxxClient#getXXX,com.orange.application.xxxClient#getYYY"
*/
@Value("{log_aspect_part_on_method}")
private String partOnMethods;
/**
* 当logSwitch为3时 需排除的方法签名 逗号分隔
* eg. "com.orange.application.xxxClient#getXXX,com.orange.application.xxxClient#getYYY"
*/
@Value("${log_aspect_part_off_method}")
private String partOffMethods;
@Around("@annotation(logAnnotation)")
//采取环绕通知
public Object processLog (ProceedingJoinPoint joinPoint,LogAnnotation logAnnotation) throws Exception {
//获取方法签名服务
String serviceName = joinPoint.getSignature().getDeclaringTypeName();
//获取方法签名方法名
String methodName = joinPoint.getSignature().getName();
String methodSignature = serviceName + "#" + methodName;
//判断检查开关是否命中
Boolean isHit = judgeIsHit(methodSignature);
Object obj = null;
//定义日志输出
List<String> logArray = Lists.newArrayList();
try {
if(!isHit) {
//如果没命中,即不需要打印,则执行逻辑
obj = joinPoint.proceed();
}else{
//根据枚举层级 输出对应的log详情 具体日志格式化省略...
logArray.add(formatParam(joinPoint.getArgs()));
Stopwatch stopwatch =Stopwatch.createStarted();
obj = joinPoint.proceed();
stopwatch.stop();
//统计日志耗时 添加到logArray详情 具体日志格式化省略...
logArray.add(formatTimeElapse(stopwatch.toString()));
//统计响应返回值 添加到logArray详情 具体日志格式化省略...
logArray.add(formatResult(obj.toString()));
}
}catch (Exception e) {
//处理业务异常
dealBizException(e,joinPoint);
} catch (Throwable throwable) {
dealException(joinPoint);
} finally {
if(isHit){
//如果命中,则打印日志
log.info(formatLog(methodSignature,logArray));
}
}
return obj;
}
/**
* 判断方法签名是否命中
*/
private Boolean judgeIsHit(String methodSignature) {
if(LogSwitch.of(logSwitch) == null || LogDimension.of(logDimension) == null) {
// sign log.... 没配置
return false;
}
if(LogSwitch.ON.getCode() == logSwitch){
//配置的全部打开
return true;
}
if(LogSwitch.PART_ON.getCode() == logSwitch) {
//配置的部分打开,命中partOnMethods
return !StringUtils.isEmpty(partOnMethods) && partOnMethods.contains(methodSignature);
}
if(LogSwitch.PART_OFF.getCode() == logSwitch) {
//配置的部分打开,命中partOffMethods
return !StringUtils.isEmpty(partOffMethods) && !partOffMethods.contains(methodSignature);
}
return false;
}
复制代码
总结一下
动态日志打印由相应的Aspect
进行日志统一输出主要解决的是日志输出逻辑固定、日志输出过多,排查问题无法动态调整日志内容。结合apollo
以及注解 可动态调节关注点、 可动态控制日志输出内容 的统一切面工具。使用方式也特别简单,在代码内需要被LogAspect
动态管理日志的方法加上@LogAnnotation
注解即可。
上面一切完成,那么今后你再也不用担心查看接口日志找不到了,你可以按照你想要的配置一下就可以啦!
对更多的关于 封装、通用组件、通用代码 方面感兴趣的同学可关注主页 通用组件封装专栏 后续将会分享更多的这块知识。
原创不易,喜欢博主的同学点点赞啦、 关注关注啦,好了,下期见 。
最后
- 文章均原创,原创不易,感谢掘金平台,觉得有收获,帮忙三连哈,感谢
- 微信搜索公众号:橘松Java技术窝,交个朋友,进互联网技术交流群
- 文章涉及的所有代码、时序图、架构图均共享,可通过公众号加群免费索要
- 文章若有错误,欢迎评论留言指出,也欢迎转载,麻烦标注下出处就好