Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
ec94fd3
[java] Unify debug-logging switches into one real mechanism
MohabMohie Jul 29, 2026
ff32eab
[java][bidi][devtools] Raise the debug-logger switch from direct-cons…
MohabMohie Jul 29, 2026
a31a75a
[java][bidi][devtools] Document the debug-logging Connection construc…
MohabMohie Jul 29, 2026
4296a51
[java] Repair Debug's externally-removed handler; scope LoggingOption…
MohabMohie Jul 29, 2026
072512c
[java] Decide Debug's level raise off the effective level, not just i…
MohabMohie Jul 29, 2026
4d51cec
Merge branch 'trunk' into debug-logging-mechanism-stage1
MohabMohie Jul 29, 2026
c569d65
[java] Enforce, not just assert, the null-own-level precondition in t…
MohabMohie Jul 29, 2026
325168c
[java] Stop suppressing Selenium debug records from Grid's root handl…
MohabMohie Jul 29, 2026
d270074
[java] Assert the no-duplicate marker appears exactly once on stderr
MohabMohie Jul 29, 2026
59383f6
[java] Force the stubbed SE_DEBUG off before re-syncing Debug in cleanup
MohabMohie Jul 29, 2026
b35594b
Merge branch 'trunk' into debug-logging-mechanism-stage1
MohabMohie Jul 29, 2026
529bb21
Merge branch 'trunk' into debug-logging-mechanism-stage1
MohabMohie Jul 29, 2026
96ec9cf
Fix logging snapshot in RetryRequest: use live Debug.getDebugLogLevel…
MohabMohie Jul 29, 2026
ae54eb0
Move Debug.configureLogger() out of constructors' pre-validation path…
MohabMohie Jul 29, 2026
f9e03d9
Merge branch 'trunk' into debug-logging-mechanism-stage1
MohabMohie Jul 30, 2026
2f5c101
[java] Scope debug property to JUL diagnostics
MohabMohie Aug 1, 2026
1a69c0d
[java] Propagate requested debug level
MohabMohie Aug 1, 2026
65a4e4e
[java] Repair configured debug logger level
MohabMohie Aug 1, 2026
9b8a93e
[java] Restore repaired debug logger level
MohabMohie Aug 1, 2026
d536e3a
[java] Make debug handler inspection atomic
MohabMohie Aug 1, 2026
f87fd10
[java] Remove unused debug handler APIs
MohabMohie Aug 1, 2026
c7ba454
[java] Clarify debug logger configuration
MohabMohie Aug 3, 2026
7372c1e
[java] Clarify debug logger repair behavior
MohabMohie Aug 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions java/src/org/openqa/selenium/bidi/Connection.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
import org.jspecify.annotations.Nullable;
import org.openqa.selenium.Beta;
import org.openqa.selenium.WebDriverException;
import org.openqa.selenium.internal.Debug;
import org.openqa.selenium.internal.Either;
import org.openqa.selenium.internal.Require;
import org.openqa.selenium.json.Json;
Expand Down Expand Up @@ -77,9 +78,23 @@ public class Connection implements Closeable {
private final WebSocket socket;
private final AtomicBoolean underlyingSocketClosed = new AtomicBoolean(false);

/**
* Creates a new BiDi connection to the given URL using the given HTTP client. Before the socket
* opens, the current Selenium debug switches are reflected onto the {@code org.openqa.selenium}
* logger via {@link Debug#configureLogger()}, so connections constructed directly (bypassing
* {@code RemoteWebDriver}/{@code DriverFinder}) still honor {@code -Dselenium.debug} and friends.
*
* @param client the HTTP client used to open the underlying web socket; must not be null
* @param url the URL to open the web socket connection to; must not be null
*/
public Connection(HttpClient client, String url) {
// Reflect the current debug switches before this connection starts logging its wire
// diagnostics at FINE -- callers that construct a Connection directly (never going through
// RemoteWebDriver or DriverFinder) would otherwise never trigger the raise. Idempotent and
// cheap, same pattern as DriverFinder.getBinaryPaths().
Require.nonNull("HTTP client", client);
Require.nonNull("URL to connect to", url);
Debug.configureLogger();

this.client = client;
this.socket = this.client.openSocket(new HttpRequest(GET, url), new Listener());
Expand Down
19 changes: 19 additions & 0 deletions java/src/org/openqa/selenium/devtools/Connection.java
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
import org.jspecify.annotations.Nullable;
import org.openqa.selenium.WebDriverException;
import org.openqa.selenium.devtools.idealized.target.model.SessionID;
import org.openqa.selenium.internal.Debug;
import org.openqa.selenium.internal.Either;
import org.openqa.selenium.internal.Require;
import org.openqa.selenium.json.Json;
Expand Down Expand Up @@ -91,9 +92,27 @@ public Connection(HttpClient client, String url) {
this(client, url, ClientConfig.defaultConfig());
}

/**
* Creates a new CDP connection to the given URL using the given HTTP client and client
* configuration. Before the socket opens, the current Selenium debug switches are reflected onto
* the {@code org.openqa.selenium} logger via {@link Debug#configureLogger()}, so connections
* constructed directly (bypassing {@code RemoteWebDriver}/{@code DriverFinder}) still honor
* {@code -Dselenium.debug} and friends. The deprecated 2-arg constructor delegates here, so this
* single call point covers both.
*
* @param client the HTTP client used to open the underlying web socket; must not be null
* @param url the URL to open the web socket connection to
* @param clientConfig the client configuration to use when opening the connection
*/
public Connection(HttpClient client, String url, ClientConfig clientConfig) {
// Reflect the current debug switches before this connection starts logging its wire
// diagnostics at FINE -- callers that construct a Connection directly (never going through
// RemoteWebDriver or DriverFinder) would otherwise never trigger the raise. Idempotent and
// cheap, same pattern as DriverFinder.getBinaryPaths(). The deprecated 2-arg constructor
// delegates here, so this single call point covers both.
this.client = Require.nonNull("HTTP client", client);
this.wsConfig = wsClientConfig(clientConfig, url);
Debug.configureLogger();
this.socket = this.client.openSocket(new HttpRequest(GET, wsConfig.baseUri()), new Listener());
this.isClosed = new AtomicBoolean();
}
Expand Down
177 changes: 161 additions & 16 deletions java/src/org/openqa/selenium/internal/Debug.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,37 +17,67 @@

package org.openqa.selenium.internal;

import java.util.Arrays;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.ConsoleHandler;
import java.util.logging.Filter;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.SimpleFormatter;
import java.util.logging.StreamHandler;
import org.jspecify.annotations.Nullable;

/** Used to provide information about whether Selenium is running under debug mode. */
public class Debug {

private static final boolean IS_DEBUG;
private static final AtomicBoolean DEBUG_WARNING_LOGGED = new AtomicBoolean(false);
private static final Logger SELENIUM_LOGGER = Logger.getLogger("org.openqa.selenium");
private static boolean loggerConfigured = false;

static {
IS_DEBUG =
Boolean.getBoolean("selenium.debug") || Boolean.getBoolean("selenium.webdriver.verbose");
}
private static boolean loggerConfigured = false;
private static Handler installedHandler = null;
private static Level previousLevel = null;
private static boolean levelRaisedByDebug = false;
private static Level configuredLevel = null;
private static Level levelSetByDebug = null;

private Debug() {
// Utility class
}

/**
* Reports whether Selenium debug logging has been requested via the {@code selenium.debug} or the
* legacy {@code selenium.webdriver.verbose} system property. Read live on every call, so a
* property change made at runtime is reflected immediately.
*
* @return true when either the {@code selenium.debug} or the {@code selenium.webdriver.verbose}
* system property is set to {@code true}; false otherwise
*/
public static boolean isDebugging() {
return IS_DEBUG;
return Boolean.getBoolean("selenium.debug") || Boolean.getBoolean("selenium.webdriver.verbose");
}
Comment thread
MohabMohie marked this conversation as resolved.

/**
* Returns the log level that debug output should be reported at: {@link Level#INFO} when {@link
* #isDebugging()} is true, {@link Level#FINE} otherwise.
*
* @deprecated Individual log statements no longer change what severity they report at based on
* this switch; {@link #configureLogger()} raises the real {@code org.openqa.selenium} logger
* to {@link Level#FINE} instead, which is the ordinary way to see Selenium's debug output.
* Enable it with {@code -Dselenium.debug=true}, the {@code SE_DEBUG} environment variable, or
* directly via {@code Logger.getLogger("org.openqa.selenium").setLevel(Level.FINE)}. This
* method's own behavior is unchanged and kept only for existing call sites still comparing
* against it.
* @return {@link Level#INFO} when debugging is enabled; {@link Level#FINE} otherwise
*/
@Deprecated(forRemoval = true)
public static Level getDebugLogLevel() {
return isDebugging() ? Level.INFO : Level.FINE;
}

static synchronized boolean isHandlerCurrentlyInstalled() {
return installedHandler != null
&& Arrays.asList(SELENIUM_LOGGER.getHandlers()).contains(installedHandler);
}

public static boolean isDebugAll() {
boolean everything = Boolean.parseBoolean(System.getenv("SE_DEBUG"));
if (everything && DEBUG_WARNING_LOGGED.compareAndSet(false, true)) {
Expand All @@ -59,16 +89,131 @@ public static boolean isDebugAll() {
return everything;
}

public static void configureLogger() {
if (!isDebugAll() || loggerConfigured) {
/**
* Computes {@code logger}'s effective level: its own level if set, otherwise the first non-null
* level found walking up its {@link Logger#getParent()} chain, falling back to {@link Level#INFO}
* (JUL's own root default) if none is ever set. {@link Logger} has no single built-in method for
* this, but walking the parent chain is how the JVM itself resolves it internally when deciding
* whether a record is loggable.
*
* @param logger the logger to compute the effective level of
* @return the effective level; never {@code null}
*/
private static Level effectiveLevel(Logger logger) {
for (Logger current = logger; current != null; current = current.getParent()) {
Level level = current.getLevel();
if (level != null) {
return level;
}
}
return Level.INFO;
}

@Nullable
private static Level getRequestedLogLevel() {
if (isDebugAll()) {
return Level.FINE;
}
if (isDebugging()) {
return Level.FINE;
}
return null;
}

/**
* Reflects the current debug switches ({@code -Dselenium.debug=true}, {@code
* -Dselenium.webdriver.verbose=true}, {@code SE_DEBUG}) onto the real {@code org.openqa.selenium}
* logger: raises it to {@link Level#FINE} when it is currently less verbose than {@link
* Level#FINE}; a level already at {@link Level#FINE} or more verbose is left untouched. It also
* attaches a handler Selenium owns, filtered to exclude {@link Level#INFO} and above. Caller-owned
* direct or ancestor handlers that accept {@link Level#FINE} can still receive and print FINE
* records, so FINE output can be duplicated. Calls are no-ops when the requested configuration
* is already consistent, but repair the Selenium-owned handler and FINE loggability after
* external divergence. Reversible: once every switch is off, the
* next call removes exactly the handler this method installed and restores the logger's level to
* what it was before debugging turned on, only when this method was the one that raised it and
* unless something else changed the level in the meantime -- that change is left alone rather
* than clobbered. This can't distinguish an external override that happens to also set exactly
* {@link Level#FINE}: since JUL has no level-change listener to tell the two apart, that specific
* case still restores the pre-debug level. Safe to call from concurrent driver construction.
*
* <p>Cross-binding note: the Python binding does the analogous thing at import time (the {@code
* SE_DEBUG} block at the top of {@code py/selenium/webdriver/__init__.py}): when the {@code
* SE_DEBUG} environment variable is set it puts the {@code selenium} logger at {@code DEBUG} and
* attaches an unfiltered {@code StreamHandler} if the logger has none of its own. Two deliberate
* differences here: Java only raises the level when the logger is currently less verbose than
* {@link Level#FINE} (Python sets {@code DEBUG} unconditionally), and Java's Selenium-owned
* handler is filtered to records below {@link Level#INFO}. Caller-owned direct or ancestor
* handlers accepting {@link Level#FINE} can still print FINE records, so duplicates remain
* possible.
*/
public static synchronized void configureLogger() {
Level requestedLevel = getRequestedLogLevel();
boolean shouldDebug = requestedLevel != null;
// When shouldDebug is on and already configured, only skip if the handler this method
// installed is still actually attached -- something outside this class (e.g. a LogManager
// reset, or unrelated code calling removeHandler() directly) can remove it without ever
// going through configureLogger(), and that divergence must be repaired here rather than
// silently left until the debug switch itself changes.
if (shouldDebug == loggerConfigured
&& (!shouldDebug
|| (isHandlerCurrentlyInstalled()
&& requestedLevel.equals(configuredLevel)
&& effectiveLevel(SELENIUM_LOGGER).intValue() <= requestedLevel.intValue()))) {
return;
}
Comment thread
qodo-code-review[bot] marked this conversation as resolved.

SELENIUM_LOGGER.setLevel(Level.FINE);
if (shouldDebug) {
// Capture the original own level on a genuine off->on transition. A repair call must not
// overwrite this snapshot with the level it is repairing.
if (!loggerConfigured) {
configuredLevel = requestedLevel;
previousLevel = SELENIUM_LOGGER.getLevel();
levelRaisedByDebug = false;
levelSetByDebug = null;
}

if (effectiveLevel(SELENIUM_LOGGER).intValue() > requestedLevel.intValue()) {
SELENIUM_LOGGER.setLevel(requestedLevel);
levelSetByDebug = requestedLevel;
levelRaisedByDebug = true;
}

configuredLevel = requestedLevel;
if (isHandlerCurrentlyInstalled()) {
installedHandler.setLevel(requestedLevel);
} else {
Handler handler = new ConsoleHandler();
handler.setLevel(requestedLevel);
Filter belowInfo = record -> record.getLevel().intValue() < Level.INFO.intValue();
handler.setFilter(belowInfo);
SELENIUM_LOGGER.addHandler(handler);
installedHandler = handler;
}
} else {
// installedHandler can already be null here if it was removed externally and debugging
// turned off before any repair call ever ran -- Logger.removeHandler(null) throws NPE per
// its javadoc, so guard against that.
if (installedHandler != null) {
SELENIUM_LOGGER.removeHandler(installedHandler);
installedHandler.close();
installedHandler = null;
}
// Restore only when Debug itself raised the level AND nothing else changed it since. The
// equality guard keeps the existing "external override while debugging" protection;
// levelRaisedByDebug additionally covers the case where Debug never touched the level at
// all and so has nothing to restore.
if (levelRaisedByDebug
&& levelSetByDebug != null
&& levelSetByDebug.equals(SELENIUM_LOGGER.getLevel())) {
SELENIUM_LOGGER.setLevel(previousLevel);
}
levelRaisedByDebug = false;
previousLevel = null;
configuredLevel = null;
levelSetByDebug = null;
}

StreamHandler handler = new StreamHandler(System.err, new SimpleFormatter());
handler.setLevel(Level.FINE);
SELENIUM_LOGGER.addHandler(handler);
loggerConfigured = true;
loggerConfigured = shouldDebug;
}
}
27 changes: 25 additions & 2 deletions java/src/org/openqa/selenium/remote/RemoteWebDriver.java
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,15 @@ public class RemoteWebDriver
PrintsPage,
TakesScreenshot {

// Guarantees (JLS 12.4.2) that debug logging is configured before ANY subclass constructor
// body runs -- including argument expressions passed to a subclass's own super(...) call, e.g.
// ChromeDriver/FirefoxDriver's DriverFinder/SeleniumManager discovery, which logs at FINE
// before super(...) is ever reached. configureLogger() is idempotent, so this and the call in
// the canonical instance constructor below are both safe to keep: this one covers logging that
// happens before an instance exists, the other picks up a property changed after this class
// already loaded.
static {
org.openqa.selenium.internal.Debug.configureLogger();
Debug.configureLogger();
}

private static final Logger LOG = Logger.getLogger(RemoteWebDriver.class.getName());
Expand Down Expand Up @@ -203,14 +210,30 @@ public RemoteWebDriver(CommandExecutor executor, Capabilities capabilities) {
this(executor, capabilities, ClientConfig.defaultConfig());
}

/**
* Creates a new driver that runs its commands through the given executor, requesting a new
* session with the given capabilities. Before the session starts, the current Selenium debug
* switches are reflected onto the {@code org.openqa.selenium} logger via {@link
* Debug#configureLogger()}, so a debug property changed at runtime takes effect for every
* driver constructed afterwards.
*
* @param executor the command executor used to communicate with the remote end; must not be
* null
* @param capabilities the capabilities requested for the new session; null is treated as an
* empty set of capabilities
* @param clientConfig the HTTP client configuration for the connection; must not be null
*/
public RemoteWebDriver(
CommandExecutor executor, Capabilities capabilities, ClientConfig clientConfig) {
// Instance-time (not class-load-time) so a property change made after this class has already
// loaded still takes effect for drivers constructed afterwards.
this.clientConfig = Require.nonNull("Client config", clientConfig);
this.executor = Require.nonNull("Command executor", executor);
Debug.configureLogger();
this.capabilities = requireNonNullElseGet(capabilities, () -> new ImmutableCapabilities());
Comment thread
qodo-code-review[bot] marked this conversation as resolved.

try {
startSession(capabilities);
startSession(this.capabilities);
} catch (RuntimeException e) {
try {
quit();
Expand Down
6 changes: 2 additions & 4 deletions java/src/org/openqa/selenium/remote/http/RetryRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,10 @@
import java.net.ConnectException;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.openqa.selenium.internal.Debug;

public class RetryRequest implements Filter {

private static final Logger LOG = Logger.getLogger(RetryRequest.class.getName());
private static final Level LOG_LEVEL = Debug.getDebugLogLevel();

private static final int RETRIES_ON_CONNECTION_FAILURE = 3;
private static final int RETRIES_ON_SERVER_ERROR = 2;
Expand All @@ -50,7 +48,7 @@ public HttpHandler apply(HttpHandler next) {

// must be a connection failure and check whether we have retries left for this
if (isConnectionFailure && i < RETRIES_ON_CONNECTION_FAILURE) {
LOG.log(LOG_LEVEL, "Retry #" + (i + 1) + " on ConnectException", ex);
LOG.log(Level.FINE, "Retry #" + (i + 1) + " on ConnectException", ex);
continue;
}

Expand All @@ -65,7 +63,7 @@ public HttpHandler apply(HttpHandler next) {

// must be a server error and check whether we have retries left for this
if (isServerError && i < RETRIES_ON_SERVER_ERROR) {
LOG.log(LOG_LEVEL, "Retry #" + (i + 1) + " on ServerError: " + response.getStatus());
LOG.log(Level.FINE, "Retry #" + (i + 1) + " on ServerError: " + response.getStatus());
continue;
}

Expand Down
1 change: 1 addition & 0 deletions java/test/org/openqa/selenium/devtools/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ load("//java:defs.bzl", "JUNIT5_DEPS", "java_library", "java_selenium_test_suite
SMALL_TESTS = [
"CdpEndpointFinderTest.java",
"CdpVersionFinderTest.java",
"ConnectionTest.java",
]

java_test_suite(
Expand Down
Loading