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

epugh pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr-mcp.git


The following commit(s) were added to refs/heads/main by this push:
     new 04b0c72  fix(logging): load logback-spring.xml without breaking native 
stdout (#189)
04b0c72 is described below

commit 04b0c723b88d6099081d6b17c6170414c3b2f2d3
Author: Aditya Parikh <[email protected]>
AuthorDate: Fri Sep 11 10:16:07 2026 -0400

    fix(logging): load logback-spring.xml without breaking native stdout (#189)
    
    `logback-spring.xml` was never loaded. HTTP mode therefore ran with no
    appenders at all — no console logging and no OTLP log export — and any
    startup failure exited 1 printing nothing but the Spring banner.
    
    Reproduce on `main`:
    
        PROFILES=http java -jar build/libs/solr-mcp-<v>.jar   # 0 stdout lines
    
    Root cause: Spring Boot's 
`AbstractLoggingSystem.initializeWithConventions()`
    checks the *standard* Logback locations first (`logback-test.xml`,
    `logback.xml`, ...). Finding one with `logging.file.name` unset, it calls
    `reinitialize()` and returns early — `logback-spring.xml` is never 
consulted,
    taking every `<springProfile>` appender with it. Boot's reference
    documentation is explicit that `<springProfile>` "cannot be used in the
    standard logback.xml file because it is loaded too early".
    
    Deleting `logback.xml` is not the fix, because there are two logging
    initialization phases and each file serves one:
    
      1. logback's own `ContextInitializer`, on the first `LoggerFactory` touch,
         before Spring Boot exists. It only ever scans the standard locations; 
it
         has never heard of `logback-spring.xml`. With no file it falls back to
         `BasicConfigurator` and — in a native image, where logback cannot read
         its own manifest and so always raises `|-WARN ... Versions of
         logback-classic and ? are different or unknown` — trips
         `StatusPrinter.printInCaseOfErrorsOrWarnings()`, flushing its whole
         `|-INFO` status list to stdout. In the native STDIO image those lines
         land in the MCP JSON-RPC stream and `initialize()` times out.
    
      2. Spring Boot's `LoggingApplicationListener`, which owns the per-profile
         appenders and must load `logback-spring.xml`.
    
    So keep both files and set `logging.config` to break the collision:
    
        logging.config=${LOGGING_CONFIG:classpath:logback-spring.xml}
    
    `AbstractLoggingSystem.initialize()` branches to
    `initializeWithSpecificConfig` and returns when `logging.config` is 
non-empty,
    skipping the standard locations entirely. Under AOT the same property 
decides
    what `processAot` serializes into `META-INF/spring/logback-model`, which is
    what the native image actually replays — verifiable with:
    
        strings build/resources/aot/META-INF/spring/logback-model \
          | grep -E 'SpringProfile|OpenTelemetry'
    
    Also in this change:
    
    - Use Boot's own `console-appender.xml` instead of a hand-rolled
      `ConsoleAppender`, so `logging.pattern.console` / 
`logging.charset.console` /
      `logging.threshold.console` behave as in a stock Boot app. The hand-rolled
      encoder's fallback was already dead code: `defaults.xml` defines
      `CONSOLE_LOG_PATTERN`, so `${CONSOLE_LOG_PATTERN:-...}` always resolved to
      Boot's pattern — the rendered format is unchanged.
    - Register both logging files as native-image resources in 
`SolrNativeHints`.
      In a native image `getResource()` only sees registered resources, so an
      unregistered `logback.xml` is exactly as absent as a deleted one.
    - Add `LoggingConfigurationTest`, pinning the pairing: a standard-location
      file is allowed only while `logging.config` routes past it, and that file
      must declare no appenders (phase 1 runs before any profile is known, so an
      appender there reaches STDIO's stdout).
    - Correct the Logging Architecture section of `AGENTS.md`, which claimed
      "logback-spring.xml — Loaded by Spring Boot, overrides logback.xml." That
      was the intent, not the behaviour.
    
    Verification:
    
      ./gradlew clean build                          376 tests, 0 failures, 7 
skipped
      ./gradlew dockerIntegrationTest -Pnative       43 tests, 0 failures
        (native STDIO image: 0 logback status lines on stdout,
         0 "Could NOT find resource [logback.xml]", 0 JSON parse errors)
      PROFILES=http java -jar <bootJar>              47 stdout lines (0 before)
    
    The native STDIO Docker image is the only thing that covers phase 1: on the
    JVM the version lookup succeeds, there is no WARN, and nothing is ever
    printed. That is why `McpClientStdioIntegrationTest` and `nativeTest` both
    stay green either way, and why `DockerImageMcpClientStdioIntegrationTest`
    under `dockerIntegrationTest -Pnative` is the test that matters here.
    
    
    Claude-Session: https://claude.ai/code/session_013ZsUJxVPvynDWGb2Lu83XS
    
    Signed-off-by: Aditya Parikh <[email protected]>
    Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
---
 AGENTS.md                                          |  94 ++++++++++---
 .../solr/mcp/server/config/SolrNativeHints.java    |  18 ++-
 src/main/resources/application.properties          |  15 ++
 src/main/resources/logback-spring.xml              |  72 ++++++----
 src/main/resources/logback.xml                     |  44 ++++--
 .../server/config/LoggingConfigurationTest.java    | 151 +++++++++++++++++++++
 .../mcp/server/config/SolrNativeHintsTest.java     |   7 +
 7 files changed, 342 insertions(+), 59 deletions(-)

diff --git a/AGENTS.md b/AGENTS.md
index 89db734..e0156f7 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -132,23 +132,83 @@ artifact ships the SBOM without per-image wiring.
 ### Logging Architecture
 
 The STDIO transport uses stdout for JSON-RPC messages, so any stray stdout 
output
-corrupts the protocol. Logging is configured in two layers:
-
-- **`logback.xml`** — Loaded by logback BEFORE Spring Boot initializes. 
Contains only
-  a `NopStatusListener` to suppress logback's internal status messages 
(`|-INFO`,
-  `|-WARN`) that would otherwise be written directly to stdout. Required for 
native
-  image where logback falls through to `BasicConfigurator` without it.
-- **`logback-spring.xml`** — Loaded by Spring Boot, overrides `logback.xml`. 
Uses
-  `<springProfile>` blocks to scope appenders per transport mode:
-  - **HTTP**: CONSOLE appender (stdout) + OpenTelemetry appender (OTLP log 
export with
-    `captureExperimentalAttributes` and `captureKeyValuePairAttributes` 
enabled).
-  - **STDIO**: No appenders defined. Relies on `logging.pattern.console=` in
-    `application-stdio.properties` to produce empty output from Spring Boot's 
default
-    console appender. The OTEL appender is intentionally excluded to keep 
stdout clean.
-- **`application-stdio.properties`** — Sets `logging.pattern.console=` (empty 
pattern)
-  which suppresses all Spring-managed console logging after Spring Boot 
initializes.
-
-**Init order**: logback.xml → Spring Boot starts → logback-spring.xml → 
application-{profile}.properties
+corrupts the protocol. Logging is configured in **two phases**, and both files 
are
+load-bearing:
+
+| Phase | Who configures | File | Why |
+|---|---|---|---|
+| 1 | logback's own `ContextInitializer`, on the first `LoggerFactory` touch | 
`logback.xml` | `NopStatusListener`, no appenders |
+| 2 | Spring Boot's `LoggingApplicationListener` | `logback-spring.xml` | 
`<springProfile>` appenders |
+
+**Phase 1 — why `logback.xml` must exist.** `ContextInitializer` only ever 
scans the
+*standard* locations (`logback-test.xml`, `logback.xml`). It has never heard of
+`logback-spring.xml`; that name is a Spring Boot convention. With no 
standard-location
+file it falls back to `BasicConfigurator`. In a **native image** logback 
cannot read its
+own manifest, so it always raises `|-WARN … Versions of logback-classic and ? 
are
+different or unknown`, which trips 
`StatusPrinter.printInCaseOfErrorsOrWarnings()` and
+flushes the whole `|-INFO` status list to **stdout** — landing in the middle 
of the MCP
+JSON-RPC stream. On the JVM the version lookup succeeds, there is no WARN and 
nothing is
+printed, so this is reproducible *only* in the native image:
+`DockerImageMcpClientStdioIntegrationTest` under `./gradlew 
dockerIntegrationTest
+-Pnative` is the sole test that covers it.
+
+**Phase 2 — why `logging.config` must be set.** 
`AbstractLoggingSystem.initialize()`
+resolves `logging.config` first and returns; only when it is empty does it 
fall through
+to `initializeWithConventions()`, which finds the standard-location 
`logback.xml`,
+reinitializes from it and **returns** — never loading the `-spring` variant. 
That is not
+"one overrides the other", it **disables** the `-spring` file, taking every
+`<springProfile>` appender with it. Boot's documented rule: `<springProfile>` 
"cannot be
+used in the standard `logback.xml` file because it is loaded too early."
+
+That trap was live in this repo (both files, no `logging.config`): HTTP mode 
ran with no
+appenders at all — no console logs, no OTLP log export, and startup failures 
exited 1
+showing only the Spring banner. `application.properties` now sets
+
+```properties
+logging.config=${LOGGING_CONFIG:classpath:logback-spring.xml}
+```
+
+which takes the `initializeWithSpecificConfig` branch and skips the standard 
locations
+altogether. `LoggingConfigurationTest` fails the build if either half of the 
pairing is
+removed, or if an appender is ever added to the phase-1 file.
+
+Contents of `logback-spring.xml`:
+
+- A `NopStatusListener` suppressing logback's internal status messages 
(`|-INFO`,
+  `|-WARN`), which are written straight to stdout and bypass the appenders.
+- `<springProfile>` blocks scoping appenders per transport mode:
+  - **HTTP**: Boot's own `console-appender.xml` (so `logging.pattern.console` /
+    `logging.charset.console` / `logging.threshold.console` behave as in a 
stock Boot
+    app) + OpenTelemetry appender (OTLP log export with 
`captureExperimentalAttributes`
+    and `captureKeyValuePairAttributes` enabled).
+  - **STDIO**: No appenders defined, so nothing can reach stdout. The OTEL 
appender is
+    intentionally excluded too.
+- `application-stdio.properties` additionally sets `logging.pattern.console=` 
(empty
+  pattern) as a second line of defence.
+
+`SolrNativeHints` registers **both** files as native-image resources — in a 
native image
+`getResource()` only sees registered resources, so an unregistered 
`logback.xml` is
+exactly as absent as a deleted one, and phase 1 falls straight back to
+`BasicConfigurator`.
+
+Phase 2 works differently under AOT: `LogbackLoggingSystem` checks
+`initializeFromAotGeneratedArtifactsIfPossible()` *before* reading 
`logging.config`, and
+replays `META-INF/spring/logback-model` — the model `processAot` serialized 
from whatever
+configuration Boot loaded at AOT time. So `logging.config` has to be set for 
the AOT run
+too, which it is, being in `application.properties`. Verify with:
+
+```bash
+strings build/resources/aot/META-INF/spring/logback-model | grep -E 
'SpringProfile|OpenTelemetry'
+```
+
+`SpringProfileModel` is serialized unresolved, so profiles are still evaluated 
at runtime;
+the `logback-spring.xml` resource hint is belt-and-braces for the non-AOT path.
+
+**Init order**: logback.xml → Spring Boot starts → `logging.config` → 
logback-spring.xml
+→ application-{profile}.properties
+
+**Debugging tip**: if an HTTP-mode startup fails with no output, logging 
config is the
+first suspect — check that `logging.config` still resolves to 
`logback-spring.xml`.
 
 ### Docker image strategy
 
diff --git 
a/src/main/java/org/apache/solr/mcp/server/config/SolrNativeHints.java 
b/src/main/java/org/apache/solr/mcp/server/config/SolrNativeHints.java
index 584f7ce..55805a5 100644
--- a/src/main/java/org/apache/solr/mcp/server/config/SolrNativeHints.java
+++ b/src/main/java/org/apache/solr/mcp/server/config/SolrNativeHints.java
@@ -125,12 +125,20 @@ public class SolrNativeHints {
                                        
"org.springaicommunity.mcp.context.DefaultMetaProvider",
                                        
MemberCategory.INVOKE_DECLARED_CONSTRUCTORS);
 
-                       // Include logback.xml in the native image so logback's 
early
-                       // initialization (before Spring Boot) finds it and 
applies the
-                       // NopStatusListener. Without this, logback falls 
through to
-                       // BasicConfigurator and writes status messages to 
stdout,
-                       // corrupting the MCP STDIO JSON-RPC framing.
+                       // Both logging configurations must be reachable in the 
native image;
+                       // they serve different initialization phases.
+                       //
+                       // logback.xml is read by logback's own 
ContextInitializer before
+                       // Spring Boot exists. In a native image getResource() 
only sees
+                       // registered resources, so without this hint logback 
finds nothing,
+                       // falls back to BasicConfigurator and dumps its |-INFO 
status lines
+                       // to stdout — which corrupts the MCP STDIO JSON-RPC 
framing.
+                       //
+                       // logback-spring.xml is what Spring Boot loads 
(application.properties
+                       // sets logging.config to it) and carries the 
<springProfile>
+                       // appenders. See LoggingConfigurationTest.
                        hints.resources().registerPattern("logback.xml");
+                       hints.resources().registerPattern("logback-spring.xml");
                }
        }
 }
