在某种情况下,我们需要在不影响原有代码的基础上自定义log4j的输出格式。
例如这样的需求,硬性规定了项目的日志格式为:
日期 日志等级 ClassName:line - [版本号] [请求ip地址] [项目应用名称] [服务接口模块] [模块方法] [业务参数1] [业务参数2] [业务参数3] 日志详细内容(必须为json格式)
示例:
2022-05-10 14:04:50,972 INFO ViolationService:51 - [v1.0.0] [192.168.137.47] [merchant-service.cx580.com] [OrderController] [messageList] [null] [] [] {"body":"订单状态消息列表resp:{\"code\":1000,\"msg\":\"成功\"}"}
其中:
版本号是指当前服务接口实际的版本信息,例如V1.0.1;
请求ip地址为用户真实的请求ip;
项目应用名称为项目的名称或者标识,例如支付服务定义应用名称为payService;
服务接口模块是指请求接口对应的模块代码,例如请求订单接口,则接口模块为OrderControlller;
模块方法是指接口对应的请求方法,例如下单接口对应模块方法为createOrder;
业务参数1可根据实际情况写入相应的业务数据,录入订单号orderId,该参数可为空;
业务参数2同上;
业务参数3同上;
日志详细内容是指请求接口时需打印出来的描述信息,例如创建订单异常时,在异常捕捉方法体中描述异常详细信息,日志内容需定义到一个json结构中。
以上是我遇到的场景,这时在不影响原有项目代码的基础上,我们做出日志格式的调整,使用如下方案:
1.通过log4j的占位替换符%X{}配合MDC格式化日志,使用AOP切面在请求线程开始处填充替换符变量
2.继承log4j的具体apper类,重写subApp方法,修改日志输出的内容格式。
此时log4j文件如下
log4j.rootCategory=INFO, stdout, file, errorfile#log4j.category.com.cx=DEBUGlog4j.logger.error=errorfile log4j.apper.stdout=com.test.common.GrayLogConsoleApperlog4j.apper.stdout.layout=org.apache.log4j.PatternLayoutlog4j.apper.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss,SSS} %5p %c{1}:%L%X{log_version}%X{log_ip}%X{log_item}%X{log_module}%X{log_method}%X{log_req_params} %m%n log4j.apper.file=com.test.common.GrayLogDailyRollingFileApperlog4j.apper.file.file=${log.dir}/${spring.application.name}.loglog4j.apper.file.DatePattern=.yyyy-MM-ddlog4j.apper.file.layout=org.apache.log4j.PatternLayoutlog4j.apper.file.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss,SSS} %5p %c{1}:%L%X{log_version}%X{log_ip}%X{log_item}%X{log_module}%X{log_method}%X{log_req_params} %m%n log4j.apper.errorfile=com.test.common.GrayLogDailyRollingFileApperlog4j.apper.errorfile.file=${log.dir}/${spring.application.name}_error.loglog4j.apper.errorfile.DatePattern=.yyyy-MM-ddlog4j.apper.errorfile.Threshold = ERRORlog4j.apper.errorfile.layout=org.apache.log4j.PatternLayoutlog4j.apper.errorfile.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss,SSS} %5p %c{1}:%L%X{log_version}%X{log_ip}%X{log_item}%X{log_module}%X{log_method}%X{log_req_params} %m%n在log4j.properties文件,我们做了两个变动,一个是添加了%X{value}的变量,另一个则是将原本的DailyRollingFileApper修改成了com.test.common.GrayLogConsoleApper。
处理log4j的变量,对代码进行controller切面,在一个http请求java的入口中放入线程变量,该线程变量在当次http请求生命周期内生效。
切面代码如下:
@Around("execution(public * com.test.controller..*.*(..))") public Object aroundController(ProceedingJoinPoint joinPoint) { ServletRequestAttributes attributes =(ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request =attributes.getRequest(); String execIp = request.getHeader("X-Real-IP"); if(StringUtils.isBlank(execIp)){ execIp=request.getRemoteAddr(); } String execClass= joinPoint.getTarget().getClass().getSimpleName(); String execMethod = joinPoint.getSignature().getName(); Map<String,String[]> map = request.getParameterMap(); List<String> paramsList = new ArrayList<>(); for(Map.Entry<String,String []> m : map.entrySet()){ String [] value = m.getValue(); paramsList.add( m.getKey() + "=" + StringUtils.join(value,",")); } String execParams = "[" + StringUtils.join(paramsList,"&") + "] [] []"; MDC.put("log_version"," - [V1.0.0]"); MDC.put("log_item"," [violation-mini]"); MDC.put("log_module"," [" + execClass + "]"); MDC.put("log_method"," [" + execMethod+ "]"); MDC.put("log_req_params"," " + execParams); MDC.put("log_ip"," [" + execIp+ "]"); Object result= null; try { result = joinPoint.proceed(); } catch (Throwable throwable) { LOGGER.error("方法异常:",throwable); } return result; }至此,格式中的MDC变量都已被放入成功。
下一步,将原本的日志内容套上json外套。
新建GrayLogConsoleApper类继承具体的apper类
代码如下:
package com.test.common; import net.sf.json.JSONObject;import org.apache.commons.lang.StringUtils;import org.apache.log4j.ConsoleApper;import org.apache.log4j.spi.LoggingEvent;import org.apache.log4j.spi.ThrowableInformation; import java.lang.reflect.Field; /** * @Author: Lxx * @Description: * @Date: Created in 17:29 2022/5/30 */public class GrayLogConsoleApper exts ConsoleApper { @Override protected void subApp(LoggingEvent event) { try { Class<LoggingEvent> clazz = LoggingEvent.class; Field filed = clazz.getDeclaredField("throwableInfo"); filed.setAccessible(true); Object exception = filed.get(event); JSONObject json = new JSONObject(); if(exception != null){ if(exception instanceof ThrowableInformation){ ThrowableInformation throwableInformation = (ThrowableInformation) exception; String [] details = throwableInformation.getThrowableStrRep(); String error_msg = StringUtils.join(details,"\r\n"); json.put("exception",error_msg); } } filed.set(event,null); boolean flag = false; Field filed1 = clazz.getDeclaredField("message"); filed1.setAccessible(true); Object message = filed1.get(event); if (message instanceof String) { String msg = (String) message; if (message != null) { flag = true; } json.put("body", msg); filed1.set(event, json.toString()); } if(!flag){ Field filed2 = clazz.getDeclaredField("reredMessage"); filed2.setAccessible(true); Object message2 = filed2.get(event); if (message2 instanceof String) { String msg = (String) message2; json.put("body", msg); filed2.set(event, json.toString()); } } } catch (Exception e) { e.printStackTrace(); } super.subApp(event); }}至此,已为日志内容套上json外套,并且当有异常日志时,将异常的堆栈信息放入json的exception中输出出来,不打印堆栈信息。
最终结果:
2022-06-09 00:48:31,849 INFO LogAspect:65 - [V1.0.0] [223.88.53.135] [violation-mini] [TestController] [queryList] [appName=abc&authType=test&avatar=&nickName=&token=asdfasdfadsfasdf&userId=asdfasdfasdfasdf&userType=aaaaa] [] [] {"body":"结果为:ResponseResult{code=0, msg=null, errormsg=查询成功, data={}, successFlag=false}"}