记录接口api调用日志

记录api调用日志

Created: December 11, 2024 2:57 PM

1.定义实体类

基类:

@Data
@SuperBuilder
@AllArgsConstructor
@NoArgsConstructor
public class BaseEntity implements Serializable {    /**
     * 主键ID
     */
    @DbComment("主键ID")
    @Id
    @Column(length = 50)
    @NotBlank(message = "id不能为空")
    private String id;

    /**
     * 记录创建时间
     */
    @DbComment("记录创建时间")
    @NullDefault(NullDefault.DEFAULT_DATE)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @ApiModelProperty(hidden = true)
    private Timestamp itime;

    /**
     * 记录最新修改时间
     */
    @DbComment("记录最新修改时间")
    @NullDefault(NullDefault.DEFAULT_DATE)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @ApiModelProperty(hidden = true)
    private Timestamp utime;

    /**
     * 创建人
     */
    @DbComment("创建人")
    @Column(length = 50)
    @ApiModelProperty(hidden = true)
    private String iuserid;

    /**
     * 创建人名称
     */
    @DbComment("创建人名称")
    @Column(length = 100)
    @ApiModelProperty(hidden = true)
    private String iusername;

    /**
     * 修改人id
     */
    @DbComment("修改人id")
    @Column(length = 50)
    @ApiModelProperty(hidden = true)
    private String uuserid;

    /**
     * 修改人名称
     */
    @DbComment("修改人名称")
    @Column(length = 100)
    @ApiModelProperty(hidden = true)
    private String uusername;
}

import javax.persistence.Column;
import javax.persistence.Lob;
import javax.persistence.Table;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;

/**
 * @author: Mr.Guo
 * @since: 2025/9/9 17:41
 * <p>@description:<br>
 *
 * <P>@point:<br>
 * <P>@update:<br>
 */
@Data
@SuperBuilder
@AllArgsConstructor
@NoArgsConstructor
@Table(name = "zx_api_invoke_log")
public class ApiRecordEntity extends BaseEntity {
    /**
     * 企业ID
     */
    @Column(name = "company_id", length = 50)
    @DbComment("企业/组织唯一标识")
    private String qyid;
    /**
     * API路径
     */
    @Column(name = "api_path", length = 500, nullable = false)
    @DbComment("API调用路径")
    private String apiPath;

    /**
     * 请求方法
     */
    @Column(name = "http_method", length = 10, nullable = false)
    @DbComment("HTTP请求方法(GET/POST等)")
    private String httpMethod;

    /**
     * 请求头(JSON格式)
     */
    @Lob
    @DbComment("HTTP请求头(JSON格式)")
    private String requestHeaders;

    /**
     * 请求参数(JSON格式)
     */
    @Lob
    @DbComment("请求参数(JSON格式)")
    private String requestParams;

    /**
     * 请求体内容
     */
    @Lob
    @DbComment("请求体内容")
    private String requestBody;

    /**
     * 客户端IP
     */
    @Column(name = "client_ip", length = 50)
    @DbComment("客户端IP地址")
    private String clientIp;

    // 响应信息
    /**
     * 响应状态码
     */
    @Column(name = "response_status", nullable = false)
    @DbComment("HTTP响应状态码")
    private Integer responseStatus;

    /**
     * 响应头(JSON格式)
     */
    @Lob
    @DbComment("HTTP响应头(JSON格式)")
    private String responseHeaders;

    /**
     * 响应体内容
     */
    @Lob
    @DbComment("响应体内容")
    private String responseBody;

    /**
     * 调用耗时(毫秒)
     */
    @Column(name = "cost_time")
    @DbComment("API调用耗时(毫秒)")
    private Long costTime;

    /**
     * 业务状态码
     */
    @Column(name = "biz_code", length = 50)
    @DbComment("业务状态码")
    private String bizCode;

    /**
     * 调用结果
     */
    @Column(name = "success", nullable = false)
    @DbComment("是否调用成功")
    private Boolean success;

    /**
     * 错误信息
     */
    @Column(name = "error_msg", length = 1000)
    @DbComment("错误信息")
    private String errorMsg;

    /**
     * 追踪ID
     */
    @Column(name = "trace_id", length = 64)
    @DbComment("分布式追踪ID")
    private String traceId;

    /**
     * 用户代理
     */
    @Column(name = "user_agent", length = 500)
    @DbComment("客户端User-Agent")
    private String userAgent;
}

2.重复读写流包装类实现:


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

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