diff --git a/src/main/resources/application.properties 
b/src/main/resources/application.properties
index c2038f5..48b8b4a 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -23,3 +23,18 @@ spring.ai.mcp.server.version=1.0.0
 solr.url=${SOLR_URL:http://localhost:8983/solr/}
 # Enable virtual threads for improved concurrency
 spring.threads.virtual.enabled=true
+# Logging configuration.
+#
+# Point Spring Boot explicitly at logback-spring.xml. Without this, Boot runs
+# AbstractLoggingSystem.initializeWithConventions(), which finds the
+# standard-location logback.xml first, reinitializes from it and returns --
+# logback-spring.xml is never loaded and every <springProfile> appender is
+# silently dropped. Setting logging.config takes the 
initializeWithSpecificConfig
+# branch instead, which skips the standard locations entirely.
+#
+# logback.xml still ships, but only to configure logback's OWN initialization,
+# which runs before Spring Boot and only ever looks at the standard locations.
+# See logback.xml and LoggingConfigurationTest.
+#
+# LOGGING_CONFIG in the environment still overrides this.
+logging.config=${LOGGING_CONFIG:classpath:logback-spring.xml}
diff --git a/src/main/resources/logback-spring.xml 
b/src/main/resources/logback-spring.xml
index 0045aac..7b95675 100644
--- a/src/main/resources/logback-spring.xml
+++ b/src/main/resources/logback-spring.xml
@@ -15,40 +15,56 @@
   See the License for the specific language governing permissions and
   limitations under the License.
 -->
+<!--
+    The application's real logging configuration: everything Spring 
Boot-managed
+    lives here, including the per-profile appenders.
+
+    It must keep the "-spring" name - <springProfile> "cannot be used in the
+    standard logback.xml file because it is loaded too early" (Spring Boot
+    reference documentation).
+
+    It is reached only because application.properties sets
+    logging.config=classpath:logback-spring.xml. Without that property Spring
+    Boot runs AbstractLoggingSystem.initializeWithConventions(), which resolves
+    the standard Logback locations first, finds logback.xml, reinitializes from
+    it and returns - this file would never be loaded and every <springProfile>
+    appender below would silently disappear.
+
+    logback.xml is a separate, deliberate file covering the phase before Spring
+    Boot exists; see its comment. LoggingConfigurationTest enforces that the 
two
+    stay paired with logging.config.
+-->
 <configuration>
     <!--
