This is an automated email from the ASF dual-hosted git repository.

jamesbognar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/juneau.git


The following commit(s) were added to refs/heads/master by this push:
     new 05a3a1ce93 feat: pluggable /loggers backends (Logback + Log4j2 
adapters via LogBackend SPI)
05a3a1ce93 is described below

commit 05a3a1ce930a1d3fd315393eed1236f12b114b45
Author: James Bognar <[email protected]>
AuthorDate: Fri Jun 19 10:31:36 2026 -0400

    feat: pluggable /loggers backends (Logback + Log4j2 adapters via LogBackend 
SPI)
---
 juneau-bom/pom.xml                                 |   1 +
 .../rest/server/management/Loggers_Test.java       |  54 ++++++--
 .../juneau-rest-server-management-logging/pom.xml  | 139 +++++++++++++++++++++
 .../management/logging/Log4j2LogBackend.java       | 123 ++++++++++++++++++
 .../management/logging/LogbackLogBackend.java      | 122 ++++++++++++++++++
 .../server/management/logging/package-info.java    |  31 +++++
 .../management/logging/Log4j2LogBackend_Test.java  |  81 ++++++++++++
 .../management/logging/LogbackLogBackend_Test.java |  91 ++++++++++++++
 .../rest/server/management/JulLogBackend.java      |  73 +++++++++++
 .../juneau/rest/server/management/LogBackend.java  |  86 +++++++++++++
 .../rest/server/management/LoggersManager.java     |  83 +++++-------
 .../rest/server/management/LoggersMixin.java       |  26 ++--
 .../rest/server/management/LoggersResource.java    |  25 ++--
 .../rest/server/management/LoggersSettings.java    |  39 +++++-
 juneau-rest/pom.xml                                |   1 +
 pom.xml                                            |   2 +
 16 files changed, 888 insertions(+), 89 deletions(-)

diff --git a/juneau-bom/pom.xml b/juneau-bom/pom.xml
index 40d6492361..19d60bee71 100644
--- a/juneau-bom/pom.xml
+++ b/juneau-bom/pom.xml
@@ -59,6 +59,7 @@
                        
<dependency><groupId>org.apache.juneau</groupId><artifactId>juneau-rest-server-auth-oidc-rp</artifactId><version>${project.version}</version></dependency>
                        
<dependency><groupId>org.apache.juneau</groupId><artifactId>juneau-rest-server-metrics-micrometer</artifactId><version>${project.version}</version></dependency>
                        
<dependency><groupId>org.apache.juneau</groupId><artifactId>juneau-rest-server-tracing-otel</artifactId><version>${project.version}</version></dependency>
+                       
<dependency><groupId>org.apache.juneau</groupId><artifactId>juneau-rest-server-management-logging</artifactId><version>${project.version}</version></dependency>
                        
<dependency><groupId>org.apache.juneau</groupId><artifactId>juneau-rest-server-reactive</artifactId><version>${project.version}</version></dependency>
                        
<dependency><groupId>org.apache.juneau</groupId><artifactId>juneau-rest-server-reactive-reactor</artifactId><version>${project.version}</version></dependency>
                        
<dependency><groupId>org.apache.juneau</groupId><artifactId>juneau-rest-server-view-jsp</artifactId><version>${project.version}</version></dependency>
diff --git 
a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/management/Loggers_Test.java
 
b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/management/Loggers_Test.java
index ed063fd566..a4a7b5c568 100644
--- 
a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/management/Loggers_Test.java
+++ 
b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/management/Loggers_Test.java
@@ -52,34 +52,35 @@ class Loggers_Test extends TestBase {
        // 
=================================================================================
 
        @Test void a01_managerGetSetClear() {
+               // Manager with a null context resolves the JUL default backend.
                var m = new LoggersManager();
-               m.setLevel(LName, "FINE");
-               assertEquals("FINE", m.getLevel(LName));
-               assertTrue(m.getLevels().containsKey(LName));
-               assertEquals("FINE", m.getLevels().get(LName));
+               m.setLevel(null, LName, "FINE");
+               assertEquals("FINE", m.getLevel(null, LName));
+               assertTrue(m.getLevels(null).containsKey(LName));
+               assertEquals("FINE", m.getLevels(null).get(LName));
                // Blank clears the level (inherits) -> empty string.
-               m.setLevel(LName, "");
-               assertEquals("", m.getLevel(LName));
+               m.setLevel(null, LName, "");
+               assertEquals("", m.getLevel(null, LName));
        }
 
        @Test void a02_managerUnknownLoggerNull() {
-               assertNull(new 
LoggersManager().getLevel("no.such.logger.anywhere.xyz"));
+               assertNull(new LoggersManager().getLevel(null, 
"no.such.logger.anywhere.xyz"));
        }
 
        @Test void a03_managerRootAlias() {
                // "ROOT" resolves to the empty-named root logger; it's always 
present in the snapshot.
-               assertTrue(new 
LoggersManager().getLevels().containsKey("ROOT"));
+               assertTrue(new 
LoggersManager().getLevels(null).containsKey("ROOT"));
        }
 
        @Test void a04_managerInvalidLevelThrows() {
                var m = new LoggersManager();
-               assertThrows(IllegalArgumentException.class, () -> 
m.setLevel(LName, "NOPE"));
+               assertThrows(IllegalArgumentException.class, () -> 
m.setLevel(null, LName, "NOPE"));
        }
 
        @Test void a05_managerNullLevelClears() {
                var m = new LoggersManager();
-               m.setLevel(LName, "FINE");
-               m.setLevel(LName, null);
+               m.setLevel(null, LName, "FINE");
+               m.setLevel(null, LName, null);
                assertNull(Logger.getLogger(LName).getLevel());
        }
 
@@ -87,13 +88,40 @@ class Loggers_Test extends TestBase {
                var s = new LoggersManager().resolveSettings(null);
                assertSame(LoggersSettings.DEFAULT, s);
                assertFalse(s.isWriteEnabled());
+               // The default settings drive the JUL backend.
+               assertSame(JulLogBackend.INSTANCE, s.getBackend());
        }
 
        @Test void a07_rootAndNullResolveToRootLogger() {
                var m = new LoggersManager();
                // "ROOT" and null both map to the empty-named root logger, 
which is always present.
-               assertNotNull(m.getLevel("ROOT"));
-               assertNotNull(m.getLevel(null));
+               assertNotNull(m.getLevel(null, "ROOT"));
+               assertNotNull(m.getLevel(null, null));
+       }
+
+       @Test void a08_julBackendDirect() {
+               // JulLogBackend carries the JUL logic directly (the default 
backend).
+               var b = JulLogBackend.INSTANCE;
+               b.setLevel(LName, "FINE");
+               assertEquals("FINE", b.getLevel(LName));
+               assertTrue(b.getLevels().containsKey("ROOT"));
+               b.setLevel(LName, null);
+               assertEquals("", b.getLevel(LName));
+       }
+
+       @Test void a09_explicitBackendSelection() {
+               // A LoggersSettings with an explicitly-declared backend drives 
that backend, not JUL.
+               var calls = new java.util.ArrayList<String>();
+               LogBackend fake = new LogBackend() {
+                       @Override public java.util.Map<String,String> 
getLevels() { calls.add("getLevels"); return java.util.Map.of("x", "DEBUG"); }
+                       @Override public String getLevel(String name) { 
calls.add("getLevel:" + name); return "DEBUG"; }
+                       @Override public void setLevel(String name, String 
level) { calls.add("setLevel:" + name + "=" + level); }
+               };
+               var s = LoggersSettings.create().backend(fake).build();
+               assertSame(fake, s.getBackend());
+               s.getBackend().setLevel("x", "DEBUG");
+               assertEquals("DEBUG", s.getBackend().getLevel("x"));
+               assertTrue(calls.contains("setLevel:x=DEBUG"));
        }
 
        // 
