From 9b1b76c8884147699bb29cc422af2b13fa4836b4 Mon Sep 17 00:00:00 2001 From: Nick Shanin Date: Sun, 4 Oct 2026 10:26:40 -0600 Subject: [PATCH 1/4] SOLR-18317: Fix the Admin UI logging caller so standalone mode does not send nodes=all --- changelog/unreleased/SOLR-18317.yml | 7 ++++++ .../web/js/angular/controllers/logging.js | 25 ++++++++++++++++--- solr/webapp/web/js/angular/services.js | 10 +++++--- 3 files changed, 35 insertions(+), 7 deletions(-) create mode 100644 changelog/unreleased/SOLR-18317.yml diff --git a/changelog/unreleased/SOLR-18317.yml b/changelog/unreleased/SOLR-18317.yml new file mode 100644 index 000000000000..03d74711b8e4 --- /dev/null +++ b/changelog/unreleased/SOLR-18317.yml @@ -0,0 +1,7 @@ +title: Admin UI no longer sends nodes=all in standalone mode. +type: fixed +authors: + - name: Nick Shanin +links: + - name: SOLR-18317 + url: https://issues.apache.org/jira/browse/SOLR-18317 diff --git a/solr/webapp/web/js/angular/controllers/logging.js b/solr/webapp/web/js/angular/controllers/logging.js index 2011bf9f7d56..a3529c3d420f 100644 --- a/solr/webapp/web/js/angular/controllers/logging.js +++ b/solr/webapp/web/js/angular/controllers/logging.js @@ -165,9 +165,28 @@ solrAdminApp.controller('LoggingController', // broadcast the level change to every live node. The v2 NodeLoggingApis endpoint is // single-node only until SOLR-16738 wires it up to the new V2SolrRequestBasedProxy (see the // TODO in NodeLogging.java). Move this to LoggingV2.modifyLocalLogLevel once that lands. - Logging.setLevel({set: setString}, function(data) { - $scope.refresh(); - }); + var doSetLevel = function() { + var params = {set: setString}; + // "nodes=all" only makes sense in SolrCloud mode; in standalone (user-managed) mode the + // request is handled locally, so the param is omitted entirely. + if ($scope.isCloudEnabled) { + params.nodes = "all"; + } + Logging.setLevel(params, function(data) { + $scope.refresh(); + }); + }; + // isCloudEnabled populates asynchronously via resetMenu(), so wait for it to settle + // before deciding whether to broadcast (same pattern as in paramsets.js). + if ($scope.isCloudEnabled === undefined) { + var unwatchCloudEnabled = $scope.$watch('isCloudEnabled', function(value) { + if (value === undefined) return; + unwatchCloudEnabled(); + doSetLevel(); + }); + } else { + doSetLevel(); + } }; $scope.refresh(); diff --git a/solr/webapp/web/js/angular/services.js b/solr/webapp/web/js/angular/services.js index 6e10997f743a..e6fa6b37cfaa 100644 --- a/solr/webapp/web/js/angular/services.js +++ b/solr/webapp/web/js/angular/services.js @@ -189,11 +189,13 @@ solrAdminServices.factory('Metrics', }]) .factory('Logging', ['$resource', function($resource) { - // This v1 factory only covers "setLevel", which needs the "nodes=all" broadcast-to-every-node - // behavior that the v2 NodeLoggingApis endpoint doesn't support yet (see SOLR-16738). Retire - // this factory once setLevel moves to LoggingV2. + // This v1 factory only covers "setLevel", which in SolrCloud mode needs the "nodes=all" + // broadcast-to-every-node behavior that the v2 NodeLoggingApis endpoint doesn't support yet + // (see SOLR-16738). The caller (LoggingLevelController) passes nodes:'all' only when + // SolrCloud is enabled; in standalone mode the param is omitted entirely. Retire this + // factory once setLevel moves to LoggingV2. return $resource('admin/info/logging', {'wt':'json', '_':Date.now()}, { - "setLevel": {params: {nodes:'all'}} + "setLevel": {} }); }]) .factory('Zookeeper', From 9473910c5406b857daa9a83c5031fae088c9f9c2 Mon Sep 17 00:00:00 2001 From: Nick Shanin Date: Sun, 4 Oct 2026 10:26:41 -0600 Subject: [PATCH 2/4] SOLR-18317: Settle the UI mode on system-info failure --- solr/webapp/web/js/angular/app.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/solr/webapp/web/js/angular/app.js b/solr/webapp/web/js/angular/app.js index 1bbaa4b9163e..f61b4e8d51c1 100644 --- a/solr/webapp/web/js/angular/app.js +++ b/solr/webapp/web/js/angular/app.js @@ -554,7 +554,14 @@ solrAdminApp.controller('MainController', function($scope, $route, $rootScope, $ SystemV2.getNodeSystemInfo({}, function(error, data, response) { $timeout(function() { - if (error) { ApiErrorHandler.handle(response); return; } + if (error) { + ApiErrorHandler.handle(response); + // The mode could not be determined. Treat the node as standalone so that + // pages waiting for isCloudEnabled to settle (e.g. the logging page) fall + // back to local-only behavior instead of waiting forever. + $scope.isCloudEnabled = false; + return; + } $scope.isCloudEnabled = data.mode.match( /solrcloud/i ); $scope.usersPermissions = data.security.permissions; $scope.isSecurityEnabled = data.security.authenticationPlugin != null; From 923cbbaa81145dc4e691fe2a97f9367d24961203 Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Mon, 5 Oct 2026 09:12:37 -0400 Subject: [PATCH 3/4] Lets add a test for standalone I think some other changes may cause a regression, so lets add some testing. --- .../webapp/AdminUiLoggingStandaloneTest.java | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 solr/webapp/src/test/org/apache/solr/webapp/AdminUiLoggingStandaloneTest.java diff --git a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiLoggingStandaloneTest.java b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiLoggingStandaloneTest.java new file mode 100644 index 000000000000..581ceba266f1 --- /dev/null +++ b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiLoggingStandaloneTest.java @@ -0,0 +1,103 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.webapp; + +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import org.apache.solr.common.util.NamedList; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.WebElement; + +/** + * Tests the log-level editor on a standalone (user-managed) node. + * + *