-        Suppress logback internal status messages (|-INFO, |-WARN lines).
-        Without this, logback writes status output directly to stdout during
-        initialization, BEFORE the profile-specific root logger is configured.
-        In STDIO mode this corrupts the MCP JSON-RPC framing.
+        Suppress logback's internal status messages (|-INFO, |-WARN lines),
+        which are written straight to stdout and bypass the appenders below.
+        In STDIO mode that output would corrupt the MCP JSON-RPC framing.
     -->
     <statusListener class="ch.qos.logback.core.status.NopStatusListener"/>
 
-    <!-- Import Spring Boot's default logging configuration -->
+    <!-- Spring Boot's conversion rules and CONSOLE_LOG_* properties -->
     <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
 
     <!--
-        HTTP mode - Full observability with console logging and OTEL export
-        Used when running as HTTP server with PROFILES=http
+        HTTP mode - console logging plus OTLP log export.
 
-        Appenders are defined inside the profile block so they do not exist
-        under the STDIO profile. This avoids logback "not referenced" warnings
-        and keeps STDIO stdout completely clean.
-    -->
-    <!--
-        "http & !stdio": if both profiles are activated at once 
(PROFILES=stdio,http)
-        the CONSOLE appender would write diagnostics to stdout, corrupting the 
MCP
-        JSON-RPC stream that STDIO transports over it. Keeping stdio dominant 
here
-        means the combination degrades safely instead of silently breaking.
+        Appenders are declared inside the profile block so they do not exist
+        under the STDIO profile at all, which keeps STDIO stdout clean without
+        relying on a filter or an empty pattern.
+
+        "http & !stdio": if both profiles are activated at once
+        (PROFILES=stdio,http) the CONSOLE appender would write diagnostics to
+        stdout and corrupt the JSON-RPC stream that STDIO transports over it.
+        Keeping stdio dominant means the combination degrades safely instead of
+        silently breaking.
     -->
     <springProfile name="http &amp; !stdio">