/**
 * @author: Mr.Guo
 * @since: 2025/9/10 16:19
 * <p>@description:<br>
 *
 * <P>@point:<br>
 * <P>@update:<br>
 */
public class FixedResponseWrapper extends HttpServletResponseWrapper {
    private final ByteArrayOutputStream contentBuffer = new ByteArrayOutputStream();
    private ServletOutputStream outputStream;
    private PrintWriter writer;

    public FixedResponseWrapper(HttpServletResponse response) {
        super(response);
    }

    @Override
    public ServletOutputStream getOutputStream() throws IOException {
        if (outputStream == null) {
            outputStream = new TeeOutputStream(super.getOutputStream(), contentBuffer);
        }
        return outputStream;
    }

    @Override
    public PrintWriter getWriter() throws IOException {
        if (writer == null) {
            writer = new PrintWriter(new OutputStreamWriter(getOutputStream(), StandardCharsets.UTF_8));

        }
        return writer;
    }

    public byte[] getContentAsBytes() {
        if (writer != null) {
            writer.flush();
        }
        return contentBuffer.toByteArray();
    }

    public void copyToResponse() throws IOException {
        // 不进行任何缓冲操作,直接写入原始响应
        if (!isCommitted() && contentBuffer.size() > 0) {
            getResponse().getOutputStream().write(contentBuffer.toByteArray());
        }
    }

    private static class TeeOutputStream extends ServletOutputStream {
        private final OutputStream branch;
        private final OutputStream original;

        public TeeOutputStream(OutputStream original, OutputStream branch) {
            this.original = original;
            this.branch = branch;
        }

        @Override public void write(int b) throws IOException {
            original.write(b);
            branch.write(b);
        }

        @Override public void write(byte[] b) throws IOException {
            original.write(b);
            branch.write(b);
        }

        @Override public void write(byte[] b, int off, int len) throws IOException {
            original.write(b, off, len);
            branch.write(b, off, len);
        }

        @Override public void flush() throws IOException {
            original.flush();
            branch.flush();
        }

        @Override public void close() throws IOException {
            original.close();
            branch.close();
        }

        @Override public boolean isReady() { return true; }
        @Override public void setWriteListener(WriteListener writeListener) {}
    }
}

3.记录日志业务类实现:

package com.zx.base.common.filter.log;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zx.base.common.domain.ApiRecordEntity;
import com.zx.base.common.mapper.ApiRecordMapper;
import com.zx.framework.security.pojo.UserInfo;
import com.zx.framework.util.tools.CodeUtil;

import org.slf4j.MDC;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.web.util.ContentCachingRequestWrapper;

import java.nio.charset.StandardCharsets;
import java.sql.Timestamp;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import cn.hutool.json.JSONUtil;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;

/**
 * @author: Mr.Guo
 * @since: 2025/9/9 18:20
 * <p>@description:<br>
 *
 * <P>@point:<br>
 * <P>@update:<br>
 */
@Slf4j
@Component
public class ApiRecordService {
    @Resource
    private ApiRecordMapper apiRecordMapper;

