问题:
定制化开发在日常开发过程中,定制化开发如果使用架构,定制化开发那么日志查询就是一个问题,比如A定制化开发定制化开发服务调用了B服务,B服务调用了C服务,这个时候C定制化开发服务报错了,定制化开发导致整个请求异常失败,定制化开发如果想排查这个问题,定制化开发没有日志整合的话,定制化开发我们排查问题原因就变的很麻烦
解决方案:
定制化开发在网关服务接收到请求定制化开发的时候生成一个traceId,然后将traceId在每个服务间传递,同时日志打印的时候将traceId一起打印出来,这样在使用ELK去查询日志的时候,只需要搜索一个traceId,就可以查询的到整个请求的全链路日志信息了。
准备:
1:网关服务添加自定义拦截器
import cn.hutool.core.lang.UUID;import cn.hutool.core.util.ObjectUtil;import cn.hutool.core.util.StrUtil;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.JSONObject;import com.auth0.jwt.JWT;import lombok.extern.slf4j.Slf4j;import org.slf4j.MDC;import org.springframework.cloud.gateway.filter.GatewayFilterChain;import org.springframework.cloud.gateway.filter.GlobalFilter;import org.springframework.core.Ordered;import org.springframework.http.HttpHeaders;import org.springframework.http.HttpMethod;import org.springframework.http.HttpStatus;import org.springframework.http.server.reactive.ServerHttpRequest;import org.springframework.http.server.reactive.ServerHttpResponse;import org.springframework.stereotype.Component;import org.springframework.util.AntPathMatcher;import org.springframework.web.server.ServerWebExchange;import reactor.core.publisher.Flux;import reactor.core.publisher.Mono;import java.nio.charset.StandardCharsets;import java.util.ArrayList;import java.util.Base64;import java.util.HashMap;import java.util.List;@Slf4j@Componentpublic class AuthorizeFilter implements GlobalFilter, Ordered { private static final String TRACE_ID = "traceId"; private static final AntPathMatcher matcher = new AntPathMatcher(); @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); ServerHttpResponse response = exchange.getResponse(); //对请求对象request进行增强 ServerHttpRequest req = request.mutate().headers(httpHeaders -> { //httpHeaders 封装了所有的请求头 String traceId = UUID.randomUUID().toString(true); MDC.put(TRACE_ID, traceId); httpHeaders.set(TRACE_ID, traceId); }).build(); //设置增强的request到exchange对象中 exchange.mutate().request(req); String url = request.getURI().getPath(); log.info("接收到请求:{}", url); // 跨域放行 if (request.getMethod() == HttpMethod.OPTIONS) { response.setStatusCode(HttpStatus.OK); return Mono.empty(); } // 不需要拦截的接口直接放行 if (needLogin(request.getPath().toString())) { log.info("不拦截放行"); return chain.filter(exchange); } // 授权验证 if (!this.auth(exchange)) { return this.responseBody(exchange, 406, "请先登录"); } log.info("认证成功,放行"); return chain.filter(exchange); } /** * 是否需要登录 * * @param uri 请求URI * @return boolean */ public static boolean needLogin(String uri) { // test List<String> uriList = new ArrayList<>(); uriList.add("/user/login"); uriList.add("/demo/**"); uriList.add("/**"); for (String pattern : uriList) { if (matcher.match(pattern, uri)) { // 不需要拦截 return true; } } return false; } /** * 认证拦截 */ private boolean auth(ServerWebExchange exchange) { String token = this.getToken(exchange.getRequest()); log.info("token:{}", token); if (StrUtil.isBlank(token)) { return false; } JSONObject userInfo = getUserInfo(token); return !ObjectUtil.isNull(userInfo); } private JSONObject getUserInfo(String token) { JSONObject jsonObject; String tokenNew = token.substring(7); String ss = JWT.decode(tokenNew).getPayload(); Base64.Decoder decoder = Base64.getDecoder(); jsonObject = JSON.parseObject(new String(decoder.decode(ss))); return jsonObject; } /** * 获取token */ public String getToken(ServerHttpRequest request) { String token = request.getHeaders().getFirst("Authorization"); if (StrUtil.isBlank(token)) { return request.getQueryParams().getFirst("Authorization"); } return token; } /** * 设置响应体 **/ public Mono<Void> responseBody(ServerWebExchange exchange, Integer code, String msg) { HashMap<Object, Object> hashMap = new HashMap<>(); hashMap.put("code", code); hashMap.put("msg", msg); String message = JSON.toJSONString(hashMap); byte[] bytes = message.getBytes(StandardCharsets.UTF_8); return this.responseHeader(exchange).getResponse() .writeWith(Flux.just(exchange.getResponse().bufferFactory().wrap(bytes))); } /** * 设置响应体的请求头 */ public ServerWebExchange responseHeader(ServerWebExchange exchange) { ServerHttpResponse response = exchange.getResponse(); response.getHeaders().add(HttpHeaders.CONTENT_TYPE, "application/json"); return exchange.mutate().response(response).build(); } @Override public int getOrder() { return 0; }}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 151
- 152
- 153
上述代码中可以只看下图部分,其他拦截授权等demo可以无视
通过上面我们自定义拦截器,对request进行了增强,在header中添加了一个traceId,值呢就是用UUID生成出来的随机字符串
同时使用MDC将traceId进行了put操作
下面我们会用MDC进行日志打印相关操作
2:网关配置文件
logging: file: path: /opt/log/gateway config: classpath:logbak-conf.xml
- 1
- 2
- 3
- 4
3:网关日志配置文件logbak
<?xml version="1.0" encoding="UTF-8" ?><configuration debug="false"> <!--定义日志文件的存储地址 勿在 LogBack 的配置中使用相对路径--> <property name="LOG_HOME" value="${LOG_PATH:-.}"/> <!-- 控制台输出设置 --> <!-- 彩色日志格式,magenta:洋红,boldMagenta:粗红,yan:青色,·⊱══> --> <property name="CONSOLE_LOG_PATTERN" value="%boldMagenta([%d{yyyy-MM-dd HH:mm:ss.SSS}]) %cyan([%X{traceId}]) %boldMagenta(%-5level) %blue(%logger{15}) %magenta(==>) %cyan(%msg%n)"/> <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>${CONSOLE_LOG_PATTERN}</pattern> <charset>utf8</charset> </encoder> </appender> <!-- 按天输出日志设置 --> <appender name="DAY_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <!-- 日志文件输出的文件名 --> <FileNamePattern>${LOG_HOME}/%d{yyyy-MM-dd}_ms_gateway.%i.log </FileNamePattern> <!-- 日志文件保留天数 --> <MaxHistory>7</MaxHistory> <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"> <maxFileSize>50MB</maxFileSize> </timeBasedFileNamingAndTriggeringPolicy> </rollingPolicy> <filter class="ch.qos.logback.classic.filter.LevelFilter"> <level>INFO</level> <!-- 设置拦截的对象为INFO级别日志 --> <onMatch>ACCEPT</onMatch> <!-- 当遇到了INFO级别时,启用改段配置 --> <onMismatch>DENY</onMismatch> <!-- 没有遇到INFO级别日志时,屏蔽改段配置 --> </filter> <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <!-- 格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符 --> <pattern>%cyan([%X{traceId}]) %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern> </encoder> </appender> <!-- 按天输出ERROR级别日志设置 --> <appender name="DAY_ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <!-- 日志文件输出的文件名 --> <FileNamePattern>${LOG_HOME}/%d{yyyy-MM-dd}_ms_gateway_error.%i.log </FileNamePattern> <!-- 日志文件保留天数 --> <MaxHistory>7</MaxHistory> <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"> <maxFileSize>50MB</maxFileSize> </timeBasedFileNamingAndTriggeringPolicy> </rollingPolicy> <filter class="ch.qos.logback.classic.filter.LevelFilter"> <level>ERROR</level> <!-- 设置拦截的对象为ERROR级别日志 --> <onMatch>ACCEPT</onMatch> <!-- 当遇到了ERROR级别时,启用改段配置 --> <onMismatch>DENY</onMismatch> <!-- 没有遇到ERROR级别日志时,屏蔽改段配置 --> </filter> <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <!-- 格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符 --> <pattern>%cyan([%X{traceId}]) %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern> </encoder> </appender> <!-- 日志输出级别,OFF level > FATAL > ERROR > WARN > INFO > DEBUG > ALL level --> <logger name="com.sand" level="INFO"/> <logger name="com.apache.ibatis" level="INFO"/> <logger name="java.sql.Statement" level="INFO"/> <logger name="java.sql.Connection" level="INFO"/> <logger name="java.sql.PreparedStatement" level="INFO"/> <logger name="org.springframework" level="WARN"/> <logger name="com.baomidou.mybatisplus" level="WARN"/> <!-- 开发环境:打印控制台和输出到文件 --> <springProfile name="dev"> <root level="INFO"> <appender-ref ref="CONSOLE"/> <appender-ref ref="DAY_FILE"/> <appender-ref ref="DAY_ERROR_FILE"/> </root> </springProfile> <!-- 生产环境:打印控制台和输出到文件 --> <springProfile name="pro"> <root level="INFO"> <appender-ref ref="CONSOLE"/> <appender-ref ref="DAY_FILE"/> <appender-ref ref="DAY_ERROR_FILE"/> </root> </springProfile></configuration>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
上述配置文件中的 [%X{traceId}] 可以将我们通过MDC.put操作设置的值带入进来,这样就可以将traceId打印到日志里了。
4:接口入口服务aop切面接收traceId
package com.weibo.platform.aop;import com.alibaba.fastjson.JSON;import com.fasterxml.jackson.databind.ObjectMapper;import com.weibo.common.enums.BizExceptionEnum;import com.weibo.common.exception.BizException;import com.weibo.common.resp.ApiResponse;import org.apache.dubbo.rpc.RpcContext;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.Around;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.reflect.MethodSignature;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.slf4j.MDC;import org.springframework.stereotype.Component;import org.springframework.web.context.request.RequestContextHolder;import org.springframework.web.context.request.ServletRequestAttributes;import javax.servlet.http.HttpServletRequest;import java.lang.reflect.Method;/** * 日志aop * 记录对外,依赖服务的请求数据 */@Component@Aspectpublic class LogAspect { private static final Logger log = LoggerFactory.getLogger(LogAspect.class); private static final String TRACE_ID = "traceId"; ObjectMapper mapper = new ObjectMapper(); /** * 外部接口调用的日志监控 * * @param joinPoint 连接点 * @return {@link Object} */ @Around(value = "execution(* com.weibo.platform.controller..*.* (..))") public Object doRequestAround(ProceedingJoinPoint joinPoint) throws Throwable { try { // 日志链路 ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); assert attributes != null; HttpServletRequest request = attributes.getRequest(); String traceId = request.getHeader(TRACE_ID); MDC.put(TRACE_ID, traceId); RpcContext.getContext().setAttachment(TRACE_ID, traceId); // 参数打印 Object result; MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); Method method = methodSignature.getMethod(); String name = method.getName(); Object[] args = joinPoint.getArgs(); Object object = joinPoint.getTarget(); log.info("class :{}, method :{}, param :{}", object.getClass().getName(), name, mapper.writeValueAsString(args)); result = joinPoint.proceed(); log.info("class :{}, method :{}, result :{}", object.getClass().getName(), name, genResultString(result)); return result; } catch (Exception e) { log.error("Error :", e); if (!(e instanceof BizException)) { return new ApiResponse<>(BizExceptionEnum.SYS_ERROR); } else { return new ApiResponse<>(((BizException) e).getCode(), ((BizException) e).getMsg()); } } } /** * 创结果字符串 * * @param result 结果 * @return {@link String} */ private String genResultString(Object result) { //如果结果为空,只直接返回 if (result == null) { return null; } String val = JSON.toJSONString(result); if (val.length() > 1024) { return val.substring(0, 1023); } return val; }}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
上述代码中比较重要的部分是开头部分,入下图
- 从请求头中获取网关服务添加的traceId
- 将traceId设置到MDC中(用于该服务日志打印traceId,也需要logbak.xml)
- 将traceId设置到dubbo的RpcContext中(用于将traceId传递到下个服务,后续微服务间联动都将traceId通过RpcContext传递)
5:统一全局返回值,返回值中加traceId字段
所有的接口均使用全局响应实体返回,返回的时候通过MDC自动将traceId设置到返回值中
package com.weibo.common.resp;import com.weibo.common.enums.BizExceptionEnum;import lombok.Data;import org.slf4j.MDC;import java.io.Serializable;@Datapublic class ApiResponse<T> implements Serializable { private static final long serialVersionUID = -6025817568658364567L; private static final String TRACE_ID = "traceId"; private Integer code; private String msg; private T data; private String traceId; public ApiResponse(Integer code, String msg) { this.traceId = MDC.get(TRACE_ID); this.code = code; this.msg = msg; this.data = null; } public ApiResponse(Integer code, String msg, T data) { this.traceId = MDC.get(TRACE_ID); this.code = code; this.msg = msg; this.data = data; } public ApiResponse(T data) { this.traceId = MDC.get(TRACE_ID); this.code = BizExceptionEnum.SUCCESS.getCode(); this.msg = BizExceptionEnum.SUCCESS.getMsg(); this.data = data; } public ApiResponse(BizExceptionEnum enums) { this.traceId = MDC.get(TRACE_ID); this.code = enums.getCode(); this.msg = enums.getMsg(); this.data = null; }}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
6:通过aop全局捕获异常封装全局响应
aop执行方法时一旦发生异常,将捕获异常,然后封装全局响应对象返回给前端,不将异常外漏,如果是自定义业务异常,同样的道理将异常信息的code和msg返回给前端。
7:用户服务通过RpcContext获取traceId
package com.weibo.user.filter;import org.apache.dubbo.rpc.RpcContext;import org.slf4j.MDC;import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;public class RpcFilter implements HandlerInterceptor { private static final String TRACE_ID = "traceId"; /** * 目标方法执行前 * 该方法在控制器处理请求方法前执行,其返回值表示是否中断后续操作 * 返回 true 表示继续向下执行,返回 false 表示中断后续操作 * * @param request 请求 * @param response 响应 * @param handler 处理程序 * @return boolean */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String traceId = RpcContext.getContext().getAttachment(TRACE_ID); MDC.put(TRACE_ID, traceId); return true; }}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
效果:
网关服务日志:
traceId:fd6f8174714745f4a1ac7dada1b3949b
入口服务日志:
traceId:fd6f8174714745f4a1ac7dada1b3949b
返回值:
traceId:fd6f8174714745f4a1ac7dada1b3949b
这样就可以在请求的返回值中获取traceId,一旦有异常或者错误,可以通过返回的这个traceId进行日志搜索、问题排查。
备注:
- 除了网关服务外,其他服务均需要添加logbak.xml文件实现打印traceId。
- 各rpc微服务间通过dubbo的RpcContext来进行传递。
综上就可以实现通过一个traceId查询到全链路的日志了。