-        <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
-            <encoder>
-                <pattern>${CONSOLE_LOG_PATTERN:-%d{yyyy-MM-dd HH:mm:ss.SSS} 
%5p --- [%15.15t] %-40.40logger{39} : %m%n}
-                </pattern>
-                <charset>UTF-8</charset>
-            </encoder>
-        </appender>
+        <!--
+            Spring Boot's own CONSOLE appender, so the logging.pattern.console,
+            logging.charset.console and logging.threshold.console properties
+            behave exactly as they do in a stock Boot application.
+        -->
+        <include 
resource="org/springframework/boot/logging/logback/console-appender.xml"/>
 
         <!--
             OpenTelemetry appender for log export.
@@ -68,10 +84,8 @@
     </springProfile>
 
     <!--
-        STDIO mode (default) - stdout suppressed via logging.pattern.console=
-        in application-stdio.properties. No logback-level overrides needed;
-        Spring Boot's empty console pattern keeps stdout clean for MCP
-        JSON-RPC framing.
+        STDIO mode (default) - deliberately no appenders, so nothing can reach
+        stdout and break the MCP JSON-RPC framing. application-stdio.properties
+        additionally sets logging.pattern.console= as a second line of defence.
     -->
-
-</configuration>
\ No newline at end of file
+</configuration>
diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml
index 9463f02..22918f3 100644
--- a/src/main/resources/logback.xml
+++ b/src/main/resources/logback.xml
@@ -16,16 +16,44 @@
   limitations under the License.
 -->
 <!--