    /**
     * 添加API调用记录
     *
     * @param userInfo 用户信息
     * @param request  HTTP请求对象
     * @param response HTTP响应对象
     */
    @SneakyThrows void addApiRecord(UserInfo userInfo, HttpServletRequest request, HttpServletResponse response,
                                    FixedResponseWrapper wrapper) {
        log.debug("addApiRecord() called with: userInfo = [" + userInfo + "], request = [" + request + "], response = [" + response + "]");
        ApiRecordEntity apiRecordEntity = ApiRecordEntity.builder()
                .id(CodeUtil.getGUID())
                .iuserid(userInfo.getUserid())
                .iusername(userInfo.getNickname())
                .qyid(userInfo.getEnterid())
                .itime(new Timestamp(System.currentTimeMillis()))
                .build();
        long startTime = (Long) request.getAttribute("apiStartTime");
        try {
            // 获取包装后的响应
            if (wrapper != null) {
                // 获取响应内容
                byte[] content = wrapper.getContentAsBytes();
                String contentStr = new String(content, StandardCharsets.UTF_8);
                apiRecordEntity.setResponseBody(contentStr);
                // 解析业务状态码
                parseBusinessStatus(apiRecordEntity, contentStr);
                try {
                    // 请求信息
                    apiRecordEntity.setApiPath(request.getRequestURI());
                    apiRecordEntity.setHttpMethod(request.getMethod());
                    apiRecordEntity.setRequestHeaders(JSONUtil.toJsonStr(getRequestHeaders(request)));
                    apiRecordEntity.setRequestParams(JSONUtil.toJsonStr(request.getParameterMap()));
                    apiRecordEntity.setRequestBody(getRequestBody(request));
                    apiRecordEntity.setClientIp(getClientIp(request));

                    // 用户代理信息
                    apiRecordEntity.setUserAgent(request.getHeader("User-Agent"));
                    // 响应信息
                    apiRecordEntity.setResponseStatus(response.getStatus());
                    apiRecordEntity.setResponseHeaders(JSONUtil.toJsonStr(getResponseHeaders(response)));

                    // 设置调用结果
                    boolean isSuccess = response.getStatus() < 400 &&
                            (apiRecordEntity.getBizCode() == null ||
                                    "SUCCESS".equals(apiRecordEntity.getBizCode()) ||
                                    "200".equals(apiRecordEntity.getBizCode()));
                    apiRecordEntity.setSuccess(isSuccess);

                    // 追踪ID(可以从MDC或请求头获取)
                    String traceId = MDC.get("traceId");
                    if (traceId == null) {
                        traceId = request.getHeader("X-Trace-Id");
                    }
                    apiRecordEntity.setTraceId(traceId != null ? traceId : CodeUtil.getGUID());

                    request.setAttribute("logId",apiRecordEntity.getId());

                } catch (Exception e) {
                    log.error("记录API调用日志异常", e);
                    apiRecordEntity.setSuccess(false);
                    apiRecordEntity.setErrorMsg("记录日志时发生异常");
                } finally {
                    // 计算耗时
                    apiRecordEntity.setCostTime(System.currentTimeMillis() - startTime);
                    // 异步保存日志(推荐)
                    asyncSaveApiRecord(apiRecordEntity);
                }
            }
        } catch (Exception e) {
            log.error("API记录失败", e);
        }
    }

    // 获取Spring自动配置的ObjectMapper
    @Autowired
    private ObjectMapper objectMapper;

    private void parseBusinessStatus(ApiRecordEntity record, String responseBody) {
        try {
            JsonNode json = objectMapper.readTree(responseBody);
            if (json != null) {
                record.setBizCode(json.path("code").asText("UNKNOWN"));
                if (json.has("msg")) {
                    record.setErrorMsg(json.get("msg").asText());
                }
                if (json.has("message")) {
                    record.setErrorMsg(json.get("message").asText());
                }
            }
        } catch (Exception e) {
            log.debug("解析响应JSON失败", e);
        }
    }

    // 辅助方法:获取请求头Map
    private Map<String, String> getRequestHeaders(HttpServletRequest request) {
        Map<String, String> headers = new HashMap<>();
        Enumeration<String> headerNames = request.getHeaderNames();
        while (headerNames.hasMoreElements()) {
            String name = headerNames.nextElement();
            headers.put(name, request.getHeader(name));
        }
        return headers;
    }

    // 辅助方法:获取响应头Map
    private Map<String, String> getResponseHeaders(HttpServletResponse response) {
        Map<String, String> headers = new HashMap<>();
        Collection<String> headerNames = response.getHeaderNames();
        for (String name : headerNames) {
            headers.put(name, response.getHeader(name));
        }
        return headers;
    }

    // 辅助方法:获取客户端真实IP
    private String getClientIp(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }

    // 辅助方法:获取请求体内容
    private String getRequestBody(HttpServletRequest request) {
        try {
            if (request instanceof ContentCachingRequestWrapper) {
                ContentCachingRequestWrapper wrapper = (ContentCachingRequestWrapper) request;
                return new String(wrapper.getContentAsByteArray(), request.getCharacterEncoding());
            }
            return "NOT_CACHED_REQUEST_BODY";
        } catch (Exception e) {
            log.warn("获取请求体失败", e);
            return "ERROR_READING_BODY";
        }
    }

    // 异步保存API记录
    @Async
    public void asyncSaveApiRecord(ApiRecordEntity record) {
        try {
            apiRecordMapper.insert(record);
        } catch (Exception e) {
            log.error("保存API调用记录失败", e);
        }
    }
}

4.过滤器里拦截

package com.zx.base.common.filter.log;

import com.zx.framework.security.pojo.UserInfo;

