5 Коміти 5b60d79a6b ... c3696d4873

Автор SHA1 Опис Дата
  lixuesong c3696d4873 feat: 增强JSON日志输出功能 2 тижнів тому
  lixuesong 6730e4eff4 feat: 调整JSON格式日志输出 3 тижнів тому
  lixuesong 1a757ee7ae fix: 移除Feign客户端调用日志功能 3 тижнів тому
  lixuesong d9a32d0cc2 feat: 增强Feign客户端日志功能并优化日志输出格式 3 тижнів тому
  lixuesong 078da4e38c feat: 新增Feign客户端调用日志功能并优化日志配置 3 тижнів тому

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

@@ -30,6 +30,8 @@
 		<aliyun.version>3.0.52.ALL</aliyun.version>
 		<commons-io.version>2.8.0</commons-io.version>
 		<itext.version>5.5.13.2</itext.version>
+		<!--JSON 日志 encoder:兼容 logback 1.2.3 / Spring Boot 2.3.12(升级 logback 1.3+/Boot 3 前不可升 7.x+)-->
+		<logstash-logback-encoder.version>6.6</logstash-logback-encoder.version>
 		<maven.deploy.skip>true</maven.deploy.skip>
 	</properties>
 
@@ -105,6 +107,17 @@
 				<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>
+			<!--JSON 日志 encoder(yyc-common-log 的 sys-log-file-appender.xml 使用)-->
+			<dependency>
+				<groupId>net.logstash.logback</groupId>
+				<artifactId>logstash-logback-encoder</artifactId>
+				<version>${logstash-logback-encoder.version}</version>
+			</dependency>
 			<dependency>
 				<groupId>com.baomidou</groupId>
 				<artifactId>mybatis-plus-extension</artifactId>

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

@@ -34,16 +34,14 @@
 
 
 	<dependencies>
-		<!--工具类核心包-->
+		<!--工具类核心包(版本由 yyc-common-bom 统一管理)-->
 		<dependency>
 			<groupId>net.yyc.common</groupId>
 			<artifactId>yyc-common-core</artifactId>
-			<version>1.0.0-SNAPSHOT</version>
 		</dependency>
 		<dependency>
 			<groupId>net.yyc.common</groupId>
 			<artifactId>yyc-common-feign</artifactId>
-			<version>1.0.0-SNAPSHOT</version>
 		</dependency>
 		<!--安全依赖获取上下文信息-->
 		<dependency>
@@ -58,5 +56,10 @@
 			<groupId>org.springframework.cloud</groupId>
 			<artifactId>spring-cloud-starter-sleuth</artifactId>
 		</dependency>
+		<!--JSON 日志输出:阿里云 SLS 采集(LogstashEncoder,sys-log-file-appender.xml 使用),版本由 yyc-common-bom 管理-->
+		<dependency>
+			<groupId>net.logstash.logback</groupId>
+			<artifactId>logstash-logback-encoder</artifactId>
+		</dependency>
 	</dependencies>
 </project>

+ 1 - 1
yyc-common-log/src/main/java/net/yyc/common/log/RequestLoggingFilter.java

@@ -111,7 +111,7 @@ public class RequestLoggingFilter extends OncePerRequestFilter {
             logMsg.setRespBody(new String(response.getContentAsByteArray(), CharsetUtil.CHARSET_UTF_8));
         }
         logMsg.setDuration(System.currentTimeMillis() - startTime);