=================================================================================
diff --git a/juneau-rest/juneau-rest-server-management-logging/pom.xml 
b/juneau-rest/juneau-rest-server-management-logging/pom.xml
new file mode 100644
index 0000000000..24273117c5
--- /dev/null
+++ b/juneau-rest/juneau-rest-server-management-logging/pom.xml
@@ -0,0 +1,139 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; 
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
https://maven.apache.org/xsd/maven-4.0.0.xsd";>
+
+       <modelVersion>4.0.0</modelVersion>
+
+       <parent>
+               <groupId>org.apache.juneau</groupId>
+               <artifactId>juneau-rest</artifactId>
+               <version>10.0.0-SNAPSHOT</version>
+       </parent>
+
+       <artifactId>juneau-rest-server-management-logging</artifactId>
+       <name>Apache Juneau REST Server Logging Backends</name>
+       <description>Apache Juneau REST Server - non-JUL LogBackend adapters 
for the /loggers management endpoint (opt-in: Logback, Log4j2)</description>
+       <packaging>bundle</packaging>
+
+       <properties>
+               
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+               <!-- logback.version / log4j.version are hoisted to the root 
pom so the juneau-bom can pin them. -->
+       </properties>
+
+       <dependencies>
+               <dependency>
+                       <groupId>org.apache.juneau</groupId>
+                       <artifactId>juneau-rest-server</artifactId>
+                       <version>${project.version}</version>
+               </dependency>
+
+               <!--
+                       logback-classic and log4j-core are the backend runtimes 
this module's adapters drive.  Both are
+                       intentionally declared `provided` so neither leaks as a 
transitive dependency of this module OR any
+                       module that depends on it.  A consumer enables a 
backend by declaring it themselves (at the version
+                       they want) and registering the matching LogBackend via 
LoggersSettings.backend(...).
+
+                       Containment guarantee: a `dependency:tree` on 
juneau-rest-server (the upstream core) must never
+                       surface logback or log4j.
+               -->
+               <dependency>
+                       <groupId>ch.qos.logback</groupId>
+                       <artifactId>logback-classic</artifactId>
+                       <version>${logback.version}</version>
+                       <scope>provided</scope>
+               </dependency>
+               <dependency>
+                       <groupId>org.apache.logging.log4j</groupId>
+                       <artifactId>log4j-core</artifactId>
+                       <version>${log4j.version}</version>
+                       <scope>provided</scope>
+               </dependency>
+
+               <!-- Test scope -->
+               <dependency>
+                       <groupId>org.junit.jupiter</groupId>
+                       <artifactId>junit-jupiter</artifactId>
+                       <version>${junit.version}</version>
+                       <scope>test</scope>
+               </dependency>
+               <dependency>
+                       <groupId>org.apache.juneau</groupId>
+                       <artifactId>juneau-test-utils</artifactId>
+                       <version>${project.version}</version>
+                       <scope>test</scope>
+               </dependency>
+       </dependencies>
+
+       <build>
+               <plugins>
+                       <plugin>
+                               <groupId>org.apache.maven.plugins</groupId>
+                               <artifactId>maven-source-plugin</artifactId>
+                               <executions>
+                                       <execution>
+                                               <id>attach-sources</id>
+                                               <phase>verify</phase>
+                                               <goals>
+                                                       <goal>jar-no-fork</goal>
+                                               </goals>
+                                       </execution>
+                               </executions>
+                       </plugin>
+                       <plugin>
+                               <groupId>org.apache.felix</groupId>
+                               <artifactId>maven-bundle-plugin</artifactId>
+                               <extensions>true</extensions>
+                               <configuration>
+                                       
<supportIncrementalBuild>true</supportIncrementalBuild>
+                               </configuration>
+                               <executions>
+                                       <execution>
+                                               <id>bundle-manifest</id>
+                                               <phase>process-classes</phase>
+                                               <goals>
+                                                       <goal>manifest</goal>
+                                               </goals>
+                                       </execution>
+                               </executions>
+                       </plugin>
+                       <plugin>
+                               <groupId>org.apache.maven.plugins</groupId>
+                               <artifactId>maven-jar-plugin</artifactId>
+                       </plugin>
+                       <plugin>
+                               <groupId>org.jacoco</groupId>
+                               <artifactId>jacoco-maven-plugin</artifactId>
+                               <executions>
+                                       <execution>
+                                               <id>default-prepare-agent</id>
+                                               <goals>
+                                                       
<goal>prepare-agent</goal>
+                                               </goals>
+                                       </execution>
+                                       <execution>
+                                               <id>default-report</id>
+                                               <phase>prepare-package</phase>
+                                               <goals>
+                                                       <goal>report</goal>
+                                               </goals>
+                                       </execution>
+                               </executions>
+                       </plugin>
+               </plugins>
+       </build>
+</project>
diff --git 
a/juneau-rest/juneau-rest-server-management-logging/src/main/java/org/apache/juneau/rest/server/management/logging/Log4j2LogBackend.java
 
