From 14b8cc625bd4654bb73562d2aa47ba3bfcfede28 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Thu, 15 Oct 2026 17:18:09 +0800 Subject: [PATCH] fix(appointment): retain safe synchronization failure diagnostics --- .../AppointmentWorkSynchronizationJob.java | 15 ++++++++++++++- .../AppointmentWorkSynchronizationJobTest.java | 16 ++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 lastleg-service/src/test/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkSynchronizationJobTest.java diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkSynchronizationJob.java b/lastleg-service/src/main/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkSynchronizationJob.java index bd5b4410a..9137ff83c 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkSynchronizationJob.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkSynchronizationJob.java @@ -21,7 +21,20 @@ public class AppointmentWorkSynchronizationJob { cursor=result.complete()?null:result.nextCursor(); } catch(RuntimeException error) { cursor=null; - log.warn("Appointment work synchronization incomplete; next scan restarts from first page: {}",error.getClass().getSimpleName()); + log.warn("Appointment work synchronization incomplete; next scan restarts from first page: {}",failureDiagnostic(error)); } } + /** Keep causes and code locations, never exception messages that may contain remote payloads or credentials. */ + static String failureDiagnostic(Throwable error) { + var causes=new java.util.ArrayList(); + var seen=java.util.Collections.newSetFromMap(new java.util.IdentityHashMap()); + for(var current=error;current!=null && causes.size()<6 && seen.add(current);current=current.getCause()) { + String status=current instanceof org.springframework.web.server.ResponseStatusException response?"["+response.getStatusCode().value()+"]":""; + String location=java.util.Arrays.stream(current.getStackTrace()) + .filter(frame->frame.getClassName().startsWith("com.baiyue.") && !frame.getClassName().equals(AppointmentWorkPolicy.class.getName())) + .findFirst().map(frame->"@"+frame.getClassName()+":"+frame.getLineNumber()).orElse(""); + causes.add(current.getClass().getSimpleName()+status+location); + } + return String.join(" <- ",causes); + } } diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkSynchronizationJobTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkSynchronizationJobTest.java new file mode 100644 index 000000000..b573ab02e --- /dev/null +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkSynchronizationJobTest.java @@ -0,0 +1,16 @@ +package com.baiyue.lastleg.appointmentwork; + +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; +import org.springframework.web.server.ResponseStatusException; +import static org.assertj.core.api.Assertions.assertThat; + +class AppointmentWorkSynchronizationJobTest { + @Test void diagnosticKeepsCauseAndCodeWithoutRemoteMessages() { + var cause=new IllegalArgumentException("Authorization: Bearer secret; private remote body"); + var error=new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE,"private response",cause); + assertThat(AppointmentWorkSynchronizationJob.failureDiagnostic(error)) + .contains("ResponseStatusException[503]", "IllegalArgumentException", "AppointmentWorkSynchronizationJobTest:") + .doesNotContain("secret", "private", "Authorization", "Bearer"); + } +}