Explorar el Código

feat: 新增Feign客户端调用日志功能并优化日志配置

lixuesong hace 3 semanas
padre
commit
078da4e38c

+ 1 - 1
pom.xml

@@ -5,7 +5,7 @@
 
 	<groupId>net.yyc.common</groupId>
 	<artifactId>yyc-common-parent</artifactId>
-	<version>1.0.0-SNAPSHOT</version>
+	<version>1.0.1-SNAPSHOT</version>
 	<name>yyc-common-parent</name>
 	<packaging>pom</packaging>
 

+ 5 - 0
yyc-common-bom/pom.xml

@@ -105,6 +105,11 @@
 				<artifactId>yyc-common-mq</artifactId>
 				<version>${yyc.common.version}</version>
 			</dependency>
+			<dependency>
+				<groupId>net.yyc.common</groupId>
+				<artifactId>yyc-common-log</artifactId>
+				<version>${yyc.common.version}</version>
+			</dependency>
 			<dependency>
 				<groupId>com.baomidou</groupId>
 				<artifactId>mybatis-plus-extension</artifactId>

+ 6 - 0
yyc-common-log/pom.xml

@@ -58,5 +58,11 @@
 			<groupId>org.springframework.cloud</groupId>
 			<artifactId>spring-cloud-starter-sleuth</artifactId>
 		</dependency>
+		<!--JSON 日志输出:阿里云 SLS 采集(LogstashEncoder,sys-log-file-appender.xml 使用)-->
+		<dependency>
+			<groupId>net.logstash.logback</groupId>
+			<artifactId>logstash-logback-encoder</artifactId>
+			<version>6.6</version>
+		</dependency>
 	</dependencies>
 </project>

+ 95 - 0
yyc-common-log/src/main/java/net/yyc/common/log/FeignDefaultRequestLogging.java

@@ -0,0 +1,95 @@
+package net.yyc.common.log;
+
+import cn.hutool.core.io.IoUtil;
+import cn.hutool.core.net.NetUtil;
+import cn.hutool.core.util.CharsetUtil;
+import net.yyc.common.log.config.LogConfig;
+import net.yyc.common.log.vo.LogMessage;
+import feign.Client;
+import feign.Request;
+import feign.Response;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.util.CollectionUtils;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Feign客户端调用日志输出
+ */
+@Slf4j
+public class FeignDefaultRequestLogging extends Client.Default {
+
+    /**
+     * 请求EVENT
+     */
+    private static final String LOG_EVENT = "FEIGN";
+
+    /**
+     * 打印日志开关
+     */
+    private boolean enable;
+
+    /**
+     * body最大长度
+     */
+    private int maxLength;
+
+    public FeignDefaultRequestLogging(LogConfig logConfig) {
+        super(null, null);
+        this.enable = logConfig.isRequestFeignEnable();
+        this.maxLength = logConfig.getRequestMaxLength();
+    }
+
+    @Override
+    public Response execute(Request request, Request.Options options) throws IOException {
+        if (!enable) {
+            return super.execute(request, options);
+        }
+
+        long startTime = System.currentTimeMillis();
+
+        Response response = super.execute(request, options);
+
+        //拼接日志
+        LogMessage logMsg = new LogMessage();
+        logMsg.setEvent(LOG_EVENT);
+        logMsg.setMethod(request.httpMethod().name());
+        logMsg.setRequestUri(request.url());
+        logMsg.setIp(NetUtil.getLocalhostStr());
+        logMsg.setReqHeaders(assembleHeaders(request.headers()));
+
+        byte[] reqBody = request.body();
+        if (Objects.nonNull(reqBody)) {
+            logMsg.setReqBody(new String(reqBody, 0, Math.min(reqBody.length, maxLength), CharsetUtil.CHARSET_UTF_8));
+        }
+        logMsg.setHttpStatus(response.status());
+        logMsg.setRespHeaders(assembleHeaders(response.headers()));
+
+        Response.Body responseBody = response.body();
+        if (responseBody != null) {
+            byte[] respBody = IoUtil.readBytes(responseBody.asInputStream());
+            logMsg.setRespBody(new String(respBody, 0, Math.min(respBody.length, maxLength), CharsetUtil.CHARSET_UTF_8));
+            response = response.toBuilder().body(respBody).build();
+        }
+        logMsg.setDuration(System.currentTimeMillis() - startTime);
+        log.info(logMsg.buildMessage());
+        return response;
+    }
+
+    private String assembleHeaders(Map<String, Collection<String>> headers) {
+        if (CollectionUtils.isEmpty(headers)) {
+            return null;
+        }
+        StringBuilder sb = new StringBuilder();
+
+        for (Map.Entry<String, Collection<String>> entrySet : headers.entrySet()) {
+            String key = entrySet.getKey();
+            Collection<String> values = entrySet.getValue();
+            sb.append(key).append("=").append(String.join(";", values)).append(LogMessage.DEFAULT_LOG_FIELD_SEPARATOR_2);
+        }
+        return sb.substring(0, sb.length() - 1);
+    }
+}

