blob: c9608d73dd736137ff4f1202b356b17fe84ecdee [file]
/*
* 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;
import static org.assertj.core.api.Assertions.assertThat;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.core.status.OnConsoleStatusListener;
import ch.qos.logback.core.status.StatusUtil;
import ch.qos.logback.core.util.StatusListenerConfigHelper;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.util.Properties;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/**
* Pins the logging setup to Spring Boot's conventions.
*
* <p>
* Exactly one logback configuration ships, {@code logback-spring.xml}, and Boot
* finds it by convention. A standard-location file ({@code logback.xml}) would
* be applied by logback itself before Boot starts and would then make
* {@code AbstractLoggingSystem.initializeWithConventions()} stop there, never
* loading the {@code -spring} variant and silently dropping every
* {@code <springProfile>} appender. Boot's reference documentation:
* {@code <springProfile>} <em>"cannot be used in the standard logback.xml file
* because it is loaded too early"</em>.
*
* <p>
* The one thing that must happen before Boot exists is silencing logback's own
* status output. In a native image logback cannot read its version from the
* manifest, raises a {@code |-WARN}, and {@code LogbackServiceProvider} then
* prints the whole status list to stdout - which STDIO reserves for JSON-RPC.
* {@code LogbackServiceProvider} skips that print whenever a status listener is
* installed, and {@code ContextInitializer.autoConfig()} installs one from the
* {@code logback.statusListenerClass} system property. {@link Main} sets that
* property first thing, before anything can touch {@code LoggerFactory}.
*
* @see <a href=
* "https://docs.spring.io/spring-boot/reference/features/logging.html#features.logging.custom-log-configuration">Spring
* Boot - Custom Log Configuration</a>
* @see <a href=
* "https://logback.qos.ch/manual/configuration.html#dumpingStatusData">Logback
* - status data</a>
*/
class LoggingConfigurationTest {
/** Locations logback's {@code ContextInitializer} scans on its own. */
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";
private String previousStatusListenerClass;
@BeforeEach
void rememberStatusListenerProperty() {
previousStatusListenerClass = System.getProperty(Main.LOGBACK_STATUS_LISTENER_PROPERTY);
System.clearProperty(Main.LOGBACK_STATUS_LISTENER_PROPERTY);
}
@AfterEach
void restoreStatusListenerProperty() {
if (previousStatusListenerClass == null) {
System.clearProperty(Main.LOGBACK_STATUS_LISTENER_PROPERTY);
} else {
System.setProperty(Main.LOGBACK_STATUS_LISTENER_PROPERTY, previousStatusListenerClass);
}
}
/** 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();
}
/**
* A standard-location file would be found first by
* {@code initializeWithConventions()} and would disable the {@code -spring}
* variant.
*/
@Test
void noStandardLocationLogbackFileShipsOnTheClasspath() {
for (String location : STANDARD_LOGBACK_LOCATIONS) {
assertThat(getClass().getClassLoader().getResource(location))
.as("%s must not ship: Spring Boot would initialize from it and never load %s, "
+ "silently dropping every <springProfile> appender", location, SPRING_VARIANT)
.isNull();
}
}
/**
* With no standard-location file there is nothing to steer Boot past, so
* {@code logging.config} stays unset and Boot resolves {@code -spring} by
* convention. The property remains available to operators via the environment.
*/
@Test
void bootResolvesTheSpringVariantByConvention() {
assertThat(applicationProperties().getProperty("logging.config"))
.as("logging.config is an operator override for an external file, not a way to pick between "
+ "classpath files; leave it unset and let Boot find %s by convention", SPRING_VARIANT)
.isNull();
}
/**
* The status listener has to be in place before logback prints its status list,
* i.e. before the first {@code LoggerFactory} touch. The only code that runs
* that early is {@code main()}.
*/
@Test
void mainInstallsAStatusListenerLogbackHonoursDuringItsOwnInitialization() {
Main.silenceLogbackStatusOutput();
LoggerContext context = new LoggerContext();
StatusListenerConfigHelper.installIfAsked(context);
assertThat(StatusUtil.contextHasStatusListener(context))
.as("LogbackServiceProvider prints the status list to stdout unless a listener is installed; "
+ "%s must name one", Main.LOGBACK_STATUS_LISTENER_PROPERTY)
.isTrue();
}
/** An operator debugging logback itself keeps their {@code -D} override. */
@Test
void mainDoesNotOverrideAnOperatorSuppliedStatusListener() {
String operatorChoice = OnConsoleStatusListener.class.getName();
System.setProperty(Main.LOGBACK_STATUS_LISTENER_PROPERTY, operatorChoice);
Main.silenceLogbackStatusOutput();
assertThat(System.getProperty(Main.LOGBACK_STATUS_LISTENER_PROPERTY)).isEqualTo(operatorChoice);
}
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;
}
}