文章目录
- 1、目标
- 2、源代码
- 3、实现逻辑
- 操作参数定义
- 日志拦截器
- 本地服务日志拦截
- 调用微服务模块的日志保存接口继承
- 日志工具类
- 链路跟踪自实现
- 4、 logback.xml配置
- 5、测试类
- 测试http请求文件
- 测试接口层
- 外传
springboot 统一日志 链路跟踪 dubbo3链路 springboot log-starter 设计和实现- 统一日志和链路跟踪 管理、设计和实现
1、目标
1、实现微服务间直接使用log starter
2、springboot ds starter多数据源切换,使用dubbo微服务调用应用
3、统一日志处理
4、链路跟踪实现
5、dubbo3.1链路跟踪处理
2、源代码
springboot版本:2.3.1.RELEASE
dynamic-datasource版本:3.5.1
dubbo版本:3.1.0
nacos版本:2.1
实现源代码地址 分支:microservice-boot-1.0.3-logAndPlat
代码演示和测试:
microservice-boot-common模块
microservice-boot-plat模块
3、实现逻辑
最终log-stater实现截图:
操作参数定义
此模块使用注解的方式配置,也可以使用web interceptor拦截方式,但是post的数据处理会有问题,自行思考
package org.lwd.microservice.boot.middle.log.annotation;
import org.lwd.microservice.boot.middle.log.type.LogTypeEnum;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 操作参数定义
* @author weidong
* @version V1.0.0
* @since 2023/6/21
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OperationLog {
/**
* 项目模块
*/
String busModule() default "";
/**
* 操作项
*/
String title() default "";
/**
* 操作内容
*/
String context() default "";
/**
* 日志类型
*/
LogTypeEnum logType() default LogTypeEnum.DEFAULT;
/**
* 是否保存到db
*/
boolean saveDB() default true;
/**
* 是否异步记录日志
*/
boolean async() default true;
/**
* 是否记录方法入参
*/
boolean logParams() default true;
/**
* 是否记录方法出参
*/
boolean logResult() default true;
}
日志拦截器
package org.lwd.microservice.boot.middle.log.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.lwd.microservice.boot.middle.log.annotation.OperationLog;
import org.lwd.microservice.boot.middle.log.entity.LogConsoleTypeEnum;
import org.lwd.microservice.boot.middle.log.utils.LogUtils;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.stereotype.Component;
import java.util.Date;
/**
* 切面,日志拦截的逻辑
*
* @author weidong
* @version V1.0.0
* @since 2023/6/21
*/
@Slf4j
@Aspect
@Component
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class OperationLogAspect {
@Around("@annotation(operateLog)")
public Object around(ProceedingJoinPoint joinPoint, OperationLog operateLog) throws Throwable {
if (operateLog == null) {
operateLog = ((MethodSignature) joinPoint.getSignature()).getMethod().getAnnotation(OperationLog.class);
}
Date startTime = new Date();
Object result = null;
try {
// 执行目标方法
result = joinPoint.proceed();
LogUtils.doLog(LogConsoleTypeEnum.API, joinPoint, operateLog, startTime, result, null);
return result;
} catch (Throwable exception) {
LogUtils.doLog(LogConsoleTypeEnum.API, joinPoint, operateLog, startTime, null, exception);
throw exception;
}
}
/*
@Pointcut("@annotation(org.lwd.microservice.boot.middle.log.annotation.OperationLogInterceptor)")
public void logInterceptorPointcut() {
}
@Before("logInterceptorPointcut()")
public void beforeLog(JoinPoint joinPoint) {
// 在方法执行前执行的逻辑
log.info("Before logging...");
}
@After("logInterceptorPointcut()")
public void afterLog(JoinPoint joinPoint) {
// 在方法执行后执行的逻辑
log.info("After logging...");
}
@AfterReturning(pointcut = "logInterceptorPointcut()", returning = "result")
public void afterReturningLog(JoinPoint joinPoint, Object result) {
// 在方法返回结果后执行的逻辑
log.info("After returning logging...");
}
@AfterThrowing(pointcut = "logInterceptorPointcut()", throwing = "exception")
public void afterThrowingLog(JoinPoint joinPoint, Throwable exception) {
// 在方法抛出异常后执行的逻辑
log.info("After throwing logging...");
}*/
}
本地服务日志拦截
package org.lwd.microservice.boot.middle.log.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.lwd.microservice.boot.middle.log.entity.LogConsoleTypeEnum;
import org.lwd.microservice.boot.middle.log.utils.LogUtils;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.stereotype.Component;
import java.util.Date;
/**
* 系统服务日志
*
* @author lwd
* @since 2023/06/20
*/
@Slf4j
@Aspect
@Component
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class ServiceLogAspect {
/**
* 服务请求的拦截和处理
*
* @param joinPoint 目标地址
* @return Object
* @throws Throwable
*/
@Around("@within(org.springframework.stereotype.Service)")
public Object doServiceAround(ProceedingJoinPoint joinPoint) throws Throwable {
Date startTime = new Date();
Object result = null;
try {
// 执行目标方法
result = joinPoint.proceed();
LogUtils.doLog(LogConsoleTypeEnum.SERVICE, joinPoint, null, startTime, result, null);
return result;
} catch (Throwable exception) {
LogUtils.doLog(LogConsoleTypeEnum.SERVICE, joinPoint, null, startTime, null, exception);
throw exception;
}
}
}
调用微服务模块的日志保存接口继承
package org.lwd.microservice.boot.middle.log.service;
import org.lwd.microservice.boot.middle.log.entity.OperationLogDTO;
import java.util.concurrent.Future;
/**
* 日志保存拓展接口
*
* @author lwd
* @since 2023/06/20
*/
public interface ModuleLogService {
/**
* 记录操作日志(异步)
*
* @param operateLogDTO 操作日志请求
* @return true: 记录成功,false: 记录失败
*/
Future<Boolean> savePlatLogAsync(OperationLogDTO operateLogDTO);
/**
* 记录平台日志(同步)
*
* @param operateLogDTO 操作日志请求
* @return true: 记录成功,false: 记录失败
*/
Boolean savePlatLog(OperationLogDTO operateLogDTO);
}
日志工具类
LogUtils ,挺多,弄出来一部分,核心思想,就是获取参数,打印日志
public static void doLog(LogConsoleTypeEnum logConsoleTypeEnum, ProceedingJoinPoint joinPoint, OperationLog operateLog, Date startTime, Object result, Throwable exception) {
Object target = joinPoint.getTarget();
if (target instanceof ModuleLogService) {
return;
}
OperationLogDTO operationLogDTO = new OperationLogDTO();
// 填充TraceId
operationLogDTO.setTraceId(LogContextUtils.getTraceId());
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String strDate = sdf.format(DateUtil.date());
operationLogDTO.setStartTime(strDate);
// 填充用户信息
//setUserInfo(operationLogDTO);
// 填充模块信息
setModuleInfo(operationLogDTO, joinPoint, operateLog);
// 填充请求信息
setRequestInfo(operationLogDTO);
// 填充方法信息
setMethodInfo(operationLogDTO, joinPoint, operateLog, startTime, result, exception);
if (exception != null) {
//从控制台清理返回结果
operationLogDTO.setResultData(null);
printlnLog("Exception", operationLogDTO);
}
if (operateLog == null) {
//从控制台清理返回结果
operationLogDTO.setResultData(null);
printlnLog(logConsoleTypeEnum.getName(), operationLogDTO);
} else {
//可按不同级别保存日志内容
if (operateLog.saveDB() && logConsoleTypeEnum.getName().equals("API")) {
ModuleLogService moduleLogService = SpringContextUtil.getBeanSkipCheck(ModuleLogService.class);
if (moduleLogService != null) {
// 保存日志到db
if (operateLog.async()) {
moduleLogService.savePlatLogAsync(operationLogDTO);
} else {
moduleLogService.savePlatLog(operationLogDTO);
}
return;
}
}
//从控制台清理返回结果
operationLogDTO.setResultData(null);
printlnLog(logConsoleTypeEnum.getName(), operationLogDTO);
}
}
链路跟踪自实现
自己生成tradeId和自行管理及处理
package org.lwd.microservice.boot.middle.log.filter;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.lwd.microservice.boot.core.constant.CoreConstant;
import org.lwd.microservice.boot.core.constant.FilterOrderConstant;
import org.lwd.microservice.boot.middle.log.utils.LogContextUtils;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* traceId过滤器
*
* @author: lwd
* @since 2023/06/25
*/
@Slf4j
@Component
@Order(FilterOrderConstant.LOG_FILTER)
public class WebTraceIdFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// //这里set后必须在后续操作中删除,不然会导致内存泄漏
try {
String traceId = request.getHeader(CoreConstant.TRACE_ID);
if (StrUtil.isBlank(traceId)) {
traceId = LogContextUtils.getTraceId();
}
LogContextUtils.setTraceId(traceId);
String logContext = request.getHeader(CoreConstant.CONTEXT_ID);
if (StrUtil.isNotBlank(logContext)) {
LogContextUtils.setLogContent(logContext);
}
//继续执行
filterChain.doFilter(request, response);
} finally {
//清除MDC
LogContextUtils.removeMDC();
}
}
}
4、 logback.xml配置
<?xml version="1.0" encoding="UTF-8"?>
<!-- configuration file for LogBack (slf4J implementation)
See here for more details: http://gordondickens.com/wordpress/2013/03/27/sawing-through-the-java-loggers/ -->
<configuration scan="true" scanPeriod="30 seconds">
<contextName>microservice-boot-plat-log</contextName>
<property name="log.path" value="/Users/weidong/data/logs/microservice-boot-plat"/>
<property name="server.name" value="microservice-boot-plat"/>
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
<property name="log.pattern"
value="%-5level %d{yyyy-MM-dd HH:mm:ss.SSS} - %logger{0} traceId:%X{traceId} - %msg%n"/>
<property name="log.file" value="${log.path}/%d{yyyy-MM-dd}.log.gz"/>
<property name="log.error.file" value="${log.path}/error.log"/>
<contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator">
<resetJUL>true</resetJUL>
</contextListener>
<!-- To enable JMX Management -->
<jmxConfigurator/>
<appender name="log" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log.file}</fileNamePattern>
<maxHistory>15</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
</appender>
<!--控制台输出 -->
<appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${log.pattern}</pattern>
</encoder>
</appender>
<!-- kafka输出 -->
<!--<appender name="KAFKA" class="io.confluent.log4j5.KafkaAppender">
<destinationTopic>kafka_topic_name</destinationTopic>
<keySerializer>org.apache.kafka.common.serialization.StringSerializer</keySerializer>
<valueSerializer>org.apache.kafka.common.serialization.StringSerializer</valueSerializer>
<producerConfigurations>
<property>
<name>bootstrap.servers</name>
<value>kafka_broker_server:9092</value>
</property>
</producerConfigurations>
</appender>
<logger name="com.example" level="INFO" additivity="false">
<appender-ref ref="KAFKA"/>
</logger>-->
<logger name="com.lwd.microservice.boot.*" level="INFO"/>
<logger name="org.springframework" level="INFO"/>
<logger name="druid.sql.Statement" level="info"/>
<root level="info">
<!--<appender-ref ref="log"/>-->
<!--<appender-ref ref="KAFKA" />-->
<appender-ref ref="stdout"/>
</root>
</configuration>
5、测试类
测试http请求文件
GET http://localhost:8022/dubbo/get
Accept: application/json
###
GET http://localhost:8022/dubbo/testTimeout
Accept: application/json
###
GET http://localhost:8022/test/tget?name=llwd
###
POST http://localhost:8022/test/tpost
Content-Type: application/json
{
"id": "1",
"name": "lwd",
"sex": 1
}
###
GET http://localhost:8022/test/detail?pk=llwd
###
测试接口层
package org.lwd.microservice.boot.plat.controller;
import com.alibaba.fastjson2.JSON;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.config.annotation.DubboReference;
import org.lwd.microservice.boot.common.api.dto.VisitDubboDTO;
import org.lwd.microservice.boot.common.api.dubbo.VisitDubboService;
import org.lwd.microservice.boot.core.entity.BaseResult;
import org.lwd.microservice.boot.core.entity.WebResult;
import org.lwd.microservice.boot.middle.log.annotation.OperationLog;
import org.lwd.microservice.boot.middle.log.type.LogTypeEnum;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.constraints.NotEmpty;
import java.util.Map;
/**
* @author weidong
* @version V1.0.0
* @description
* @since 2023/4/7
*/
@Slf4j
@RestController
@RequestMapping("/test/")
@CrossOrigin
public class UserController {
@DubboReference(check = false, timeout = 6000)
VisitDubboService visitDubboService;
@GetMapping(value = "/tget")
public String testInterGet(String name) {
log.info("----testInterGet---:{}", name);
return JSON.toJSONString(name);
}
// @OperationLog(busModule = "plat",title = "测试日志",context = "",logType = LogTypeEnum.SELECT,async = false)
@PostMapping(value = "/tpost")
public String testInterPost(@RequestBody Map<String, Object> param) {
log.info("----testInterPost---:{}", JSON.toJSONString(param));
return JSON.toJSONString(param);
}
/**
* 根据主键查询系统访问记录详情
*
* @param pk 主键
* @return VO
*/
@GetMapping("detail")
@OperationLog(busModule = "plat", title = "测试dubbo日志", context = "测试dubbo tradeId", logType = LogTypeEnum.SELECT,async = false)
public WebResult<Boolean> detailVisitByPk(@Validated @NotEmpty String pk) {
VisitDubboDTO visitDubboDTO = new VisitDubboDTO();
visitDubboDTO.setServerIpAddress("2.2.2.2");
visitDubboService.saveVisitDubboService(visitDubboDTO);
WebResult<Boolean> webResult = WebResult.success();
webResult.setData(true);
return webResult;
}
}
下篇文章再说dubbo3链路和 springboot log-starter 设计和实现