文章目录
前言
Springcloud
微服务项目为了在最小的改动下,加入链路追踪TraceId
,决定把日志框架从logback
升级为log4j2
。
(以下是升级的过程和可能遇到的问题,仅以记录~)
提示:以下是本篇文章正文内容,下面案例可供参考
一、logback升级log4j2
1、添加相关log4j2的依赖
代码如下(示例):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
<version>2.5.15</version>
</dependency>
2、去除logback相关依赖
logback是web自带的,所以要去除springboot-web里的logging包:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
如果没有删除干净就会报下方的错:
小tip:如何查看依赖冲突
查看依赖冲突:直接在idea的plugin插件中下载
maven-helper
(安装完成pom.xml文件下方会出现Dependency Analyzer
,点击会出现当前pom文件里的所有引用依赖,冲突的话会标蓝/绿异常)
3、log4j2.xml相关配置
{[TraceId:%X{trace_id}]}
:打印TraceId的日志配置
代码如下(示例):
<?xml version="1.0" encoding="UTF-8"?>
<!--Configuration后面的status,这个用于设置log4j2自身内部的信息输出,可以不设置,当设置成trace时,你会看到log4j2内部各种详细输出-->
<!--monitorInterval:Log4j能够自动检测修改配置 文件和重新配置本身,设置间隔秒数-->
<!--log4j2.xml改成log4j2-spring.xml交由spring加载-->
<configuration monitorInterval="5">
<!--日志级别以及优先级排序: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL -->
<!--变量配置-->
<Properties>
<!-- 定义日志输出的根目录 -->
<Property name="LOG_PATH">./all-logs</Property>
<property name="LOG_FILE">product-server</property>
<!-- 格式化输出:%date表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度 %msg:日志消息,%n是换行符-->
<!-- %style,%highlight:彩色控制台输出 %logger{36} 表示 Logger 名字最长36个字符 -->
<property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %style{%-5level}{bright,magenta} [%thread] %style{[TraceId:%X{trace_id}]}{bright,Cyan} %logger{36} - %msg%n" />
</Properties>
<appenders>
<console name="Console" target="SYSTEM_OUT">
<!--输出日志的格式 disableAnsi="false"为了开启log4j2的彩色日志-->
<PatternLayout pattern="${LOG_PATTERN}" disableAnsi="false"/>
<!--控制台只输出level及其以上级别的信息(onMatch),其他的直接拒绝(onMismatch)-->
<ThresholdFilter level="debug" onMatch="ACCEPT" onMismatch="DENY"/>
</console>
<!-- 按照每天生成日志文件 -->
<RollingFile name="RollingFile" fileName="${LOG_PATH}/${LOG_FILE}.log"
filePattern="${LOG_PATH}/${LOG_FILE}.log.%d{yyyy-MM-dd}.%i.log.history">
<PatternLayout pattern="${LOG_PATTERN}"/>
<Policies>
<!--每个文件的大小限制-->
<SizeBasedTriggeringPolicy size="50MB"/>
<!--每天零点滚动一次-->
<TimeBasedTriggeringPolicy interval="1" modulate="true"/>
</Policies>
<!-- 每天的文件分片数量-按最大来算(按个人需求划分) -->
<DefaultRolloverStrategy max="102">
<Delete basePath="${LOG_PATH}/logs/" maxDepth="2">
<IfFileName glob="*.log.history">
<IfAny>
<!-- 日志文件保留30天,超过的日志会被清除 -->
<IfLastModified age="30d" />
<!-- 文件总大小,超过的日志会被清除 -->
<IfAccumulatedFileSize exceeds="5GB" />
</IfAny>
</IfFileName>
</Delete>
</DefaultRolloverStrategy>
</RollingFile>
</appenders>
<Loggers>
<Root level="INFO">
<AppenderRef ref="Console"/>
<AppenderRef ref="RollingFile"/>
</Root>
</Loggers>
</configuration>
二、插入链路TraceId相关配置
本篇是采用
slf4j.MDC
方式,因为是Springcloud项目所以在网关、log配置处进行了过滤器处理(相关配置是通过Nacos)
网关
代码如下(示例):调用方如果没有传入TraceId,则后端自己生成。
(以下的CommonConstant.LOG_TRACE_ID = "trace_id";
和日志里的值对应)
package com.example.gatewayserver.filter;
import com.example.common.core.constant.CommonConstant;
import com.example.common.log.properties.TraceProperties;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.Map;
import java.util.UUID;
/**
* 请求头部TraceId过滤器
*
*/
@Component
public class HeaderTraceIdFilter implements GlobalFilter , Ordered {
@Value("${mc.gateway.headerTraceIdFilter:0}")
private int filterOrder;
@Autowired
private TraceProperties traceProperties;
@Override
public Mono<Void> filter(final ServerWebExchange exchange, final GatewayFilterChain chain) {
if (traceProperties.getEnable()) {
final ServerHttpRequest request = exchange.getRequest();
final Map<String, String> headers = request.getHeaders().toSingleValueMap();
//增加请求id
if (StringUtils.isBlank(headers.get(CommonConstant.LOG_TRACE_ID))) {
final ServerHttpRequest requestLast = request.mutate()
.header(CommonConstant.LOG_TRACE_ID, UUID.randomUUID().toString())
.build();
exchange.mutate().request(requestLast).build();
}
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return filterOrder;
}
}
common-log
代码如下(示例):TraceFilter
package com.example.common.log.filter;
import com.example.common.log.properties.TraceProperties;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.skywalking.apm.toolkit.trace.TraceContext;
import org.slf4j.MDC;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.annotation.Resource;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
/**
* 日志链路追踪过滤器.
**/
@ConditionalOnClass(Filter.class)
@Slf4j
public class TraceFilter extends OncePerRequestFilter {
@Resource
private TraceProperties traceProperties;
@Override
protected void doFilterInternal(final HttpServletRequest request,
final HttpServletResponse response,
final FilterChain filterChain) {
try {
final List<String> requestHeaders = traceProperties.getRequestHeadParamNames();
requestHeaders.stream()
.filter(s -> StringUtils.isNotBlank(request.getHeader(s)))
.forEach(s -> MDC.put(s, request.getHeader(s)));
filterChain.doFilter(request, response);
} catch (Exception e){
log.error("traceFilter Error", e);
} finally {
MDC.clear();
}
}
}
TraceProperties
代码如下(示例):
package com.example.common.log.properties;
import lombok.Getter;
import lombok.Setter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
/**
* 日志链路追踪配置
**/
@Setter
@Getter
@Component
public class TraceProperties {
/** 是否开启日志链路追踪. */
private Boolean enable = Boolean.TRUE;
/** 需要进行放入日志中的参数值. */
@Value("${common.trace.requestHeadParamNames}")
private List<String> requestHeadParamNames;
/**
* 需要进行放入日志中的参数值.
* @return
*/
public List<String> getRequestHeadParamNames() {
// 接收到请求,记录请求内容
return Optional.ofNullable(this.requestHeadParamNames)
.filter(o -> !o.isEmpty())
.orElse(new ArrayList<>());
//不用nacos配置,直接塞值
// ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
// HttpServletRequest request = attributes.getRequest();
// Enumeration<String> headers = request.getHeaderNames();
// return Optional.ofNullable(Collections.list(headers))
}
}
组件和组件之间通信此项目用的是Feign
,所以要想TraceId链路完整、唯一,需要增加一个Feign请求拦截器
FeignLogInterceptor
代码如下(示例):
(以下的CommonConstant.LOG_TRACE_ID = "trace_id";
和日志里的值对应)
package com.example.common.log.interceptor;
import com.example.common.core.constant.CommonConstant;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.MDC;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
/**
* Feign请求拦截器
*/
@Slf4j
@Component
@Configuration
public class FeignLogInterceptor implements RequestInterceptor {
private static final String TRACE_ID = CommonConstant.LOG_TRACE_ID;
@Override
public void apply(final RequestTemplate template) {
final String traceId = MDC.get(TRACE_ID);
if(StringUtils.isNotEmpty(traceId)){
template.header(TRACE_ID, traceId);
}
}
}
三、log4j2异步子线程无法获取TraceId的问题
这也是本次改造升级最重要的原因:log4j2可以做到无侵入性的解决异步子线程的问题!!!
解决方法:
resources下新增
log4j2.component.properties
配置类,内容:isThreadContextMapInheritable=true
(这里有个糗事:当初配置的时候,其中单词大小写写错了,直接用的图片复制内容粘贴的,导致这个方法不生效。)
最终效果:
三、SkyWalking
全链路追踪TID
(拓展)
官网地址:
Apache SkyWalking
Skywalking搭建、优点及基础功能可参考其他博主:
1.SkyWalking相关依赖
代码如下(示例):
<!-- skywalking 日志输出查询-->
<dependency>
<groupId>org.apache.skywalking</groupId>
<artifactId>apm-toolkit-log4j-2.x</artifactId>
<version>8.5.0</version>
</dependency>
<!-- skywalking 链路TID生成-->
<dependency>
<groupId>org.apache.skywalking</groupId>
<artifactId>apm-toolkit-trace</artifactId>
<version>8.5.0</version>
</dependency>
TID生成:
TraceContext.traceId()
2.TraceFilter-过滤器MDC增加TID(日志用):
3.log4j2.xml中的相关配置(加入SkyWalking-TID)
<?xml version="1.0" encoding="UTF-8"?>
<!--Configuration后面的status,这个用于设置log4j2自身内部的信息输出,可以不设置,当设置成trace时,你会看到log4j2内部各种详细输出-->
<!--monitorInterval:Log4j能够自动检测修改配置 文件和重新配置本身,设置间隔秒数-->
<!--log4j2.xml改成log4j2-spring.xml交由spring加载-->
<configuration monitorInterval="5">
<!--日志级别以及优先级排序: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL -->
<!--变量配置-->
<Properties>
<!-- 定义日志输出的根目录 -->
<Property name="LOG_PATH">./all-logs</Property>
<property name="LOG_FILE">auth-server</property>
<!-- 格式化输出:%date表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度 %msg:日志消息,%n是换行符-->
<!-- %style,%highlight:彩色控制台输出 %logger{36} 表示 Logger 名字最长36个字符 -->
<property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %style{%-5level}{bright,magenta} [%thread] %style{[TraceId:%X{trace_id}]}{bright,Cyan} %style{[TID:%X{TID}]}{bright,Yellow} %logger{36} - %msg%n" />
</Properties>
<appenders>
<console name="Console" target="SYSTEM_OUT">
<!--输出日志的格式 disableAnsi="false"为了开启log4j2的彩色日志-->
<PatternLayout pattern="${LOG_PATTERN}" disableAnsi="false"/>
<!--控制台只输出level及其以上级别的信息(onMatch),其他的直接拒绝(onMismatch)-->
<ThresholdFilter level="debug" onMatch="ACCEPT" onMismatch="DENY"/>
</console>
<!-- skywalking grpc 日志收集 8.4.0版本开始支持 -->
<GRPCLogClientAppender name="grpc-log">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [traceId:%X{trace_id}] [%t] %-5level %logger{36} - %msg%n"/>
</GRPCLogClientAppender>
<!-- 按照每天生成日志文件 -->
<RollingFile name="RollingFile" fileName="${LOG_PATH}/${LOG_FILE}.log"
filePattern="${LOG_PATH}/${LOG_FILE}.log.%d{yyyy-MM-dd}.%i.log.history">
<PatternLayout pattern="${LOG_PATTERN}"/>
<Policies>
<!--每个文件的大小限制-->
<SizeBasedTriggeringPolicy size="50MB"/>
<!--每天零点滚动一次-->
<TimeBasedTriggeringPolicy interval="1" modulate="true"/>
</Policies>
<!-- 每天的文件分片数量-按最大来算 -->
<DefaultRolloverStrategy max="102">
<Delete basePath="${LOG_PATH}/logs/" maxDepth="2">
<IfFileName glob="*.log.history">
<IfAny>
<!-- 日志文件保留30天,超过的日志会被清除 -->
<IfLastModified age="30d" />
<!-- 文件总大小,超过的日志会被清除 -->
<IfAccumulatedFileSize exceeds="5GB" />
</IfAny>
</IfFileName>
</Delete>
</DefaultRolloverStrategy>
</RollingFile>
</appenders>
<Loggers>
<logger name="com.a.eye.skywalking.ui" level="debug" additivity="false">
<AppenderRef ref="Console"/>
<AppenderRef ref="grpc-log"/>
</logger>
<logger name="org.apache.skywalking.apm.dependencies" level="INFO"></logger>
<Root level="INFO">
<AppenderRef ref="Console"/>
<AppenderRef ref="grpc-log"/>
<AppenderRef ref="RollingFile"/>
</Root>
</Loggers>
</configuration>
此处注意:
只有加入grpc-log
配置,SkyWalking的可视化界面的日志、以及追踪里面的相关日志才能生成!
4.最终显示效果
日志效果:
SkyWalking效果:
追踪TID:
相关日志:
①:
②:
③:
日志:
TID都是:03c9039466ba4c5fb816ea0e6226f61c.153.17090021036670001实现全链路追踪。
说明
关于SkyWalking的可视化界面相关作用优点可另行查询,这里只做日志的补充。我们都知道一个好的日志打印,对真实生产中排除问题及分析的帮助有多大。
如果文中有疑问的欢迎讨论、指正(抱拳),互相学习,感谢关注。