fix(shadow): repair UTC sampling and persist v3 diagnostics

This commit is contained in:
zgw
2026-09-07 10:12:41 +08:00
parent 5ce6393337
commit 79e0db29f9
15 changed files with 678 additions and 34 deletions

View File

@@ -9,6 +9,7 @@ import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.sql.SQLException;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
@@ -346,9 +347,32 @@ public class ShadowObservationGateway {
long elapsedMicros = startedNanos == 0L ? 0L
: TimeUnit.NANOSECONDS.toMicros(Math.max(0L, System.nanoTime() - startedNanos));
LOG.warn("shadow_failure requestId={} warehouse={} snapshotGeneration={} stage={} "
+ "exceptionClass={} elapsedMicros={}",
+ "exceptionClass={} elapsedMicros={} {}",
requestId, warehouseId, generation, stage,
failure.getClass().getName(), elapsedMicros);
failure.getClass().getName(), elapsedMicros, diagnosticMetadata(failure));
}
private static String diagnosticMetadata(Throwable failure) {
Throwable cause = failure;
SQLException sqlFailure = null;
// Bounded traversal; never log exception messages, SQL text or wrapped business payloads.
for (int depth = 0; depth < 8; depth++) {
if (cause instanceof SQLException sql) {
sqlFailure = sql;
}
if (cause.getCause() == null || cause.getCause() == cause || depth == 7) {
break;
}
cause = cause.getCause();
}
String sqlState = sqlFailure == null ? null : sqlFailure.getSQLState();
StackTraceElement[] trace = cause.getStackTrace();
String source = trace.length == 0 ? "-"
: trace[0].getClassName() + '.' + trace[0].getMethodName() + ':' + trace[0].getLineNumber();
return "causeClass=" + cause.getClass().getName()
+ " sqlState=" + (sqlState != null && sqlState.matches("[A-Za-z0-9]{5}") ? sqlState : "-")
+ " sqlCode=" + (sqlFailure == null ? 0 : sqlFailure.getErrorCode())
+ " causeAt=" + source;
}
private String failureDescription(ShadowFailureStage stage, Throwable failure) {

View File

@@ -0,0 +1,46 @@
package com.ruoyi.algorithm.shadow.mapper;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.time.Instant;
import java.util.Calendar;
import java.util.TimeZone;
/** Explicitly referenced only by shadow mappings whose datetime2 values are UTC. */
public final class ShadowUtcInstantTypeHandler extends BaseTypeHandler<Instant> {
@Override
public void setNonNullParameter(PreparedStatement statement, int index, Instant value, JdbcType jdbcType)
throws SQLException {
statement.setTimestamp(index, Timestamp.from(value), utcCalendar());
}
@Override
public Instant getNullableResult(ResultSet results, String column) throws SQLException {
return instant(results.getTimestamp(column, utcCalendar()));
}
@Override
public Instant getNullableResult(ResultSet results, int column) throws SQLException {
return instant(results.getTimestamp(column, utcCalendar()));
}
@Override
public Instant getNullableResult(CallableStatement statement, int column) throws SQLException {
return instant(statement.getTimestamp(column, utcCalendar()));
}
private static Instant instant(Timestamp timestamp) {
return timestamp == null ? null : timestamp.toInstant();
}
private static Calendar utcCalendar() {
// Calendar is mutable and may be modified by a JDBC driver; never share it across calls.
return Calendar.getInstance(TimeZone.getTimeZone("UTC"));
}
}

View File

@@ -71,7 +71,28 @@
</encoder>
</appender>
<!-- 系统模块日志级别控制 -->
<!-- Shadow diagnostics only; business appenders and their filters remain unchanged. -->
<appender name="file_shadow" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/shadow-diagnostics.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${log.path}/shadow-diagnostics.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>10MB</maxFileSize>
<maxHistory>14</maxHistory>
<totalSizeCap>100MB</totalSizeCap>
</rollingPolicy>
<encoder>
<charset>UTF-8</charset>
<pattern>%d{yyyy-MM-dd'T'HH:mm:ss.SSSXXX,UTC} [%thread] %-5level %logger - %msg%n</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>WARN</level>
</filter>
</appender>
<logger name="com.ruoyi.algorithm.shadow">
<appender-ref ref="file_shadow"/>
</logger>
<!-- 系统模块日志级别控制 -->
<logger name="com.ruoyi" level="info" />
<!-- Spring日志级别控制 -->
<logger name="org.springframework" level="warn" />
@@ -90,4 +111,4 @@
<logger name="sys-user" level="info">
<appender-ref ref="sys-user"/>
</logger>
</configuration>
</configuration>

View File

@@ -89,8 +89,16 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
@instance_id = #{instanceId};
</update>
<select id="selectPublishedSnapshot"
resultType="com.ruoyi.algorithm.shadow.ShadowSnapshotDescriptor">
<resultMap id="publishedSnapshot" type="com.ruoyi.algorithm.shadow.ShadowSnapshotDescriptor">
<result property="snapshotGeneration" column="snapshotGeneration"/>
<result property="warehouseId" column="warehouseId"/>
<result property="snapshotStatus" column="snapshotStatus"/>
<result property="synchronizedAt" column="synchronizedAt"
typeHandler="com.ruoyi.algorithm.shadow.mapper.ShadowUtcInstantTypeHandler"/>
<result property="sourceSignature" column="sourceSignature"/>
</resultMap>
<select id="selectPublishedSnapshot" resultMap="publishedSnapshot">
select state.snapshot_generation as snapshotGeneration,
state.warehouse_id as warehouseId,
state.snapshot_status as snapshotStatus,
@@ -109,7 +117,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
where state.warehouse_id = #{warehouseId}
and state.snapshot_status = 'PUBLISHED'
and state.snapshot_generation = #{inputVersion}
and state.synchronized_at = #{snapshotSynchronizedAt}
and state.synchronized_at = #{snapshotSynchronizedAt, jdbcType=TIMESTAMP, typeHandler=com.ruoyi.algorithm.shadow.mapper.ShadowUtcInstantTypeHandler}
and convert(varchar(64), state.source_signature, 2) = #{sourceSignature}
</insert>
@@ -126,12 +134,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
metric_total_micros = target.metric_total_micros + #{metricTotalMicros},
metric_max_micros = case when target.metric_max_micros &gt; #{metricMaxMicros}
then target.metric_max_micros else #{metricMaxMicros} end,
flushed_at = #{flushedAt}
flushed_at = #{flushedAt, jdbcType=TIMESTAMP, typeHandler=com.ruoyi.algorithm.shadow.mapper.ShadowUtcInstantTypeHandler}
when not matched then insert (
warehouse_id, metric_name, bucket_upper_micros,
metric_count, metric_total_micros, metric_max_micros, flushed_at)
values (
source.warehouse_id, source.metric_name, source.bucket_upper_micros,
#{metricCount}, #{metricTotalMicros}, #{metricMaxMicros}, #{flushedAt});
#{metricCount}, #{metricTotalMicros}, #{metricMaxMicros}, #{flushedAt, jdbcType=TIMESTAMP, typeHandler=com.ruoyi.algorithm.shadow.mapper.ShadowUtcInstantTypeHandler});
</update>
</mapper>