diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/impl/OkHttpServices.java b/marklogic-client-api/src/main/java/com/marklogic/client/impl/OkHttpServices.java index f3cf56ba9..059a97aca 100644 --- a/marklogic-client-api/src/main/java/com/marklogic/client/impl/OkHttpServices.java +++ b/marklogic-client-api/src/main/java/com/marklogic/client/impl/OkHttpServices.java @@ -30,6 +30,7 @@ import com.marklogic.client.query.QueryManager.QueryView; import com.marklogic.client.semantics.*; import com.marklogic.client.util.EditableNamespaceContext; +import com.marklogic.client.util.LoggingUtil; import com.marklogic.client.util.RequestLogger; import com.marklogic.client.util.RequestParameters; import jakarta.mail.BodyPart; @@ -79,24 +80,6 @@ public class OkHttpServices implements RESTServices { static final private Logger logger = LoggerFactory.getLogger(OkHttpServices.class); - /** - * Controls the verbosity of OkHttp HTTP traffic logging. Accepted values: {@code NONE}, {@code BASIC}, - * {@code HEADERS}, {@code BODY}. - * - *
Security warning: {@code HEADERS} and {@code BODY} levels log HTTP request and response - * headers and/or bodies, which may contain MarkLogic credentials, OAuth/SAML tokens, or sensitive document - * content. These levels must never be enabled in production environments. {@code Authorization} and - * {@code x-auth-token} header values are automatically redacted from log output, but body content (including - * MarkLogic document data) is not redacted. - */ - private static final String OKHTTP_LOGGINGINTERCEPTOR_LEVEL = "com.marklogic.client.okhttp.httplogginginterceptor.level"; - - /** - * Controls where OkHttp log output is written. Accepted values: {@code LOGGER} (SLF4J debug logger), - * {@code STDERR}, or leave unset for stdout. - */ - private static final String OKHTTP_LOGGINGINTERCEPTOR_OUTPUT = "com.marklogic.client.okhttp.httplogginginterceptor.output"; - private static final String DOCUMENT_URI_PREFIX = "/documents?uri="; static final private int DELAY_FLOOR = 125; @@ -221,7 +204,7 @@ private OkHttpClient connect(ConnectionConfig config) { OkHttpClient.Builder clientBuilder = OkHttpUtil.newOkHttpClientBuilder(config.host, config.securityContext, config.clientConfigurators); Properties props = System.getProperties(); - if (props.containsKey(OKHTTP_LOGGINGINTERCEPTOR_LEVEL)) { + if (props.containsKey(LoggingUtil.OKHTTP_NETWORK_LEVEL)) { configureOkHttpLogging(clientBuilder, props); } this.configureDelayAndRetry(props); @@ -239,17 +222,17 @@ private OkHttpClient connect(ConnectionConfig config) { * never be enabled in production environments. */ private void configureOkHttpLogging(OkHttpClient.Builder clientBuilder, Properties props) { - final boolean useLogger = "LOGGER".equalsIgnoreCase(props.getProperty(OKHTTP_LOGGINGINTERCEPTOR_OUTPUT)); - final boolean useStdErr = "STDERR".equalsIgnoreCase(props.getProperty(OKHTTP_LOGGINGINTERCEPTOR_OUTPUT)); + final boolean useLogger = "LOGGER".equalsIgnoreCase(props.getProperty(LoggingUtil.OKHTTP_NETWORK_OUTPUT)); + final boolean useStdErr = "STDERR".equalsIgnoreCase(props.getProperty(LoggingUtil.OKHTTP_NETWORK_OUTPUT)); HttpLoggingInterceptor networkInterceptor = new HttpLoggingInterceptor(new RedactingHttpLogger(useLogger, useStdErr)); - if ("BASIC".equalsIgnoreCase(props.getProperty(OKHTTP_LOGGINGINTERCEPTOR_LEVEL))) { + if ("BASIC".equalsIgnoreCase(props.getProperty(LoggingUtil.OKHTTP_NETWORK_LEVEL))) { networkInterceptor = networkInterceptor.setLevel(HttpLoggingInterceptor.Level.BASIC); - } else if ("BODY".equalsIgnoreCase(props.getProperty(OKHTTP_LOGGINGINTERCEPTOR_LEVEL))) { + } else if ("BODY".equalsIgnoreCase(props.getProperty(LoggingUtil.OKHTTP_NETWORK_LEVEL))) { networkInterceptor = networkInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY); - } else if ("HEADERS".equalsIgnoreCase(props.getProperty(OKHTTP_LOGGINGINTERCEPTOR_LEVEL))) { + } else if ("HEADERS".equalsIgnoreCase(props.getProperty(LoggingUtil.OKHTTP_NETWORK_LEVEL))) { networkInterceptor = networkInterceptor.setLevel(HttpLoggingInterceptor.Level.HEADERS); - } else if ("NONE".equalsIgnoreCase(props.getProperty(OKHTTP_LOGGINGINTERCEPTOR_LEVEL))) { + } else if ("NONE".equalsIgnoreCase(props.getProperty(LoggingUtil.OKHTTP_NETWORK_LEVEL))) { networkInterceptor = networkInterceptor.setLevel(HttpLoggingInterceptor.Level.NONE); } clientBuilder.addNetworkInterceptor(networkInterceptor); diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/RedactingHttpLogger.java b/marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/RedactingHttpLogger.java index 740d22fc3..ed98dfe58 100644 --- a/marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/RedactingHttpLogger.java +++ b/marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/RedactingHttpLogger.java @@ -3,6 +3,7 @@ */ package com.marklogic.client.impl.okhttp; +import com.marklogic.client.util.LoggingUtil; import okhttp3.logging.HttpLoggingInterceptor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -14,6 +15,11 @@ * and {@code x-auth-token} header values before writing each log message to * the configured output target (SLF4J logger, stderr, or stdout). * + *
When the output target is {@code LOGGER}, messages are written to the + * SLF4J logger category {@link com.marklogic.client.util.LoggingUtil#OKHTTP_NETWORK_LOGGER} at + * {@code INFO} level. Users can enable or disable this output without + * touching DEBUG logging for unrelated client internals. + * *
Security warning: Even with header redaction, * {@code BODY}-level logging writes full HTTP request and response bodies to * the log target. This may include MarkLogic document content and must never @@ -30,7 +36,7 @@ public class RedactingHttpLogger implements HttpLoggingInterceptor.Logger { Pattern.compile("(?i)(authorization|x-auth-token):.*"); static final String SENSITIVE_HEADER_REDACTION_REPLACEMENT = "$1: [REDACTED]"; - private static final Logger logger = LoggerFactory.getLogger(RedactingHttpLogger.class); + private static final Logger logger = LoggerFactory.getLogger(LoggingUtil.OKHTTP_NETWORK_LOGGER); private final boolean useLogger; private final boolean useStdErr; @@ -44,7 +50,7 @@ public RedactingHttpLogger(boolean useLogger, boolean useStdErr) { public void log(String message) { String redacted = redactSensitiveHeaders(message); if (useLogger) { - logger.debug(redacted); + logger.info(redacted); } else if (useStdErr) { System.err.println(redacted); } else { diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/util/LoggingUtil.java b/marklogic-client-api/src/main/java/com/marklogic/client/util/LoggingUtil.java new file mode 100644 index 000000000..63b58f479 --- /dev/null +++ b/marklogic-client-api/src/main/java/com/marklogic/client/util/LoggingUtil.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2010-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved. + */ +package com.marklogic.client.util; + +/** + * Provides public constants for configuring MarkLogic Java Client logging behaviour. + * + *
The Java Client uses OkHttp for all HTTP communication with MarkLogic. Optional network + * traffic logging is controlled by two Java system properties: + * + *
When the output is set to {@code LOGGER}, log lines are written to the SLF4J logger + * category {@link #OKHTTP_NETWORK_LOGGER} at {@code INFO} level. Configure this category + * in your logging framework to enable network diagnostics without enabling debug logging + * for unrelated client internals. For example, in {@code logback.xml}: + * + *
{@code
+ *
+ * }
+ *
+ * Or in a Spring Boot {@code application.properties} file: + * + *
{@code
+ * logging.level.marklogic.okhttp.network=INFO
+ * }
+ *
+ * Security warning: {@code Authorization} and {@code x-auth-token} header + * values are automatically redacted from all log output. However, {@code HEADERS} and + * {@code BODY} levels may expose other sensitive data including MarkLogic document content. + * Never enable {@code HEADERS} or {@code BODY} in a production environment. + * + * @since 8.2.0 + */ +public final class LoggingUtil { + + /** + * Name of the Java system property that controls the verbosity of OkHttp network logging. + * Value: {@code "marklogic.okhttp.network.level"} + * + *
Accepted values (case-insensitive): {@code BASIC}, {@code HEADERS}, {@code BODY}, + * {@code NONE}. Setting this property to any recognised value activates the network + * logging interceptor. + * + *
Security warning: {@code HEADERS} and {@code BODY} levels log HTTP + * request and response headers and/or bodies, which may contain MarkLogic credentials, + * OAuth/SAML tokens, or sensitive document content. These levels must never be + * enabled in production environments. {@code Authorization} and {@code x-auth-token} + * header values are automatically redacted, but body content is not. + */ + public static final String OKHTTP_NETWORK_LEVEL = "marklogic.okhttp.network.level"; + + /** + * Name of the Java system property that controls where OkHttp network log lines are written. + * Value: {@code "marklogic.okhttp.network.output"} + * + *
Accepted values (case-insensitive): + *
Configure this category at {@code INFO} level in your logging framework to enable + * network traffic logging. Example {@code logback.xml} configuration: + *
{@code
+ *
+ * }
+ *
+ * Example Spring Boot {@code application.properties}: + *
{@code
+ * logging.level.marklogic.okhttp.network=INFO
+ * }
+ */
+ public static final String OKHTTP_NETWORK_LOGGER = "marklogic.okhttp.network";
+
+ private LoggingUtil() {
+ }
+}
diff --git a/marklogic-client-api/src/test/java/com/marklogic/client/impl/okhttp/RedactingHttpLoggerTest.java b/marklogic-client-api/src/test/java/com/marklogic/client/impl/okhttp/RedactingHttpLoggerTest.java
index be72165a4..3872ff60b 100644
--- a/marklogic-client-api/src/test/java/com/marklogic/client/impl/okhttp/RedactingHttpLoggerTest.java
+++ b/marklogic-client-api/src/test/java/com/marklogic/client/impl/okhttp/RedactingHttpLoggerTest.java
@@ -3,12 +3,19 @@
*/
package com.marklogic.client.impl.okhttp;
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.read.ListAppender;
+import com.marklogic.client.util.LoggingUtil;
import org.junit.jupiter.api.Test;
+import org.slf4j.LoggerFactory;
import static org.junit.jupiter.api.Assertions.*;
/**
- * Unit tests for {@link RedactingHttpLogger#redactSensitiveHeaders(String)}.
+ * Unit tests for {@link RedactingHttpLogger}.
+ * Covers header redaction ({@link RedactingHttpLogger#redactSensitiveHeaders(String)})
+ * and LOGGER-mode output category and level behaviour.
* No MarkLogic instance or network connectivity is required.
*/
class RedactingHttpLoggerTest {
@@ -88,4 +95,67 @@ void multiLineMessageRedactsOnlyAuthLines() {
assertFalse(result.contains("dXNlcjpwYXNzd29yZA=="));
assertTrue(result.contains("Content-Length: 42"));
}
+
+ @Test
+ void loggerModeWritesToNetworkLoggerCategoryAtInfoLevel() {
+ ch.qos.logback.classic.Logger networkLogger =
+ (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(LoggingUtil.OKHTTP_NETWORK_LOGGER);
+ Level originalLevel = networkLogger.getLevel();
+ boolean originalAdditive = networkLogger.isAdditive();
+ networkLogger.setAdditive(false);
+ networkLogger.setLevel(Level.INFO);
+
+ ListAppender