目录
a.打印请求日志中的url实现RequestBodyAdvice
前言
随着微服务盛行,很多公司都把系统按照业务边界拆成了很多微服务,在排错查日志的时候。因为业务链路贯穿着很多微服务节点,导致定位某个请求的日志以及上下游业务的日志会变得有些困难,那么今天所介绍的这款开源工具一定是一个不错的选择
一、TLOG开源组件集成
1.maven引入
<dependency>
<groupId>com.yomahub</groupId>
<artifactId>tlog-all-spring-boot-starter</artifactId>
<version>${tlog.version}</version>
</dependency>
2. 日志文件logback-spring.xml修改
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!--设置系统日志目录-->
<springProperty scope="context" name="moduleName" source="spring.application.name" defaultValue="tclAppLog"/>
<property name="LOG_PATH" value="./logs" />
<property name="LOG_NAME" value="${moduleName}" />
<!-- 日志记录器,日期滚动记录 -->
<appender name="FILEERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 正在记录的日志文件的路径及文件名 -->
<file>${LOG_PATH}/${LOG_NAME}/log_error.log</file>
<!-- 日志记录器的滚动策略,按日期,按大小记录 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 归档的日志文件的路径,例如今天是2013-12-21日志,当前写的日志文件路径为file节点指定,可以将此文件与file指定文件路径设置为不同路径,从而将当前日志文件或归档日志文件置不同的目录。
而2013-12-21的日志文件在由fileNamePattern指定。%d{yyyy-MM-dd}指定日期格式,%i指定索引 -->
<fileNamePattern>${LOG_PATH}/${LOG_NAME}/error/log-error-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<!-- 除按日志记录之外,还配置了日志文件不能超过2M,若超过2M,日志文件会以索引0开始,
命名日志文件,例如log-error-2013-12-21.0.log -->
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>10MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<!-- 追加方式记录日志 -->
<append>true</append>
<!--这里替换成AspectLogbackEncoder-->
<encoder class="com.tcl.log.core.AspectLogbackEncoder">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %X{tl} [%thread] %-5level %logger{50} - %msg[%A][%R]%n</pattern>
</encoder>
<!-- 此日志文件只记录info级别的 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>error</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 日志记录器,日期滚动记录 -->
<appender name="FILEINFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 正在记录的日志文件的路径及文件名 -->
<file>${LOG_PATH}/${LOG_NAME}/log_info.log</file>
<!-- 日志记录器的滚动策略,按日期,按大小记录 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 归档的日志文件的路径,例如今天是2013-12-21日志,当前写的日志文件路径为file节点指定,可以将此文件与file指定文件路径设置为不同路径,从而将当前日志文件或归档日志文件置不同的目录。
而2013-12-21的日志文件在由fileNamePattern指定。%d{yyyy-MM-dd}指定日期格式,%i指定索引 -->
<fileNamePattern>${LOG_PATH}/${LOG_NAME}/info/log-info-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<!-- 除按日志记录之外,还配置了日志文件不能超过2M,若超过2M,日志文件会以索引0开始,
命名日志文件,例如log-error-2013-12-21.0.log -->
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>10MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<!-- 追加方式记录日志 -->
<append>true</append>
<!--这里替换成AspectLogbackEncoder-->
<encoder class="com.tcl.log.core.AspectLogbackEncoder">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %X{tl} [%thread] %-5level %logger{50} - %msg[%A][%R]%n</pattern>
</encoder>
<!-- 此日志文件只记录info级别的 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>info</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!--设置一个向上传递的appender,所有级别的日志都会输出-->
<appender name="FILEALL" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/${LOG_NAME}/log_all.log</file>
<!-- 日志记录器的滚动策略,按日期,按大小记录 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 归档的日志文件的路径,例如今天是2013-12-21日志,当前写的日志文件路径为file节点指定,可以将此文件与file指定文件路径设置为不同路径,从而将当前日志文件或归档日志文件置不同的目录。
而2013-12-21的日志文件在由fileNamePattern指定。%d{yyyy-MM-dd}指定日期格式,%i指定索引 -->
<fileNamePattern>${LOG_PATH}/${LOG_NAME}/all/log-all-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<!-- 除按日志记录之外,还配置了日志文件不能超过2M,若超过2M,日志文件会以索引0开始,
命名日志文件,例如log-error-2013-12-21.0.log -->
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>10MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<!-- 追加方式记录日志 -->
<append>true</append>
<!--这里替换成AspectLogbackEncoder-->
<encoder class="com.tcl.log.core.AspectLogbackEncoder">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %X{tl} [%thread] %-5level %logger{50} - %msg[%A][%R]%n</pattern>
</encoder>
</appender>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!--这里替换成AspectLogbackEncoder-->
<encoder class="com.tcl.log.core.AspectLogbackEncoder">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %X{tl} [%thread] %-5level %logger{50} - %msg[%A][%R]%n</pattern>
</encoder>
<!--此日志appender是为开发使用,只配置最底级别,控制台输出的日志级别是大于或等于此级别的日志信息-->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>debug</level>
</filter>
</appender>
<logger name="org.springframework" level="${LOG_LEVEL:-info}" />
<logger name="org.hibernate" level="${LOG_LEVEL:-info}" />
<logger name="org.mybatis" level="${LOG_LEVEL:-info}"></logger>
<logger name="org.apache" level="${LOG_LEVEL:-info}"></logger>
<!-- 生产环境下,将此级别配置为适合的级别,以免日志文件太多或影响程序性能 -->
<root level="${LOG_LEVEL:-info}">
<appender-ref ref="FILEERROR" />
<appender-ref ref="FILEINFO" />
<appender-ref ref="FILEALL" />
<!-- 生产环境将请stdout,testfile去掉 -->
<appender-ref ref="STDOUT" />
</root>
</configuration>
二、TLOG扩展开发自己的starter
1.项目层级
2.扩展点:1.打印请求url 2.打印响应日志
a.打印请求日志中的url实现RequestBodyAdvice
/**
* 日志请求拦截
* @author liangxi.zeng
*/
@ControllerAdvice
@Component
public class LogRequestBodyAdvice implements RequestBodyAdvice {
private static final Logger log = LoggerFactory.getLogger(LogRequestBodyAdvice.class);
@Autowired
private LogProperties logProperties;
@Autowired
private LogContext logContext;
@Override
public boolean supports(MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
return logProperties.getResponse().isEnabled() && !StringUtils.isEmpty(TLogContext.getTraceId());
}
@Override
public HttpInputMessage beforeBodyRead(HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) throws IOException {
return httpInputMessage;
}
@Override
public Object afterBodyRead(Object body, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
try {
if(logProperties.getRequest().isEnabled()) {
String url = logContext.getUrlTl().get();
if (StringUtils.isEmpty(url)) {
url = LogUtil.getUrl(methodParameter);
}
if (!LogUtil.isIgnore(logProperties.getRequest().getIgnoreList(), url)) {
HttpHeaders headers = httpInputMessage.getHeaders();
if (headers != null) {
List<String> contentTypeList = headers.get(LogUtil.CONTENT_TYPE);
if (!CollectionUtils.isEmpty(contentTypeList) &&
!LogUtil.isUpload(contentTypeList.get(0))) {
log.info("requestBody参数:{}", JSON.toJSONString(body));
}
}
}
}
} catch (Exception e) {
log.error("日志请求打印异常,不影响业务,吞并异常",e);
}
return body;
}
@Override
public Object handleEmptyBody(Object body, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
return body;
}
}
b.打印响应日志实现
ResponseBodyAdvice
/**
* 脱敏日志响应拦截
* @author liangxi.zeng
*/
@ControllerAdvice
@Component
public class LogResponseBodyAdvice implements ResponseBodyAdvice<Object> {
private static final Logger log = LoggerFactory.getLogger(LogResponseBodyAdvice.class);
@Autowired
private LogProperties logProperties;
@Autowired
private LogContext logContext;
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return logProperties.getResponse().isEnabled() && !StringUtils.isEmpty(TLogContext.getTraceId());
}
/**
* @param body
* @param methodParameter
* @param selectedContentType
* @param selectedConverterType
* @param request
* @param response
* @return
*/
@Override
public Object beforeBodyWrite(Object body, MethodParameter methodParameter, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
try {
if(logProperties.getResponse().isEnabled()) {
String url = logContext.getUrlTl().get();
if (StringUtils.isEmpty(url)) {
url = LogUtil.getUrl(methodParameter);
}
if (!LogUtil.isIgnore(logProperties.getResponse().getIgnoreList(), url)) {
StringBuffer logBuffer = new StringBuffer("结束请求URL[{}]的调用,");
StopWatch stopWatch = logContext.getInvokeTimeTL().get();
if (Objects.nonNull(stopWatch)) {
stopWatch.stop();
logBuffer.append("耗时为:").append(stopWatch.getTime()).append("毫秒;");
}
if (LogUtil.isJson(selectedContentType.toString())) {
logBuffer.append("响应内容为:").append(JSON.toJSONString(body));
} else if (LogUtil.isUpload(selectedContentType.toString())) {
logBuffer.append("文件上传,无需打印上传");
} else {
logBuffer.append("响应内容为:").append(body);
}
log.info(logBuffer.toString(), url);
}
}
} catch (Exception e) {
log.error("日志响应打印异常,不影响业务,吞并异常",e);
}
return body;
}
}
c.打印内容
三、多线程与kafka消息队列,方法中traceId的传递
1.线程池
ExecutorService pool = Executors.newFixedThreadPool(5);
pool.submit(new TLogInheritableTask() {
@Override
public void runTask() {
log.info("我是异步线程日志");
}
});
2.kafka等消息队列
TLogMqWrapBean<BizBean> tLogMqWrap = new TLogMqWrapBean(bizBean);
mqClient.send(tLogMqWrap);
对于消费者端,你需要这么做:
//从mq里接受到tLogMqWrapBean
TLogMqConsumerProcessor.process(tLogMqWrapBean, new TLogMqRunner<BizBean>() {
@Override
public void mqConsume(BizBean o) {
//业务操作
}
});
总结
TLog是一个轻量级的分布式日志标记追踪神器,10分钟即可接入,自动对日志打标签完成微服务的链路追踪。支持log4j,log4j2,logback三大日志框架,在我使用的时候,有些不满足业务需要,所以我自己在读了源码后进行了个性化定制,现在已经在项目中使用