Эх сурвалжийг харах

feat: 调整JSON格式日志输出

lixuesong 3 долоо хоног өмнө
parent
commit
6730e4eff4

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

@@ -43,11 +43,6 @@
 			<groupId>net.yyc.common</groupId>
 			<artifactId>yyc-common-feign</artifactId>
 		</dependency>
-		<!--Feign 负载均衡:日志客户端需包装为 LoadBalancerFeignClient,避免注册裸 Client 顶掉负载均衡导致服务名调用失败-->
-		<dependency>
-			<groupId>org.springframework.cloud</groupId>
-			<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
-		</dependency>
 		<!--安全依赖获取上下文信息-->
 		<dependency>
 			<groupId>org.springframework.security</groupId>

+ 4 - 3
yyc-common-log/src/main/java/net/yyc/common/log/event/SysLogListener.java

@@ -54,9 +54,10 @@ public class SysLogListener {
         // 双写:先落 JSON(sys-log.log,供 SLS 采集),再写库;文件输出异常不阻断写库
         try {
             log.info(JSONUtil.toJsonStr(event.getSysLog()));
-        } catch (Exception e) {
-            // 不可再次序列化 event.getSysLog():失败原因多半是序列化本身,重复执行会二次抛异常并中断写库
-            ERROR_LOG.warn("操作日志 JSON 输出失败: {}", event.getSysLog(), e);
+        } catch (Throwable e) {
+            // Throwable 而非 Exception:循环引用等场景 hutool 序列化可能抛 StackOverflowError,同样不能中断写库。
+            // 也不可再次序列化/toString event.getSysLog():失败原因多半在实体本身,触碰它会二次抛异常中断写库,只记类名
+            ERROR_LOG.warn("操作日志 JSON 输出失败, 实体: {}", event.getSysLog().getClass().getName(), e);
         }
         try {
             sysLogHandler.saveLog(event.getSysLog());

+ 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;
+	}
+
+}

+ 5 - 1
yyc-common-log/src/main/java/net/yyc/common/log/util/SysLogUtils.java

@@ -24,7 +24,11 @@ import javax.servlet.http.HttpServletRequest;
 public class SysLogUtils {
 
 	/**
-	 * 敏感字段脱敏:对 args 深拷贝并打码,返回仅用于日志记录的新数组,不影响业务参数。
+	 * 敏感字段脱敏:对 args 逐个处理后返回仅用于日志记录的新数组,不影响业务参数。
+	 *
+	 * <p>注意:可被 hutool 解析为 JSON 对象的参数会被替换为 {@link JSONObject}(仅含脱敏后字段,
+	 * 类型不再是原 DTO),下游 {@code SysLogHandler.buildLogEntity} 不要对启用脱敏的参数做强类型判断;
+	 * 顶层不含目标 key、或无法解析为 JSON 对象(含循环引用)的参数原样保留。仅处理顶层字段,不递归嵌套。</p>
 	 *
 	 * @param args            请求参数
 	 * @param sensitiveFields 敏感字段 key

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

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<included>
+    <!-- 兼容旧包路径 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>

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

@@ -0,0 +1,58 @@
+<?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 按帧检索。 -->
+    <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.yyc.common.log.logback.StackTraceFramesJsonProvider"/>
+            <fieldNames>
+                <version>[ignore]</version>
+                <levelValue>[ignore]</levelValue>
+            </fieldNames>
+        </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.yyc.common.log.logback.StackTraceFramesJsonProvider"/>
+            <fieldNames>
+                <version>[ignore]</version>
+                <levelValue>[ignore]</levelValue>
+            </fieldNames>
+        </encoder>
+        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
+            <level>ERROR</level>
+        </filter>
+    </appender>
+</included>

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

@@ -17,6 +17,8 @@
         <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>

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

@@ -14,6 +14,8 @@
         <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>