-        return logMsg.buildMessage();
+        return logMsg.buildJson();
     }
 
     private String assembleReqHeaders(HttpServletRequest request) {

+ 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 {
 
     /**

+ 0 - 6
yyc-common-log/src/main/java/net/yyc/common/log/config/LogConfig.java

@@ -13,12 +13,6 @@ public class LogConfig {
 	@Value("${log.request.enable:true}")
 	private boolean requestEnable;
 
-	/**
-	 * 配置关键字:Feign请求日志开关
-	 */
-	@Value("${log.feign.enable:true}")
-	private boolean requestFeignEnable;
-
 	/**
 	 * 配置关键字:请求日志输出长度
 	 */

+ 20 - 1
yyc-common-log/src/main/java/net/yyc/common/log/event/SysLogListener.java

@@ -23,27 +23,46 @@ import cn.hutool.json.JSONUtil;
 import net.yyc.common.log.aspect.SysLogHandler;
 import lombok.AllArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.context.event.EventListener;
 import org.springframework.core.annotation.Order;
 import org.springframework.scheduling.annotation.Async;
 
 /**
  * @author hnqz 异步监听日志事件
+ *
+ * <p><b>注意:写库行为保留,后续可能移除</b>,届时由下方 JSON 输出(文件/SLS 采集)替代。</p>
  */
 @Slf4j
 @AllArgsConstructor
 public class SysLogListener {
 
+    /**
+     * 失败诊断日志通道:类上的 log 所属 logger(net.yyc.common.log.event)被 sys-log-file-appender
+     * 定向输出(additivity=false、LevelFilter 仅收 INFO),WARN/ERROR 不会进任何文件。
+     * 此处用独立 logger 名走 root(console/debug.log/error.log),保证失败可排障。
+     */
+    private static final Logger ERROR_LOG = LoggerFactory.getLogger("net.yyc.common.log.SysLogError");
+
     private final SysLogHandler sysLogHandler;  // 业务侧注入
 
     @Async
     @Order
     @EventListener(SysLogEvent.class)
     public void saveSysLog(SysLogEvent event) {
+        // 双写:先落 JSON(sys-log.log,供 SLS 采集),再写库;文件输出异常不阻断写库
+        try {
+            log.info(JSONUtil.toJsonStr(event.getSysLog()));
+        } catch (Throwable e) {
+            // Throwable 而非 Exception:循环引用等场景 hutool 序列化可能抛 StackOverflowError,同样不能中断写库。
+            // 也不可再次序列化/toString event.getSysLog():失败原因多半在实体本身,触碰它会二次抛异常中断写库,只记类名
+            ERROR_LOG.warn("操作日志 JSON 输出失败, 实体: {}", event.getSysLog().getClass().getName(), e);
+        }
         try {
             sysLogHandler.saveLog(event.getSysLog());
         } catch (Exception e) {
-            log.error("审计日志保存失败, {}", JSONUtil.toJsonStr(event.getSysLog()), e);
+            ERROR_LOG.error("审计日志保存失败: {}", event.getSysLog(), e);
         }
     }
 

+ 97 - 0
yyc-common-log/src/main/java/net/yyc/common/log/logback/StackTraceFramesJsonProvider.java

@@ -0,0 +1,97 @@
+/*
+ *
+ *      Copyright (c) 2018-2025, hnqz All rights reserved.
+ *
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice,
+ *  this list of conditions and the following disclaimer in the
+ *  documentation and/or other materials provided with the distribution.
+ *
+ *  Author: hnqz
+ *
+ */
+
+package net.yyc.common.log.logback;
+
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.classic.spi.IThrowableProxy;
+import ch.qos.logback.classic.spi.StackTraceElementProxy;
+import com.fasterxml.jackson.core.JsonGenerator;
+import net.logstash.logback.composite.AbstractFieldJsonProvider;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 将异常堆栈按帧拆成 JSON 字符串数组输出,默认字段名 {@code stack_frames}。
+ *
+ * <p>与 LogstashEncoder 自带的 {@code stack_trace}(单行转义字符串,6.6 默认完整不截断)并存,
+ * 数组元素依次为:异常头(类名: 消息)、各调用帧 {@code at ...}、
+ * {@code Caused by:} 异常头及其帧、{@code ... N common frames omitted}(与原生堆栈输出一致)。
+ * 不做任何截断,便于 SLS 对单个堆栈帧进行检索与聚合统计。
+ *
+ * <p>用法(LogstashEncoder 支持追加自定义 provider):
+ * <pre>
+ * &lt;encoder class="net.logstash.logback.encoder.LogstashEncoder"&gt;
+ *     &lt;provider class="net.yyc.common.log.logback.StackTraceFramesJsonProvider"/&gt;
+ * &lt;/encoder&gt;
+ * </pre>
+ *
+ * @author hnqz
+ * @date 2026-08-28
+ */
+public class StackTraceFramesJsonProvider extends AbstractFieldJsonProvider<ILoggingEvent> {
+
+	/**
+	 * 默认输出字段名
+	 */
+	public static final String FIELD_STACK_FRAMES = "stack_frames";
+
+	public StackTraceFramesJsonProvider() {
+		setFieldName(FIELD_STACK_FRAMES);
+	}
+
+	@Override
+	public void writeTo(JsonGenerator generator, ILoggingEvent event) throws IOException {
+		IThrowableProxy throwable = event.getThrowableProxy();
+		if (throwable == null) {
+			return;
+		}
+		List<String> frames = new ArrayList<>();
+		IThrowableProxy current = throwable;
+		while (current != null) {
+			// 首个异常直接输出异常头,其余 cause 链补充 Caused by: 前缀,与原生堆栈格式保持一致
+			if (current == throwable) {
+				frames.add(buildHeader(current));
+			}
+			else {
+				frames.add("Caused by: " + buildHeader(current));
+			}
+			// cause 链中每个异常的帧数组是全量的,与上层重复的公共帧数量记录在 commonFrames,
+			// 原生输出只打印 length - commonFrames 帧,再以 "... N common frames omitted" 结尾,此处保持一致
+			StackTraceElementProxy[] steArray = current.getStackTraceElementProxyArray();
+			int commonFrames = current.getCommonFrames();
+			for (int i = 0; i < steArray.length - commonFrames; i++) {
+				frames.add("at " + steArray[i].getStackTraceElement());
+			}
+			if (commonFrames > 0) {
+				frames.add("... " + commonFrames + " common frames omitted");
+			}
+			current = current.getCause();
+		}
+		generator.writeArrayFieldStart(getFieldName());
+		for (String frame : frames) {
+			generator.writeString(frame);
+		}
+		generator.writeEndArray();
+	}
+
+	private String buildHeader(IThrowableProxy throwable) {
+		String message = throwable.getMessage();
+		return message == null ? throwable.getClassName() : throwable.getClassName() + ": " + message;
+	}
+
+}

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

@@ -0,0 +1,104 @@
+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 逐个处理后返回仅用于日志记录的新数组,不影响业务参数。
+	 *
+	 * <p>注意:可被 hutool 解析为 JSON 对象的参数会被替换为 {@link JSONObject}(仅含脱敏后字段,
+	 * 类型不再是原 DTO),下游 {@code SysLogHandler.buildLogEntity} 不要对启用脱敏的参数做强类型判断;
+	 * 顶层不含目标 key、或无法解析为 JSON 对象(含循环引用)的参数原样保留。仅处理顶层字段,不递归嵌套。</p>
+	 *
+	 * @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 (Throwable e) {
+			// 非 JSON 对象等无法脱敏场景(含循环引用序列化可能抛出的 StackOverflowError),
+			// 保留原值,脱敏失败绝不能影响业务主流程
+			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();
+	}
+
+}

+ 20 - 0
yyc-common-log/src/main/java/net/yyc/common/log/vo/LogMessage.java

@@ -1,6 +1,7 @@
 package net.yyc.common.log.vo;
 
 import cn.hutool.core.util.StrUtil;
+import cn.hutool.json.JSONObject;
 import lombok.Data;
 
 /**
@@ -90,4 +91,23 @@ public class LogMessage {
         };
         return DEFAULT_LOG_FIELD_SEPARATOR_1 + String.join(String.valueOf(DEFAULT_LOG_FIELD_SEPARATOR_1), strings);
     }
+
+    /**
+     * JSON 单行输出(SLS Logtail JSON 模式采集,字段自描述,空值沿用 "-" 占位与旧格式语义一致)
+     * <p>字段全部显式 set,保证行内字段稳定;body 等用户可控内容由 JSON 转义保证结构不被污染。</p>
+     */
+    public String buildJson() {
+        JSONObject json = new JSONObject();
+        json.set("event", event);
+        json.set("method", method);
+        json.set("requestUri", requestUri);
+        json.set("ip", StrUtil.emptyToDefault(ip, DEFAULT_FIELD_PLACEHOLDER));
+        json.set("reqHeaders", StrUtil.emptyToDefault(reqHeaders, DEFAULT_FIELD_PLACEHOLDER));
+        json.set("reqBody", StrUtil.emptyToDefault(reqBody, DEFAULT_FIELD_PLACEHOLDER));
+        json.set("httpStatus", httpStatus);
+        json.set("respHeaders", StrUtil.emptyToDefault(respHeaders, DEFAULT_FIELD_PLACEHOLDER));
+        json.set("respBody", StrUtil.emptyToDefault(respBody, DEFAULT_FIELD_PLACEHOLDER));
+        json.set("duration", duration);
+        return json.toString();
+    }
 }

+ 4 - 32
yyc-common-log/src/main/resources/com/qunzhixinxi/hnqz/common/log/request-logging-file-appender.xml

@@ -1,35 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <included>
-    <appender name="REQUEST_LOGGING_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
-        <!-- 正在记录的日志文件的路径及文件名 -->
-        <file>${log.path}/request-logging.log</file>
-        <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
-        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
-            <!-- 归档的日志文件的路径,%d{yyyy-MM-dd}指定日期格式 -->
-			<fileNamePattern>${log.path}/%d{yyyy-MM}/request-logging-%d{yyyy-MM-dd}-%i.log.gz</fileNamePattern>
-            <cleanHistoryOnStart>true</cleanHistoryOnStart>
-            <!-- 保留10天的日志 -->
-            <maxHistory>30</maxHistory>
-            <!--用来指定单个日志文件的上限大小,那么到了这个值,就会拆分日志-->
-            <maxFileSize>50MB</maxFileSize>
-        </rollingPolicy>
-        <!-- 追加方式记录日志 -->
-        <append>true</append>
-        <!-- 日志文件的格式 -->
-        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
-			<pattern>%date [%thread] %-5level [%X{traceId},%X{spanId}] [%logger{50}] %file:%line - %msg%n</pattern>
-            <charset>utf-8</charset>
-        </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.RequestLoggingFilter" level="INFO" additivity="false">
-        <appender-ref ref="console" />
-        <appender-ref ref="REQUEST_LOGGING_FILE" />
-    </logger>
+    <!-- 兼容旧包路径 include:本文件仅转发到新包路径,后续版本将移除。
+         下游 logback-spring.xml 请尽快改为:
+         <include resource="net/yyc/common/log/request-logging-file-appender.xml"/> -->
+    <include resource="net/yyc/common/log/request-logging-file-appender.xml"/>
 </included>

+ 69 - 0
yyc-common-log/src/main/resources/net/yyc/common/log/json-file-appenders.xml

@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<included>
+    <!-- debug/error 的 JSON 单行冗余 appender(独立 json/ 目录),供阿里云 SLS Logtail JSON 模式采集。
+         仅定义 appender,由下游 logback-spring.xml include 后自行挂载,例如:
+             <include resource="net/yyc/common/log/json-file-appenders.xml"/>
+             <root level="INFO">
+                 <appender-ref ref="console"/>
+                 <appender-ref ref="debug"/>
+                 <appender-ref ref="debug-json"/>
+                 <appender-ref ref="error"/>
+                 <appender-ref ref="error-json"/>
+             </root>
+         堆栈:stack_trace 字段完整不截断(6.6 默认 ExtendedThrowableProxyConverter,显式声明锁死,
+         升级 encoder 7.4+ 时其默认会变为 ShortenedThrowableConverter 截断堆栈);
+         另输出 stack_frames 帧数组便于 SLS 按帧检索。
+         调用位置:CallerDataJsonProvider 输出 caller_class_name / caller_method_name /
+         caller_file_name / caller_line_number(日志语句所在源文件与行号,便于从 SLS 回溯到代码);
+         因 debug/error 文本 appender 已含 %file:%line,caller data 本就被计算,无额外开销。
+         物理文件:customFields 输出 log_file(本行写入时的活动文件完整路径)。
+         滚动归档后定位规则:日志按 @timestamp 的日期归档到
+         {log.path}/json/{yyyy-MM}/{log_file 文件名去掉 .log}.{yyyy-MM-dd}.{i}.log.gz
+         (同一天仅一个文件时 i=0;单日超 50MB 会有多个 i,按时间戳在归档内检索)。 -->
+    <appender name="debug-json" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${log.path}/json/debug.log</file>
+        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
+            <fileNamePattern>${log.path}/json/%d{yyyy-MM, aux}/debug.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
+            <cleanHistoryOnStart>true</cleanHistoryOnStart>
+            <!-- 保留7天的日志 -->
+            <maxHistory>7</maxHistory>
+            <maxFileSize>50MB</maxFileSize>
+        </rollingPolicy>
+        <append>true</append>
+        <encoder class="net.logstash.logback.encoder.LogstashEncoder">
+            <throwableConverter class="ch.qos.logback.classic.pattern.ExtendedThrowableProxyConverter"/>
+            <provider class="net.logstash.logback.composite.loggingevent.CallerDataJsonProvider"/>
+            <provider class="net.yyc.common.log.logback.StackTraceFramesJsonProvider"/>
+            <fieldNames>
+                <version>[ignore]</version>
+                <levelValue>[ignore]</levelValue>
+            </fieldNames>
+            <customFields>{"log_file":"${log.path}/json/debug.log"}</customFields>
+        </encoder>
+    </appender>
+
+    <appender name="error-json" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${log.path}/json/error.log</file>
+        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
+            <fileNamePattern>${log.path}/json/%d{yyyy-MM}/error.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
+            <cleanHistoryOnStart>true</cleanHistoryOnStart>
+            <!-- 保留7天的日志 -->
+            <maxHistory>7</maxHistory>
+            <maxFileSize>50MB</maxFileSize>
+        </rollingPolicy>
+        <append>true</append>
+        <encoder class="net.logstash.logback.encoder.LogstashEncoder">
+            <throwableConverter class="ch.qos.logback.classic.pattern.ExtendedThrowableProxyConverter"/>
+            <provider class="net.logstash.logback.composite.loggingevent.CallerDataJsonProvider"/>
+            <provider class="net.yyc.common.log.logback.StackTraceFramesJsonProvider"/>
+            <fieldNames>
+                <version>[ignore]</version>
+                <levelValue>[ignore]</levelValue>
+            </fieldNames>
+            <customFields>{"log_file":"${log.path}/json/error.log"}</customFields>
+        </encoder>
+        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
+            <level>ERROR</level>
+        </filter>
+    </appender>
+</included>

+ 39 - 0
yyc-common-log/src/main/resources/net/yyc/common/log/request-logging-file-appender.xml

@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<included>
+    <appender name="REQUEST_LOGGING_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <!-- 正在记录的日志文件的路径及文件名 -->
+        <file>${log.path}/request-logging.log</file>
+        <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
+        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
+            <!-- 归档的日志文件的路径,%d{yyyy-MM-dd}指定日期格式 -->
+			<fileNamePattern>${log.path}/%d{yyyy-MM}/request-logging-%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 JSON 模式采集(与 sys-log/debug 等文件一致,透传 MDC traceId/spanId) -->
+        <encoder class="net.logstash.logback.encoder.LogstashEncoder">
+            <throwableConverter class="ch.qos.logback.classic.pattern.ExtendedThrowableProxyConverter"/>
+            <provider class="net.yyc.common.log.logback.StackTraceFramesJsonProvider"/>
+            <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.RequestLoggingFilter" level="INFO" additivity="false">
+        <appender-ref ref="console" />
+        <appender-ref ref="REQUEST_LOGGING_FILE" />
+    </logger>
+</included>

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

@@ -0,0 +1,36 @@
+<?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">
+            <throwableConverter class="ch.qos.logback.classic.pattern.ExtendedThrowableProxyConverter"/>
+            <provider class="net.yyc.common.log.logback.StackTraceFramesJsonProvider"/>
+            <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>