diff --git a/gradle.properties b/gradle.properties
index 8e1e8a4c0fe..d205b0a73f6 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -43,7 +43,7 @@ developers=Graeme Rocher
kapt.use.worker.api=true
# Dependency Versions
-micronautMavenPluginVersion=3.5.2
+micronautMavenPluginVersion=3.5.3
chromedriverVersion=79.0.3945.36
geckodriverVersion=0.26.0
webdriverBinariesVersion=1.4
@@ -52,7 +52,7 @@ kotlin.stdlib.default.dependency=false
# For the docs
graalVersion=22.0.0.2
-micronautSecurityVersion=3.9.3
+micronautSecurityVersion=3.9.4
org.gradle.caching=true
org.gradle.parallel=true
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index e241f149059..ae873c82885 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -73,7 +73,7 @@ managed-micronaut-cache = "3.5.0"
managed-micronaut-cassandra = "5.1.1"
managed-micronaut-coherence = "3.7.2"
managed-micronaut-crac = "1.2.0"
-managed-micronaut-data = "3.9.6"
+managed-micronaut-data = "3.9.7"
managed-micronaut-discovery = "3.3.0"
managed-micronaut-elasticsearch = "4.3.0"
managed-micronaut-email = "1.5.0"
@@ -100,7 +100,7 @@ managed-micronaut-neo4j = "5.2.0"
managed-micronaut-nats = "3.1.0"
managed-micronaut-netflix = "2.1.0"
managed-micronaut-object-storage = "1.1.0"
-managed-micronaut-openapi = "4.8.5"
+managed-micronaut-openapi = "4.8.6"
managed-micronaut-oraclecloud = "2.3.4"
managed-micronaut-picocli = "4.3.0"
managed-micronaut-problem = "2.6.0"
@@ -112,7 +112,7 @@ managed-micronaut-rss = "3.2.0"
managed-micronaut-rxjava1 = "1.0.0"
managed-micronaut-rxjava2 = "1.3.0"
managed-micronaut-rxjava3 = "2.4.0"
-managed-micronaut-security = "3.9.3"
+managed-micronaut-security = "3.9.4"
managed-micronaut-serialization = "1.5.2"
managed-micronaut-servlet = "3.3.5"
managed-micronaut-spring = "4.5.1"
@@ -122,11 +122,11 @@ managed-micronaut-test-resources = "1.2.3"
managed-micronaut-toml = "1.1.3"
managed-micronaut-tracing = "4.5.0"
managed-micronaut-tracing-legacy = "3.2.7"
-managed-micronaut-views = "3.8.1"
+managed-micronaut-views = "3.8.2"
managed-micronaut-xml = "3.2.0"
managed-neo4j = "3.5.35"
managed-neo4j-java-driver = "4.4.9"
-managed-netty = "4.1.87.Final"
+managed-netty = "4.1.90.Final"
managed-reactive-pg-client = "0.11.4"
managed-reactive-streams = "1.0.4"
# This should be kept aligned with https://github.com/micronaut-projects/micronaut-reactor/blob/master/gradle.properties from the BOM
diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/HttpResponseSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/HttpResponseSpec.groovy
index df3fe92a1cc..02005fb800c 100644
--- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/HttpResponseSpec.groovy
+++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/HttpResponseSpec.groovy
@@ -153,11 +153,10 @@ class HttpResponseSpec extends AbstractMicronautSpec {
HttpHeaders headers = response.headers
then: // The content length header was replaced, not appended
- !headers.names().contains("content-type")
- !headers.names().contains("Content-Length")
- headers.contains("content-length")
response.header("Content-Type") == "text/plain"
response.header("Content-Length") == "3"
+ response.header("content-type") == "text/plain"
+ response.header("content-length") == "3"
}
void "test server header"() {
diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/staticresources/StaticResourceTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/staticresources/StaticResourceTest.java
new file mode 100644
index 00000000000..853fb27ead9
--- /dev/null
+++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/staticresources/StaticResourceTest.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2017-2023 original authors
+ *
+ * Licensed 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
+ *
+ * https://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 io.micronaut.http.server.tck.tests.staticresources;
+
+import io.micronaut.core.util.CollectionUtils;
+import io.micronaut.http.HttpHeaders;
+import io.micronaut.http.HttpRequest;
+import io.micronaut.http.HttpStatus;
+import io.micronaut.http.MediaType;
+import io.micronaut.http.server.tck.AssertionUtils;
+import io.micronaut.http.uri.UriBuilder;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.util.Collections;
+
+import static io.micronaut.http.server.tck.TestScenario.asserts;
+
+@SuppressWarnings({
+ "java:S5960", // We're allowed assertions, as these are used in tests only
+ "checkstyle:MissingJavadocType",
+ "checkstyle:DesignForExtension"
+})
+public class StaticResourceTest {
+ public static final String SPEC_NAME = "StaticResourceTest";
+
+ @Test
+ public void staticResource() throws IOException {
+ asserts(SPEC_NAME,
+ CollectionUtils.mapOf(
+ "micronaut.router.static-resources.assets.mapping", "/assets/**",
+ "micronaut.router.static-resources.assets.paths", "classpath:assets"),
+ HttpRequest.GET(UriBuilder.of("/assets").path("hello.txt").build()).accept(MediaType.TEXT_PLAIN),
+ (server, request) -> AssertionUtils.assertDoesNotThrow(server, request,
+ HttpStatus.OK,
+ "Hello World",
+ Collections.singletonMap(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN)));
+ }
+}
diff --git a/http-server-tck/src/main/resources/assets/hello.txt b/http-server-tck/src/main/resources/assets/hello.txt
new file mode 100644
index 00000000000..557db03de99
--- /dev/null
+++ b/http-server-tck/src/main/resources/assets/hello.txt
@@ -0,0 +1 @@
+Hello World
diff --git a/runtime/src/main/java/io/micronaut/logging/impl/LogbackLoggingSystem.java b/runtime/src/main/java/io/micronaut/logging/impl/LogbackLoggingSystem.java
index bb7c0cc3ad4..f1f98b53106 100644
--- a/runtime/src/main/java/io/micronaut/logging/impl/LogbackLoggingSystem.java
+++ b/runtime/src/main/java/io/micronaut/logging/impl/LogbackLoggingSystem.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2017-2020 original authors
+ * Copyright 2017-2023 original authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -23,6 +23,7 @@
import io.micronaut.core.annotation.Nullable;
import io.micronaut.logging.LogLevel;
import io.micronaut.logging.LoggingSystem;
+import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import org.slf4j.LoggerFactory;
@@ -41,8 +42,35 @@ public final class LogbackLoggingSystem implements LoggingSystem {
private final String logbackXmlLocation;
+ /**
+ * @deprecated Use {@link LogbackLoggingSystem#LogbackLoggingSystem(String, String)} instead
+ * @param logbackXmlLocation
+ */
+ @Deprecated
public LogbackLoggingSystem(@Nullable @Property(name = "logger.config") String logbackXmlLocation) {
- this.logbackXmlLocation = logbackXmlLocation != null ? logbackXmlLocation : DEFAULT_LOGBACK_LOCATION;
+ this(
+ System.getProperty("logback.configurationFile"),
+ logbackXmlLocation
+ );
+ }
+
+ /**
+ * @param logbackExternalConfigLocation The location of the logback configuration file set via logback properties
+ * @param logbackXmlLocation The location of the logback configuration file set via micronaut properties
+ * @since 3.8.8
+ */
+ @Inject
+ public LogbackLoggingSystem(
+ @Nullable @Property(name = "logback.configurationFile") String logbackExternalConfigLocation,
+ @Nullable @Property(name = "logger.config") String logbackXmlLocation
+ ) {
+ if (logbackExternalConfigLocation != null) {
+ this.logbackXmlLocation = logbackExternalConfigLocation;
+ } else if (logbackXmlLocation != null) {
+ this.logbackXmlLocation = logbackXmlLocation;
+ } else {
+ this.logbackXmlLocation = DEFAULT_LOGBACK_LOCATION;
+ }
}
@Override
diff --git a/runtime/src/main/java/io/micronaut/logging/impl/LogbackUtils.java b/runtime/src/main/java/io/micronaut/logging/impl/LogbackUtils.java
index 29bb45f652f..a395313c558 100644
--- a/runtime/src/main/java/io/micronaut/logging/impl/LogbackUtils.java
+++ b/runtime/src/main/java/io/micronaut/logging/impl/LogbackUtils.java
@@ -24,6 +24,8 @@
import io.micronaut.core.annotation.Nullable;
import io.micronaut.logging.LoggingSystemException;
+import java.io.File;
+import java.net.MalformedURLException;
import java.net.URL;
import java.util.Iterator;
import java.util.ServiceLoader;
@@ -37,6 +39,8 @@
*/
public final class LogbackUtils {
+ private static final String DEFAULT_LOGBACK_13_PROGRAMMATIC_CONFIGURATOR = "ch.qos.logback.classic.util.DefaultJoranConfigurator";
+
private LogbackUtils() {
}
@@ -50,18 +54,35 @@ private LogbackUtils() {
public static void configure(@NonNull ClassLoader classLoader,
@NonNull LoggerContext context,
@NonNull String logbackXmlLocation) {
- configure(context, logbackXmlLocation, () -> classLoader.getResource(logbackXmlLocation));
+ configure(context, logbackXmlLocation, () -> {
+ // Check classpath first
+ URL resource = classLoader.getResource(logbackXmlLocation);
+ if (resource != null) {
+ return resource;
+ }
+ // Check file system
+ File file = new File(logbackXmlLocation);
+ if (file.exists()) {
+ try {
+ resource = file.toURI().toURL();
+ } catch (MalformedURLException e) {
+
+ throw new LoggingSystemException("Error creating URL for off-classpath resource", e);
+ }
+ }
+ return resource;
+ });
}
/**
* Configures a Logger Context.
- *
+ *
* Searches fpr a custom {@link Configurator} via a service loader.
* If not present it configures the context with the resource.
*
- * @param context Logger Context
+ * @param context Logger Context
* @param logbackXmlLocation the location of the xml logback config file
- * @param resourceSupplier A resource for example logback.xml
+ * @param resourceSupplier A resource for example logback.xml
*/
private static void configure(
@NonNull LoggerContext context,
@@ -69,7 +90,7 @@ private static void configure(
Supplier resourceSupplier
) {
Configurator configurator = loadFromServiceLoader();
- if (configurator != null) {
+ if (isSupportedConfigurator(context, configurator)) {
context.getStatusManager().add(new InfoStatus("Using " + configurator.getClass().getName(), context));
programmaticConfiguration(context, configurator);
} else {
@@ -86,6 +107,17 @@ private static void configure(
}
}
+ private static boolean isSupportedConfigurator(LoggerContext context, Configurator configurator) {
+ if (configurator == null) {
+ return false;
+ }
+ if (DEFAULT_LOGBACK_13_PROGRAMMATIC_CONFIGURATOR.equals(configurator.getClass().getName())) {
+ context.getStatusManager().add(new InfoStatus("Skipping " + configurator.getClass().getName() + " as it's assumed to be from an unsupported version of Logback", context));
+ return false;
+ }
+ return true;
+ }
+
/**
* Taken from {@link ch.qos.logback.classic.util.ContextInitializer#autoConfig}.
*/
diff --git a/settings.gradle b/settings.gradle
index 1eebe9ed7b6..fd8adac26dd 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -70,6 +70,8 @@ include "test-suite-graal"
include "test-suite-groovy"
include "test-suite-groovy"
include "test-suite-logback"
+include "test-suite-logback-14"
+include "test-suite-logback-external-configuration"
include "test-utils"
// benchmarks
diff --git a/test-suite-logback-14/build.gradle b/test-suite-logback-14/build.gradle
new file mode 100644
index 00000000000..e0a46cc5dee
--- /dev/null
+++ b/test-suite-logback-14/build.gradle
@@ -0,0 +1,26 @@
+plugins {
+ id("io.micronaut.build.internal.convention-test-library")
+}
+
+description = "logback tests with a new version of logback than we support, just to check it runs. Can be removed when we upgrade as part of 4.0.0"
+
+// Logback 1.4.x is Java 11+ compatible
+def logbackVersion = JavaVersion.current().isJava11Compatible() ? "1.4.6" : "1.3.6"
+
+dependencies {
+ testAnnotationProcessor(projects.injectJava)
+
+ testImplementation(libs.managed.micronaut.test.spock) {
+ exclude(group: "io.micronaut", module: "micronaut-aop")
+ }
+ testImplementation(projects.context)
+ testImplementation(projects.injectGroovy)
+
+ // Use a newer version of logback than we support
+ testImplementation("ch.qos.logback:logback-classic:${logbackVersion}")
+
+ testImplementation(projects.management)
+ testImplementation(projects.httpClient)
+
+ testRuntimeOnly(projects.httpServerNetty)
+}
diff --git a/test-suite-logback-14/src/test/groovy/io/micronaut/logback/LoggerConfigurationSpec.groovy b/test-suite-logback-14/src/test/groovy/io/micronaut/logback/LoggerConfigurationSpec.groovy
new file mode 100644
index 00000000000..7e79856d9ea
--- /dev/null
+++ b/test-suite-logback-14/src/test/groovy/io/micronaut/logback/LoggerConfigurationSpec.groovy
@@ -0,0 +1,31 @@
+package io.micronaut.logback
+
+import ch.qos.logback.classic.Level
+import ch.qos.logback.classic.Logger
+import io.micronaut.context.annotation.Property
+import io.micronaut.http.client.HttpClient
+import io.micronaut.http.client.annotation.Client
+import io.micronaut.test.extensions.spock.annotation.MicronautTest
+import jakarta.inject.Inject
+import org.slf4j.LoggerFactory
+import spock.lang.Specification
+
+@MicronautTest
+// Setting a level in a property forces a refresh, so the XML configuration is ignored. Without this in 3.8.x, the test fails.
+@Property(name = "logger.levels.set.by.property", value = "DEBUG")
+class LoggerConfigurationSpec extends Specification {
+
+ @Inject
+ @Client("/")
+ HttpClient client
+
+ void "if configuration is supplied, xml should be ignored"() {
+ given:
+ Logger fromXml = (Logger) LoggerFactory.getLogger("xml.config")
+ Logger fromProperties = (Logger) LoggerFactory.getLogger("set.by.property")
+
+ expect:
+ fromXml.level == Level.TRACE
+ fromProperties.level == Level.DEBUG
+ }
+}
diff --git a/test-suite-logback-14/src/test/groovy/io/micronaut/logback/LoggerEndpointSpec.groovy b/test-suite-logback-14/src/test/groovy/io/micronaut/logback/LoggerEndpointSpec.groovy
new file mode 100644
index 00000000000..d7aef0db31d
--- /dev/null
+++ b/test-suite-logback-14/src/test/groovy/io/micronaut/logback/LoggerEndpointSpec.groovy
@@ -0,0 +1,71 @@
+package io.micronaut.logback
+
+import ch.qos.logback.classic.Logger
+import ch.qos.logback.classic.spi.ILoggingEvent
+import ch.qos.logback.core.AppenderBase
+import io.micronaut.context.annotation.Property
+import io.micronaut.http.HttpRequest
+import io.micronaut.http.MediaType
+import io.micronaut.http.client.HttpClient
+import io.micronaut.http.client.annotation.Client
+import io.micronaut.logback.controllers.HelloWorldController
+import io.micronaut.test.extensions.spock.annotation.MicronautTest
+import jakarta.inject.Inject
+import org.slf4j.LoggerFactory
+import spock.lang.Issue
+import spock.lang.Specification
+
+@MicronautTest
+@Property(name = "logger.levels.io.micronaut.logback", value = "INFO")
+@Property(name = "endpoints.loggers.enabled", value = "true")
+@Property(name = "endpoints.loggers.sensitive", value = "false")
+@Property(name = "endpoints.loggers.write-sensitive", value = "false")
+@Issue("https://github.com/micronaut-projects/micronaut-core/issues/8679")
+class LoggerEndpointSpec extends Specification {
+
+ @Inject
+ @Client("/")
+ HttpClient client
+
+ void "logback configuration from properties is as expected"() {
+ when:
+ def response = client.toBlocking().retrieve("/loggers/io.micronaut.logback")
+
+ then:
+ response.contains("INFO")
+ }
+
+ void "logback can be configured"() {
+ given:
+ MemoryAppender appender = new MemoryAppender()
+ Logger l = (Logger) LoggerFactory.getLogger("io.micronaut.logback.controllers")
+ l.addAppender(appender)
+ appender.start()
+
+ when:
+ def response = client.toBlocking().retrieve("/", String)
+
+ then: 'response is as expected'
+ response == HelloWorldController.RESPONSE
+
+ and: 'no log message is emitted'
+ appender.events.empty
+
+ when: 'log level is changed to TRACE'
+ def body = '{ "configuredLevel": "TRACE" }'
+ def post = HttpRequest.POST("/loggers/io.micronaut.logback.controllers", body).contentType(MediaType.APPLICATION_JSON_TYPE)
+ client.toBlocking().exchange(post)
+
+ and:
+ response = client.toBlocking().retrieve("/", String)
+
+ then: 'response is as expected'
+ response == HelloWorldController.RESPONSE
+
+ and: 'log message is emitted'
+ appender.events == [HelloWorldController.LOG_MESSAGE]
+
+ cleanup:
+ appender.stop()
+ }
+}
diff --git a/test-suite-logback-14/src/test/groovy/io/micronaut/logback/LoggerLevelSpec.groovy b/test-suite-logback-14/src/test/groovy/io/micronaut/logback/LoggerLevelSpec.groovy
new file mode 100644
index 00000000000..b953c4869e0
--- /dev/null
+++ b/test-suite-logback-14/src/test/groovy/io/micronaut/logback/LoggerLevelSpec.groovy
@@ -0,0 +1,46 @@
+package io.micronaut.logback
+
+import ch.qos.logback.classic.Logger
+import ch.qos.logback.classic.spi.ILoggingEvent
+import ch.qos.logback.core.AppenderBase
+import io.micronaut.context.annotation.Property
+import io.micronaut.http.HttpRequest
+import io.micronaut.http.MediaType
+import io.micronaut.http.client.HttpClient
+import io.micronaut.http.client.annotation.Client
+import io.micronaut.logback.controllers.HelloWorldController
+import io.micronaut.test.extensions.spock.annotation.MicronautTest
+import jakarta.inject.Inject
+import org.slf4j.LoggerFactory
+import spock.lang.Issue
+import spock.lang.Specification
+
+@MicronautTest
+@Property(name = "logger.levels.io.micronaut.logback.controllers", value = "TRACE")
+@Issue("https://github.com/micronaut-projects/micronaut-core/issues/8678")
+class LoggerLevelSpec extends Specification {
+
+ @Inject
+ @Client("/")
+ HttpClient client
+
+ void "logback can be configured via properties"() {
+ given:
+ MemoryAppender appender = new MemoryAppender()
+ Logger l = (Logger) LoggerFactory.getLogger("io.micronaut.logback.controllers")
+ l.addAppender(appender)
+ appender.start()
+
+ when:
+ def response = client.toBlocking().retrieve("/", String)
+
+ then: 'response is as expected'
+ response == HelloWorldController.RESPONSE
+
+ and: 'log message is emitted'
+ appender.events == [HelloWorldController.LOG_MESSAGE]
+
+ cleanup:
+ appender.stop()
+ }
+}
diff --git a/test-suite-logback-14/src/test/groovy/io/micronaut/logback/MemoryAppender.groovy b/test-suite-logback-14/src/test/groovy/io/micronaut/logback/MemoryAppender.groovy
new file mode 100644
index 00000000000..3e978a6888e
--- /dev/null
+++ b/test-suite-logback-14/src/test/groovy/io/micronaut/logback/MemoryAppender.groovy
@@ -0,0 +1,17 @@
+package io.micronaut.logback
+
+import ch.qos.logback.classic.spi.ILoggingEvent
+import ch.qos.logback.core.AppenderBase
+import groovy.transform.CompileStatic
+import groovy.transform.PackageScope
+
+@PackageScope
+@CompileStatic
+class MemoryAppender extends AppenderBase {
+ List events = []
+
+ @Override
+ protected void append(ILoggingEvent e) {
+ events << e.formattedMessage
+ }
+}
diff --git a/test-suite-logback-14/src/test/java/io/micronaut/logback/Application.java b/test-suite-logback-14/src/test/java/io/micronaut/logback/Application.java
new file mode 100644
index 00000000000..8f06535b06c
--- /dev/null
+++ b/test-suite-logback-14/src/test/java/io/micronaut/logback/Application.java
@@ -0,0 +1,15 @@
+package io.micronaut.logback;
+
+import io.micronaut.runtime.Micronaut;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class Application {
+
+ private static final Logger LOG = LoggerFactory.getLogger(Application.class);
+
+ public static void main(String[] args) {
+ LOG.trace("starting the app");
+ Micronaut.run(Application.class, args);
+ }
+}
diff --git a/test-suite-logback-14/src/test/java/io/micronaut/logback/controllers/HelloWorldController.java b/test-suite-logback-14/src/test/java/io/micronaut/logback/controllers/HelloWorldController.java
new file mode 100644
index 00000000000..d431f7bb72b
--- /dev/null
+++ b/test-suite-logback-14/src/test/java/io/micronaut/logback/controllers/HelloWorldController.java
@@ -0,0 +1,21 @@
+package io.micronaut.logback.controllers;
+
+import io.micronaut.http.annotation.Controller;
+import io.micronaut.http.annotation.Get;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Controller
+public class HelloWorldController {
+
+ public static final String RESPONSE = "Hello world!";
+ public static final String LOG_MESSAGE = "inside hello world";
+
+ private static final Logger LOG = LoggerFactory.getLogger(HelloWorldController.class);
+
+ @Get
+ String index() {
+ LOG.trace(LOG_MESSAGE);
+ return RESPONSE;
+ }
+}
diff --git a/test-suite-logback-14/src/test/resources/logback.xml b/test-suite-logback-14/src/test/resources/logback.xml
new file mode 100644
index 00000000000..be7932b2810
--- /dev/null
+++ b/test-suite-logback-14/src/test/resources/logback.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+
+
+
diff --git a/test-suite-logback-external-configuration/build.gradle.kts b/test-suite-logback-external-configuration/build.gradle.kts
new file mode 100644
index 00000000000..a35d4e5419d
--- /dev/null
+++ b/test-suite-logback-external-configuration/build.gradle.kts
@@ -0,0 +1,18 @@
+plugins {
+ id("io.micronaut.build.internal.convention-test-library")
+}
+
+dependencies {
+ testAnnotationProcessor(projects.injectJava)
+
+ testImplementation(libs.managed.micronaut.test.spock) {
+ exclude(group="io.micronaut", module="micronaut-aop")
+ }
+ testImplementation(projects.context)
+ testImplementation(projects.injectGroovy)
+ testImplementation(libs.managed.logback)
+ testImplementation(projects.management)
+ testImplementation(projects.httpClient)
+
+ testRuntimeOnly(projects.httpServerNetty)
+}
diff --git a/test-suite-logback-external-configuration/src/external/external-logback.xml b/test-suite-logback-external-configuration/src/external/external-logback.xml
new file mode 100644
index 00000000000..a21b01a7a8d
--- /dev/null
+++ b/test-suite-logback-external-configuration/src/external/external-logback.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+
+
+
diff --git a/test-suite-logback-external-configuration/src/test/groovy/io/micronaut/logback/ExternalConfigurationSpec.groovy b/test-suite-logback-external-configuration/src/test/groovy/io/micronaut/logback/ExternalConfigurationSpec.groovy
new file mode 100644
index 00000000000..3a0f4fed2dc
--- /dev/null
+++ b/test-suite-logback-external-configuration/src/test/groovy/io/micronaut/logback/ExternalConfigurationSpec.groovy
@@ -0,0 +1,56 @@
+package io.micronaut.logback
+
+import ch.qos.logback.classic.Level
+import ch.qos.logback.classic.Logger
+import io.micronaut.context.ApplicationContext
+import io.micronaut.runtime.server.EmbeddedServer
+import org.slf4j.LoggerFactory
+import spock.lang.See
+import spock.lang.Specification
+import spock.util.environment.RestoreSystemProperties
+
+@See("https://logback.qos.ch/manual/configuration.html#auto_configuration")
+class ExternalConfigurationSpec extends Specification {
+
+ @RestoreSystemProperties
+ def "should use the external configuration"() {
+ given:
+ System.setProperty("logback.configurationFile", "src/external/external-logback.xml")
+
+ when:
+ Logger fromXml = (Logger) LoggerFactory.getLogger("i.should.not.exist")
+ Logger external = (Logger) LoggerFactory.getLogger("external.logging")
+
+ then: 'logback.xml is ignored as we have set a configurationFile'
+ fromXml.level == null
+
+ and: 'external configuration is used'
+ external.level == Level.TRACE
+ }
+
+ @RestoreSystemProperties
+ def "should still use the external config if custom levels are defines"() {
+ given:
+ System.setProperty("logback.configurationFile", "src/external/external-logback.xml")
+
+ when:
+ def server = ApplicationContext.run(EmbeddedServer, [
+ "logger.levels.app.customisation": "DEBUG"
+ ])
+ Logger fromXml = (Logger) LoggerFactory.getLogger("i.should.not.exist")
+ Logger custom = (Logger) LoggerFactory.getLogger("app.customisation")
+ Logger external = (Logger) LoggerFactory.getLogger("external.logging")
+
+ then: 'logback.xml is ignored as we have set a configurationFile'
+ fromXml.level == null
+
+ and: 'custom levels are still respected'
+ custom.level == Level.DEBUG
+
+ and: 'external configuration is used'
+ external.level == Level.TRACE
+
+ cleanup:
+ server.stop()
+ }
+}
diff --git a/test-suite-logback-external-configuration/src/test/resources/logback.xml b/test-suite-logback-external-configuration/src/test/resources/logback.xml
new file mode 100644
index 00000000000..7b053ed9a39
--- /dev/null
+++ b/test-suite-logback-external-configuration/src/test/resources/logback.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+
+
+