文章目录

  • 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实现截图:

dubbo日志输出级别 dubbo统一日志_spring

操作参数定义

此模块使用注解的方式配置,也可以使用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 设计和实现