+ 6 - 0
yyc-common-log/src/main/java/net/yyc/common/log/annotation/SysLog.java

@@ -32,4 +32,10 @@ public @interface SysLog {
 	 */
 	String value();
 
+	/**
+	 * 敏感字段(请求参数中按 key 脱敏,仅用于日志记录,不影响业务参数)
+	 * @return {String[]}
+	 */
+	String[] sensitiveFields() default {};
+
 }

+ 4 - 1
yyc-common-log/src/main/java/net/yyc/common/log/aspect/SysLogAspect.java

@@ -21,6 +21,7 @@ package net.yyc.common.log.aspect;
 
 import net.yyc.common.log.annotation.SysLog;
 import net.yyc.common.log.event.SysLogEvent;
+import net.yyc.common.log.util.SysLogUtils;
 import lombok.AllArgsConstructor;
 import lombok.SneakyThrows;
 import lombok.extern.slf4j.Slf4j;
@@ -60,7 +61,9 @@ public class SysLogAspect {
         long startTime = System.currentTimeMillis();
         Object result = point.proceed();
         long endTime = System.currentTimeMillis();
-        Object logEntity = handler.buildLogEntity(sysLog.value(), request, args, result, endTime - startTime);
+        // 敏感字段脱敏(仅用于日志记录,不影响业务参数)
+        Object[] logArgs = SysLogUtils.maskArgs(args, sysLog.sensitiveFields());
+        Object logEntity = handler.buildLogEntity(sysLog.value(), request, logArgs, result, endTime - startTime);
         publisher.publishEvent(new SysLogEvent(logEntity));
         return result;
     }

+ 6 - 0
yyc-common-log/src/main/java/net/yyc/common/log/aspect/SysLogHandler.java

@@ -2,6 +2,12 @@ package net.yyc.common.log.aspect;
 
 import javax.servlet.http.HttpServletRequest;
 
