From 6495f70b8cff7b41b29175b841f79d79a92791dd Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Mon, 10 Aug 2026 08:40:17 +0900
Subject: [PATCH 1/2] test(cdc): reject replication probe diagnostic leakage
---
.../xtrmetl/cdc/service/ReplicationSlotProbeTest.java | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/cdc-service/src/test/java/com/xtrmetl/cdc/service/ReplicationSlotProbeTest.java b/cdc-service/src/test/java/com/xtrmetl/cdc/service/ReplicationSlotProbeTest.java
index 927f3808..c8940bc5 100644
--- a/cdc-service/src/test/java/com/xtrmetl/cdc/service/ReplicationSlotProbeTest.java
+++ b/cdc-service/src/test/java/com/xtrmetl/cdc/service/ReplicationSlotProbeTest.java
@@ -51,14 +51,19 @@ void reportsNotFoundWhenEmpty() {
}
@Test
- void failOpenOnDataAccessError() {
+ void failOpenOnDataAccessErrorWithoutExposingDriverDiagnostics() {
JdbcTemplate jdbc = mock(JdbcTemplate.class);
+ String sensitiveDriverMessage =
+ "jdbc:postgresql://db.internal:5432/prod?user=admin&password=super-secret";
when(jdbc.queryForList(anyString(), eq("xtrmetl_slot")))
- .thenThrow(new DataAccessResourceFailureException("down"));
+ .thenThrow(new DataAccessResourceFailureException(sensitiveDriverMessage));
Map result = new ReplicationSlotProbe(jdbc).probeSlot("xtrmetl_slot");
assertFalse((Boolean) result.get("available"));
assertEquals("query_failed", result.get("error"));
+ assertEquals("Replication slot state unavailable", result.get("message"));
+ assertFalse(result.toString().contains("super-secret"));
+ assertFalse(result.toString().contains("jdbc:postgresql://"));
}
}
From f056afbd79567a7a5276170b02090fcbc7eab493 Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Mon, 10 Aug 2026 08:42:31 +0900
Subject: [PATCH 2/2] fix(cdc): keep replication probe diagnostics out of
status
---
.../cdc/service/ReplicationSlotProbe.java | 35 +++++++++++--------
1 file changed, 21 insertions(+), 14 deletions(-)
diff --git a/cdc-service/src/main/java/com/xtrmetl/cdc/service/ReplicationSlotProbe.java b/cdc-service/src/main/java/com/xtrmetl/cdc/service/ReplicationSlotProbe.java
index aeaf28da..431fcf4e 100644
--- a/cdc-service/src/main/java/com/xtrmetl/cdc/service/ReplicationSlotProbe.java
+++ b/cdc-service/src/main/java/com/xtrmetl/cdc/service/ReplicationSlotProbe.java
@@ -16,12 +16,14 @@
* Reads PostgreSQL logical replication slot state for operator status (no secrets).
*
* Uses the primary datasource ({@link JdbcTemplate}). Fail-open: DB errors become
- * {@code available=false} rather than failing the status API.
+ * {@code available=false} rather than failing the status API. Database-driver exception
+ * details are intentionally excluded from both the returned status and ordinary logs.
*/
@Service
public class ReplicationSlotProbe {
private static final Logger log = LoggerFactory.getLogger(ReplicationSlotProbe.class);
+ private static final String QUERY_FAILED_MESSAGE = "Replication slot state unavailable";
// restart_lsn / confirmed_flush_lsn are pg_lsn; lag bytes via pg_wal_lsn_diff against current insert LSN.
private static final String SLOT_SQL = """
@@ -45,15 +47,31 @@ ELSE pg_wal_lsn_diff(pg_current_wal_lsn(), confirmed_flush_lsn)
private final JdbcTemplate jdbcTemplate;
+ /**
+ * Creates a replication-slot probe backed by the service's primary PostgreSQL datasource.
+ *
+ * @param jdbcTemplate JDBC access used for the read-only replication-slot query
+ */
public ReplicationSlotProbe(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
+ /**
+ * Probes the slot named by {@code CDC_SLOT_NAME}, or the legacy default when it is absent.
+ *
+ * @return a finite operator status map that never includes database-driver diagnostics
+ */
public Map probeConfiguredSlot() {
String slotName = EnvUtils.getEnv("CDC_SLOT_NAME", "xtrmetl_slot");
return probeSlot(slotName);
}
+ /**
+ * Probes one PostgreSQL logical replication slot without exposing connection diagnostics.
+ *
+ * @param slotName replication slot to query
+ * @return slot state when available, or a stable {@code query_failed} status when the query fails
+ */
public Map probeSlot(String slotName) {
Map result = new LinkedHashMap<>();
result.put("slotName", slotName);
@@ -78,11 +96,11 @@ public Map probeSlot(String slotName) {
result.put("flushLagBytes", toLong(row.get("flush_lag_bytes")));
return result;
} catch (DataAccessException e) {
- log.debug("Replication slot probe failed for {}: {}", slotName, e.toString());
+ log.debug("Replication slot probe unavailable: query_failed");
result.put("available", false);
result.put("found", false);
result.put("error", "query_failed");
- result.put("message", safeMessage(e));
+ result.put("message", QUERY_FAILED_MESSAGE);
return result;
}
}
@@ -101,15 +119,4 @@ private static Long toLong(@Nullable Object value) {
return null;
}
}
-
- private static String safeMessage(DataAccessException e) {
- String msg = e.getMostSpecificCause() != null
- ? e.getMostSpecificCause().getMessage()
- : e.getMessage();
- if (msg == null || msg.isBlank()) {
- return e.getClass().getSimpleName();
- }
- // Avoid leaking connection strings if drivers embed them.
- return msg.length() > 200 ? msg.substring(0, 200) + "…" : msg;
- }
}