-    Early logback configuration loaded BEFORE Spring Boot initializes.
+    Logging configuration for logback's OWN initialization, which happens on 
the
+    first LoggerFactory touch, long before Spring Boot exists.
 
-    Logback's own initialization discovers this file (logback.xml) and
-    applies it immediately. Without it, logback falls through to its
-    BasicConfigurator, which writes internal status messages (|-INFO,
-    |-WARN) directly to stdout — corrupting the MCP STDIO JSON-RPC
-    framing in native images.
+    ch.qos.logback.classic.util.ContextInitializer only ever looks at the
+    standard locations (logback-test.xml, logback.xml). It does not know about
+    logback-spring.xml - that name is a Spring Boot convention resolved later 
by
+    LoggingApplicationListener. So this file, not logback-spring.xml, is what
+    governs that first phase.
 
-    Spring Boot subsequently loads logback-spring.xml, which overrides
-    this configuration with profile-specific settings (HTTP vs STDIO).
+    Without it logback falls through to BasicConfigurator, which installs a
+    ConsoleAppender and - because the native image cannot read logback's 
manifest
+    and therefore always raises
+
+        |-WARN ... Versions of logback-classic and ? are different or unknown
+
+    - trips StatusPrinter.printInCaseOfErrorsOrWarnings() and flushes the whole
+    status list to stdout:
+
+        |-INFO ... Could NOT find resource [logback.xml]
+        |-INFO ... Trying to configure with BasicConfigurator
+        |-INFO ... Setting up default configuration.
+
+    In the native STDIO image those lines land in the middle of the MCP 
JSON-RPC
+    stream; the client fails to parse them and initialize() times out. That is
+    only reproducible in the native image (on the JVM the version lookup
+    succeeds, so there is no WARN and nothing is ever printed), which means
+    DockerImageMcpClientStdioIntegrationTest under
+    `./gradlew dockerIntegrationTest -Pnative` is the only test that covers it.
+
+    The NopStatusListener below suppresses that output. This file deliberately
+    declares no appenders, so nothing can reach stdout in the meantime.
+
+    This file does NOT shadow logback-spring.xml: application.properties sets
+    logging.config=classpath:logback-spring.xml, which makes Spring Boot take
+    AbstractLoggingSystem.initializeWithSpecificConfig() and skip the standard
+    locations altogether. Drop that property and Boot would reinitialize from
+    this file and return, silently discarding every <springProfile> appender.
+    LoggingConfigurationTest enforces the pairing.
 -->
 <configuration>
     <statusListener class="ch.qos.logback.core.status.NopStatusListener"/>