+/**
+ * 操作日志写库 SPI(业务侧实现)。
+ *
+ * <p><b>注意:写库行为保留,后续可能移除</b>,届时由文件/SLS 采集替代(见 {@link net.yyc.common.log.event.SysLogListener})。
+ * 各项目若不需要写库,实现为空方法即可。</p>
+ */
 public interface SysLogHandler {
 
     /**

+ 8 - 0
yyc-common-log/src/main/java/net/yyc/common/log/event/SysLogListener.java

@@ -29,6 +29,8 @@ import org.springframework.scheduling.annotation.Async;
 
 /**
  * @author hnqz 异步监听日志事件
+ *
+ * <p><b>注意:写库行为保留,后续可能移除</b>,届时由下方 JSON 输出(文件/SLS 采集)替代。</p>
  */
 @Slf4j
 @AllArgsConstructor
@@ -40,6 +42,12 @@ public class SysLogListener {
     @Order
     @EventListener(SysLogEvent.class)
     public void saveSysLog(SysLogEvent event) {
+        // 双写:先落 JSON(sys-log.log,供 SLS 采集),再写库;文件输出异常不阻断写库
+        try {
+            log.info(JSONUtil.toJsonStr(event.getSysLog()));
+        } catch (Exception e) {
+            log.warn("操作日志 JSON 输出失败, {}", JSONUtil.toJsonStr(event.getSysLog()), e);
+        }
         try {
             sysLogHandler.saveLog(event.getSysLog());
         } catch (Exception e) {

+ 99 - 0
yyc-common-log/src/main/java/net/yyc/common/log/util/SysLogUtils.java

@@ -0,0 +1,99 @@
+package net.yyc.common.log.util;
+
+import cn.hutool.core.util.ArrayUtil;
+import cn.hutool.extra.servlet.ServletUtil;
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
+import lombok.experimental.UtilityClass;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.oauth2.provider.OAuth2Authentication;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * 日志通用工具类
+ *
+ * <p>仅包含与业务无关的通用能力(敏感字段脱敏、客户端信息采集),供切面及业务侧 SysLogHandler 使用。</p>
+ *
+ * @author hnqz
+ */
+@UtilityClass
+public class SysLogUtils {
+
+	/**
+	 * 敏感字段脱敏:对 args 深拷贝并打码,返回仅用于日志记录的新数组,不影响业务参数。
+	 *
+	 * @param args            请求参数
+	 * @param sensitiveFields 敏感字段 key
+	 * @return 脱敏后的参数数组(无敏感字段或无法解析时原样返回)
+	 */
+	public Object[] maskArgs(Object[] args, String[] sensitiveFields) {
+		if (ArrayUtil.isEmpty(args) || ArrayUtil.isEmpty(sensitiveFields)) {
+			return args;
+		}
+		Object[] newArgs = new Object[args.length];
+		for (int i = 0; i < args.length; i++) {
+			newArgs[i] = maskSingle(args[i], sensitiveFields);
+		}
+		return newArgs;
+	}
+
+	private Object maskSingle(Object arg, String[] sensitiveFields) {
+		try {
+			JSONObject json = JSONUtil.parseObj(arg);
+			for (String field : sensitiveFields) {
+				if (json.containsKey(field)) {
+					json.set(field, "******");
+				}
+			}
+			return json;
+		}
+		catch (Exception e) {
+			// 非 JSON 对象等无法脱敏场景,保留原值,避免日志异常影响主流程
+			return arg;
+		}
+	}
+
+	/**
+	 * 获取当前请求
+	 */
+	public HttpServletRequest getRequest() {
+		ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
+		return attributes == null ? null : attributes.getRequest();
+	}
+
+	/**
+	 * 获取客户端 IP
+	 */
+	public String getClientIP() {
+		HttpServletRequest request = getRequest();
+		return request == null ? null : ServletUtil.getClientIP(request);
+	}
+
+	/**
+	 * 获取客户端 clientId
+	 */
+	public String getClientId() {
+		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+		if (authentication instanceof OAuth2Authentication) {
+			OAuth2Authentication auth2Authentication = (OAuth2Authentication) authentication;
+			return auth2Authentication.getOAuth2Request().getClientId();
+		}
+		return null;
+	}
+
+	/**
+	 * 获取当前用户名
+	 */
+	public String getUsername() {
+		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+		if (authentication == null) {
+			return null;
+		}
+		return authentication.getName();
+	}
+
+}

+ 0 - 0
yyc-common-log/src/main/resources/com/qunzhixinxi/hnqz/common/log/request-logging-file-appender.xml → yyc-common-log/src/main/resources/net/yyc/common/log/request-logging-file-appender.xml


+ 34 - 0
yyc-common-log/src/main/resources/net/yyc/common/log/sys-log-file-appender.xml

@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<included>
+    <!-- 操作日志(@SysLog / 登录事件):JSON 单行,独立落 sys-log.log,供 SLS Logtail 采集
+         SysLogListener 在落库前先打 INFO,logger 限定 net.yyc.common.log.event -->
+    <appender name="SYS_LOG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${log.path}/sys-log.log</file>
+        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
+            <fileNamePattern>${log.path}/%d{yyyy-MM}/sys-log-%d{yyyy-MM-dd}-%i.log.gz</fileNamePattern>
+            <cleanHistoryOnStart>true</cleanHistoryOnStart>
+            <!-- 保留7天的日志 -->
+            <maxHistory>7</maxHistory>
+            <maxFileSize>50MB</maxFileSize>
+        </rollingPolicy>
+        <append>true</append>
+        <!-- 日志文件格式:JSON 单行,供 SLS Logtail 采集(LogstashEncoder 默认 UTF-8,透传 MDC traceId/spanId) -->
+        <encoder class="net.logstash.logback.encoder.LogstashEncoder">
+            <fieldNames>
+                <version>[ignore]</version>
+                <levelValue>[ignore]</levelValue>
+            </fieldNames>
+        </encoder>
+        <!-- 此日志文件只记录info级别的 -->
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <level>info</level>
+            <onMatch>ACCEPT</onMatch>
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+
+    <logger name="net.yyc.common.log.event" level="INFO" additivity="false">
+        <appender-ref ref="console" />
+        <appender-ref ref="SYS_LOG_FILE" />
+    </logger>
+</included>