springboot单体项目链路日志跟踪及接口耗时

最近接触一个新的传统项目,在联调过程中,查看日志特别不方便,既无trackId,也无接口耗时,如果用户量上来后,完全无法过滤出当前请求的日志,所以写了该博客。话不多说,直接上代码

1、实体类user

package com.yk.domain;

import lombok.Data;

@Data
public class User {

    private Long id;

    private String username;

    private String sex;


}

2、接口统计返回实体封装类

/**
 * @author : yk
 * @date : 2024/03/11
 * @description : 封装的基础 result
 */
@Data
public class CommonResult<T> implements Serializable {


    private Integer code;

    private String message;

    private String traceId;

    private Long costTime;

    private T data;

    public CommonResult(Integer code) {
        this.code = code;
        if(code.equals(HttpStatus.OK.value())){
            this.message = HttpStatus.OK.name();
        }
    }

}

2、controller层

@Slf4j
@RestController
@RequestMapping("user")
public class UserController {


    @GetMapping("/query")
    public CommonResult<User> query(@RequestParam(name = "id", required = false, defaultValue = "1") Long id) {
        User user = new User();
        user.setId(id);
        user.setUsername("yk");
        user.setSex("男");
        CommonResult<User> apiResult = new CommonResult<>(HttpStatus.OK.value());
        apiResult.setData(user);
        return apiResult;
    }


    @PostMapping("/queryUser")
    public CommonResult<User> queryAlert(@RequestBody User user) {
        CommonResult<User> apiResult = new CommonResult<>(HttpStatus.OK.value());
        apiResult.setData(user);
        return apiResult;
    }

}

3、aspect包

public class TraceIdUtil {
    public static final String REGEX = "-";
    public static final String TRACE_ID = "trace_id";

    /**
     * 从header和参数中获取traceId
     * 从网关传入数据
     *
     * @param request  HttpServletRequest
     * @return traceId
     */
    public static String getTraceIdByRequest(HttpServletRequest request) {
        String traceId = request.getParameter(TRACE_ID);
        if (StringUtils.isBlank(traceId)) {
            traceId = request.getHeader(TRACE_ID);
        }
        return traceId;
    }


    /**
     * 传递traceId至MDC
     *
     * @param traceId  链路id
     */
    public static void setTraceId(String traceId) {
        if (StringUtils.isNotBlank(traceId)) {
            MDC.put(TRACE_ID, traceId);
        }
    }

    /**
     * 构建traceId
     *
     * @return
     */
    public static String getTraceId() {
        if (StringUtils.isBlank(MDC.get(TRACE_ID))) {
            String traceId = UUID.randomUUID().toString().replaceAll(REGEX, StringUtils.EMPTY);
            setTraceId(traceId);
            return traceId;
        }
        return MDC.get(TRACE_ID);

    }

    /**
     * 清理traceId
     */
    public static void removeTraceId() {
        MDC.remove(TRACE_ID);
    }
}
package com.yk.aspect;


import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.WriteListener;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletResponseWrapper;

import java.io.ByteArrayOutputStream;
import java.io.IOException;

public class MyServletResponseWrapper extends HttpServletResponseWrapper {
    private final ByteArrayOutputStream buffer;

    private final ServletOutputStream out;

    public MyServletResponseWrapper(HttpServletResponse httpServletResponse) {
        super(httpServletResponse);
        buffer = new ByteArrayOutputStream();
        out = new WrapperOutputStream(buffer);
    }

    @Override
    public ServletOutputStream getOutputStream() {
        return out;
    }

    @Override
    public void flushBuffer()
            throws IOException {
        if (out != null) {
            out.flush();
        }
    }

    public byte[] getContent() throws IOException {
        flushBuffer();
        return buffer.toByteArray();
    }

    static class WrapperOutputStream extends ServletOutputStream {
        private final ByteArrayOutputStream bos;

        public WrapperOutputStream(ByteArrayOutputStream bos) {
            this.bos = bos;
        }

        @Override
        public void write(int b) {
            bos.write(b);
        }

        @Override
        public boolean isReady() {
            return false;

        }

        @Override
        public void setWriteListener(WriteListener writeListener) {

        }
    }
}
@Getter
public class MyServletRequestWrapper extends HttpServletRequestWrapper {


    private final String body;