SOLR-18317: the UI used to unconditionally send {@code nodes=all} to {@code + * /admin/info/logging}, a parameter that only makes sense in SolrCloud mode (it broadcasts the + * level change to every live node) and used to NPE server-side without a {@code ZkController}. + * This exercises the fixed {@code LoggingLevelController.setLevel} against a real standalone node, + * where the mapping test coverage ({@code AdminUiLoggingScreenTest}) only ever runs against a + * SolrCloud cluster. + */ +public class AdminUiLoggingStandaloneTest extends AdminUiStandaloneTestBase { + + @BeforeClass + public static void startStandaloneNode() throws Exception { + Path home = buildStandaloneHome("collection1"); + standaloneJetty = startStandaloneJetty(home); + baseUrl = standaloneJetty.getBaseUrl().toString(); + } + + @AfterClass + public static void stopStandaloneNode() throws Exception { + if (standaloneJetty != null) { + standaloneJetty.stop(); + standaloneJetty = null; + } + } + + @Test + public void testChangeLogLevelViaUi() throws Exception { + String logger = "org.apache.solr.core"; + openPage("~logging/level", By.id("loggingtree")); + + WebElement anchor = + waitFor(By.cssSelector("#loggingtree a.jstree-anchor[title='" + logger + "']")); + anchor.click(); + click(By.xpath("//li[a/@title='" + logger + "']//a[normalize-space()='WARN']")); + assertLoggerLevel(logger, "WARN"); + + // revert to unset; the logger then reports the inherited level with set=false + click(By.cssSelector("#loggingtree a.jstree-anchor[title='" + logger + "']")); + click(By.xpath("//li[a/@title='" + logger + "']//a[normalize-space()='UNSET']")); + assertLoggerLevel(logger, null); + assertNoSevereConsoleErrors(); + } + + /** + * Asserts the level a logger was explicitly set to, or with {@code expectedLevel} null, that the + * logger has no explicit level (it then reports the inherited effective level with set=false). + * + *

Before SOLR-18317, the hardcoded {@code nodes=all} param made this request NPE server-side + * in standalone mode, so the UI's success callback (and thus {@code $scope.refresh()}) never + * ran; this would time out here rather than observing the new level. + */ + @SuppressWarnings("unchecked") + private void assertLoggerLevel(String logger, String expectedLevel) throws Exception { + waitUntil( + "logger " + logger + " has level " + (expectedLevel == null ? "(unset)" : expectedLevel), + () -> { + try { + NamedList response = adminApi("/admin/info/logging", params()); + for (Map entry : (List>) response.get("loggers")) { + if (logger.equals(entry.get("name"))) { + return expectedLevel == null + ? Boolean.FALSE.equals(entry.get("set")) + : expectedLevel.equals(entry.get("level")) + && Boolean.TRUE.equals(entry.get("set")); + } + } + return false; + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } +} From fadbaee999f15ff9ea22b736e834d2dfbccb1fd5 Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Mon, 5 Oct 2026 09:42:28 -0400 Subject: [PATCH 4/4] tidy --- .../solr/webapp/AdminUiLoggingStandaloneTest.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiLoggingStandaloneTest.java b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiLoggingStandaloneTest.java index 581ceba266f1..548d2d5326de 100644 --- a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiLoggingStandaloneTest.java +++ b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiLoggingStandaloneTest.java @@ -31,10 +31,10 @@ * *

SOLR-18317: the UI used to unconditionally send {@code nodes=all} to {@code * /admin/info/logging}, a parameter that only makes sense in SolrCloud mode (it broadcasts the - * level change to every live node) and used to NPE server-side without a {@code ZkController}. - * This exercises the fixed {@code LoggingLevelController.setLevel} against a real standalone node, - * where the mapping test coverage ({@code AdminUiLoggingScreenTest}) only ever runs against a - * SolrCloud cluster. + * level change to every live node) and used to NPE server-side without a {@code ZkController}. This + * exercises the fixed {@code LoggingLevelController.setLevel} against a real standalone node, where + * the mapping test coverage ({@code AdminUiLoggingScreenTest}) only ever runs against a SolrCloud + * cluster. */ public class AdminUiLoggingStandaloneTest extends AdminUiStandaloneTestBase { @@ -76,8 +76,8 @@ public void testChangeLogLevelViaUi() throws Exception { * logger has no explicit level (it then reports the inherited effective level with set=false). * *

Before SOLR-18317, the hardcoded {@code nodes=all} param made this request NPE server-side - * in standalone mode, so the UI's success callback (and thus {@code $scope.refresh()}) never - * ran; this would time out here rather than observing the new level. + * in standalone mode, so the UI's success callback (and thus {@code $scope.refresh()}) never ran; + * this would time out here rather than observing the new level. */ @SuppressWarnings("unchecked") private void assertLoggerLevel(String logger, String expectedLevel) throws Exception {