b/juneau-rest/juneau-rest-server-management-logging/src/main/java/org/apache/juneau/rest/server/management/logging/Log4j2LogBackend.java
new file mode 100644
index 0000000000..7070705b6f
--- /dev/null
+++ 
b/juneau-rest/juneau-rest-server-management-logging/src/main/java/org/apache/juneau/rest/server/management/logging/Log4j2LogBackend.java
@@ -0,0 +1,123 @@
+/*
+ * 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.juneau.rest.server.management.logging;
+
+import java.util.*;
+
+import org.apache.juneau.rest.server.management.*;
+import org.apache.logging.log4j.*;
+import org.apache.logging.log4j.core.*;
+import org.apache.logging.log4j.core.config.*;
+
+/**
+ * {@link LogBackend} adapter for <a class="doclink" 
href="https://logging.apache.org/log4j/2.x/";>Apache Log4j 2</a>,
+ * for use behind the {@code /loggers} management endpoint.
+ *
+ * <p>
+ * Drives the concrete Log4j2 {@link LoggerContext} / {@link Configuration} 
&mdash; <b>not</b> SLF4J the facade
+ * (an SLF4J&rarr;Log4j2 binding still routes here because the emitting 
backend is Log4j2). Register it explicitly:
+ *
+ * <p class='bjava'>
+ *     <ja>@Bean</ja>
+ *     <jk>public</jk> LoggersSettings loggersSettings() {
+ *             <jk>return</jk> 
LoggersSettings.<jsm>create</jsm>().enableWrite().backend(<jk>new</jk> 
Log4j2LogBackend()).build();
+ *     }
+ * </p>
+ *
+ * <h5 class='topic'>Level mapping</h5>
+ *
+ * <p>
+ * Levels are Log4j2 standard level names ({@code "TRACE"}, {@code "DEBUG"}, 
{@code "INFO"}, {@code "WARN"},
+ * {@code "ERROR"}, {@code "FATAL"}, {@code "OFF"}, {@code "ALL"}). Reads 
report each {@link LoggerConfig}'s
+ * <b>configured</b> level. Sets are process-lifetime-only ({@link 
Configurator#setLevel(String, Level)}); the
+ * root logger is addressed as {@code "ROOT"}.
+ *
+ * <p>
+ * Log4j2 has no "inherited / no own level" sentinel the way JUL/Logback do 
&mdash; every {@link LoggerConfig} in
+ * the configuration carries a concrete level &mdash; so reads return a level 
name (never the empty-string
+ * "inherited" marker) for configured loggers, and {@code null} for a name 
that has no {@code LoggerConfig}.
+ *
+ * @since 10.0.0
+ */
+public class Log4j2LogBackend implements LogBackend {
+
+       private final LoggerContext context;
+
+       /**
+        * Constructor using the current Log4j2 {@link LoggerContext}.
+        */
+       public Log4j2LogBackend() {
+               this((LoggerContext) LogManager.getContext(false));
+       }
+
+       /**
+        * Constructor with an explicit {@link LoggerContext} (for tests / 
multi-context setups).
+        *
+        * @param context The Log4j2 logger context. Must not be <jk>null</jk>.
+        */
+       public Log4j2LogBackend(LoggerContext context) {
+               this.context = Objects.requireNonNull(context, "context");
+       }
+
+       @Override /* LogBackend */
+       public Map<String,String> getLevels() {
+               var out = new TreeMap<String,String>();
+               var cfg = context.getConfiguration();
+               // The root LoggerConfig is NOT in getLoggers() (that map holds 
only the named, explicitly-configured
+               // loggers) - it's reachable via getRootLogger().  Surface it 
under the "ROOT" key.
+               var root = cfg.getRootLogger();
+               if (root != null)
+                       out.put("ROOT", root.getLevel() == null ? "" : 
root.getLevel().name());
+               for (var lc : cfg.getLoggers().values()) {
+                       if (lc.getName().isEmpty())
+                               continue;  // defensive: root already handled 
above
+                       out.put(lc.getName(), lc.getLevel() == null ? "" : 
lc.getLevel().name());
+               }
+               return out;
+       }
+
+       @Override /* LogBackend */
+       public String getLevel(String name) {
+               var cfg = context.getConfiguration();
+               var resolved = resolveName(name);
+               var lc = resolved.isEmpty() ? cfg.getRootLogger() : 
cfg.getLoggers().get(resolved);
+               if (lc == null)
+                       return null;
+               var level = lc.getLevel();
+               return level == null ? "" : level.name();
+       }
+
+       @Override /* LogBackend */
+       public void setLevel(String name, String level) {
+               var target = (name == null || name.equals("ROOT")) ? 
LogManager.ROOT_LOGGER_NAME : name;
+               // Null/blank clears the override back to the parent's level 
(Configurator treats null as "remove override").
+               Configurator.setLevel(target, level == null || level.isBlank() 
? null : parse(level.trim()));
+       }
+
+       private static Level parse(String level) {
+               // Level.getLevel(name) returns null for an unknown name 
(Level.toLevel would silently default) - hard-fail
+               // to match the JUL backend's IllegalArgumentException contract.
+               var l = Level.getLevel(level.toUpperCase(Locale.ROOT));
+               if (l == null)
+                       throw new IllegalArgumentException("Not a valid Log4j2 
level: '" + level + "'.");
+               return l;
+       }
+
+       private static String resolveName(String name) {
+               return (name == null || name.equals("ROOT")) ? "" : name;
+       }
+}
diff --git 
a/juneau-rest/juneau-rest-server-management-logging/src/main/java/org/apache/juneau/rest/server/management/logging/LogbackLogBackend.java
 
b/juneau-rest/juneau-rest-server-management-logging/src/main/java/org/apache/juneau/rest/server/management/logging/LogbackLogBackend.java
new file mode 100644
index 0000000000..3775840a9c
--- /dev/null
+++ 
b/juneau-rest/juneau-rest-server-management-logging/src/main/java/org/apache/juneau/rest/server/management/logging/LogbackLogBackend.java
@@ -0,0 +1,122 @@
+/*
+ * 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.juneau.rest.server.management.logging;
+
+import java.util.*;
+
+import org.apache.juneau.rest.server.management.*;
+import org.slf4j.*;
+
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.LoggerContext;
+
+/**
+ * {@link LogBackend} adapter for <a class="doclink" 
href="https://logback.qos.ch";>Logback</a> (the SLF4J native
+ * backend), for use behind the {@code /loggers} management endpoint.
+ *
+ * <p>
+ * Drives the concrete Logback {@link LoggerContext} &mdash; <b>not</b> SLF4J 
the facade. Register it explicitly:
+ *
+ * <p class='bjava'>
+ *     <ja>@Bean</ja>
+ *     <jk>public</jk> LoggersSettings loggersSettings() {
+ *             <jk>return</jk> 
LoggersSettings.<jsm>create</jsm>().enableWrite().backend(<jk>new</jk> 
LogbackLogBackend()).build();
+ *     }
+ * </p>
+ *
+ * <h5 class='topic'>Level mapping</h5>
+ *
+ * <p>
+ * Levels are Logback level names ({@code "TRACE"}, {@code "DEBUG"}, {@code 
"INFO"}, {@code "WARN"},
+ * {@code "ERROR"}, {@code "OFF"}). Reads report each logger's 
<b>configured</b> level (the level set on the logger
+ * itself, or the empty string when it inherits from an ancestor). Sets are 
process-lifetime-only
+ * ({@code Logger.setLevel(...)}); the root logger is addressed as {@code 
"ROOT"}.
+ *
+ * @since 10.0.0
+ */
+public class LogbackLogBackend implements LogBackend {
+
+       private final LoggerContext context;
+
+       /**
+        * Constructor using the SLF4J-bound Logback {@link LoggerContext}.
+        *
+        * @throws IllegalStateException If the bound SLF4J backend is not 
Logback.
+        */
+       public LogbackLogBackend() {
+               var factory = LoggerFactory.getILoggerFactory();
+               if (! (factory instanceof LoggerContext lc))
+                       throw new IllegalStateException(
+                               "The bound SLF4J backend is not Logback 
(ILoggerFactory=" + factory.getClass().getName()
+                               + ").  /loggers drives the concrete backend, 
not the SLF4J facade.");
+               this.context = lc;
+       }
+
+       /**
+        * Constructor with an explicit {@link LoggerContext} (for tests / 
multi-context setups).
+        *
+        * @param context The Logback logger context. Must not be <jk>null</jk>.
+        */
+       public LogbackLogBackend(LoggerContext context) {
+               this.context = Objects.requireNonNull(context, "context");
+       }
+
+       @Override /* LogBackend */
+       public Map<String,String> getLevels() {
+               var out = new TreeMap<String,String>();
+               for (var logger : context.getLoggerList()) {
+                       var level = logger.getLevel();  // configured level; 
null = inherited
+                       var name = logger.getName();
+                       var key = Logger.ROOT_LOGGER_NAME.equals(name) ? "ROOT" 
: name;
+                       out.put(key, level == null ? "" : level.toString());
+               }
+               return out;
+       }
+
+       @Override /* LogBackend */
+       public String getLevel(String name) {
+               // Logback exists-check: getLogger(...) creates on demand, so 
probe the existing list first.
+               var resolved = resolveName(name);
+               for (var logger : context.getLoggerList())
+                       if (logger.getName().equals(resolved)) {
+                               var level = logger.getLevel();
+                               return level == null ? "" : level.toString();
+                       }
+               return null;
+       }
+
+       @Override /* LogBackend */
+       public void setLevel(String name, String level) {
+               var logger = context.getLogger(resolveName(name));
+               // A null/blank level clears the logger's own level so it 
inherits from its ancestor.
+               // Level.toLevel(null) returns DEBUG, so guard the inherit case 
explicitly.
+               logger.setLevel(level == null || level.isBlank() ? null : 
parse(level.trim()));
+       }
+
+       private static Level parse(String level) {
+               // Logback's Level.toLevel(name, default) silently falls back; 
we want a hard failure on a bad name to
+               // match the JUL backend's IllegalArgumentException contract.
+               var l = Level.toLevel(level, null);
+               if (l == null)
+                       throw new IllegalArgumentException("Not a valid Logback 
level: '" + level + "'.");
+               return l;
+       }
+
+       private static String resolveName(String name) {
+               return (name == null || name.equals("ROOT")) ? 
Logger.ROOT_LOGGER_NAME : name;
+       }
+}
diff --git 
a/juneau-rest/juneau-rest-server-management-logging/src/main/java/org/apache/juneau/rest/server/management/logging/package-info.java
 
b/juneau-rest/juneau-rest-server-management-logging/src/main/java/org/apache/juneau/rest/server/management/logging/package-info.java
new file mode 100644
index 0000000000..4b397f6e21
--- /dev/null
+++ 
b/juneau-rest/juneau-rest-server-management-logging/src/main/java/org/apache/juneau/rest/server/management/logging/package-info.java
@@ -0,0 +1,31 @@
+/*
+ * 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.
+ */
+/**
+ * Opt-in non-JUL {@link org.apache.juneau.rest.server.management.LogBackend 
LogBackend} adapters for the
+ * {@code /loggers} management endpoint.
+ *
+ * <p>
+ * {@code juneau-rest-server} ships only the {@code java.util.logging} 
backend, keeping the core management
+ * surface dependency-free. This add-on supplies the Logback
+ * ({@link 
org.apache.juneau.rest.server.management.logging.LogbackLogBackend}) and Log4j2
+ * ({@link org.apache.juneau.rest.server.management.logging.Log4j2LogBackend}) 
adapters, with the backends
+ * declared {@code provided} so they never leak transitively. A consumer 
drives one by declaring it explicitly via
+ * {@code LoggersSettings.create().backend(new LogbackLogBackend())} &mdash; 
the endpoint never auto-detects.
+ *
+ * @since 10.0.0
+ */
+package org.apache.juneau.rest.server.management.logging;
diff --git 
a/juneau-rest/juneau-rest-server-management-logging/src/test/java/org/apache/juneau/rest/server/management/logging/Log4j2LogBackend_Test.java
 
b/juneau-rest/juneau-rest-server-management-logging/src/test/java/org/apache/juneau/rest/server/management/logging/Log4j2LogBackend_Test.java
new file mode 100644
index 0000000000..f15e9e3bfd
--- /dev/null
+++ 
b/juneau-rest/juneau-rest-server-management-logging/src/test/java/org/apache/juneau/rest/server/management/logging/Log4j2LogBackend_Test.java
@@ -0,0 +1,81 @@
+/*
+ * 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.juneau.rest.server.management.logging;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.apache.juneau.rest.server.management.*;
+import org.apache.logging.log4j.*;
+import org.apache.logging.log4j.core.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Tests for {@link Log4j2LogBackend} driven against the current Log4j2 {@link 
LoggerContext}, verifying it honors
+ * the {@link LogBackend} contract (configured-level reads, process-lifetime 
sets via {@code Configurator}, "ROOT"
+ * alias, hard-fail on bad level).
+ */
+class Log4j2LogBackend_Test extends org.apache.juneau.TestBase {
+
+       private static final String LNAME = "org.apache.juneau.test.log4j2.X";
+
+       private Log4j2LogBackend backend;
+
+       @BeforeEach
+       void setup() {
+               backend = new Log4j2LogBackend((LoggerContext) 
LogManager.getContext(false));
+       }
+
+       @Test void a01_isLogBackend() {
+               assertInstanceOf(LogBackend.class, backend);
+       }
+
+       @Test void a02_setAndGetConfiguredLevel() {
+               backend.setLevel(LNAME, "DEBUG");
+               assertEquals("DEBUG", backend.getLevel(LNAME));
+               assertEquals("DEBUG", backend.getLevels().get(LNAME));
+       }
+
+       @Test void a03_levelNamesAreLog4jStyle() {
+               backend.setLevel(LNAME, "WARN");
+               assertEquals("WARN", backend.getLevel(LNAME));
+               backend.setLevel(LNAME, "ERROR");
+               assertEquals("ERROR", backend.getLevel(LNAME));
+       }
+
+       @Test void a04_rootAddressableUnderRootKey() {
+               backend.setLevel("ROOT", "INFO");
+               assertTrue(backend.getLevels().containsKey("ROOT"));
+               assertEquals("INFO", backend.getLevel("ROOT"));
+       }
+
+       @Test void a05_unknownLoggerReturnsNull() {
+               assertNull(backend.getLevel("no.such.logger.anywhere.xyz"));
+       }
+
+       @Test void a06_invalidLevelThrows() {
+               assertThrows(IllegalArgumentException.class, () -> 
backend.setLevel(LNAME, "NOPE"));
+       }
+
+       @Test void a07_caseInsensitiveLevelName() {
+               backend.setLevel(LNAME, "debug");
+               assertEquals("DEBUG", backend.getLevel(LNAME));
+       }
+
+       @Test void a08_defaultCtorResolvesContext() {
+               assertDoesNotThrow(() -> new Log4j2LogBackend());
+       }
+}
diff --git 
a/juneau-rest/juneau-rest-server-management-logging/src/test/java/org/apache/juneau/rest/server/management/logging/LogbackLogBackend_Test.java
 
b/juneau-rest/juneau-rest-server-management-logging/src/test/java/org/apache/juneau/rest/server/management/logging/LogbackLogBackend_Test.java
new file mode 100644
index 0000000000..c56f9f24ad
--- /dev/null
+++ 
b/juneau-rest/juneau-rest-server-management-logging/src/test/java/org/apache/juneau/rest/server/management/logging/LogbackLogBackend_Test.java
@@ -0,0 +1,91 @@
+/*
+ * 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.juneau.rest.server.management.logging;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.apache.juneau.rest.server.management.*;
+import org.junit.jupiter.api.*;
+
+import ch.qos.logback.classic.*;
+
+/**
+ * Tests for {@link LogbackLogBackend} driven against an explicit Logback 
{@link LoggerContext}, verifying it
+ * honors the {@link LogBackend} contract (configured-level reads, 
empty-string-inherited, process-lifetime sets,
+ * "ROOT" alias, hard-fail on bad level).
+ */
+class LogbackLogBackend_Test extends org.apache.juneau.TestBase {
+
+       private static final String LNAME = "org.apache.juneau.test.logback.X";
+
+       private LoggerContext ctx;
+       private LogbackLogBackend backend;
+
+       @BeforeEach
+       void setup() {
+               ctx = new LoggerContext();
+               backend = new LogbackLogBackend(ctx);
+       }
+
+       @Test void a01_isLogBackend() {
+               assertInstanceOf(LogBackend.class, backend);
+       }
+
+       @Test void a02_setAndGetConfiguredLevel() {
+               backend.setLevel(LNAME, "DEBUG");
+               assertEquals("DEBUG", backend.getLevel(LNAME));
+               assertEquals("DEBUG", backend.getLevels().get(LNAME));
+       }
+
+       @Test void a03_levelNamesAreLogbackStyle() {
+               backend.setLevel(LNAME, "WARN");
+               assertEquals("WARN", backend.getLevel(LNAME));
+       }
+
+       @Test void a04_blankClearsToInherited() {
+               backend.setLevel(LNAME, "DEBUG");
+               backend.setLevel(LNAME, "");
+               // Own level cleared -> inherits -> empty string.
+               assertEquals("", backend.getLevel(LNAME));
+       }
+
+       @Test void a05_nullClearsToInherited() {
+               backend.setLevel(LNAME, "DEBUG");
+               backend.setLevel(LNAME, null);
+               assertEquals("", backend.getLevel(LNAME));
+       }
+
+       @Test void a06_rootAlwaysPresentUnderRootKey() {
+               // A fresh LoggerContext always has a ROOT logger (defaults to 
DEBUG).
+               assertTrue(backend.getLevels().containsKey("ROOT"));
+               backend.setLevel("ROOT", "INFO");
+               assertEquals("INFO", backend.getLevel("ROOT"));
+       }
+
+       @Test void a07_unknownLoggerReturnsNull() {
+               assertNull(backend.getLevel("no.such.logger.anywhere.xyz"));
+       }
+
+       @Test void a08_invalidLevelThrows() {
+               assertThrows(IllegalArgumentException.class, () -> 
backend.setLevel(LNAME, "NOPE"));
+       }
+
+       @Test void a09_defaultCtorRequiresLogbackBinding() {
+               // The test classpath binds SLF4J to Logback, so the no-arg 
ctor resolves the bound context.
+               assertDoesNotThrow(() -> new LogbackLogBackend());
+       }
+}
diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/management/JulLogBackend.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/management/JulLogBackend.java
new file mode 100644
index 0000000000..6cb76b790d
--- /dev/null
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/management/JulLogBackend.java
@@ -0,0 +1,73 @@
+/*
+ * 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.juneau.rest.server.management;
+
+import java.util.*;
+import java.util.logging.*;
+
+/**
+ * {@link LogBackend} implementation for {@link java.util.logging 
java.util.logging} (JUL).
+ *
+ * <p>
+ * This is the default backend driven by {@link LoggersManager} when no {@link 
LoggersSettings} declares another
+ * one, and it carries the original v1 {@code /loggers} behavior so the JUL 
path is unchanged: reads report each
+ * logger's own ({@link Logger#getLevel() configured}) level (empty string 
when inherited), the root logger is
+ * keyed {@code "ROOT"}, and sets are process-lifetime-only via {@link 
Logger#setLevel(Level)}.
+ *
+ * @since 10.0.0
+ */
+public class JulLogBackend implements LogBackend {
+
+       /** Process-wide shared instance (stateless). */
+       public static final JulLogBackend INSTANCE = new JulLogBackend();
+
+       @Override /* LogBackend */
+       public Map<String,String> getLevels() {
+               var out = new TreeMap<String,String>();
+               var lm = LogManager.getLogManager();
+               var names = Collections.list(lm.getLoggerNames());
+               for (var name : names) {
+                       var logger = lm.getLogger(name);
+                       if (logger == null)
+                               continue;
+                       var level = logger.getLevel();
+                       var key = name.isEmpty() ? "ROOT" : name;
+                       out.put(key, level == null ? "" : level.getName());
+               }
+               return out;
+       }
+
+       @Override /* LogBackend */
+       public String getLevel(String name) {
+               var logger = 
LogManager.getLogManager().getLogger(resolveName(name));
+               if (logger == null)
+                       return null;
+               var level = logger.getLevel();
+               return level == null ? "" : level.getName();
+       }
+
+       @Override /* LogBackend */
+       public void setLevel(String name, String level) {
+               var logger = Logger.getLogger(resolveName(name));
+               logger.setLevel(level == null || level.isBlank() ? null : 
Level.parse(level.trim()));
+       }
+
+       private static String resolveName(String name) {
+               // The root logger is the empty-string-named logger; expose it 
under the friendlier "ROOT" alias.
+               return (name == null || name.equals("ROOT")) ? "" : name;
+       }
+}
diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/management/LogBackend.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/management/LogBackend.java
new file mode 100644
index 0000000000..233eaa1ec0
--- /dev/null
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/management/LogBackend.java
@@ -0,0 +1,86 @@
+/*
+ * 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.juneau.rest.server.management;
+
+import java.util.*;
+
+/**
+ * SPI behind the {@code /loggers} management endpoint: reads and sets logger 
levels for one concrete logging
+ * backend at runtime.
+ *
+ * <p>
+ * {@code juneau-rest-server} ships only the {@link JulLogBackend 
java.util.logging} implementation, keeping the
+ * core management surface dependency-free. Support for other backends is 
opt-in: the
+ * {@code juneau-rest-server-management-logging} add-on ships {@code 
LogbackLogBackend} / {@code Log4j2LogBackend}
+ * (with the backends declared {@code provided}), and any consumer can 
implement this interface for a backend
+ * Juneau does not cover.
+ *
+ * <h5 class='topic'>Explicit selection (no auto-drive)</h5>
+ *
+ * <p>
+ * Consistent with Juneau's explicit-over-magic stance, the backend is 
<b>explicitly declared</b> by registering a
+ * {@link LoggersSettings} bean built with {@link 
LoggersSettings.Builder#backend(LogBackend)}. When none is
+ * declared, {@link LoggersManager} drives {@link JulLogBackend} &mdash; the 
endpoint never silently classpath-scans
+ * for, and then drives, a backend the operator didn't choose.
+ *
+ * <h5 class='topic'>Level contract</h5>
+ *
+ * <p>
+ * Levels are exchanged as backend-native level-name strings (e.g. JUL {@code 
"FINE"}/{@code "INFO"}, Logback /
+ * Log4j2 {@code "DEBUG"}/{@code "INFO"}). Reads report the logger's 
<b>configured</b> level &mdash; the level set
+ * on the logger itself, or the empty string when it inherits from an 
ancestor. Sets are
+ * <b>process-lifetime-only</b> (they do not rewrite the backend's 
configuration file). The root logger is
+ * addressed under the key {@code "ROOT"}.
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ *     <li class='jc'>{@link JulLogBackend}
+ *     <li class='jc'>{@link LoggersManager}
+ *     <li class='jc'>{@link LoggersSettings}
+ *     <li class='link'><a class="doclink" 
href="https://juneau.apache.org/docs/topics/ManagementSurface";>Management 
Surface</a>
+ * </ul>
+ *
+ * @since 10.0.0
+ */
+public interface LogBackend {
+
+       /**
+        * Returns a snapshot of all known loggers and their configured levels.
+        *
+        * @return A sorted map of logger name &rarr; configured level name 
(empty string = inherited from an
+        *      ancestor). The root logger is keyed {@code "ROOT"}. Never 
<jk>null</jk>.
+        */
+       Map<String,String> getLevels();
+
+       /**
+        * Returns the configured level of a single logger.
+        *
+        * @param name The logger name ({@code "ROOT"} or empty for the root 
logger).
+        * @return The configured level name, the empty string if the level is 
inherited, or <jk>null</jk> if no such
+        *      logger is currently registered.
+        */
+       String getLevel(String name);
+
+       /**
+        * Sets (or clears) the level of a single logger at runtime 
(process-lifetime-only).
+        *
+        * @param name The logger name ({@code "ROOT"} or empty for the root 
logger).
+        * @param level The backend-native level name, or <jk>null</jk>/blank 
to clear the logger's own level so it
+        *      inherits from its ancestor.
+        * @throws IllegalArgumentException If {@code level} is non-blank but 
not a valid level name for the backend.
+        */
+       void setLevel(String name, String level);
+}
diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/management/LoggersManager.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/management/LoggersManager.java
index dae1a33d6b..2409b538ca 100644
--- 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/management/LoggersManager.java
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/management/LoggersManager.java
@@ -17,25 +17,27 @@
 package org.apache.juneau.rest.server.management;
 
 import java.util.*;
-import java.util.logging.*;
 
 import org.apache.juneau.rest.server.*;
 
 /**
- * Shared worker for the {@code /loggers} management endpoint:  reads and sets
- * {@link java.util.logging java.util.logging} (JUL) levels at runtime.
+ * Shared worker for the {@code /loggers} management endpoint:  reads and sets 
logger levels at runtime by
+ * delegating to the {@link LogBackend} resolved from the host context.
  *
  * <p>
  * Both the {@link LoggersMixin mixin} and {@link LoggersResource resource} 
flavors delegate here, so the
  * two forms cannot drift &mdash; the same pattern the health package uses 
with its aggregator.
  *
- * <h5 class='section'>Notes:</h5><ul>
- *     <li class='warn'>This endpoint is <b>JUL-only</b> in v1.  Applications 
that route logging through
- *             SLF4J&rarr;Logback or Log4j2 will not have their levels changed 
here; backend-aware level control is
- *             tracked as a separate follow-on.
- * </ul>
+ * <h5 class='section'>Backend</h5>
+ * <p>
+ * The driven backend defaults to {@link JulLogBackend java.util.logging}.  A 
consumer drives a different backend
+ * (e.g. Logback / Log4j2 via {@code juneau-rest-server-management-logging}) 
by <i>explicitly</i> declaring it on a
+ * {@link LoggersSettings} bean via {@link 
LoggersSettings.Builder#backend(LogBackend)} &mdash; the endpoint never
+ * auto-detects.  The response shape (logger name &rarr; configured level, 
empty string = inherited) is identical
+ * across backends.
  *
  * <h5 class='section'>See Also:</h5><ul>
+ *     <li class='jc'>{@link LogBackend}
  *     <li class='link'><a class="doclink" 
href="https://juneau.apache.org/docs/topics/ManagementSurface";>Management 
Surface</a>
  * </ul>
  *
@@ -44,66 +46,46 @@ import org.apache.juneau.rest.server.*;
 public class LoggersManager {
 
        /**
-        * Returns a snapshot of all known loggers and their <i>configured</i> 
levels.
-        *
-        * <p>
-        * Walks {@link LogManager#getLoggerNames()} and reports each logger's 
own level
-        * ({@link Logger#getLevel()}), or {@code null} (rendered as the empty 
string) when the logger inherits
-        * its level from an ancestor.  The root logger is reported under the 
key {@code "ROOT"}.
+        * Returns a snapshot of all known loggers and their configured levels, 
for the backend resolved from the
+        * supplied context.
         *
-        * @return A sorted map of logger name &rarr; configured level name 
(never <jk>null</jk>).
+        * @param context The REST context whose bean store supplies the {@link 
LoggersSettings} / {@link LogBackend}.
+        *      May be <jk>null</jk> (uses the JUL default).
+        * @return A sorted map of logger name &rarr; configured level name 
(empty string = inherited).  Never <jk>null</jk>.
         */
-       public Map<String,String> getLevels() {
-               var out = new TreeMap<String,String>();
-               var lm = LogManager.getLogManager();
-               var names = Collections.list(lm.getLoggerNames());
-               for (var name : names) {
-                       var logger = lm.getLogger(name);
-                       if (logger == null)
-                               continue;
-                       var level = logger.getLevel();
-                       var key = name.isEmpty() ? "ROOT" : name;
-                       out.put(key, level == null ? "" : level.getName());
-               }
-               return out;
+       public Map<String,String> getLevels(RestContext context) {
+               return resolveSettings(context).getBackend().getLevels();
        }
 
        /**
-        * Returns the configured level of a single logger.
+        * Returns the configured level of a single logger, for the backend 
resolved from the supplied context.
         *
+        * @param context The REST context whose bean store supplies the 
backend.  May be <jk>null</jk>.
         * @param name The logger name ({@code "ROOT"} or empty for the root 
logger).
         * @return The configured level name, the empty string if the level is 
inherited, or <jk>null</jk> if no
         *      such logger is currently registered.
         */
-       public String getLevel(String name) {
-               var logger = 
LogManager.getLogManager().getLogger(resolveName(name));
-               if (logger == null)
-                       return null;
-               var level = logger.getLevel();
-               return level == null ? "" : level.getName();
+       public String getLevel(RestContext context, String name) {
+               return resolveSettings(context).getBackend().getLevel(name);
        }
 
        /**
-        * Sets (or clears) the level of a single logger at runtime.
-        *
-        * <p>
-        * A non-null, non-blank {@code level} is parsed via {@link 
Level#parse(String)} and applied; a
-        * <jk>null</jk> or blank {@code level} clears the logger's own level 
so it inherits from its ancestor.
-        * The named logger is created on demand via {@link 
Logger#getLogger(String)} if it does not yet exist
-        * (matching JUL semantics).
+        * Sets (or clears) the level of a single logger at runtime 
(process-lifetime-only), for the backend resolved
+        * from the supplied context.
         *
+        * @param context The REST context whose bean store supplies the 
backend.  May be <jk>null</jk>.
         * @param name The logger name ({@code "ROOT"} or empty for the root 
logger).
-        * @param level The level name (e.g. {@code "FINE"}, {@code "INFO"}, 
{@code "OFF"}), or <jk>null</jk>/blank to inherit.
-        * @throws IllegalArgumentException If {@code level} is non-blank but 
not a valid {@link Level} name.
+        * @param level The backend-native level name (e.g. {@code 
"FINE"}/{@code "INFO"} for JUL,
+        *      {@code "DEBUG"}/{@code "INFO"} for Logback/Log4j2), or 
<jk>null</jk>/blank to inherit.
+        * @throws IllegalArgumentException If {@code level} is non-blank but 
not a valid level name for the backend.
         */
-       public void setLevel(String name, String level) {
-               var logger = Logger.getLogger(resolveName(name));
-               logger.setLevel(level == null || level.isBlank() ? null : 
Level.parse(level.trim()));
+       public void setLevel(RestContext context, String name, String level) {
+               resolveSettings(context).getBackend().setLevel(name, level);
        }
 
        /**
         * Resolves the {@link LoggersSettings} from the host context's bean 
store, falling back to the
-        * read-only default.
+        * read-only (JUL-backed) default.
         *
         * @param context The REST context whose bean store is searched.  May 
be <jk>null</jk>.
         * @return The registered settings, or {@link LoggersSettings#DEFAULT} 
when none is registered.
@@ -116,9 +98,4 @@ public class LoggersManager {
                        return LoggersSettings.DEFAULT;
                return 
context.getBeanStore().getBean(LoggersSettings.class).orElse(LoggersSettings.DEFAULT);
        }
-
-       private static String resolveName(String name) {
-               // The root logger is the empty-string-named logger; expose it 
under the friendlier "ROOT" alias.
-               return (name == null || name.equals("ROOT")) ? "" : name;
-       }
 }
diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/management/LoggersMixin.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/management/LoggersMixin.java
index 859203bfeb..eabaf5748e 100644
--- 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/management/LoggersMixin.java
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/management/LoggersMixin.java
@@ -24,7 +24,7 @@ import org.apache.juneau.rest.server.*;
 import org.apache.juneau.rest.server.servlet.*;
 
 /**
- * Mixin flavor of the {@code /loggers} runtime log-level management endpoint 
(JUL).
+ * Mixin flavor of the {@code /loggers} runtime log-level management endpoint.
  *
  * <p>
  * Composes the logger read/set endpoints into a host resource via
@@ -36,13 +36,15 @@ import org.apache.juneau.rest.server.servlet.*;
  * set-level endpoint <b>mutates runtime logging</b> and should be guarded 
(deny-by-default) when assembled into a
  * management group &mdash; see the actuator group's exposure policy.
  *
- * <h5 class='section'>Notes:</h5><ul>
- *     <li class='warn'>JUL-only in v1 &mdash; see {@link LoggersManager}.
+ * <h5 class='section'>Backend:</h5><ul>
+ *     <li>Drives {@link JulLogBackend java.util.logging} by default; declare 
a Logback / Log4j2 backend
+ *             explicitly via {@link 
LoggersSettings.Builder#backend(LogBackend)} &mdash; see {@link LogBackend}.
  * </ul>
  *
  * <h5 class='section'>See Also:</h5><ul>
  *     <li class='jc'>{@link LoggersResource}
  *     <li class='jc'>{@link LoggersManager}
+ *     <li class='jc'>{@link LogBackend}
  * </ul>
  *
  * @since 10.0.0
@@ -55,20 +57,22 @@ public class LoggersMixin extends RestMixin {
        /**
         * [GET /loggers] - All loggers and their configured levels.
         *
+        * @param req The HTTP request.
         * @return A sorted map of logger name to configured level (empty 
string = inherited).
         */
        @RestGet(
                path="/loggers",
                summary="Runtime logger levels",
-               description="Lists all java.util.logging loggers and their 
configured levels (empty = inherited from ancestor)."
+               description="Lists all loggers and their configured levels 
(empty = inherited from ancestor)."
        )
-       public Map<String,String> getLoggers() {
-               return manager.getLevels();
+       public Map<String,String> getLoggers(RestRequest req) {
+               return manager.getLevels(req.getContext());
        }
 
        /**
         * [GET /loggers/{name}] - One logger's configured level.
         *
+        * @param req The HTTP request.
         * @param name The logger name ("ROOT" for the root logger).
         * @return The configured level name, empty string if inherited.
         * @throws NotFound If no logger with that name is registered.
@@ -76,10 +80,10 @@ public class LoggersMixin extends RestMixin {
        @RestGet(
                path="/loggers/{name}",
                summary="Runtime logger level",
-               description="Returns the configured level of a single 
java.util.logging logger."
+               description="Returns the configured level of a single logger."
        )
-       public String getLogger(@Path("name") String name) {
-               var level = manager.getLevel(name);
+       public String getLogger(RestRequest req, @Path("name") String name) {
+               var level = manager.getLevel(req.getContext(), name);
                if (level == null)
                        throw new NotFound("No logger named ''{0}'' is 
registered.", name);
                return level;
@@ -106,9 +110,9 @@ public class LoggersMixin extends RestMixin {
        public String setLogger(RestRequest req, @Path("name") String name, 
@Content String level) {
                if (! 
manager.resolveSettings(req.getContext()).isWriteEnabled())
                        throw new Forbidden("The /loggers set-level endpoint is 
disabled.  Register a LoggersSettings bean with write enabled to use it.");
-               manager.setLevel(name, level);
+               manager.setLevel(req.getContext(), name, level);
                // setLevel creates the logger on demand, so getLevel always 
returns non-null here (level name or "" if inherited).
-               return manager.getLevel(name);
+               return manager.getLevel(req.getContext(), name);
        }
 
        /**
diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/management/LoggersResource.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/management/LoggersResource.java
index 0bbe6d7c32..698eba3f77 100644
--- 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/management/LoggersResource.java
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/management/LoggersResource.java
@@ -24,7 +24,7 @@ import org.apache.juneau.rest.server.*;
 import org.apache.juneau.rest.server.servlet.*;
 
 /**
- * Child-resource flavor of the {@code /loggers} runtime log-level management 
endpoint (JUL).
+ * Child-resource flavor of the {@code /loggers} runtime log-level management 
endpoint.
  *
  * <p>
  * Mounts as a routed child via {@link Rest#children() 
@Rest(children=LoggersResource.class)} under the
@@ -33,13 +33,15 @@ import org.apache.juneau.rest.server.servlet.*;
  * {@link BasicRestResource} so the returned beans serialize via the {@code 
BasicUniversalConfig} set
  * (mirrors {@code HealthResource}).
  *
- * <h5 class='section'>Notes:</h5><ul>
- *     <li class='warn'>JUL-only in v1 &mdash; see {@link LoggersManager}.
+ * <h5 class='section'>Backend:</h5><ul>
+ *     <li>Drives {@link JulLogBackend java.util.logging} by default; declare 
a Logback / Log4j2 backend
+ *             explicitly via {@link 
LoggersSettings.Builder#backend(LogBackend)} &mdash; see {@link LogBackend}.
  * </ul>
  *
  * <h5 class='section'>See Also:</h5><ul>
  *     <li class='jc'>{@link LoggersMixin}
  *     <li class='jc'>{@link LoggersManager}
+ *     <li class='jc'>{@link LogBackend}
  * </ul>
  *
  * @since 10.0.0
@@ -57,15 +59,16 @@ public class LoggersResource extends BasicRestResource {
        @RestGet(
                path="/*",
                summary="Runtime logger levels",
-               description="Lists all java.util.logging loggers and their 
configured levels (empty = inherited from ancestor)."
+               description="Lists all loggers and their configured levels 
(empty = inherited from ancestor)."
        )
-       public Map<String,String> getLoggers() {
-               return manager.getLevels();
+       public Map<String,String> getLoggers(RestRequest req) {
+               return manager.getLevels(req.getContext());
        }
 
        /**
         * [GET /{name}] - One logger's configured level.
         *
+        * @param req The HTTP request.
         * @param name The logger name ("ROOT" for the root logger).
         * @return The configured level name, empty string if inherited.
         * @throws NotFound If no logger with that name is registered.
@@ -73,10 +76,10 @@ public class LoggersResource extends BasicRestResource {
        @RestGet(
                path="/{name}",
                summary="Runtime logger level",
-               description="Returns the configured level of a single 
java.util.logging logger."
+               description="Returns the configured level of a single logger."
        )
-       public String getLogger(@Path("name") String name) {
-               var level = manager.getLevel(name);
+       public String getLogger(RestRequest req, @Path("name") String name) {
+               var level = manager.getLevel(req.getContext(), name);
                if (level == null)
                        throw new NotFound("No logger named ''{0}'' is 
registered.", name);
                return level;
@@ -103,9 +106,9 @@ public class LoggersResource extends BasicRestResource {
        public String setLogger(RestRequest req, @Path("name") String name, 
@Content String level) {
                if (! 
manager.resolveSettings(req.getContext()).isWriteEnabled())
                        throw new Forbidden("The /loggers set-level endpoint is 
disabled.  Register a LoggersSettings bean with write enabled to use it.");
-               manager.setLevel(name, level);
+               manager.setLevel(req.getContext(), name, level);
                // setLevel creates the logger on demand, so getLevel always 
returns non-null here (level name or "" if inherited).
-               return manager.getLevel(name);
+               return manager.getLevel(req.getContext(), name);
        }
 
        /**
diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/management/LoggersSettings.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/management/LoggersSettings.java
index f67268f17a..94fab2b9e8 100644
--- 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/management/LoggersSettings.java
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/management/LoggersSettings.java
@@ -27,8 +27,18 @@ package org.apache.juneau.rest.server.management;
  * no such bean is present, {@link LoggersManager} resolves the default 
(writes disabled) and the set-level
  * endpoints respond {@code 403 Forbidden}.
  *
+ * <h5 class='topic'>Backend selection (explicit)</h5>
+ *
+ * <p>
+ * The {@code /loggers} endpoint drives a {@link LogBackend}.  By default that 
is
+ * {@link JulLogBackend java.util.logging}.  To drive a different backend 
(e.g. Logback or Log4j2 via the
+ * {@code juneau-rest-server-management-logging} add-on), declare it 
<i>explicitly</i> with
+ * {@link Builder#backend(LogBackend)} &mdash; the endpoint never 
classpath-scans for, and then drives, a backend
+ * the operator did not choose.
+ *
  * <h5 class='section'>See Also:</h5><ul>
  *     <li class='jc'>{@link LoggersManager}
+ *     <li class='jc'>{@link LogBackend}
  *     <li class='link'><a class="doclink" 
href="https://juneau.apache.org/docs/topics/ManagementSurface";>Management 
Surface</a>
  * </ul>
  *
@@ -36,13 +46,15 @@ package org.apache.juneau.rest.server.management;
  */
 public class LoggersSettings {
 
-       /** The default (read-only) settings used when no bean is registered. */
+       /** The default (read-only, JUL-backed) settings used when no bean is 
registered. */
        public static final LoggersSettings DEFAULT = create().build();
 
        private final boolean writeEnabled;
+       private final LogBackend backend;
 
        private LoggersSettings(Builder b) {
                this.writeEnabled = b.writeEnabled;
+               this.backend = b.backend == null ? JulLogBackend.INSTANCE : 
b.backend;
        }
 
        /**
@@ -61,11 +73,20 @@ public class LoggersSettings {
                return writeEnabled;
        }
 
+       /**
+        * @return The explicitly-declared {@link LogBackend} the {@code 
/loggers} endpoint drives, or
+        *      {@link JulLogBackend#INSTANCE} when none was declared.  Never 
<jk>null</jk>.
+        */
+       public LogBackend getBackend() {
+               return backend;
+       }
+
        /**
         * Builder for {@link LoggersSettings}.
         */
        public static class Builder {
                private boolean writeEnabled;
+               private LogBackend backend;
 
                /**
                 * Enables the {@code PUT}/{@code POST} set-level endpoints.
@@ -77,6 +98,22 @@ public class LoggersSettings {
                        return this;
                }
 
+               /**
+                * Explicitly declares the {@link LogBackend} the {@code 
/loggers} endpoint drives.
+                *
+                * <p>
+                * When not set, the endpoint drives {@link JulLogBackend 
java.util.logging}.  Pass a Logback / Log4j2
+                * backend (from {@code juneau-rest-server-management-logging}) 
or a custom implementation to drive a
+                * different backend &mdash; this is the explicit opt-in; the 
endpoint does not auto-detect.
+                *
+                * @param value The backend.  <jk>null</jk> resets to the JUL 
default.
+                * @return This object.
+                */
+               public Builder backend(LogBackend value) {
+                       backend = value;
+                       return this;
+               }
+
                /**
                 * Builds the settings.
                 *
diff --git a/juneau-rest/pom.xml b/juneau-rest/pom.xml
index 6ffb83c5f1..4a259535b3 100644
--- a/juneau-rest/pom.xml
+++ b/juneau-rest/pom.xml
@@ -42,6 +42,7 @@
                <module>juneau-rest-server-auth-oidc-rp</module>
                <module>juneau-rest-server-metrics-micrometer</module>
                <module>juneau-rest-server-tracing-otel</module>
+               <module>juneau-rest-server-management-logging</module>
                <module>juneau-rest-server-reactive</module>
                <module>juneau-rest-server-reactive-reactor</module>
                <module>juneau-rest-server-view-jsp</module>
diff --git a/pom.xml b/pom.xml
index 682c60dae6..2356dca9d7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -74,6 +74,8 @@
                <opentelemetry.version>1.62.0</opentelemetry.version>
                <jakarta.servlet-api.version>6.1.0</jakarta.servlet-api.version>
                
<jakarta.validation-api.version>3.0.2</jakarta.validation-api.version>
+               <logback.version>1.5.18</logback.version>
+               <log4j.version>2.24.3</log4j.version>
        </properties>
 
        <modules>

Reply via email to