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 809cb84bf2 TODO-368: JUL/Spring control plane for REST debug logging
809cb84bf2 is described below

commit 809cb84bf2e7201ab207ab82aaba250a7b95a303
Author: James Bognar <[email protected]>
AuthorDate: Sun Aug 16 06:51:27 2026 -0400

    TODO-368: JUL/Spring control plane for REST debug logging
    
    - Emit REST debug records at stable INFO level; tier (INFO/DEBUG/TRACE) 
controls message detail only.
    - Add proxy-safe logger naming (LoggerNaming.userClassName) and wire 
RestContext default logger derivation.
    - Add Spring Boot auto-config (JuneauRestLoggingAutoConfiguration) with 
reset-resistant Logback->JUL LevelChangePropagator, provided logback-classic, 
optional OSGi import + manifest assertion, and no-Logback fallback.
    - Integration coverage for resource/op-level level propagation; docs + 
migration + release-note updates.
---
 .../RestDebugLoggingPropagation_HostResource.java  |  47 ++++
 ...estDebugLoggingPropagation_Springboot_Test.java | 243 +++++++++++++++++++++
 .../juneau/rest/mock/RestDebugCapture_Test.java    | 152 ++++++++++++-
 juneau-rest/juneau-rest-server-springboot/pom.xml  |  58 +++++
 .../JuneauResetResistantLevelChangePropagator.java |  32 +++
 .../JuneauRestLoggingAutoConfiguration.java        | 167 ++++++++++++++
 ...rk.boot.autoconfigure.AutoConfiguration.imports |   1 +
 ...gAutoConfiguration_NoLogbackClasspath_Test.java |  49 +++++
 .../JuneauRestLoggingAutoConfiguration_Test.java   | 164 ++++++++++++++
 .../org/apache/juneau/rest/server/RestContext.java |   4 +-
 .../juneau/rest/server/logging/LoggerNaming.java   |  68 ++++++
 .../rest/server/logging/RestDebugPipeline.java     |  19 +-
 .../rest/server/logging/LoggerNaming_Test.java     |  67 ++++++
 .../server/logging/RestDebugPipeline_Test.java     |  20 +-
 14 files changed, 1073 insertions(+), 18 deletions(-)

diff --git 
a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/springboot/logging/RestDebugLoggingPropagation_HostResource.java
 
b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/springboot/logging/RestDebugLoggingPropagation_HostResource.java
new file mode 100644
index 0000000000..634805eda5
--- /dev/null
+++ 
b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/springboot/logging/RestDebugLoggingPropagation_HostResource.java
@@ -0,0 +1,47 @@
+/*
+ * 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.springboot.logging;
+
+import java.io.*;
+
+import org.apache.juneau.rest.server.*;
+import org.apache.juneau.rest.server.springboot.*;
+
+/**
+ * Top-level Spring Boot host resource used by logging propagation integration 
tests.
+ *
+ * @since 10.0.0
+ */
+@Rest
+public class RestDebugLoggingPropagation_HostResource extends 
BasicSpringRestServlet {
+       private static final long serialVersionUID = 1L;
+
+       @RestPost(path="/echo")
+       public String echo(RestRequest req) throws IOException {
+               return req.getContent().asString();
+       }
+
+       @RestGet(path="/one")
+       public String one() {
+               return "one";
+       }
+
+       @RestGet(path="/two")
+       public String two() {
+               return "two";
+       }
+}
diff --git 
a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/springboot/logging/RestDebugLoggingPropagation_Springboot_Test.java
 
