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; - } } 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://")); } }