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}
— <b>not</b> SLF4J the facade
+ * (an SLF4J→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
— every {@link LoggerConfig} in
+ * the configuration carries a concrete level — 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} — <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())} —
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} — 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 — 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 → 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 — 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→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)} — the endpoint never
+ * auto-detects. The response shape (logger name → 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 → 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 → 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 — see the actuator group's exposure policy.
*
- * <h5 class='section'>Notes:</h5><ul>
- * <li class='warn'>JUL-only in v1 — 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)} — 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 — 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)} — 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)} — 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 — 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>