b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/springboot/logging/RestDebugLoggingPropagation_Springboot_Test.java
new file mode 100644
index 0000000000..0eb12f9f6f
--- /dev/null
+++ 
b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/springboot/logging/RestDebugLoggingPropagation_Springboot_Test.java
@@ -0,0 +1,243 @@
+/*
+ * 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.springboot.logging;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.net.*;
+import java.net.http.*;
+import java.net.http.HttpResponse.*;
+import java.time.*;
+import java.util.*;
+import java.util.logging.*;
+
+import org.apache.juneau.*;
+import org.junit.jupiter.api.*;
+import org.springframework.boot.*;
+import org.springframework.boot.autoconfigure.*;
+import org.springframework.boot.builder.*;
+import org.springframework.boot.web.servlet.*;
+import org.springframework.context.*;
+import org.springframework.context.annotation.*;
+
+/**
+ * Spring Boot end-to-end tests proving that logging-level properties drive 
JUL debug detail.
+ *
+ * @since 10.0.0
+ */
[email protected]
+class RestDebugLoggingPropagation_Springboot_Test extends TestBase {
+
+       private static final String HOST = 
RestDebugLoggingPropagation_HostResource.class.getName();
+       private static final String OP_ONE = HOST + ".one";
+       private static final String OP_TWO = HOST + ".two";
+       private static final String OP_ECHO = HOST + ".echo";
+
+       private static final HttpClient HTTP = HttpClient.newBuilder()
+               .connectTimeout(Duration.ofSeconds(5))
+               .followRedirects(HttpClient.Redirect.NEVER)
+               .build();
+
+       @SpringBootConfiguration
+       @EnableAutoConfiguration
+       static class App {
+               @Bean public RestDebugLoggingPropagation_HostResource host() { 
return new RestDebugLoggingPropagation_HostResource(); }
+               @Bean public 
ServletRegistrationBean<RestDebugLoggingPropagation_HostResource> 
hostRegistration(RestDebugLoggingPropagation_HostResource host) {
+                       return new ServletRegistrationBean<>(host, "/api/*");
+               }
+       }
+
+       private static final class CollectingHandler extends Handler {
+               private final List<LogRecord> records = new ArrayList<>();
+
+               @Override
+               public void publish(LogRecord record) {
+                       if (isLoggable(record))
+                               records.add(record);
+               }
+
+               @Override public void flush() {}
+               @Override public void close() {}
+
+               List<LogRecord> records() {
+                       return records;
+               }
+       }
+
+       private static final class LoggerState {
+               private final Level level;
+               private final boolean useParentHandlers;
+               private final Handler[] handlers;
+
+               LoggerState(Logger logger) {
+                       level = logger.getLevel();
+                       useParentHandlers = logger.getUseParentHandlers();
+                       handlers = logger.getHandlers();
+               }
+
+               void restore(Logger logger) {
+                       for (var h : logger.getHandlers())
+                               logger.removeHandler(h);
+                       for (var h : handlers)
+                               logger.addHandler(h);
+                       logger.setUseParentHandlers(useParentHandlers);
+                       logger.setLevel(level);
+               }
+       }
+
+       private ConfigurableApplicationContext start(String...extraProperties) {
+               var props = new ArrayList<String>();
+               props.add("spring.main.banner-mode=off");
+               props.add("server.port=0");
+               props.add("logging.level.root=WARN");
+               props.add("juneau.rest.logging.propagate-levels=true");
+               props.addAll(Arrays.asList(extraProperties));
+               return new SpringApplicationBuilder(App.class)
+                       .web(WebApplicationType.SERVLET)
+                       .properties(props.toArray(String[]::new))
+                       .run();
+       }
+
+       private int port(ConfigurableApplicationContext ctx) {
+               return ctx.getEnvironment().getProperty("local.server.port", 
Integer.class, -1);
+       }
+
+       private HttpResponse<String> get(int port, String path, 
String...headers) throws Exception {
+               var b = HttpRequest.newBuilder()
+                       .uri(URI.create("http://localhost:"; + port + path))
+                       .timeout(Duration.ofSeconds(10))
+                       .GET();
+               for (var i = 0; i < headers.length; i += 2)
+                       b.header(headers[i], headers[i + 1]);
+               return HTTP.send(b.build(), BodyHandlers.ofString());
+       }
+
+       private HttpResponse<String> post(int port, String path, String body, 
String...headers) throws Exception {
+               var b = HttpRequest.newBuilder()
+                       .uri(URI.create("http://localhost:"; + port + path))
+                       .timeout(Duration.ofSeconds(10))
+                       .POST(HttpRequest.BodyPublishers.ofString(body));
+               for (var i = 0; i < headers.length; i += 2)
+                       b.header(headers[i], headers[i + 1]);
+               return HTTP.send(b.build(), BodyHandlers.ofString());
+       }
+
+       @Test void 
b01_tracePropertyDrivesFinestBodyDetail_withoutProgrammaticSetLevel() throws 
Exception {
+               var logger = Logger.getLogger(OP_ECHO);
+               var state = new LoggerState(logger);
+               var handler = new CollectingHandler();
+               try {
+                       for (var h : logger.getHandlers())
+                               logger.removeHandler(h);
+                       logger.setUseParentHandlers(false);
+                       handler.setLevel(Level.INFO);
+                       logger.addHandler(handler);
+
+                       try (var app = start("logging.level." + HOST + 
"=TRACE")) {
+                               var port = port(app);
+                               var resp = post(port, "/api/echo", 
"phase4-body", "Content-Type", "text/plain");
+                               assertEquals(200, resp.statusCode());
+                               assertTrue(resp.body().contains("phase4-body"), 
resp.body());
+                       }
+
+                       var record = handler.records().stream().filter(x -> 
OP_ECHO.equals(x.getLoggerName())).reduce((a, b) -> b).orElse(null);
+                       assertNotNull(record, "TRACE property should drive JUL 
detail without direct Logger.setLevel(...) calls");
+                       assertEquals(Level.INFO, record.getLevel());
+                       assertTrue(record.getMessage().contains("phase4-body"), 
record.getMessage());
+               } finally {
+                       state.restore(logger);
+               }
+       }
+
+       @Test void 
b02_debugAndInfoPropertiesSelectHeadersVsBasic_allEmitsStayInfo() throws 
Exception {
+               var logger = Logger.getLogger(OP_ONE);
+               var state = new LoggerState(logger);
+               var handler = new CollectingHandler();
+               try {
+                       for (var h : logger.getHandlers())
+                               logger.removeHandler(h);
+                       logger.setUseParentHandlers(false);
+                       handler.setLevel(Level.INFO);
+                       logger.addHandler(handler);
+
+                       try (var app = start("logging.level." + HOST + 
"=DEBUG")) {
+                               var port = port(app);
+                               var resp = get(port, "/api/one", "X-Debug", 
"true");
+                               assertEquals(200, resp.statusCode());
+                               assertTrue(resp.body().contains("one"), 
resp.body());
+                       }
+                       var debugRecord = handler.records().stream().filter(x 
-> OP_ONE.equals(x.getLoggerName())).reduce((a, b) -> b).orElse(null);
+                       assertNotNull(debugRecord);
+                       assertEquals(Level.INFO, debugRecord.getLevel());
+                       
assertTrue(debugRecord.getMessage().contains("---Request Headers---"), 
debugRecord.getMessage());
+                       handler.records().clear();
+
+                       try (var app = start("logging.level." + HOST + 
"=INFO")) {
+                               var port = port(app);
+                               var resp = get(port, "/api/one", "X-Debug", 
"true");
+                               assertEquals(200, resp.statusCode());
+                               assertTrue(resp.body().contains("one"), 
resp.body());
+                       }
+                       var infoRecord = handler.records().stream().filter(x -> 
OP_ONE.equals(x.getLoggerName())).reduce((a, b) -> b).orElse(null);
+                       assertNotNull(infoRecord);
+                       assertEquals(Level.INFO, infoRecord.getLevel());
+                       assertTrue(infoRecord.getMessage().contains("[200] HTTP 
GET /api/one"), infoRecord.getMessage());
+                       
assertFalse(infoRecord.getMessage().contains("---Request Headers---"), 
infoRecord.getMessage());
+               } finally {
+                       state.restore(logger);
+               }
+       }
+
+       @Test void b05_operationScopedPropertyElevatesOnlyOneOperation() throws 
Exception {
+               var parent = Logger.getLogger(HOST);
+               var opOne = Logger.getLogger(OP_ONE);
+               var opTwo = Logger.getLogger(OP_TWO);
+               var parentState = new LoggerState(parent);
+               var opOneState = new LoggerState(opOne);
+               var opTwoState = new LoggerState(opTwo);
+               var handler = new CollectingHandler();
+               try {
+                       for (var h : parent.getHandlers())
+                               parent.removeHandler(h);
+                       parent.setUseParentHandlers(false);
+                       handler.setLevel(Level.INFO);
+                       parent.addHandler(handler);
+
+                       try (var app = start(
+                               "logging.level." + HOST + "=INFO",
+                               "logging.level." + HOST + ".one=TRACE"
+                       )) {
+                               var port = port(app);
+                               assertEquals(200, get(port, "/api/one", 
"X-Scoped", "one").statusCode());
+                               assertEquals(200, get(port, "/api/two", 
"X-Scoped", "two").statusCode());
+                       }
+
+                       var oneRecord = handler.records().stream().filter(x -> 
OP_ONE.equals(x.getLoggerName())).reduce((a, b) -> b).orElse(null);
+                       var twoRecord = handler.records().stream().filter(x -> 
OP_TWO.equals(x.getLoggerName())).reduce((a, b) -> b).orElse(null);
+                       assertNotNull(oneRecord, "operation-level TRACE should 
emit for .one");
+                       assertNotNull(twoRecord, "sibling operation should 
still emit at INFO detail");
+                       assertEquals(Level.INFO, oneRecord.getLevel());
+                       assertEquals(Level.INFO, twoRecord.getLevel());
+                       assertTrue(oneRecord.getMessage().contains("---Request 
Headers---"), oneRecord.getMessage());
+                       assertFalse(twoRecord.getMessage().contains("---Request 
Headers---"), twoRecord.getMessage());
+               } finally {
+                       parentState.restore(parent);
+                       opOneState.restore(opOne);
+                       opTwoState.restore(opTwo);
+               }
+       }
+}
diff --git 
a/juneau-rest/juneau-rest-mock/src/test/java/org/apache/juneau/rest/mock/RestDebugCapture_Test.java
 
b/juneau-rest/juneau-rest-mock/src/test/java/org/apache/juneau/rest/mock/RestDebugCapture_Test.java
index fe89835d76..4db7be2dda 100644
--- 
a/juneau-rest/juneau-rest-mock/src/test/java/org/apache/juneau/rest/mock/RestDebugCapture_Test.java
+++ 
b/juneau-rest/juneau-rest-mock/src/test/java/org/apache/juneau/rest/mock/RestDebugCapture_Test.java
@@ -19,6 +19,7 @@ package org.apache.juneau.rest.mock;
 import static org.junit.jupiter.api.Assertions.*;
 
 import java.io.*;
+import java.util.*;
 import java.util.logging.*;
 
 import org.apache.juneau.commons.inject.*;
@@ -62,6 +63,27 @@ class RestDebugCapture_Test {
        @Rest(path="/mix", mixins=A05_Mixin.class)
        public static class A05_HostResource {}
 
+       @Rest(path="/proxyname")
+       public static class A08_UserResource {
+               @RestGet(path="/who")
+               public String who() {
+                       return "ok";
+               }
+       }
+
+       @Rest(path="/proxyname")
+       public static class A08_UserResource$$SpringCGLIB$$ extends 
A08_UserResource {}
+
+       public static class A09_NestedHolder {
+               @Rest(path="/nestedname")
+               public static class NestedResource {
+                       @RestGet(path="/who")
+                       public String who() {
+                               return "ok";
+                       }
+               }
+       }
+
        /**
         * A resource that IS its own {@link RestDebugFormatter} 
(highest-precedence resolution path, per
         * {@code RestContext#getRestDebugFormatter()}) with a capture cap far 
below the 8&nbsp;KB wrapper default.
@@ -126,7 +148,9 @@ class RestDebugCapture_Test {
                                .filter(r -> (B_HostA.class.getName() + 
".who").equals(r.getLoggerName()))
                                .findFirst().orElse(null);
                        assertNotNull(opA, "expected a mixin op-logger record 
named <HostA>.who (host-level cascade)");
-                       assertEquals(Level.FINEST, opA.getLevel());
+                       assertEquals(Level.INFO, opA.getLevel());
+                       assertTrue(opA.getMessage().contains("---Request 
Headers---"),
+                               "HostA debug path should still resolve a 
fine-grained tier (headers/body), even though emitted records are INFO");
 
                        var opB = cb.getRecords().stream()
                                .filter(r -> (B_HostB.class.getName() + 
".who").equals(r.getLoggerName()))
@@ -135,6 +159,8 @@ class RestDebugCapture_Test {
                        assertEquals(Level.INFO, opB.getLevel(),
                                "HostB's mixin op logger must NOT inherit 
HostA's elevated level -- hosts composing the same mixin "
                                        + "must be isolated");
+                       assertFalse(opB.getMessage().contains("---Request 
Headers---"),
+                               "HostB remains at the INFO detail tier and must 
not render headers");
                }
        }
 
@@ -169,9 +195,13 @@ class RestDebugCapture_Test {
 
                        assertNotNull(recOne);
                        assertNotNull(recTwo);
-                       assertEquals(Level.FINEST, recOne.getLevel());
+                       assertEquals(Level.INFO, recOne.getLevel());
+                       assertTrue(recOne.getMessage().contains("---Request 
Headers---"),
+                               "elevated child logger should still render 
higher-tier detail");
                        assertEquals(Level.INFO, recTwo.getLevel(),
                                "sibling operation must not inherit the 
elevated per-op child logger level");
+                       assertFalse(recTwo.getMessage().contains("---Request 
Headers---"),
+                               "sibling operation at INFO tier must not render 
headers");
                } finally {
                        opOneLogger.setLevel(prevLevel);
                }
@@ -187,6 +217,80 @@ class RestDebugCapture_Test {
                }
        }
 
+       private static final class D00_CollectingHandler extends Handler {
+               private final List<java.util.logging.LogRecord> records = new 
ArrayList<>();
+
+               @Override
+               public void publish(java.util.logging.LogRecord record) {
+                       if (isLoggable(record))
+                               records.add(record);
+               }
+
+               @Override
+               public void flush() {}
+
+               @Override
+               public void close() {}
+
+               List<java.util.logging.LogRecord> records() {
+                       return records;
+               }
+
+               void clear() {
+                       records.clear();
+               }
+       }
+
+       @Test void 
d00_realHandlerVisibilityTracksHandlerThreshold_notTierLevel() throws Exception 
{
+               var logger = Logger.getLogger(D_Resource.class.getName());
+               var prevLevel = logger.getLevel();
+               var prevUseParentHandlers = logger.getUseParentHandlers();
+               var prevHandlers = logger.getHandlers();
+               var handler = new D00_CollectingHandler();
+               try {
+                       logger.setUseParentHandlers(false);
+                       for (var h : prevHandlers)
+                               logger.removeHandler(h);
+                       handler.setLevel(Level.INFO);
+                       logger.addHandler(handler);
+                       logger.setLevel(Level.FINEST);
+
+                       var client = 
org.apache.juneau.rest.mock.classic.MockRestClient.create(D_Resource.class).build();
+                       client.post("/echo", 
"real-handler-secret").run().assertContent("real-handler-secret");
+
+                       assertEquals(1, handler.records().size(),
+                               "effective logger FINEST with INFO handler 
should publish one INFO-stamped debug record");
+                       var finestRecord = handler.records().get(0);
+                       assertEquals(Level.INFO, finestRecord.getLevel());
+                       
assertTrue(finestRecord.getMessage().contains("real-handler-secret"),
+                               "FINEST detail should still render 
request/response body content");
+
+                       handler.clear();
+                       handler.setLevel(Level.WARNING);
+                       client.post("/echo", 
"real-handler-secret").run().assertContent("real-handler-secret");
+                       assertTrue(handler.records().isEmpty(),
+                               "INFO-stamped records must be filtered by 
handlers above INFO");
+
+                       handler.setLevel(Level.INFO);
+                       logger.setLevel(Level.INFO);
+                       client.post("/echo", 
"real-handler-secret").run().assertContent("real-handler-secret");
+
+                       assertEquals(1, handler.records().size(),
+                               "logger INFO + handler INFO should preserve the 
prior single visible basic-line behavior");
+                       var infoRecord = handler.records().get(0);
+                       assertEquals(Level.INFO, infoRecord.getLevel());
+                       assertTrue(infoRecord.getMessage().contains("[200] HTTP 
POST /twophase/echo"), infoRecord.getMessage());
+                       
assertFalse(infoRecord.getMessage().contains("real-handler-secret"),
+                               "INFO tier does not install capture wrappers, 
so the body must not render");
+               } finally {
+                       logger.removeHandler(handler);
+                       for (var h : prevHandlers)
+                               logger.addHandler(h);
+                       logger.setUseParentHandlers(prevUseParentHandlers);
+                       logger.setLevel(prevLevel);
+               }
+       }
+
        @Test void d01_fineTier_noCaptureWrapperInstalled_bodyNeverRendered() 
throws Exception {
                var target = Logger.getLogger(D_Resource.class.getName());
                var prevLevel = target.getLevel();
@@ -198,7 +302,7 @@ class RestDebugCapture_Test {
                        client.post("/echo", 
"two-phase-secret").run().assertContent("two-phase-secret");
 
                        assertFalse(c.isEmpty());
-                       assertEquals(Level.FINE, c.last().getLevel());
+                       assertEquals(Level.INFO, c.last().getLevel());
                        
assertFalse(c.last().getMessage().contains("two-phase-secret"),
                                "FINE tier must not install the capture 
wrapper, so the body cannot appear in the record: " + c.last().getMessage());
                } finally {
@@ -213,7 +317,7 @@ class RestDebugCapture_Test {
                        client.post("/echo", 
"two-phase-secret").run().assertContent("two-phase-secret");
 
                        assertFalse(c.isEmpty());
-                       assertEquals(Level.FINEST, c.last().getLevel());
+                       assertEquals(Level.INFO, c.last().getLevel());
                        
assertTrue(c.last().getMessage().contains("two-phase-secret"),
                                "FINEST tier must install the capture wrapper 
and render the body: " + c.last().getMessage());
                }
@@ -282,7 +386,9 @@ class RestDebugCapture_Test {
                                .findFirst().orElse(null);
                        assertNotNull(rec, "op logger must be named as a child 
of the bean-overridden logger, not "
                                + F_Resource.class.getName() + ".who");
-                       assertEquals(Level.FINEST, rec.getLevel());
+                       assertEquals(Level.INFO, rec.getLevel());
+                       assertTrue(rec.getMessage().contains("---Request 
Headers---"),
+                               "elevated override logger should still drive 
higher-tier detail");
                } finally {
                        overrideLogger.setLevel(prevLevel);
                }
@@ -304,7 +410,8 @@ class RestDebugCapture_Test {
                                .reduce((a, b) -> b)
                                .orElse(null);
                        assertNotNull(rec);
-                       assertEquals(Level.FINEST, rec.getLevel(), "the 
resolved resource logger's tier is still FINEST");
+                       assertEquals(Level.INFO, rec.getLevel(),
+                               "records remain INFO-stamped even when the 
resolved detail tier is FINEST");
                        
assertFalse(rec.getMessage().contains("X-Secret-Header"),
                                "no headers should ever render on the 404/no-op 
path, even at FINEST: " + rec.getMessage());
                        
assertFalse(rec.getMessage().contains("leak-test-value"), rec.getMessage());
@@ -322,7 +429,7 @@ class RestDebugCapture_Test {
                        client.get("/who").run().getContent().asString();
 
                        assertFalse(c.isEmpty());
-                       assertEquals(Level.FINEST, c.last().getLevel());
+                       assertEquals(Level.INFO, c.last().getLevel());
                        assertTrue(c.last().getMessage().contains("[200] HTTP 
GET /api/who"));
                        assertNull(c.last().getThrown());
                }
@@ -437,4 +544,35 @@ class RestDebugCapture_Test {
                        assertFalse(msg.contains("Request Content"), "no body 
section should render when bodyCap(0): " + msg);
                }
        }
+
+       @Test void 
a08_proxyShapedResourceUsesUserClassLogger_nestedResourceNameRemainsDistinct() 
throws Exception {
+               var userName = A08_UserResource.class.getName() + ".who";
+               var proxyName = A08_UserResource$$SpringCGLIB$$.class.getName() 
+ ".who";
+               try (var c = 
RichLogger.getLogger(A08_UserResource.class).captureEvents(Level.FINEST)) {
+                       var client = 
org.apache.juneau.rest.mock.classic.MockRestClient
+                               .create(A08_UserResource$$SpringCGLIB$$.class)
+                               .debug()
+                               .build();
+
+                       
client.get("/who").run().assertStatus().asCode().is(200);
+
+                       
assertTrue(c.getRecords().stream().map(java.util.logging.LogRecord::getLoggerName).anyMatch(userName::equals),
+                               "proxy-shaped resource should normalize to the 
user class logger name");
+                       
assertFalse(c.getRecords().stream().map(java.util.logging.LogRecord::getLoggerName).anyMatch(proxyName::equals),
+                               "proxy-shaped logger name must not leak to 
emitted records");
+               }
+
+               var nestedName = 
A09_NestedHolder.NestedResource.class.getName() + ".who";
+               try (var c = 
RichLogger.getLogger(A09_NestedHolder.NestedResource.class).captureEvents(Level.FINEST))
 {
+                       var client = 
org.apache.juneau.rest.mock.classic.MockRestClient
+                               .create(A09_NestedHolder.NestedResource.class)
+                               .debug()
+                               .build();
+
+                       
client.get("/who").run().assertStatus().asCode().is(200);
+
+                       
assertTrue(c.getRecords().stream().map(java.util.logging.LogRecord::getLoggerName).anyMatch(nestedName::equals),
+                               "ordinary nested resource names must remain 
distinct and unnormalized");
+               }
+       }
 }
diff --git a/juneau-rest/juneau-rest-server-springboot/pom.xml 
b/juneau-rest/juneau-rest-server-springboot/pom.xml
index e29e076c0e..9a6a95666b 100644
--- a/juneau-rest/juneau-rest-server-springboot/pom.xml
+++ b/juneau-rest/juneau-rest-server-springboot/pom.xml
@@ -83,6 +83,12 @@
                        <version>${project.version}</version>
                        <scope>test</scope>
                </dependency>
+               <dependency>
+                       <groupId>ch.qos.logback</groupId>
+                       <artifactId>logback-classic</artifactId>
+                       <version>${logback.version}</version>
+                       <scope>provided</scope>
+               </dependency>
        </dependencies>
 
        <build>
@@ -124,6 +130,9 @@
                                <extensions>true</extensions>
                                <configuration>
                                        
<supportIncrementalBuild>true</supportIncrementalBuild>
+                                       <instructions>
+                                               
<Import-Package>ch.qos.logback.*;resolution:=optional,*</Import-Package>
+                                       </instructions>
                                </configuration>
                        <executions>
                                <execution>
@@ -135,6 +144,55 @@
                                </execution>
                        </executions>
                        </plugin>
+                       <plugin>
+                               <groupId>org.apache.maven.plugins</groupId>
+                               <artifactId>maven-antrun-plugin</artifactId>
+                               <executions>
+                                       <execution>
+                                               
<id>verify-optional-logback-import</id>
+                                               <phase>verify</phase>
+                                               <goals>
+                                                       <goal>run</goal>
+                                               </goals>
+                                               <configuration>
+                                                       <target>
+                                                               <loadfile 
property="manifest.contents" 
srcFile="${project.build.outputDirectory}/META-INF/MANIFEST.MF" />
+                                                               <condition 
property="manifest.hasOptionalLogbackImport">
+                                                                       
<matches string="${manifest.contents}" 
pattern="(?s).*ch\.qos\.logback\..*resolution:=optional.*" />
+                                                               </condition>
+                                                               <fail 
unless="manifest.hasOptionalLogbackImport"
+                                                                       
message="Expected optional Logback import in generated MANIFEST.MF." />
+                                                       </target>
+                                               </configuration>
+                                       </execution>
+                               </executions>
+                       </plugin>
                </plugins>
        </build>
+
+       <profiles>
+               <profile>
+                       <id>no-logback-tests</id>
+                       <activation>
+                               <property>
+                                       <name>logback.excluded</name>
+                                       <value>true</value>
+                               </property>
+                       </activation>
+                       <build>
+                               <plugins>
+                                       <plugin>
+                                               
<groupId>org.apache.maven.plugins</groupId>
+                                               
<artifactId>maven-surefire-plugin</artifactId>
+                                               <configuration>
+                                                       
<classpathDependencyExcludes>
+                                                               
<classpathDependencyExclude>ch.qos.logback:logback-classic</classpathDependencyExclude>
+                                                               
<classpathDependencyExclude>ch.qos.logback:logback-core</classpathDependencyExclude>
+                                                       
</classpathDependencyExcludes>
+                                               </configuration>
+                                       </plugin>
+                               </plugins>
+                       </build>
+               </profile>
+       </profiles>
 </project>
diff --git 
a/juneau-rest/juneau-rest-server-springboot/src/main/java/org/apache/juneau/rest/server/springboot/JuneauResetResistantLevelChangePropagator.java
 
b/juneau-rest/juneau-rest-server-springboot/src/main/java/org/apache/juneau/rest/server/springboot/JuneauResetResistantLevelChangePropagator.java
new file mode 100644
index 0000000000..8b3caffe9b
--- /dev/null
+++ 
b/juneau-rest/juneau-rest-server-springboot/src/main/java/org/apache/juneau/rest/server/springboot/JuneauResetResistantLevelChangePropagator.java
@@ -0,0 +1,32 @@
+/*
+ * 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.springboot;
+
+import ch.qos.logback.classic.jul.*;
+
+/**
+ * Reset-resistant JUL propagator used by {@link 
JuneauRestLoggingAutoConfiguration}.
+ *
+ * @since 10.0.0
+ */
+class JuneauResetResistantLevelChangePropagator extends LevelChangePropagator {
+
+       @Override
+       public boolean isResetResistant() {
+               return true;
+       }
+}
diff --git 
a/juneau-rest/juneau-rest-server-springboot/src/main/java/org/apache/juneau/rest/server/springboot/JuneauRestLoggingAutoConfiguration.java
 
b/juneau-rest/juneau-rest-server-springboot/src/main/java/org/apache/juneau/rest/server/springboot/JuneauRestLoggingAutoConfiguration.java
new file mode 100644
index 0000000000..0351d32aa4
--- /dev/null
+++ 
b/juneau-rest/juneau-rest-server-springboot/src/main/java/org/apache/juneau/rest/server/springboot/JuneauRestLoggingAutoConfiguration.java
@@ -0,0 +1,167 @@
+/*
+ * 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.springboot;
+
+import java.util.*;
+
+import org.slf4j.*;
+import org.springframework.beans.factory.*;
+import org.springframework.boot.autoconfigure.*;
+import org.springframework.boot.autoconfigure.condition.*;
+import org.springframework.context.annotation.*;
+import org.springframework.core.env.*;
+
+/**
+ * Spring Boot logging bridge auto-configuration.
+ *
+ * <p>
+ * When enabled (default), and only when Logback is the active SLF4J backend, 
installs one JUL
+ * {@code LevelChangePropagator} listener so Spring logging-level changes 
propagate to JUL.
+ *
+ * <p>
+ * This is additive by default ({@code 
juneau.rest.logging.propagate-levels.reset-jul=false}) and
+ * silently no-ops when the backend is not Logback.
+ *
+ * @since 10.0.0
+ */
+@AutoConfiguration
+@ConditionalOnClass(name="ch.qos.logback.classic.jul.LevelChangePropagator")
+@ConditionalOnProperty(name="juneau.rest.logging.propagate-levels", 
havingValue="true", matchIfMissing=true)
+public class JuneauRestLoggingAutoConfiguration {
+
+       @Bean
+       @ConditionalOnMissingBean(name="juneauRestLogLevelPropagatorInstaller")
+       public Runnable juneauRestLogLevelPropagatorInstaller(Environment env) {
+               var resetJul = 
env.getProperty("juneau.rest.logging.propagate-levels.reset-jul", 
Boolean.class, false);
+               return () -> install(LoggerFactory.getILoggerFactory(), 
resetJul);
+       }
+
+       @Bean
+       @ConditionalOnBean(name="juneauRestLogLevelPropagatorInstaller")
+       public SmartInitializingSingleton 
juneauRestLogLevelPropagatorBootstrap(Runnable 
juneauRestLogLevelPropagatorInstaller) {
+               return juneauRestLogLevelPropagatorInstaller::run;
+       }
+
+       void install(ILoggerFactory loggerFactory, boolean resetJul) {
+               var loggerContextClass = 
loadClass("ch.qos.logback.classic.LoggerContext");
+               if (loggerContextClass == null || ! 
loggerContextClass.isInstance(loggerFactory))
+                       return;
+
+               var context = loggerContextClass.cast(loggerFactory);
+               if (hasPropagator(context))
+                       return;
+
+               var propagator = newPropagator();
+               if (propagator == null)
+                       return;
+
+               invoke(propagator, "setContext", context);
+               invoke(propagator, "setResetJUL", new Class<?>[] { 
boolean.class }, resetJul);
+               invokeIfPresent(propagator, "start");
+               invoke(context, "addListener", new Class<?>[] { 
loadClass("ch.qos.logback.classic.spi.LoggerContextListener") }, propagator);
+       }
+
+       private static boolean hasPropagator(Object context) {
+               var levelChangePropagatorClass = 
loadClass("ch.qos.logback.classic.jul.LevelChangePropagator");
+               if (levelChangePropagatorClass == null)
+                       return false;
+               for (var listener : listeners(context))
+                       if (levelChangePropagatorClass.isInstance(listener))
+                               return true;
+               return false;
+       }
+
+       @SuppressWarnings({
+               "unchecked" // Logback context listener list returns raw 
listener type at runtime.
+       })
+       private static List<Object> listeners(Object context) {
+               try {
+                       var out = invoke(context, "getCopyOfListenerList", new 
Class<?>[0]);
+                       if (out instanceof List<?> out2)
+                               return (List<Object>)out2;
+               } catch (Exception e) {
+                       // Fall through to field-based lookup used by older 
Logback versions.
+               }
+               for (var c = context.getClass(); c != null; c = 
c.getSuperclass()) {
+                       for (var f : c.getDeclaredFields()) {
+                               if (! List.class.isAssignableFrom(f.getType()) 
|| ! f.getName().toLowerCase(Locale.ROOT).contains("listener"))
+                                       continue;
+                               try {
+                                       f.setAccessible(true);
+                                       var out = f.get(context);
+                                       if (out instanceof List<?> out2)
+                                               return (List<Object>)out2;
+                               } catch (Exception e) {
+                                       // Try next field.
+                               }
+                       }
+               }
+               return List.of();
+       }
+
+       private static Object newPropagator() {
+               try {
+                       return 
Class.forName(JuneauResetResistantLevelChangePropagator.class.getName()).getDeclaredConstructor().newInstance();
+               } catch (Exception e) {
+                       return null;
+               }
+       }
+
+       private static Class<?> loadClass(String className) {
+               try {
+                       return Class.forName(className);
+               } catch (ClassNotFoundException e) {
+                       return null;
+               }
+       }
+
+       private static Object invoke(Object target, String method, Class<?>[] 
parameterTypes, Object...args) {
+               try {
+                       var m = target.getClass().getMethod(method, 
parameterTypes);
+                       return m.invoke(target, args);
+               } catch (Exception e) {
+                       throw new RuntimeException(e);
+               }
+       }
+
+       private static Object invoke(Object target, String method, Object arg) {
+               try {
+                       for (var m : target.getClass().getMethods()) {
+                               if (! m.getName().equals(method) || 
m.getParameterCount() != 1)
+                                       continue;
+                               if 
(m.getParameters()[0].getType().isAssignableFrom(arg.getClass()))
+                                       return m.invoke(target, arg);
+                       }
+                       throw new NoSuchMethodException(method);
+               } catch (Exception e) {
+                       throw new IllegalStateException(e);
+               }
+       }
+
+       private static void invokeIfPresent(Object target, String method) {
+               try {
+                       for (var m : target.getClass().getMethods()) {
+                               if (m.getName().equals(method) && 
m.getParameterCount() == 0) {
+                                       m.invoke(target);
+                                       return;
+                               }
+                       }
+               } catch (Exception e) {
+                       throw new IllegalStateException(e);
+               }
+       }
+}
diff --git 
a/juneau-rest/juneau-rest-server-springboot/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
 
b/juneau-rest/juneau-rest-server-springboot/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
index 92a16f18f5..7a43af371b 100644
--- 
a/juneau-rest/juneau-rest-server-springboot/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
+++ 
b/juneau-rest/juneau-rest-server-springboot/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -13,3 +13,4 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 org.apache.juneau.rest.server.springboot.JuneauRestAutoConfiguration
+org.apache.juneau.rest.server.springboot.JuneauRestLoggingAutoConfiguration
diff --git 
a/juneau-rest/juneau-rest-server-springboot/src/test/java/org/apache/juneau/rest/server/springboot/JuneauRestLoggingAutoConfiguration_NoLogbackClasspath_Test.java
 
b/juneau-rest/juneau-rest-server-springboot/src/test/java/org/apache/juneau/rest/server/springboot/JuneauRestLoggingAutoConfiguration_NoLogbackClasspath_Test.java
new file mode 100644
index 0000000000..a5b9b7608d
--- /dev/null
+++ 
b/juneau-rest/juneau-rest-server-springboot/src/test/java/org/apache/juneau/rest/server/springboot/JuneauRestLoggingAutoConfiguration_NoLogbackClasspath_Test.java
@@ -0,0 +1,49 @@
+/*
+ * 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.springboot;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.apache.juneau.*;
+import org.junit.jupiter.api.*;
+import org.junit.jupiter.api.condition.*;
+import org.springframework.boot.*;
+import org.springframework.boot.autoconfigure.*;
+import org.springframework.boot.builder.*;
+
+/**
+ * Verifies startup remains healthy when Logback is excluded from the runtime 
classpath.
+ *
+ * @since 10.0.0
+ */
[email protected]
+@EnabledIfSystemProperty(named="logback.excluded", matches="true")
+class JuneauRestLoggingAutoConfiguration_NoLogbackClasspath_Test extends 
TestBase {
+
+       @SpringBootConfiguration
+       @EnableAutoConfiguration
+       static class A01_NoLogbackApp {}
+
+       @Test void a01_contextStartsWithoutLogbackAndSkipsInstaller() {
+               try (var ctx = new 
SpringApplicationBuilder(A01_NoLogbackApp.class)
+                       .web(WebApplicationType.NONE)
+                       .properties("spring.main.banner-mode=off", 
"juneau.rest.logging.propagate-levels=true")
+                       .run()) {
+                       
assertFalse(ctx.containsBean("juneauRestLogLevelPropagatorInstaller"));
+               }
+       }
+}
diff --git 
a/juneau-rest/juneau-rest-server-springboot/src/test/java/org/apache/juneau/rest/server/springboot/JuneauRestLoggingAutoConfiguration_Test.java
 
b/juneau-rest/juneau-rest-server-springboot/src/test/java/org/apache/juneau/rest/server/springboot/JuneauRestLoggingAutoConfiguration_Test.java
new file mode 100644
index 0000000000..03f082ecca
--- /dev/null
+++ 
b/juneau-rest/juneau-rest-server-springboot/src/test/java/org/apache/juneau/rest/server/springboot/JuneauRestLoggingAutoConfiguration_Test.java
@@ -0,0 +1,164 @@
+/*
+ * 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.springboot;
+
+import static org.assertj.core.api.Assertions.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.logging.Handler;
+import java.util.logging.Logger;
+
+import org.apache.juneau.*;
+import org.junit.jupiter.api.*;
+import org.slf4j.*;
+import org.slf4j.helpers.*;
+import org.springframework.boot.*;
+import org.springframework.boot.autoconfigure.*;
+import org.springframework.boot.builder.*;
+import org.springframework.boot.test.context.runner.*;
+
+import ch.qos.logback.classic.*;
+import ch.qos.logback.classic.jul.*;
+
+/**
+ * Tests for {@link JuneauRestLoggingAutoConfiguration}.
+ *
+ * @since 10.0.0
+ */
[email protected]
+class JuneauRestLoggingAutoConfiguration_Test extends TestBase {
+
+       private final ApplicationContextRunner runner = new 
ApplicationContextRunner()
+               
.withConfiguration(AutoConfigurations.of(JuneauRestLoggingAutoConfiguration.class));
+
+       @Test void a01_discoveryComesFromAutoConfigurationImports() {
+               try (var ctx = new 
SpringApplicationBuilder(A01_DiscoveryApp.class)
+                       .web(WebApplicationType.NONE)
+                       .properties("spring.main.banner-mode=off", 
"juneau.rest.logging.propagate-levels=true")
+                       .run()) {
+                       
assertTrue(ctx.containsBean("juneauRestLogLevelPropagatorInstaller"));
+               }
+       }
+
+       @SpringBootConfiguration
+       @EnableAutoConfiguration
+       static class A01_DiscoveryApp {}
+
+       @Test void b01_defaultOn_contributesInstallerBean() {
+               runner.run(ctx -> 
assertThat(ctx).hasBean("juneauRestLogLevelPropagatorInstaller"));
+       }
+
+       @Test void b02_propertyOff_disablesInstallerBean() {
+               
runner.withPropertyValues("juneau.rest.logging.propagate-levels=false")
+                       .run(ctx -> 
assertThat(ctx).doesNotHaveBean("juneauRestLogLevelPropagatorInstaller"));
+       }
+
+       @Test void c01_nonLogbackBackend_noopsSilently() {
+               var cfg = new JuneauRestLoggingAutoConfiguration();
+               var backend = new ILoggerFactory() {
+                       @Override
+                       public org.slf4j.Logger getLogger(String name) {
+                               return NOPLogger.NOP_LOGGER;
+                       }
+               };
+               assertDoesNotThrow(() -> cfg.install(backend, false));
+       }
+
+       @Test void 
d01_installsSingleResetResistantPropagator_andSurvivesReset() {
+               var cfg = new JuneauRestLoggingAutoConfiguration();
+               var context = new LoggerContext();
+               var jul = Logger.getLogger("todo368.phase3.d01");
+               var prev = jul.getLevel();
+               try {
+                       jul.setLevel(null);
+                       cfg.install(context, false);
+                       cfg.install(context, false);
+
+                       var propagators = 
context.getCopyOfListenerList().stream()
+                               .filter(LevelChangePropagator.class::isInstance)
+                               .map(LevelChangePropagator.class::cast)
+                               .toList();
+                       assertEquals(1, propagators.size(), "installer must be 
idempotent via instanceof check");
+                       assertTrue(propagators.get(0).isResetResistant(), 
"installed listener must survive LoggerContext.reset()");
+
+                       
context.getLogger("todo368.phase3.d01").setLevel(Level.DEBUG);
+                       assertEquals(java.util.logging.Level.FINE, 
jul.getLevel(), "DEBUG should propagate to JUL FINE");
+
+                       context.reset();
+                       
context.getLogger("todo368.phase3.d01").setLevel(Level.TRACE);
+                       assertEquals(java.util.logging.Level.FINEST, 
jul.getLevel(),
+                               "propagation should remain active after reset");
+               } finally {
+                       jul.setLevel(prev);
+                       context.stop();
+               }
+       }
+
+       @Test void d02_defaultResetJulFalse_preservesExistingJulState() {
+               var cfg = new JuneauRestLoggingAutoConfiguration();
+               var context = new LoggerContext();
+               var jul = Logger.getLogger("todo368.phase3.d02");
+               var prevLevel = jul.getLevel();
+               var prevUseParentHandlers = jul.getUseParentHandlers();
+               var prevHandlers = jul.getHandlers();
+               var marker = new Handler() {
+                       @Override public void 
publish(java.util.logging.LogRecord record) {}
+                       @Override public void flush() {}
+                       @Override public void close() {}
+               };
+               try {
+                       for (var h : prevHandlers)
+                               jul.removeHandler(h);
+                       jul.addHandler(marker);
+                       jul.setUseParentHandlers(false);
+                       jul.setLevel(java.util.logging.Level.WARNING);
+
+                       cfg.install(context, false);
+
+                       assertEquals(java.util.logging.Level.WARNING, 
jul.getLevel(),
+                               "reset-jul defaults to false, so explicit JUL 
levels stay intact");
+                       assertThat(jul.getHandlers()).contains(marker);
+               } finally {
+                       jul.removeHandler(marker);
+                       for (var h : prevHandlers)
+                               jul.addHandler(h);
+                       jul.setUseParentHandlers(prevUseParentHandlers);
+                       jul.setLevel(prevLevel);
+                       context.stop();
+               }
+       }
+
+       @Test void d03_resetJulTrue_clearsAndReappliesFromLogback() {
+               var cfg = new JuneauRestLoggingAutoConfiguration();
+               var context = new LoggerContext();
+               var name = "todo368.phase3.d03";
+               var jul = Logger.getLogger(name);
+               var prev = jul.getLevel();
+               try {
+                       jul.setLevel(java.util.logging.Level.WARNING);
+                       context.getLogger(name).setLevel(Level.ERROR);
+
+                       cfg.install(context, true);
+
+                       assertEquals(java.util.logging.Level.SEVERE, 
jul.getLevel(),
+                               "reset-jul=true should reapply the explicit 
Logback level into JUL");
+               } finally {
+                       jul.setLevel(prev);
+                       context.stop();
+               }
+       }
+}
diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/RestContext.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/RestContext.java
index 25925eb010..faa5260f35 100644
--- 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/RestContext.java
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/RestContext.java
@@ -1576,11 +1576,11 @@ public class RestContext extends Context {
         * The {@link RichLogger} for this resource.
         *
         * <p>
-        * Defaults to {@code RichLogger.getLogger(resourceClass.getName())}. A 
bean-store override or
+        * Defaults to {@code 
RichLogger.getLogger(LoggerNaming.userClassName(resourceClass))}. A bean-store 
override or
         * {@code @Bean} factory method REPLACES the default.
         */
        private final Memoizer<RichLogger> logger = memoizer(() -> {
-               var v = Holder.of(RichLogger.getLogger(cn(resourceClass())));
+               var v = 
Holder.of(RichLogger.getLogger(LoggerNaming.userClassName(resourceClass())));
                beanStore().createBeanFromMethod(RichLogger.class, 
resource().get(), RestContext::isBeanMethod, v.get()).ifPresent(v::set);
                return v.get();
        });
diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/logging/LoggerNaming.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/logging/LoggerNaming.java
new file mode 100644
index 0000000000..1b478483b6
--- /dev/null
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/logging/LoggerNaming.java
@@ -0,0 +1,68 @@
+/*
+ * 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.logging;
+
+import java.util.*;
+
+/**
+ * Logger-name normalization for resource classes.
+ *
+ * <p>
+ * Walks up known proxy class names and returns the first non-proxy superclass 
so default logger naming is
+ * stable across common runtime-generated proxies while preserving ordinary 
nested-class names.
+ *
+ * @since 10.0.0
+ */
+public final class LoggerNaming {
+
+       private static final List<String> KNOWN_PROXY_INFIXES = List.of(
+               "$$SpringCGLIB$$",
+               "$$EnhancerBySpringCGLIB$$",
+               "$$EnhancerByCGLIB$$",
+               "$$FastClassBySpringCGLIB$$",
+               "$ByteBuddy$",
+               "$Proxy",
+               "$HibernateProxy$",
+               "$MockitoMock$"
+       );
+
+       private LoggerNaming() {}
+
+       /**
+        * Returns the user-class binary name for a class used in default 
logger derivation.
+        *
+        * <p>
+        * If the supplied class name contains a known proxy infix, this walks 
superclasses until it finds a
+        * class name without a known proxy infix. Ordinary nested classes are 
returned unchanged.
+        *
+        * @param c The class to normalize. Must not be <jk>null</jk>.
+        * @return The normalized binary class name.
+        */
+       public static String userClassName(Class<?> c) {
+               var original = c;
+               while (c != null && containsKnownProxyInfix(c.getName()))
+                       c = c.getSuperclass();
+               return (c != null ? c : original).getName();
+       }
+
+       private static boolean containsKnownProxyInfix(String className) {
+               for (var infix : KNOWN_PROXY_INFIXES)
+                       if (className.contains(infix))
+                               return true;
+               return false;
+       }
+}
diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/logging/RestDebugPipeline.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/logging/RestDebugPipeline.java
index f7f0b3a116..dbe7844a17 100644
--- 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/logging/RestDebugPipeline.java
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/logging/RestDebugPipeline.java
@@ -32,8 +32,9 @@ import org.apache.juneau.rest.server.*;
  *     <li>tier {@code INFO} &mdash; {@code formatBasic} only.
  *     <li>tier {@code FINE} &mdash; {@code +formatHeaders}.
  *     <li>tier {@code FINEST} &mdash; {@code +formatBody} (from the cached 
bytes).
- *     <li>below {@code INFO} &mdash; nothing is emitted.
+ *     <li>coarser than {@code INFO} ({@code WARNING}, {@code SEVERE}, {@code 
OFF}) &mdash; nothing is emitted.
  * </ul>
+ * The emitted record level is always {@link Level#INFO}; the resolved tier 
controls message detail only.
  *
  * @since 10.0.0
  */
@@ -42,7 +43,11 @@ public class RestDebugPipeline {
        private RestDebugPipeline() {}
 
        /**
-        * Emits the debug record for the completed call, if the resolved 
logger's level warrants it.
+        * Emits one debug record for the completed call, if the resolved 
logger's effective level warrants it.
+        *
+        * <p>
+        * The emitted record is always stamped at {@link Level#INFO}; the 
resolved tier controls only which
+        * sections are rendered into the message body.
         *
         * @param session The completed REST session. Must not be <jk>null</jk>.
         */
@@ -52,13 +57,13 @@ public class RestDebugPipeline {
                if (logger == null)
                        return;
 
-               var level = resolveTier(logger);
-               if (level == null)
+               var tier = resolveTier(logger);
+               if (tier == null)
                        return;
 
-               var msg = render(session, opSession, level);
+               var msg = render(session, opSession, tier);
 
-               var record = new LogRecord(level, msg);
+               var record = new LogRecord(Level.INFO, msg);
                record.setLoggerName(logger.getName());
                var thrown = session.getException();
                if (thrown != null)
@@ -93,7 +98,7 @@ public class RestDebugPipeline {
         * Maps the resolved logger's effective level to the cumulative debug 
tier.
         *
         * @param logger The resolved logger.
-        * @return {@code FINEST}/{@code FINE}/{@code INFO}, or <jk>null</jk> 
if the logger is not loggable at {@code INFO}.
+        * @return {@code FINEST}/{@code FINE}/{@code INFO}, or <jk>null</jk> 
if the logger is coarser than {@code INFO}.
         */
        static Level resolveTier(Logger logger) {
                if (logger.isLoggable(Level.FINEST))
diff --git 
a/juneau-rest/juneau-rest-server/src/test/java/org/apache/juneau/rest/server/logging/LoggerNaming_Test.java
 
b/juneau-rest/juneau-rest-server/src/test/java/org/apache/juneau/rest/server/logging/LoggerNaming_Test.java
new file mode 100644
index 0000000000..06b22065d5
--- /dev/null
+++ 
b/juneau-rest/juneau-rest-server/src/test/java/org/apache/juneau/rest/server/logging/LoggerNaming_Test.java
@@ -0,0 +1,67 @@
+/*
+ * 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.logging;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.junit.jupiter.api.*;
+
+/**
+ * Unit tests for {@link LoggerNaming}.
+ *
+ * @since 10.0.0
+ */
+class LoggerNaming_Test {
+
+       static class A00_UserClass {}
+       static class A01_Nested {
+               static class Inner {}
+       }
+       static class A02_User$$SpringCGLIB$$ extends A00_UserClass {}
+       static class A03_User$$EnhancerBySpringCGLIB$$ extends A00_UserClass {}
+       static class A04_User$$EnhancerByCGLIB$$ extends A00_UserClass {}
+       static class A05_User$$FastClassBySpringCGLIB$$ extends A00_UserClass {}
+       static class A06_User$ByteBuddy$ extends A00_UserClass {}
+       static class A07_User$Proxy0 extends A00_UserClass {}
+       static class A08_User$HibernateProxy$1 extends A00_UserClass {}
+       static class A09_User$MockitoMock$1 extends A00_UserClass {}
+       static class A10_ProxyChain$$SpringCGLIB$$ extends A06_User$ByteBuddy$ 
{}
+
+       @Test void a01_knownProxyInfixes_resolveToUserSuperclassName() {
+               var expected = A00_UserClass.class.getName();
+               assertEquals(expected, 
LoggerNaming.userClassName(A02_User$$SpringCGLIB$$.class));
+               assertEquals(expected, 
LoggerNaming.userClassName(A03_User$$EnhancerBySpringCGLIB$$.class));
+               assertEquals(expected, 
LoggerNaming.userClassName(A04_User$$EnhancerByCGLIB$$.class));
+               assertEquals(expected, 
LoggerNaming.userClassName(A05_User$$FastClassBySpringCGLIB$$.class));
+               assertEquals(expected, 
LoggerNaming.userClassName(A06_User$ByteBuddy$.class));
+               assertEquals(expected, 
LoggerNaming.userClassName(A07_User$Proxy0.class));
+               assertEquals(expected, 
LoggerNaming.userClassName(A08_User$HibernateProxy$1.class));
+               assertEquals(expected, 
LoggerNaming.userClassName(A09_User$MockitoMock$1.class));
+       }
+
+       @Test void a02_superclassWalk_handlesMultipleProxyLayers() {
+               assertEquals(A00_UserClass.class.getName(), 
LoggerNaming.userClassName(A10_ProxyChain$$SpringCGLIB$$.class));
+       }
+
+       @Test void a03_nonProxyClassName_isUnchanged() {
+               assertEquals(A00_UserClass.class.getName(), 
LoggerNaming.userClassName(A00_UserClass.class));
+       }
+
+       @Test void a04_nestedClassName_isPreserved() {
+               assertEquals(A01_Nested.Inner.class.getName(), 
LoggerNaming.userClassName(A01_Nested.Inner.class));
+       }
+}
diff --git 
a/juneau-rest/juneau-rest-server/src/test/java/org/apache/juneau/rest/server/logging/RestDebugPipeline_Test.java
 
b/juneau-rest/juneau-rest-server/src/test/java/org/apache/juneau/rest/server/logging/RestDebugPipeline_Test.java
index 2d1f5ad318..0cf7596f57 100644
--- 
a/juneau-rest/juneau-rest-server/src/test/java/org/apache/juneau/rest/server/logging/RestDebugPipeline_Test.java
+++ 
b/juneau-rest/juneau-rest-server/src/test/java/org/apache/juneau/rest/server/logging/RestDebugPipeline_Test.java
@@ -43,11 +43,27 @@ class RestDebugPipeline_Test {
                assertEquals(Level.FINE, 
RestDebugPipeline.resolveTier(newLogger(Level.FINE)));
        }
 
-       @Test void a03_info_resolvesInfo() {
+       @Test void a03_finer_resolvesFine() {
+               assertEquals(Level.FINE, 
RestDebugPipeline.resolveTier(newLogger(Level.FINER)));
+       }
+
+       @Test void a04_config_resolvesInfo() {
+               assertEquals(Level.INFO, 
RestDebugPipeline.resolveTier(newLogger(Level.CONFIG)));
+       }
+
+       @Test void a05_info_resolvesInfo() {
                assertEquals(Level.INFO, 
RestDebugPipeline.resolveTier(newLogger(Level.INFO)));
        }
 
-       @Test void a04_warning_resolvesNull() {
+       @Test void a06_warning_resolvesNull() {
                
assertNull(RestDebugPipeline.resolveTier(newLogger(Level.WARNING)));
        }
+
+       @Test void a07_severe_resolvesNull() {
+               
assertNull(RestDebugPipeline.resolveTier(newLogger(Level.SEVERE)));
+       }
+
+       @Test void a08_off_resolvesNull() {
+               assertNull(RestDebugPipeline.resolveTier(newLogger(Level.OFF)));
+       }
 }

Reply via email to