fix(shadow): repair UTC sampling and persist v3 diagnostics
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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 > #{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>
|
||||
|
||||
Reference in New Issue
Block a user