import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.io.IOException;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
@RequiredArgsConstructor
@Slf4j
public class ContentCachingFilter implements Filter {
    private final ApiRecordService apiRecordService;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        // 初始化逻辑
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        String path = httpRequest.getRequestURI();
        // 关键点1:必须每次创建新的包装器实例
        // 包装响应(关键改进:使用新的FixedResponseWrapper)
        FixedResponseWrapper wrapper = new FixedResponseWrapper((HttpServletResponse) response);
        response.reset();
        response.resetBuffer();
        try {
            chain.doFilter(request, wrapper);
            if (isSensitivePath(path)) {
                UserInfo userInfo = (UserInfo) request.getAttribute("userInfo");
                apiRecordService.addApiRecord(userInfo,httpRequest,httpServletResponse,wrapper);
            }

        } catch (Exception e) {
            log.error("doFilter: ",e );
            // 异常处理
        }finally {
            // 关键改进:确保响应被写回
            if (!wrapper.isCommitted() && wrapper.getContentAsBytes().length > 0) {
                try {
                    wrapper.copyToResponse();
                } catch (IllegalStateException e) {
                    log.error("响应已提交,无法写入:", e);
                }
            }
        }
    }
    @Override
    public void destroy() {
        // 清理资源
    }

    /**
     * 是否是要校验的地址.
     *
     * @param path
     * @return
     */
    private boolean isSensitivePath(String path) {
        return path.contains("sensitive")
                || path.contains("ocr")
                || path.contains("fadada")
                || path.contains("baidu-map")
                || path.contains("wps")
                || path.contains("amap");
    }

}
在前端开发中,记录接口调用日志是一种常见的调试和监控手段,有助于排查接口问题、分析用户行为以及提升系统稳定性。以下是一些常用的方法和实践: ### 1. 使用 `console.log` 或 `console.info` 进行简单记录 这是最基础的方式,适用于开发阶段。可以通过在请求发送前和响应返回后记录相关信息,例如请求参数、响应结果或错误信息。 ```javascript fetch('https://api.example.com/data', { method: 'GET', }) .then(response => { console.info('接口响应:', response); return response.json(); }) .then(data => { console.log('接口返回数据:', data); }) .catch(error => { console.error('接口调用失败:', error); }); ``` ### 2. 封装统一的日志记录模块 为了便于管理和扩展,可以将日志记录逻辑封装成一个独立的模块。例如,定义一个 `logger` 工具类,支持日志级别(info、warn、error)和日志上传功能。 ```javascript const logger = { info: (message) => { console.info(`[INFO] ${new Date().toISOString()} - ${message}`); }, warn: (message) => { console.warn(`[WARN] ${new Date().toISOString()} - ${message}`); }, error: (message) => { console.error(`[ERROR] ${new Date().toISOString()} - ${message}`); }, }; // 使用示例 logger.info('开始调用接口'); fetch('https://api.example.com/data') .then(response => { logger.info('接口返回状态码: ' + response.status); return response.json(); }) .catch(err => { logger.error('接口调用失败: ' + err.message); }); ``` ### 3. 结合第三方日志收集服务 对于生产环境,建议使用第三方日志收集服务,例如 Sentry、LogRocket、Datadog 等,它们提供了日志聚合、错误追踪、用户行为分析等功能。这些服务通常提供 SDK,可以轻松集成到项目中。 以 Sentry 为例: ```bash npm install @sentry/browser ``` ```javascript import * as Sentry from '@sentry/browser'; Sentry.init({ dsn: 'https://your-dsn@app.getsentry.com/project' }); fetch('https://api.example.com/data') .then(response => { if (!response.ok) { Sentry.captureMessage(`接口返回错误状态码: ${response.status}`); } return response.json(); }) .catch(err => { Sentry.captureException(err); }); ``` ### 4. 将日志发送到后端服务器 为了更细粒度地控制日志存储和分析,可以将前端日志通过 HTTP 请求发送到后端服务。后端可以将日志写入数据库或日志文件中,便于后续分析。 ```javascript function sendLogToServer(level, message) { fetch('/log', { method: 'POST', body: JSON.stringify({ level, message, timestamp: new Date().toISOString(), }), headers: { 'Content-Type': 'application/json' }, }); } // 调用示例 sendLogToServer('info', '接口调用成功'); ``` ### 5. 安全性与隐私保护 在记录接口调用日志时,需注意避免记录敏感信息(如用户密码、token 等),防止日志泄露导致安全风险。可以在日志记录前对数据进行脱敏处理,或设置白名单机制,仅记录必要的字段。 此外,可以设置日志级别控制,例如在生产环境仅记录 `error` 和 `warn` 级别的日志,减少日志冗余。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值