diff --git a/README.md b/README.md index d2b5ade..6ccafb2 100644 --- a/README.md +++ b/README.md @@ -88,8 +88,33 @@ ZeroBounceSDK.getInstance().initialize("", timeoutInMillis); ``` +### Controlling SDK logging + +The SDK is silent by default so it never emits Personally Identifiable Information (PII) unless you +explicitly opt in. To integrate the SDK with your application's logging framework, register a +`ZBLogger` implementation before issuing any API calls. The helper class `ZBLoggers` adapts +`java.util.logging` (JUL) out of the box: + +```java +import com.zerobounce.ZBLoggers; + +ZeroBounceSDK.setLogger( + ZBLoggers.jul(java.util.logging.Logger.getLogger("ZeroBounceSDK")) +); + +// Optional: enable verbose payload logging for troubleshooting only. +ZeroBounceSDK.setLogPayloads(true); +``` + +Passing `null` to `ZeroBounceSDK.setLogger(...)` resets the logger to a no-op implementation, which +disables SDK logging again. + + ## Examples +> **Note:** The snippets below print responses for demonstration purposes. Avoid logging raw API +> data that may contain PII in production systems. + Then you can use any of the SDK methods, for example: * ##### Validate an email address diff --git a/documentation.md b/documentation.md index cc6909d..84489c2 100644 --- a/documentation.md +++ b/documentation.md @@ -79,8 +79,33 @@ ZeroBounceSDK.getInstance().initialize("", timeoutInMillis); ``` +#### Controlling SDK logging + +The SDK is silent by default so it never emits Personally Identifiable Information (PII) unless you +explicitly opt in. To integrate the SDK with your application's logging framework, register a +`ZBLogger` implementation before issuing any API calls. The helper class `ZBLoggers` adapts +`java.util.logging` (JUL) out of the box: + +```java +import com.zerobounce.ZBLoggers; + +ZeroBounceSDK.setLogger( + ZBLoggers.jul(java.util.logging.Logger.getLogger("ZeroBounceSDK")) +); + +// Optional: enable verbose payload logging for troubleshooting only. +ZeroBounceSDK.setLogPayloads(true); +``` + +Passing `null` to `ZeroBounceSDK.setLogger(...)` resets the logger to a no-op implementation, which +disables SDK logging again. + + #### Examples +> **Note:** The snippets below print responses for demonstration purposes. Avoid logging raw API +> data that may contain PII in production systems. + Then you can use any of the SDK methods, for example: * ####### Validate an email address diff --git a/documentation_es.md b/documentation_es.md index b8ab39f..907719d 100644 --- a/documentation_es.md +++ b/documentation_es.md @@ -76,8 +76,32 @@ ZeroBounceSDK.getInstance().initialize(""); ZeroBounceSDK.getInstance().initialize("", timeoutInMillis); ``` +#### Control del registro del SDK + +El SDK permanece silencioso de forma predeterminada, por lo que nunca emite información personal +identificable (PII) a menos que habilites el registro explícitamente. Para integrarlo con el sistema +de registro de tu aplicación, registra una implementación de `ZBLogger` antes de realizar llamadas a +la API. La clase auxiliar `ZBLoggers` adapta `java.util.logging` (JUL) sin agregar dependencias: + +```java +import com.zerobounce.ZBLoggers; + +ZeroBounceSDK.setLogger( + ZBLoggers.jul(java.util.logging.Logger.getLogger("ZeroBounceSDK")) +); + +// Opcional: habilita el registro detallado de cargas solo para depuración. +ZeroBounceSDK.setLogPayloads(true); +``` + +Pasar `null` a `ZeroBounceSDK.setLogger(...)` restablece el registrador a una implementación que no +realiza ninguna acción, lo cual vuelve a desactivar el registro del SDK. + #### Ejemplos +> **Nota:** Los siguientes fragmentos imprimen respuestas con fines demostrativos. Evita registrar +> datos sin filtrar de la API en entornos de producción, ya que pueden contener PII. + A continuación, puedes utilizar cualquiera de los métodos del SDK. Por ejemplo: * ####### Validar una dirección de correo electrónico diff --git a/zero-bounce-sdk/src/main/java/com/zerobounce/ZBLogger.java b/zero-bounce-sdk/src/main/java/com/zerobounce/ZBLogger.java new file mode 100644 index 0000000..33b6b1f --- /dev/null +++ b/zero-bounce-sdk/src/main/java/com/zerobounce/ZBLogger.java @@ -0,0 +1,16 @@ +package com.zerobounce; + +/** + * Minimal logging abstraction used by the ZeroBounce SDK. + */ +public interface ZBLogger { + default void debug(String msg) {} + default void info(String msg) {} + default void warn(String msg) {} + default void error(String msg, Throwable t) {} + + static ZBLogger noop() { + return new ZBLogger() { + }; + } +} diff --git a/zero-bounce-sdk/src/main/java/com/zerobounce/ZBLoggers.java b/zero-bounce-sdk/src/main/java/com/zerobounce/ZBLoggers.java new file mode 100644 index 0000000..4629b33 --- /dev/null +++ b/zero-bounce-sdk/src/main/java/com/zerobounce/ZBLoggers.java @@ -0,0 +1,38 @@ +package com.zerobounce; + +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Helper factory methods for adapting common logger implementations to {@link ZBLogger}. + */ +public final class ZBLoggers { + private ZBLoggers() { + } + + public static ZBLogger jul(Logger logger) { + return new ZBLogger() { + @Override + public void debug(String msg) { + if (logger.isLoggable(Level.FINE)) { + logger.fine(msg); + } + } + + @Override + public void info(String msg) { + logger.info(msg); + } + + @Override + public void warn(String msg) { + logger.warning(msg); + } + + @Override + public void error(String msg, Throwable t) { + logger.log(Level.SEVERE, msg, t); + } + }; + } +} diff --git a/zero-bounce-sdk/src/main/java/com/zerobounce/ZeroBounceSDK.java b/zero-bounce-sdk/src/main/java/com/zerobounce/ZeroBounceSDK.java index 4793b29..866cb4d 100644 --- a/zero-bounce-sdk/src/main/java/com/zerobounce/ZeroBounceSDK.java +++ b/zero-bounce-sdk/src/main/java/com/zerobounce/ZeroBounceSDK.java @@ -33,6 +33,8 @@ public class ZeroBounceSDK { private static volatile ZeroBounceSDK instance; + private static volatile ZBLogger logger = ZBLogger.noop(); + private static volatile boolean logPayloads = false; public static ZeroBounceSDK getInstance() { // The implementation below uses a double-check locking mechanism to boost performance while keeping the @@ -48,6 +50,24 @@ public static ZeroBounceSDK getInstance() { return instance; } + /** + * Sets a custom logger that will receive log messages emitted by the SDK. + * + * @param logger the logger to use; when {@code null} logging is disabled + */ + public static void setLogger(@Nullable ZBLogger logger) { + ZeroBounceSDK.logger = logger != null ? logger : ZBLogger.noop(); + } + + /** + * Enables or disables logging of HTTP payload content. + * + * @param enabled {@code true} to include payloads in debug logs + */ + public static void setLogPayloads(boolean enabled) { + logPayloads = enabled; + } + final String apiBaseUrl = "https://api.zerobounce.net/v2"; private final String bulkApiBaseUrl = "https://bulkapi.zerobounce.net/v2"; private final String bulkApiScoringBaseUrl = "https://bulkapi.zerobounce.net/v2/scoring"; @@ -332,7 +352,7 @@ private void _sendFile( @NotNull OnErrorCallback errorCallback) throws ZBException { String urlPath = (scoring ? bulkApiScoringBaseUrl : bulkApiBaseUrl) + "/sendfile"; - System.out.println("ZeroBounceSDK::sendFile urlPath=" + urlPath); + logDebug("ZeroBounceSDK::sendFile urlPath=" + urlPath); if (emailAddressColumnIndex < 1) { throw new ZBException("Index for emailAddressColumnIndex must start from 1."); @@ -398,7 +418,7 @@ private void _sendFile( HttpEntity responseEntity = response.getEntity(); int status = response.getStatusLine().getStatusCode(); - System.out.println("ZeroBounceSDK::sendFile status: " + status); + logDebug("ZeroBounceSDK::sendFile status: " + status); StringBuilder content = new StringBuilder(); try (BufferedReader in = new BufferedReader(new InputStreamReader(responseEntity.getContent()))) { @@ -410,7 +430,7 @@ private void _sendFile( String rsp = content.toString(); - System.out.println("ZeroBounceSDK::sendFile rsp=" + rsp); + logDebug("ZeroBounceSDK::sendFile rsp=" + rsp); if (status > 299) { ErrorResponse errorResponse = ErrorResponse.parseError(rsp); @@ -726,7 +746,7 @@ private void sendRequest( @NotNull OnSuccessCallback successCallback, @NotNull OnErrorCallback errorCallback) { try { - System.out.println("ZeroBounceSDK::sendRequest urlPath=" + urlPath); + logDebug("ZeroBounceSDK::sendRequest preparing request: " + urlPath); URIBuilder ub = new URIBuilder(urlPath); if (queryParameters != null) { @@ -740,9 +760,12 @@ private void sendRequest( con.setRequestProperty("Accept", "application/json"); // con.setRequestProperty("Content-Type", "application/json"); + String httpMethod; if (body == null) { + httpMethod = "GET"; con.setRequestMethod("GET"); } else { + httpMethod = "POST"; con.setRequestMethod("POST"); con.setDoOutput(true); @@ -757,10 +780,12 @@ private void sendRequest( } } + logDebug("ZeroBounceSDK::sendRequest executing " + httpMethod + " " + urlPath); + con.setConnectTimeout(timeoutInMillis); int status = con.getResponseCode(); - System.out.println("ZeroBounceSDK::sendRequest status: " + status); + logDebug("ZeroBounceSDK::sendRequest status: " + status); Reader streamReader; if (status > 299) { streamReader = new InputStreamReader(con.getErrorStream()); @@ -779,7 +804,7 @@ private void sendRequest( con.disconnect(); String rsp = content.toString(); - System.out.println("ZeroBounceSDK::sendRequest rsp=" + rsp); + logPayload("ZeroBounceSDK::sendRequest rsp=" + rsp); if (status > 299) { ErrorResponse errorResponse = ErrorResponse.parseError(rsp); @@ -789,13 +814,27 @@ private void sendRequest( successCallback.onSuccess(response); } } catch (Exception e) { - e.printStackTrace(); + logError("ZeroBounceSDK::sendRequest failed", e); ErrorResponse errorResponse = ErrorResponse.parseError(e.getMessage()); errorCallback.onError(errorResponse); } } + private static void logDebug(String message) { + logger.debug(message); + } + + private static void logPayload(String message) { + if (logPayloads) { + logger.debug(message); + } + } + + private static void logError(String message, Exception exception) { + logger.error(message, exception); + } + /** * A class that can be used to configure the [sendFile] request. */ diff --git a/zero-bounce-sdk/src/test/java/com/zerobounce/ZeroBounceSDKTest.java b/zero-bounce-sdk/src/test/java/com/zerobounce/ZeroBounceSDKTest.java index 05f5adb..df6405f 100644 --- a/zero-bounce-sdk/src/test/java/com/zerobounce/ZeroBounceSDKTest.java +++ b/zero-bounce-sdk/src/test/java/com/zerobounce/ZeroBounceSDKTest.java @@ -25,6 +25,7 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.PrintStream; import java.net.HttpURLConnection; import java.net.URISyntaxException; import java.net.URL; @@ -1015,6 +1016,81 @@ public void getActivityData_ReturnsError() throws Exception { }); } + @Test + public void defaultLoggerEmitsNoConsoleOutput() throws Exception { + String responseJson = "{\"Credits\":2375323}"; + String urlPath = getEncodedUrl( + "https://api.zerobounce.net/v2/getcredits", + new HashMap() { + { + put("api_key", API_KEY); + } + } + ); + + PrintStream originalOut = System.out; + ByteArrayOutputStream capturedOut = new ByteArrayOutputStream(); + System.setOut(new PrintStream(capturedOut)); + + ZeroBounceSDK.setLogger(null); + ZeroBounceSDK.setLogPayloads(false); + + try { + mockRequest(urlPath, 200, responseJson, ""); + ZeroBounceSDK.getInstance().getCredits( + response -> { + }, errorResponse -> fail(errorResponse.toString())); + } finally { + System.setOut(originalOut); + ZeroBounceSDK.setLogger(null); + ZeroBounceSDK.setLogPayloads(false); + } + + assertEquals("", capturedOut.toString()); + } + + @Test + public void payloadLoggingUsesConfiguredLoggerOnly() throws Exception { + String responseJson = "{\"Credits\":2375323}"; + String urlPath = getEncodedUrl( + "https://api.zerobounce.net/v2/getcredits", + new HashMap() { + { + put("api_key", API_KEY); + } + } + ); + + List debugMessages = new ArrayList<>(); + ZBLogger testLogger = new ZBLogger() { + @Override + public void debug(String msg) { + debugMessages.add(msg); + } + }; + + PrintStream originalOut = System.out; + ByteArrayOutputStream capturedOut = new ByteArrayOutputStream(); + System.setOut(new PrintStream(capturedOut)); + + ZeroBounceSDK.setLogger(testLogger); + ZeroBounceSDK.setLogPayloads(true); + + try { + mockRequest(urlPath, 200, responseJson, ""); + ZeroBounceSDK.getInstance().getCredits( + response -> { + }, errorResponse -> fail(errorResponse.toString())); + } finally { + System.setOut(originalOut); + ZeroBounceSDK.setLogger(null); + ZeroBounceSDK.setLogPayloads(false); + } + + assertTrue(debugMessages.stream().anyMatch(message -> message.contains("ZeroBounceSDK::sendRequest rsp=" + responseJson))); + assertEquals("", capturedOut.toString()); + } + private String getEncodedUrl( String urlPath, @Nullable Map queryParameters