diff --git 
a/src/test/java/org/apache/solr/mcp/server/config/LoggingConfigurationTest.java 
b/src/test/java/org/apache/solr/mcp/server/config/LoggingConfigurationTest.java
new file mode 100644
index 0000000..1d02528
--- /dev/null
+++ 
b/src/test/java/org/apache/solr/mcp/server/config/LoggingConfigurationTest.java
@@ -0,0 +1,151 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.mcp.server.config;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UncheckedIOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Properties;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Guards the two-phase logging setup, which has a trap at each end.
+ *
+ * <p>
+ * <b>Phase 1 - logback's own initialization.</b> {@code ContextInitializer}
+ * runs on the first {@code LoggerFactory} touch, before Spring Boot exists, 
and
+ * only ever looks at the standard locations ({@code logback-test.xml},
+ * {@code logback.xml}). It has never heard of {@code logback-spring.xml}. With
+ * no standard-location file it falls back to {@code BasicConfigurator} and, in
+ * a native image, prints its whole {@code |-INFO} status list to stdout - 
which
+ * corrupts the MCP STDIO JSON-RPC stream. Hence {@code logback.xml}, carrying 
a
+ * {@code NopStatusListener} and no appenders.
+ *
+ * <p>
+ * <b>Phase 2 - Spring Boot.</b> {@code AbstractLoggingSystem.initialize()}
+ * resolves {@code logging.config} first; only when it is empty does it fall
+ * through to {@code initializeWithConventions()}, which finds the
+ * standard-location {@code logback.xml}, reinitializes from it and
+ * <em>returns</em> - never loading the {@code -spring} variant, and silently
+ * dropping every {@code <springProfile>} appender with it. Boot's reference
+ * documentation states the rule directly: {@code <springProfile>} <em>"cannot
+ * be used in the standard logback.xml file because it is loaded too
+ * early"</em>.
+ *
+ * <p>
+ * So the two files are only safe together while
+ * {@code logging.config=classpath:logback-spring.xml} is set. Remove the
+ * property and HTTP mode loses its CONSOLE and OTEL appenders; remove
+ * {@code logback.xml} and the native STDIO image stops speaking MCP. Each test
+ * below pins one half of that.
+ *
+ * @see <a href=
+ *      
"https://docs.spring.io/spring-boot/reference/features/logging.html#features.logging.custom-log-configuration";>Spring
+ *      Boot - Custom Log Configuration</a>
+ */
+class LoggingConfigurationTest {
+
+       /**
+        * Locations {@code ContextInitializer} scans, and that Spring Boot's
+        * convention-based resolution would short-circuit on.
+        */
+       private static final String[] STANDARD_LOGBACK_LOCATIONS = 
{"logback-test.xml", "logback-test.groovy",
+                       "logback.groovy", "logback.xml"};
+
+       private static final String SPRING_VARIANT = "logback-spring.xml";
+
+       /** The configuration carrying the per-profile appenders must ship. */
+       @Test
+       void springVariantIsPresentOnTheClasspath() {
+               
assertThat(getClass().getClassLoader().getResource(SPRING_VARIANT))
+                               .as("%s carries the per-profile appenders and 
must ship on the classpath", SPRING_VARIANT).isNotNull();
+       }
+
+       /**
+        * Phase 2: a standard-location file is only allowed while
+        * {@code logging.config} points past it.
+        */
+       @Test
+       void standardLocationFileIsPairedWithAnExplicitLoggingConfig() {
+               String shadowing = firstStandardLocationOnClasspath();
+               if (shadowing == null) {
+                       return;
+               }
+
+               String loggingConfig = 
applicationProperties().getProperty("logging.config");
+
+               assertThat(loggingConfig).as(
+                               "%s is on the classpath. Spring Boot only skips 
it when logging.config is set; otherwise "
+                                               + 
"AbstractLoggingSystem.initializeWithConventions() reinitializes from it and 
returns, and "
+                                               + "%s is never loaded - every 
<springProfile> appender is silently dropped",
+                               shadowing, 
SPRING_VARIANT).isNotNull().contains(SPRING_VARIANT);
+       }
+
+       /**
+        * Phase 1: whatever logback loads before Spring Boot must not be able 
to write
+        * to stdout, which STDIO reserves for JSON-RPC.
+        */
+       @Test
+       void standardLocationFileDeclaresNoAppenders() {
+               String earlyConfig = firstStandardLocationOnClasspath();
+               if (earlyConfig == null) {
+                       return;
+               }
+
+               assertThat(stripXmlComments(read(earlyConfig)))
+                               .as("%s is applied by logback before Spring 
Boot and before any profile is known, so an appender here "
+                                               + "reaches stdout in STDIO mode 
and corrupts the MCP JSON-RPC framing. Appenders belong in "
+                                               + "%s, inside a <springProfile> 
block", earlyConfig, SPRING_VARIANT)
+                               .doesNotContain("<appender");
+       }
+
+       private String firstStandardLocationOnClasspath() {
+               for (String location : STANDARD_LOGBACK_LOCATIONS) {
+                       if (getClass().getClassLoader().getResource(location) 
!= null) {
+                               return location;
+                       }
+               }
+               return null;
+       }
+
+       private Properties applicationProperties() {
+               Properties properties = new Properties();
+               try (InputStream in = 
getClass().getClassLoader().getResourceAsStream("application.properties")) {
+                       assertThat(in).as("application.properties must be on 
the classpath").isNotNull();
+                       properties.load(in);
+               } catch (IOException ex) {
+                       throw new UncheckedIOException(ex);
+               }
+               return properties;
+       }
+
+       private String read(String resource) {
+               try (InputStream in = 
getClass().getClassLoader().getResourceAsStream(resource)) {
+                       assertThat(in).as("%s must be readable from the 
classpath", resource).isNotNull();
+                       return new String(in.readAllBytes(), 
StandardCharsets.UTF_8);
+               } catch (IOException ex) {
+                       throw new UncheckedIOException(ex);
+               }
+       }
+
+       private static String stripXmlComments(String xml) {
+               return xml.replaceAll("(?s)<!--.*?-->", "");
+       }
+}
diff --git 
a/src/test/java/org/apache/solr/mcp/server/config/SolrNativeHintsTest.java 
b/src/test/java/org/apache/solr/mcp/server/config/SolrNativeHintsTest.java
index 3e12d09..1765c7c 100644
--- a/src/test/java/org/apache/solr/mcp/server/config/SolrNativeHintsTest.java
+++ b/src/test/java/org/apache/solr/mcp/server/config/SolrNativeHintsTest.java
@@ -76,4 +76,11 @@ class SolrNativeHintsTest {
                // stays silent on stdout (MCP STDIO framing).
                
assertTrue(RuntimeHintsPredicates.resource().forResource("logback.xml").test(hints));
        }
+
+       @Test
+       void registersLogbackSpringXmlResourceHint() {
+               // The configuration Spring Boot loads via logging.config; it 
carries the
+               // per-profile appenders and must survive AOT.
+               
assertTrue(RuntimeHintsPredicates.resource().forResource("logback-spring.xml").test(hints));
+       }
 }

Reply via email to