    public MyServletRequestWrapper(HttpServletRequest request) {
        super(request);
        if (HttpMethod.POST.name().equals(request.getMethod().toUpperCase(Locale.ROOT))) {
            StringBuilder stringBuilder = new StringBuilder();
            BufferedReader bufferedReader = null;
            InputStream inputStream = null;
            try {
                inputStream = request.getInputStream();
                if (inputStream != null) {
                    bufferedReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
                    char[] charBuffer = new char[128];
                    int bytesRead;
                    while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
                        stringBuilder.append(charBuffer, 0, bytesRead);
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if (inputStream != null) {
                    try {
                        inputStream.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                if (bufferedReader != null) {
                    try {
                        bufferedReader.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
            body = stringBuilder.toString();
        } else {
            body = request.getQueryString();
        }

    }

    @Override
    public ServletInputStream getInputStream() {
        final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes());
        return new ServletInputStream() {
            @Override
            public boolean isFinished() {
                return false;
            }

            @Override
            public boolean isReady() {
                return false;
            }

            @Override
            public void setReadListener(ReadListener readListener) {
            }

            @Override
            public int read() {
                return byteArrayInputStream.read();
            }
        };

    }

    @Override
    public BufferedReader getReader() {
        return new BufferedReader(new InputStreamReader(this.getInputStream()));
    }

}

import com.alibaba.fastjson.JSONObject;
import com.yk.domain.CommonResult;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.nio.charset.StandardCharsets;


@Slf4j
@Component
public class ContextFilter implements Filter {


    @Override
    public void init(FilterConfig filterConfig) {
        log.info("=====================ContextFilter init =====================>");
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
        String traceId = TraceIdUtil.getTraceId();
        //为了在post的时候能继续传递
        MyServletRequestWrapper request = new MyServletRequestWrapper((HttpServletRequest) servletRequest);
        MyServletResponseWrapper response = new MyServletResponseWrapper((HttpServletResponse) servletResponse);
        String requestURI = request.getRequestURI();

        //消耗时间
        long start = System.currentTimeMillis();
        log.info("requestUrl={},入参:{} ", requestURI, request.getBody());
        String resultParams = "";
        try {
            // 执行主体方法start==================================================================
            chain.doFilter(request, response);

            // 执行主体方法  end==================================================================
            long end = System.currentTimeMillis();

            byte[] content = response.getContent();
            if (content.length > 0) {
                resultParams = new String(content, StandardCharsets.UTF_8);
            }
            CommonResult<?> apiResult = JSONObject.parseObject(resultParams, CommonResult.class);
            apiResult.setCostTime(end - start);
            apiResult.setTraceId(traceId);
            //返回消息 否则前台收不到消息
            log.info("requestUrl={},出参:{}", requestURI, JSONObject.toJSONString(apiResult));
            servletResponse.getOutputStream().write(JSONObject.toJSONString(apiResult).getBytes());
        } catch (Exception e) {
            CommonResult<?> apiResult = new CommonResult<>(HttpStatus.BAD_REQUEST.value());
            apiResult.setCostTime(System.currentTimeMillis() - start);
            apiResult.setTraceId(traceId);
            if (e.getCause() instanceof IllegalArgumentException) {
                log.error("参数化异常 error ", e);
                apiResult.setMessage(e.getCause().getLocalizedMessage());
            } else {
                log.error("请求异常 error ", e);
                apiResult.setMessage("系统开小差,请稍后再试");
            }
            servletResponse.getOutputStream().write(JSONObject.toJSONString(apiResult).getBytes());
        } finally {
            TraceIdUtil.removeTraceId();
        }
    }

    @Override
    public void destroy() {
        TraceIdUtil.removeTraceId();
    }
}

4、application.properties

server.port=8080

#配置日志全链路跟踪 logId
logging.pattern.console=[%p]%d{yyyy-MM-dd HH:mm:ss.SSS}[%X{trace_id}][%t] %m [%c#%M:%L]%n

5、pom配置

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.yk</groupId>
    <artifactId>springboot-hellword</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springboot-hellword</name>
    <description>springboot-hellword</description>
    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.83</version>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.13.0</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

postmain请求调用看出参结果

后台日志

[INFO]2024-03-14 11:28:21.389[1e5822679b704344bb6401f64c088eed][] 请求入参id=1  [com.yk.aspect.ContextFilter#doFilter:33]
[INFO]2024-03-14 11:28:21.392[1e5822679b704344bb6401f64c088eed][] 返回值:{"costTime":3,"data":{"sex":"男","id":1,"username":"yk"},"traceId":"1e5822679b704344bb6401f64c088eed"} [com.yk.aspect.ContextFilter#doFilter:50]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值