diff --git a/README.md b/README.md index 8ef2a98..eefabdc 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,26 @@ [![CodeQL](https://github.com/dmitrysulman/logback-access-reactor-netty/actions/workflows/codeql.yml/badge.svg)](https://github.com/dmitrysulman/logback-access-reactor-netty/actions/workflows/codeql.yml) [![codecov](https://codecov.io/gh/dmitrysulman/logback-access-reactor-netty/graph/badge.svg?token=LOEJQ7K8Z7)](https://codecov.io/gh/dmitrysulman/logback-access-reactor-netty) -A Java/Kotlin library that integrates Logback Access with Reactor Netty HTTP server, providing comprehensive access logging capabilities. +A Java/Kotlin library and Spring Boot Starter that integrates Logback Access with Reactor Netty HTTP server, providing comprehensive access logging capabilities for reactive web applications. + +## Contents: + +- [Overview](#overview) +- [Features](#features) +- [Usage](#usage) +- [Using as a Spring Boot Starter](#using-as-a-spring-boot-starter) + - [Adding Spring Boot Starter to your project](#adding-spring-boot-starter-to-your-project) + - [Configuration](#configuration) + - [Application properties](#application-properties) + - [Profile-specific configuration](#profile-specific-configuration) + - [Dependencies](#dependencies) +- [Using as a standalone library](#using-as-a-standalone-library) + - [Adding dependency to your project](#adding-dependency-to-your-project) + - [Basic setup](#basic-setup) + - [Customize Logback Access configuration](#customize-logback-access-configuration) + - [Dependencies](#dependencies-1) +- [API documentation](#api-documentation) +- [See also](#see-also) ## Overview @@ -17,6 +36,7 @@ A Java/Kotlin library that integrates Logback Access with Reactor Netty HTTP ser ## Features +- Spring Boot Starter with auto-configuration - XML-based configuration support - Comprehensive HTTP request/response logging - Lazy-loaded access event properties for optimal performance @@ -24,17 +44,85 @@ A Java/Kotlin library that integrates Logback Access with Reactor Netty HTTP ser - Configurable through system properties or external configuration files - Debug mode for troubleshooting -## Dependencies +## Usage + +The Logback Access integration with Reactor Netty can be used in two ways: + +1. As a Spring Boot Starter for reactive Spring Boot applications based on `spring-boot-starter-webflux`. +2. As a standalone library for applications using Reactor Netty HTTP Server directly. + +## Using as a Spring Boot Starter + +### Adding Spring Boot Starter to your project + +The Spring Boot Starter is published on [Maven Central](https://central.sonatype.com/artifact/io.github.dmitrysulman/logback-access-reactor-netty-spring-boot-starter). To add the dependency, use the following snippet according to your build system: + +#### Gradle + +``` +implementation("io.github.dmitrysulman:logback-access-reactor-netty-spring-boot-starter:1.1.0") +``` + +#### Maven +``` + + io.github.dmitrysulman + logback-access-reactor-netty-spring-boot-starter + 1.1.0 + +``` + +### Configuration + +Default Spring Boot auto-configuration uses the `logback-access.xml` file from the current directory or the classpath, with a fallback to the [Common Log Format](https://en.wikipedia.org/wiki/Common_Log_Format). + +#### Application properties + +Several application properties can be specified inside `application.properties` file or `application.yaml` file, or as command line arguments: + +| Name | Description | Default Value | +|:---------------------------------------|:--------------------------------------------------------|:---------------------| +| `logback.access.reactor.netty.enabled` | Enable Logback Access Reactor Netty auto-configuration. | `true` | +| `logback.access.reactor.netty.config` | Config file name. | `logback-access.xml` | +| `logback.access.reactor.netty.debug` | Enable debug mode. | `false` | + +#### Profile-specific configuration + +The `` tag allows you to conditionally include or exclude parts of the configuration based on the active Spring profiles. You can use it anywhere within the `` element. Specify the applicable profile using the `name` attribute, which can be either a single profile name (e.g., `staging`) or a profile expression. For more details, see the [Spring Boot Logback Extensions Profile-specific Configuration reference guide](https://docs.spring.io/spring-boot/reference/features/logging.html#features.logging.logback-extensions.profile-specific), which describes the same usage. There are several examples: + +```xml + + + + + + + + + + + +``` + +### Dependencies - Java 17+ - Kotlin Standard Library 2.1.21 -- Reactor Netty HTTP Server 1.2.6+ (should be explicitly provided) +- Spring Boot Starter WebFlux 3.4.6+ (should be explicitly provided) - Logback-access 2.0.6 - SLF4J 2.0.17 -## Usage +## Using as a standalone library -### Adding dependency +### Adding dependency to your project + +The library is published on [Maven Central](https://central.sonatype.com/artifact/io.github.dmitrysulman/logback-access-reactor-netty). To add the dependency, use the following snippet according to your build system: + +##### Gradle + +``` +implementation("io.github.dmitrysulman:logback-access-reactor-netty:1.1.0") +``` #### Maven @@ -42,18 +130,14 @@ A Java/Kotlin library that integrates Logback Access with Reactor Netty HTTP ser io.github.dmitrysulman logback-access-reactor-netty - 1.0.7 + 1.1.0 ``` -#### Gradle - -``` -implementation("io.github.dmitrysulman:logback-access-reactor-netty:1.0.7") -``` - ### Basic Setup +To enable Logback Access integration with Reactor Netty, create a new instance of `ReactorNettyAccessLogFactory` and pass it to the `HttpServer.accessLog()` method. + #### Java ```java @@ -67,6 +151,8 @@ HttpServer.create() #### Kotlin +For Kotlin, a convenient [enableLogbackAccess()](https://dmitrysulman.github.io/logback-access-reactor-netty/logback-access-reactor-netty/io.github.dmitrysulman.logback.access.reactor.netty/enable-logback-access.html) extension function is provided to pass the factory instance. + ```kotlin val factory = ReactorNettyAccessLogFactory() HttpServer.create() @@ -76,13 +162,14 @@ HttpServer.create() .block() ``` -### Configuration +### Customize Logback Access configuration The library can be configured in several ways: -1. **Default configuration** uses the `logback-access.xml` file from the classpath or the current directory, with a fallback to the [Common Log Format](https://en.wikipedia.org/wiki/Common_Log_Format). +1. **Default configuration** uses the `logback-access.xml` file from the current directory or the classpath, with a fallback to the [Common Log Format](https://en.wikipedia.org/wiki/Common_Log_Format). 2. **System property.** Set `-Dlogback.access.reactor.netty.config` property to specify configuration file location. 3. **Programmatic configuration.** Provide configuration file filename or URL of the classpath resource directly: + ```java // Using specific configuration file by the filename var factory = new ReactorNettyAccessLogFactory("/path/to/logback-access.xml"); @@ -93,44 +180,25 @@ var factory = new ReactorNettyAccessLogFactory( ); ``` -### Spring Boot configuration - -#### Java +### Dependencies -```java -@Configuration -public class NettyAccessLogConfiguration { - @Bean - public NettyServerCustomizer accessLogNettyServerCustomizer() { - return (server) -> - server.accessLog(true, new ReactorNettyAccessLogFactory("path/to/your/logback-access.xml")); - } -} -``` - -#### Kotlin -```kotlin -@Configuration -class NettyAccessLogConfiguration { - @Bean - fun accessLogNettyServerCustomizer() = - NettyServerCustomizer { server -> - server.enableLogbackAccess(ReactorNettyAccessLogFactory("path/to/your/logback-access.xml")) - } -} -``` -See [enableLogbackAccess()](https://dmitrysulman.github.io/logback-access-reactor-netty/logback-access-reactor-netty/io.github.dmitrysulman.logback.access.reactor.netty/enable-logback-access.html) extension function documentation. +- Java 17+ +- Kotlin Standard Library 2.1.21 +- Reactor Netty HTTP Server 1.2.6+ (should be explicitly provided) +- Logback-access 2.0.6 +- SLF4J 2.0.17 -## Documentation +## API documentation -- [Java API (Javadoc)](https://javadoc.io/doc/io.github.dmitrysulman/logback-access-reactor-netty/latest/index.html) +- [Java API (Javadoc) - Spring Boot Starter](https://javadoc.io/doc/io.github.dmitrysulman/logback-access-reactor-netty-spring-boot-starter/latest/index.html) +- [Java API (Javadoc) - Standalone library](https://javadoc.io/doc/io.github.dmitrysulman/logback-access-reactor-netty/latest/index.html) - [Kotlin API (KDoc)](https://dmitrysulman.github.io/logback-access-reactor-netty/) -## Author - -[Dmitry Sulman](https://www.linkedin.com/in/dmitrysulman/) - ## See Also - [Reactor Netty HTTP Server Documentation](https://projectreactor.io/docs/netty/release/reference/http-server.html) - [Logback Access Documentation](https://logback.qos.ch/access.html) + +## Author + +[Dmitry Sulman](https://www.linkedin.com/in/dmitrysulman/) \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 5675fd5..1de0df5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -13,6 +13,7 @@ group = "io.github.dmitrysulman" dependencies { dokka(project("logback-access-reactor-netty")) + dokka(project("logback-access-reactor-netty-spring-boot-starter")) } tasks.jreleaserFullRelease { diff --git a/buildSrc/src/main/kotlin/conventions.gradle.kts b/buildSrc/src/main/kotlin/conventions.gradle.kts index 1f0c4b8..d8e84e1 100644 --- a/buildSrc/src/main/kotlin/conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/conventions.gradle.kts @@ -84,16 +84,24 @@ dokka { } externalDocumentationLinks { register("reactor-netty-docs") { - url("https://projectreactor.io/docs/netty/${libs.versions.reactorNetty.get()}/api/") - packageListUrl("https://projectreactor.io/docs/netty/${libs.versions.reactorNetty.get()}/api/package-list") + url("https://projectreactor.io/docs/netty/release/api/") + packageListUrl("https://projectreactor.io/docs/netty/release/api/package-list") } register("logback-access-docs") { - url("https://javadoc.io/doc/ch.qos.logback.access/logback-access-common/${libs.versions.logbackAccess.get()}/") - packageListUrl("https://javadoc.io/doc/ch.qos.logback.access/logback-access-common/${libs.versions.logbackAccess.get()}/element-list") + url("https://javadoc.io/doc/ch.qos.logback.access/logback-access-common/latest/") + packageListUrl("https://javadoc.io/doc/ch.qos.logback.access/logback-access-common/latest/element-list") } register("logback-core-docs") { - url("https://javadoc.io/doc/ch.qos.logback/logback-core/${libs.versions.logbackClassic.get()}/") - packageListUrl("https://javadoc.io/doc/ch.qos.logback/logback-core/${libs.versions.logbackClassic.get()}/element-list") + url("https://javadoc.io/doc/ch.qos.logback/logback-core/latest/") + packageListUrl("https://javadoc.io/doc/ch.qos.logback/logback-core/latest/element-list") + } + register("spring-framework-docs") { + url("https://docs.spring.io/spring-framework/docs/current/javadoc-api/") + packageListUrl("https://docs.spring.io/spring-framework/docs/current/javadoc-api/element-list") + } + register("spring-boot-docs") { + url("https://docs.spring.io/spring-boot/api/java/") + packageListUrl("https://docs.spring.io/spring-boot/api/java/element-list") } } } diff --git a/gradle.properties b/gradle.properties index 2c10d1d..d9527ea 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,7 @@ -version=1.0.8-SNAPSHOT +version=1.1.0-SNAPSHOT org.gradle.caching=true org.gradle.configuration-cache=false org.gradle.jvmargs=-Xmx2g org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled -org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true \ No newline at end of file +org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true +kapt.use.k2=true \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fb90c41..f235df8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,4 +1,5 @@ [versions] +assertj = "3.27.3" dokka = "2.0.0" jackson = "2.19.0" java = "17" @@ -14,8 +15,10 @@ logbackClassic = "1.5.18" mockk = "1.14.2" reactorNetty = "1.2.7" slf4j = "2.0.17" +springBoot = "3.4.6" [libraries] +assertj-core = { group = "org.assertj", name = "assertj-core", version.ref = "assertj" } dokka-plugin = { group = "org.jetbrains.dokka", name = "org.jetbrains.dokka.gradle.plugin", version.ref = "dokka" } dokka-javadoc-plugin = { group = "org.jetbrains.dokka-javadoc", name = "org.jetbrains.dokka-javadoc.gradle.plugin", version.ref = "dokka" } jackson-bom = { group = "com.fasterxml.jackson", name = "jackson-bom", version.ref = "jackson" } @@ -31,7 +34,15 @@ logback-classic = { group = "ch.qos.logback", name = "logback-classic", version. mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" } reactorNetty-http = { group = "io.projectreactor.netty", name = "reactor-netty-http", version.ref = "reactorNetty" } slf4j-api = { group = "org.slf4j", name = "slf4j-api", version.ref = "slf4j" } +spring-boot-autoconfigureProcessor = { group = "org.springframework.boot", name = "spring-boot-autoconfigure-processor", version.ref = "springBoot" } +spring-boot-configurationProcessor = { group = "org.springframework.boot", name = "spring-boot-configuration-processor", version.ref = "springBoot" } +spring-boot-starter = { group = "org.springframework.boot", name = "spring-boot-starter", version.ref = "springBoot" } +spring-boot-starter-reactorNetty = { group = "org.springframework.boot", name = "spring-boot-starter-reactor-netty", version.ref = "springBoot" } +spring-boot-starter-test = { group = "org.springframework.boot", name = "spring-boot-starter-test", version.ref = "springBoot" } +spring-boot-starter-webflux = { group = "org.springframework.boot", name = "spring-boot-starter-webflux", version.ref = "springBoot" } [plugins] dokka = { id = "org.jetbrains.dokka" } -jreleaser = { id = "org.jreleaser", version.ref = "jreleaser" } \ No newline at end of file +conventions = { id = "conventions" } +jreleaser = { id = "org.jreleaser", version.ref = "jreleaser" } +kotlin-springPlugin = { id = "org.jetbrains.kotlin.plugin.spring", version.ref = "kotlin" } \ No newline at end of file diff --git a/logback-access-reactor-netty-spring-boot-starter/build.gradle.kts b/logback-access-reactor-netty-spring-boot-starter/build.gradle.kts new file mode 100644 index 0000000..ab4b822 --- /dev/null +++ b/logback-access-reactor-netty-spring-boot-starter/build.gradle.kts @@ -0,0 +1,25 @@ +plugins { + alias(libs.plugins.conventions) + alias(libs.plugins.kotlin.springPlugin) + kotlin("kapt") +} + +description = "Spring Boot Starter for Logback Access integration with Reactor Netty" + +dependencies { + api(project(":logback-access-reactor-netty")) + + implementation(libs.spring.boot.starter) + implementation(libs.slf4j.api) + + provided(libs.spring.boot.starter.reactorNetty) + + kapt(libs.spring.boot.autoconfigureProcessor) + kapt(libs.spring.boot.configurationProcessor) + + testImplementation(libs.assertj.core) + testImplementation(libs.kotest.assertions.core.jvm) + testImplementation(libs.mockk) + testImplementation(libs.spring.boot.starter.test) + testImplementation(libs.spring.boot.starter.webflux) +} diff --git a/logback-access-reactor-netty-spring-boot-starter/src/main/kotlin/io/github/dmitrysulman/logback/access/reactor/netty/autoconfigure/ReactorNettyAccessLogFactoryAutoConfiguration.kt b/logback-access-reactor-netty-spring-boot-starter/src/main/kotlin/io/github/dmitrysulman/logback/access/reactor/netty/autoconfigure/ReactorNettyAccessLogFactoryAutoConfiguration.kt new file mode 100644 index 0000000..c399e6f --- /dev/null +++ b/logback-access-reactor-netty-spring-boot-starter/src/main/kotlin/io/github/dmitrysulman/logback/access/reactor/netty/autoconfigure/ReactorNettyAccessLogFactoryAutoConfiguration.kt @@ -0,0 +1,58 @@ +package io.github.dmitrysulman.logback.access.reactor.netty.autoconfigure + +import io.github.dmitrysulman.logback.access.reactor.netty.ReactorNettyAccessLogFactory +import io.github.dmitrysulman.logback.access.reactor.netty.joran.LogbackAccessJoranConfigurator +import org.springframework.boot.autoconfigure.AutoConfiguration +import org.springframework.boot.autoconfigure.EnableAutoConfiguration +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication +import org.springframework.boot.context.properties.EnableConfigurationProperties +import org.springframework.context.annotation.Bean +import org.springframework.core.env.Environment +import org.springframework.core.io.ResourceLoader +import org.springframework.util.ResourceUtils +import reactor.netty.http.server.HttpServer + +/** + * [Auto-configuration][EnableAutoConfiguration] for the Logback Access integration with Reactor Netty. + */ +@AutoConfiguration +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) +@ConditionalOnClass(HttpServer::class) +@ConditionalOnProperty(prefix = "logback.access.reactor.netty", name = ["enabled"], havingValue = "true", matchIfMissing = true) +@EnableConfigurationProperties(ReactorNettyAccessLogProperties::class) +class ReactorNettyAccessLogFactoryAutoConfiguration { + @Bean + @ConditionalOnMissingBean + fun reactorNettyAccessLogFactory( + properties: ReactorNettyAccessLogProperties, + resourceLoader: ResourceLoader, + environment: Environment, + ) = ReactorNettyAccessLogFactory( + getConfigUrl(properties, resourceLoader), + LogbackAccessJoranConfigurator(environment), + properties.debug ?: false, + ) + + @Bean + @ConditionalOnMissingBean + fun reactorNettyAccessLogWebServerFactoryCustomizer(reactorNettyAccessLogFactory: ReactorNettyAccessLogFactory) = + ReactorNettyAccessLogWebServerFactoryCustomizer(true, reactorNettyAccessLogFactory) + + private fun getConfigUrl( + properties: ReactorNettyAccessLogProperties, + resourceLoader: ResourceLoader, + ) = properties.config?.let { ResourceUtils.getURL(it) } + ?: getDefaultConfigurationResource(resourceLoader).url + + private fun getDefaultConfigurationResource(resourceLoader: ResourceLoader) = + resourceLoader + .getResource("${ResourceUtils.FILE_URL_PREFIX}${ReactorNettyAccessLogFactory.DEFAULT_CONFIG_FILE_NAME}") + .takeIf { it.exists() } + ?: resourceLoader + .getResource("${ResourceUtils.CLASSPATH_URL_PREFIX}${ReactorNettyAccessLogFactory.DEFAULT_CONFIG_FILE_NAME}") + .takeIf { it.exists() } + ?: resourceLoader.getResource("${ResourceUtils.CLASSPATH_URL_PREFIX}${ReactorNettyAccessLogFactory.DEFAULT_CONFIGURATION}") +} diff --git a/logback-access-reactor-netty-spring-boot-starter/src/main/kotlin/io/github/dmitrysulman/logback/access/reactor/netty/autoconfigure/ReactorNettyAccessLogProperties.kt b/logback-access-reactor-netty-spring-boot-starter/src/main/kotlin/io/github/dmitrysulman/logback/access/reactor/netty/autoconfigure/ReactorNettyAccessLogProperties.kt new file mode 100644 index 0000000..9e1a13f --- /dev/null +++ b/logback-access-reactor-netty-spring-boot-starter/src/main/kotlin/io/github/dmitrysulman/logback/access/reactor/netty/autoconfigure/ReactorNettyAccessLogProperties.kt @@ -0,0 +1,24 @@ +package io.github.dmitrysulman.logback.access.reactor.netty.autoconfigure + +import org.springframework.boot.context.properties.ConfigurationProperties + +/** + * [@ConfigurationProperties][ConfigurationProperties] for the Logback Access integration with Reactor Netty. + */ +@ConfigurationProperties("logback.access.reactor.netty") +class ReactorNettyAccessLogProperties { + /** + * Enable Logback Access Reactor Netty auto-configuration. + */ + var enabled: Boolean? = null + + /** + * Config file name. + */ + var config: String? = null + + /** + * Enable debug mode. + */ + var debug: Boolean? = null +} diff --git a/logback-access-reactor-netty-spring-boot-starter/src/main/kotlin/io/github/dmitrysulman/logback/access/reactor/netty/autoconfigure/ReactorNettyAccessLogWebServerFactoryCustomizer.kt b/logback-access-reactor-netty-spring-boot-starter/src/main/kotlin/io/github/dmitrysulman/logback/access/reactor/netty/autoconfigure/ReactorNettyAccessLogWebServerFactoryCustomizer.kt new file mode 100644 index 0000000..4219833 --- /dev/null +++ b/logback-access-reactor-netty-spring-boot-starter/src/main/kotlin/io/github/dmitrysulman/logback/access/reactor/netty/autoconfigure/ReactorNettyAccessLogWebServerFactoryCustomizer.kt @@ -0,0 +1,24 @@ +package io.github.dmitrysulman.logback.access.reactor.netty.autoconfigure + +import io.github.dmitrysulman.logback.access.reactor.netty.ReactorNettyAccessLogFactory +import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory +import org.springframework.boot.web.server.WebServerFactoryCustomizer + +/** + * [WebServerFactoryCustomizer] of the [NettyReactiveWebServerFactory] for the Logback Access integration. + */ +class ReactorNettyAccessLogWebServerFactoryCustomizer( + private val enableAccessLog: Boolean, + private val reactorNettyAccessLogFactory: ReactorNettyAccessLogFactory, +) : WebServerFactoryCustomizer { + override fun customize(factory: NettyReactiveWebServerFactory) { + factory.addServerCustomizers( + { server -> + server.accessLog( + enableAccessLog, + reactorNettyAccessLogFactory, + ) + }, + ) + } +} diff --git a/logback-access-reactor-netty-spring-boot-starter/src/main/kotlin/io/github/dmitrysulman/logback/access/reactor/netty/joran/LogbackAccessJoranConfigurator.kt b/logback-access-reactor-netty-spring-boot-starter/src/main/kotlin/io/github/dmitrysulman/logback/access/reactor/netty/joran/LogbackAccessJoranConfigurator.kt new file mode 100644 index 0000000..ce8ff6a --- /dev/null +++ b/logback-access-reactor-netty-spring-boot-starter/src/main/kotlin/io/github/dmitrysulman/logback/access/reactor/netty/joran/LogbackAccessJoranConfigurator.kt @@ -0,0 +1,44 @@ +package io.github.dmitrysulman.logback.access.reactor.netty.joran + +import ch.qos.logback.access.common.joran.JoranConfigurator +import ch.qos.logback.core.joran.spi.ElementSelector +import ch.qos.logback.core.joran.spi.RuleStore +import ch.qos.logback.core.model.Model +import ch.qos.logback.core.model.processor.DefaultProcessor +import org.springframework.core.env.Environment +import java.util.function.Supplier + +/** + * Extended version of the Logback Access [JoranConfigurator] that adds support of `` tags. + * + * See [SpringBootJoranConfigurator](https://github.com/spring-projects/spring-boot/blob/main/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/SpringBootJoranConfigurator.java). + */ +class LogbackAccessJoranConfigurator( + private val environment: Environment, +) : JoranConfigurator() { + override fun addElementSelectorAndActionAssociations(rs: RuleStore) { + super.addElementSelectorAndActionAssociations(rs) + rs.addRule(ElementSelector("*/springProfile"), ::LogbackAccessSpringProfileAction) + rs.addTransparentPathPart("springProfile") + } + + override fun sanityCheck(topModel: Model) { + super.sanityCheck(topModel) + performCheck(LogbackAccessSpringProfileWithinSecondPhaseElementSanityChecker(), topModel) + } + + override fun addModelHandlerAssociations(defaultProcessor: DefaultProcessor) { + defaultProcessor.addHandler(LogbackAccessSpringProfileModel::class.java) { _, _ -> + LogbackAccessSpringProfileModelHandler(context, environment) + } + super.addModelHandlerAssociations(defaultProcessor) + } + + override fun buildModelInterpretationContext() { + super.buildModelInterpretationContext() + modelInterpretationContext.configuratorSupplier = + Supplier { + LogbackAccessJoranConfigurator(environment).also { it.context = this.context } + } + } +} diff --git a/logback-access-reactor-netty-spring-boot-starter/src/main/kotlin/io/github/dmitrysulman/logback/access/reactor/netty/joran/LogbackAccessSpringProfileAction.kt b/logback-access-reactor-netty-spring-boot-starter/src/main/kotlin/io/github/dmitrysulman/logback/access/reactor/netty/joran/LogbackAccessSpringProfileAction.kt new file mode 100644 index 0000000..09a95e5 --- /dev/null +++ b/logback-access-reactor-netty-spring-boot-starter/src/main/kotlin/io/github/dmitrysulman/logback/access/reactor/netty/joran/LogbackAccessSpringProfileAction.kt @@ -0,0 +1,26 @@ +package io.github.dmitrysulman.logback.access.reactor.netty.joran + +import ch.qos.logback.core.joran.action.BaseModelAction +import ch.qos.logback.core.joran.spi.SaxEventInterpretationContext +import ch.qos.logback.core.model.Model +import org.xml.sax.Attributes + +/** + * Logback Access [BaseModelAction] for `` tags. Allows a section of a + * Logback Access configuration to only be enabled when a specific profile is active. + * + * See [SpringProfileAction](https://github.com/spring-projects/spring-boot/blob/main/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/SpringProfileAction.java). + * + * @see [LogbackAccessSpringProfileModel] + * @see [LogbackAccessSpringProfileModelHandler] + */ +class LogbackAccessSpringProfileAction : BaseModelAction() { + override fun buildCurrentModel( + interpretationContext: SaxEventInterpretationContext, + name: String, + attributes: Attributes, + ): Model = + LogbackAccessSpringProfileModel().apply { + this.name = attributes.getValue(NAME_ATTRIBUTE) + } +} diff --git a/logback-access-reactor-netty-spring-boot-starter/src/main/kotlin/io/github/dmitrysulman/logback/access/reactor/netty/joran/LogbackAccessSpringProfileModel.kt b/logback-access-reactor-netty-spring-boot-starter/src/main/kotlin/io/github/dmitrysulman/logback/access/reactor/netty/joran/LogbackAccessSpringProfileModel.kt new file mode 100644 index 0000000..ce7552d --- /dev/null +++ b/logback-access-reactor-netty-spring-boot-starter/src/main/kotlin/io/github/dmitrysulman/logback/access/reactor/netty/joran/LogbackAccessSpringProfileModel.kt @@ -0,0 +1,13 @@ +package io.github.dmitrysulman.logback.access.reactor.netty.joran + +import ch.qos.logback.core.model.NamedModel + +/** + * Logback Access [NamedModel] to support `` tags. + * + * See [SpringProfileModel](https://github.com/spring-projects/spring-boot/blob/main/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/SpringProfileModel.java). + * + * @see [LogbackAccessSpringProfileAction] + * @see [LogbackAccessSpringProfileModelHandler] + */ +class LogbackAccessSpringProfileModel : NamedModel() diff --git a/logback-access-reactor-netty-spring-boot-starter/src/main/kotlin/io/github/dmitrysulman/logback/access/reactor/netty/joran/LogbackAccessSpringProfileModelHandler.kt b/logback-access-reactor-netty-spring-boot-starter/src/main/kotlin/io/github/dmitrysulman/logback/access/reactor/netty/joran/LogbackAccessSpringProfileModelHandler.kt new file mode 100644 index 0000000..d94e675 --- /dev/null +++ b/logback-access-reactor-netty-spring-boot-starter/src/main/kotlin/io/github/dmitrysulman/logback/access/reactor/netty/joran/LogbackAccessSpringProfileModelHandler.kt @@ -0,0 +1,36 @@ +package io.github.dmitrysulman.logback.access.reactor.netty.joran + +import ch.qos.logback.core.Context +import ch.qos.logback.core.model.Model +import ch.qos.logback.core.model.processor.ModelHandlerBase +import ch.qos.logback.core.model.processor.ModelInterpretationContext +import ch.qos.logback.core.util.OptionHelper +import org.springframework.core.env.Environment + +/** + * Logback Access [ModelHandlerBase] model handler to support `` tags. + * + * See [SpringProfileModelHandler](https://github.com/spring-projects/spring-boot/blob/main/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/SpringProfileModelHandler.java). + * + * @see [LogbackAccessSpringProfileModel] + * @see [LogbackAccessSpringProfileAction] + */ +class LogbackAccessSpringProfileModelHandler( + context: Context, + private val environment: Environment, +) : ModelHandlerBase(context) { + override fun handle( + mic: ModelInterpretationContext, + model: Model, + ) { + val profiles = + (model as LogbackAccessSpringProfileModel) + .name + ?.split(",") + ?.map { OptionHelper.substVars(it.trim(), mic, context) } + ?: emptyList() + if (profiles.isEmpty() || !environment.matchesProfiles(*profiles.toTypedArray())) { + model.deepMarkAsSkipped() + } + } +} diff --git a/logback-access-reactor-netty-spring-boot-starter/src/main/kotlin/io/github/dmitrysulman/logback/access/reactor/netty/joran/LogbackAccessSpringProfileWithinSecondPhaseElementSanityChecker.kt b/logback-access-reactor-netty-spring-boot-starter/src/main/kotlin/io/github/dmitrysulman/logback/access/reactor/netty/joran/LogbackAccessSpringProfileWithinSecondPhaseElementSanityChecker.kt new file mode 100644 index 0000000..bd02c51 --- /dev/null +++ b/logback-access-reactor-netty-spring-boot-starter/src/main/kotlin/io/github/dmitrysulman/logback/access/reactor/netty/joran/LogbackAccessSpringProfileWithinSecondPhaseElementSanityChecker.kt @@ -0,0 +1,45 @@ +package io.github.dmitrysulman.logback.access.reactor.netty.joran + +import ch.qos.logback.core.joran.sanity.SanityChecker +import ch.qos.logback.core.model.AppenderModel +import ch.qos.logback.core.model.Model +import ch.qos.logback.core.spi.ContextAwareBase + +/** + * [SanityChecker] to ensure that `springProfile` elements are not nested + * within second-phase elements. + * + * See [SpringProfileIfNestedWithinSecondPhaseElementSanityChecker](https://github.com/spring-projects/spring-boot/blob/main/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/SpringProfileIfNestedWithinSecondPhaseElementSanityChecker.java). + */ +class LogbackAccessSpringProfileWithinSecondPhaseElementSanityChecker : + ContextAwareBase(), + SanityChecker { + override fun check(model: Model?) { + if (model == null) return + + val secondsPhaseModels = mutableListOf() + + SECOND_PHASE_TYPES.forEach { + deepFindAllModelsOfType(it, secondsPhaseModels, model) + } + + deepFindNestedSubModelsOfType(LogbackAccessSpringProfileModel::class.java, secondsPhaseModels) + .takeIf { it.isNotEmpty() } + ?.also { + addWarn(" elements cannot be nested within an element") + }?.forEach { + val first = it.first + val second = it.second + addWarn( + "Element <${first.tag}> at line ${first.lineNumber} contains a nested <${second.tag}> element at line ${second.lineNumber}", + ) + } + } + + companion object { + private val SECOND_PHASE_TYPES = + listOf( + AppenderModel::class.java, + ) + } +} diff --git a/logback-access-reactor-netty-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/logback-access-reactor-netty-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..1c89c82 --- /dev/null +++ b/logback-access-reactor-netty-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +io.github.dmitrysulman.logback.access.reactor.netty.autoconfigure.ReactorNettyAccessLogFactoryAutoConfiguration \ No newline at end of file diff --git a/logback-access-reactor-netty-spring-boot-starter/src/main/resources/dokka/Module.md b/logback-access-reactor-netty-spring-boot-starter/src/main/resources/dokka/Module.md new file mode 100644 index 0000000..1e742c1 --- /dev/null +++ b/logback-access-reactor-netty-spring-boot-starter/src/main/resources/dokka/Module.md @@ -0,0 +1,11 @@ +# Module logback-access-reactor-netty-spring-boot-starter + +Spring Boot Starter for using Logback Access integration with Reactor Netty. + +# Package io.github.dmitrysulman.logback.access.reactor.netty.autoconfigure + +This package contains the auto-configuration and configuration properties classes. + +# Package io.github.dmitrysulman.logback.access.reactor.netty.joran + +This package contains the custom Logback Access Joran configurator. \ No newline at end of file diff --git a/logback-access-reactor-netty-spring-boot-starter/src/test/kotlin/io/github/dmitrysulman/logback/access/reactor/netty/EventCaptureAppender.kt b/logback-access-reactor-netty-spring-boot-starter/src/test/kotlin/io/github/dmitrysulman/logback/access/reactor/netty/EventCaptureAppender.kt new file mode 100644 index 0000000..dde790f --- /dev/null +++ b/logback-access-reactor-netty-spring-boot-starter/src/test/kotlin/io/github/dmitrysulman/logback/access/reactor/netty/EventCaptureAppender.kt @@ -0,0 +1,16 @@ +package io.github.dmitrysulman.logback.access.reactor.netty + +import ch.qos.logback.access.common.spi.IAccessEvent +import ch.qos.logback.core.AppenderBase + +class EventCaptureAppender : AppenderBase() { + val list = mutableListOf() + + init { + start() + } + + override fun append(event: IAccessEvent) { + list.add(event) + } +} diff --git a/logback-access-reactor-netty-spring-boot-starter/src/test/kotlin/io/github/dmitrysulman/logback/access/reactor/netty/autoconfigure/LogbackAccessReactorNettyPropertiesTests.kt b/logback-access-reactor-netty-spring-boot-starter/src/test/kotlin/io/github/dmitrysulman/logback/access/reactor/netty/autoconfigure/LogbackAccessReactorNettyPropertiesTests.kt new file mode 100644 index 0000000..f618e3e --- /dev/null +++ b/logback-access-reactor-netty-spring-boot-starter/src/test/kotlin/io/github/dmitrysulman/logback/access/reactor/netty/autoconfigure/LogbackAccessReactorNettyPropertiesTests.kt @@ -0,0 +1,30 @@ +package io.github.dmitrysulman.logback.access.reactor.netty.autoconfigure + +import io.kotest.matchers.booleans.shouldBeTrue +import io.kotest.matchers.shouldBe +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.context.properties.EnableConfigurationProperties +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.context.annotation.Configuration + +@SpringBootTest( + classes = [LogbackAccessReactorNettyPropertiesTests::class], + properties = [ + "logback.access.reactor.netty.enabled=true", + "logback.access.reactor.netty.config=test-logback-access.xml", + "logback.access.reactor.netty.debug=true", + ], +) +@EnableConfigurationProperties(ReactorNettyAccessLogProperties::class) +@Configuration +class LogbackAccessReactorNettyPropertiesTests( + @Autowired private val properties: ReactorNettyAccessLogProperties, +) { + @Test + fun `smoke test`() { + properties.enabled?.shouldBeTrue() + properties.config shouldBe "test-logback-access.xml" + properties.debug?.shouldBeTrue() + } +} diff --git a/logback-access-reactor-netty-spring-boot-starter/src/test/kotlin/io/github/dmitrysulman/logback/access/reactor/netty/autoconfigure/ReactorNettyAccessLogFactoryAutoConfigurationTests.kt b/logback-access-reactor-netty-spring-boot-starter/src/test/kotlin/io/github/dmitrysulman/logback/access/reactor/netty/autoconfigure/ReactorNettyAccessLogFactoryAutoConfigurationTests.kt new file mode 100644 index 0000000..c06f34e --- /dev/null +++ b/logback-access-reactor-netty-spring-boot-starter/src/test/kotlin/io/github/dmitrysulman/logback/access/reactor/netty/autoconfigure/ReactorNettyAccessLogFactoryAutoConfigurationTests.kt @@ -0,0 +1,283 @@ +package io.github.dmitrysulman.logback.access.reactor.netty.autoconfigure + +import ch.qos.logback.core.status.OnConsoleStatusListener +import io.github.dmitrysulman.logback.access.reactor.netty.ReactorNettyAccessLogFactory +import io.kotest.matchers.booleans.shouldBeTrue +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldEndWith +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.assertj.core.api.AssertionsForInterfaceTypes.assertThat +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.getBean +import org.springframework.beans.factory.getBeansOfType +import org.springframework.boot.autoconfigure.AutoConfigurations +import org.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerFactoryAutoConfiguration +import org.springframework.boot.test.context.FilteredClassLoader +import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner +import org.springframework.boot.test.context.runner.WebApplicationContextRunner +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.core.io.Resource +import org.springframework.core.io.ResourceLoader +import org.springframework.util.ResourceUtils +import reactor.netty.http.server.HttpServer +import java.io.ByteArrayInputStream +import java.net.URL +import java.net.URLConnection + +class ReactorNettyAccessLogFactoryAutoConfigurationTests { + @Test + fun `should supply beans`() { + ReactiveWebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ReactorNettyAccessLogFactoryAutoConfiguration::class.java)) + .run { context -> + assertThat(context).hasSingleBean(ReactorNettyAccessLogFactory::class.java) + assertThat(context).hasSingleBean(ReactorNettyAccessLogWebServerFactoryCustomizer::class.java) + } + } + + @Test + fun `should not supply beans when HttpServer is not on the classpath`() { + ReactiveWebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ReactorNettyAccessLogFactoryAutoConfiguration::class.java)) + .withClassLoader(FilteredClassLoader(HttpServer::class.java)) + .run { context -> + assertThat(context).doesNotHaveBean(ReactorNettyAccessLogFactory::class.java) + assertThat(context).doesNotHaveBean(ReactorNettyAccessLogWebServerFactoryCustomizer::class.java) + } + } + + @Test + fun `should not supply beans when is not reactive web application`() { + WebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ReactorNettyAccessLogFactoryAutoConfiguration::class.java)) + .run { context -> + assertThat(context).doesNotHaveBean(ReactorNettyAccessLogFactory::class.java) + assertThat(context).doesNotHaveBean(ReactorNettyAccessLogWebServerFactoryCustomizer::class.java) + } + } + + @Test + fun `should not supply beans when disabled by the property`() { + ReactiveWebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ReactorNettyAccessLogFactoryAutoConfiguration::class.java)) + .withPropertyValues("logback.access.reactor.netty.enabled=false") + .run { context -> + assertThat(context).doesNotHaveBean(ReactorNettyAccessLogFactory::class.java) + assertThat(context).doesNotHaveBean(ReactorNettyAccessLogWebServerFactoryCustomizer::class.java) + } + } + + @Test + fun `should supply beans when explicitly enabled by the property`() { + ReactiveWebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ReactorNettyAccessLogFactoryAutoConfiguration::class.java)) + .withPropertyValues("logback.access.reactor.netty.enabled=true") + .run { context -> + assertThat(context).hasSingleBean(ReactorNettyAccessLogFactory::class.java) + assertThat(context).hasSingleBean(ReactorNettyAccessLogWebServerFactoryCustomizer::class.java) + } + } + + @Test + fun `should enable debug mode by the property`() { + ReactiveWebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ReactorNettyAccessLogFactoryAutoConfiguration::class.java)) + .withPropertyValues("logback.access.reactor.netty.debug=true") + .run { context -> + val factory = context.getBean() + factory.accessContext.statusManager.copyOfStatusListenerList + .any { it::class == OnConsoleStatusListener::class } + .shouldBeTrue() + } + } + + @Test + fun `should not enable debug mode when debug false`() { + ReactiveWebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ReactorNettyAccessLogFactoryAutoConfiguration::class.java)) + .withPropertyValues("logback.access.reactor.netty.debug=false") + .run { context -> + val factory = context.getBean() + factory.accessContext.statusManager.copyOfStatusListenerList + .none { it::class == OnConsoleStatusListener::class } + .shouldBeTrue() + } + } + + @Test + fun `should not enable debug mode when no debug property provided`() { + ReactiveWebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ReactorNettyAccessLogFactoryAutoConfiguration::class.java)) + .run { context -> + val factory = context.getBean() + factory.accessContext.statusManager.copyOfStatusListenerList + .none { it::class == OnConsoleStatusListener::class } + .shouldBeTrue() + } + } + + @Test + fun `should not supply beans when already has user defined beans in the context`() { + ReactiveWebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ReactorNettyAccessLogFactoryAutoConfiguration::class.java)) + .withUserConfiguration(CustomReactorNettyAccessLogFactoryConfiguration::class.java) + .run { context -> + assertThat(context.getBeansOfType()).hasSize(1) + assertThat(context.getBeansOfType()).hasSize(1) + } + } + + @Test + fun `should apply customizer`() { + ReactiveWebApplicationContextRunner() + .withConfiguration( + AutoConfigurations.of( + ReactiveWebServerFactoryAutoConfiguration::class.java, + ReactorNettyAccessLogFactoryAutoConfiguration::class.java, + ), + ).withUserConfiguration(MockNettyAccessLogFactoryConfiguration::class.java) + .run { context -> + val customizer = context.getBean() + verify(exactly = 1) { customizer.customize(any()) } + } + } + + @Test + fun `should load configuration from provided configuration file resource`() { + ReactiveWebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ReactorNettyAccessLogFactoryAutoConfiguration::class.java)) + .withPropertyValues("logback.access.reactor.netty.config=file:./src/test/resources/file/logback-access-file.xml") + .run { context -> + val factory = context.getBean() + factory.accessContext.getAppender("FILE").shouldNotBeNull() + } + } + + @Test + fun `should load configuration from provided configuration classpath resource`() { + ReactiveWebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ReactorNettyAccessLogFactoryAutoConfiguration::class.java)) + .withPropertyValues("logback.access.reactor.netty.config=classpath:logback-access-stdout.xml") + .run { context -> + val factory = context.getBean() + factory.accessContext.getAppender("CAPTURE").shouldNotBeNull() + } + } + + @Test + fun `should fail on not existing file resource`() { + ReactiveWebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ReactorNettyAccessLogFactoryAutoConfiguration::class.java)) + .withPropertyValues("logback.access.reactor.netty.config=file:logback-access-not-exist.xml") + .run { context -> + assertThat(context).hasFailed() + assertThat(context).failure.hasMessageContaining("Could not open URL [file:logback-access-not-exist.xml]") + } + } + + @Test + fun `should fail on not existing classpath resource`() { + ReactiveWebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ReactorNettyAccessLogFactoryAutoConfiguration::class.java)) + .withPropertyValues("logback.access.reactor.netty.config=classpath:logback-access-not-exist.xml") + .run { context -> + assertThat(context).hasFailed() + assertThat( + context, + ).failure.hasMessageContaining( + "class path resource [logback-access-not-exist.xml] cannot be resolved to URL because it does not exist", + ) + } + } + + @Test + fun `should load configuration from default filename configuration file resource`() { + val resourceLoaderMock = mockk() + val resourceMock = mockk() + val urlMock = mockk(relaxed = true) + val urlConnectionMock = mockk(relaxed = true) + every { resourceMock.exists() } returns true + every { resourceMock.url } returns urlMock + every { urlMock.file } returns "default" + every { urlMock.openConnection() } returns urlConnectionMock + every { urlConnectionMock.inputStream } returns ByteArrayInputStream("".toByteArray()) + every { resourceLoaderMock.getResource("file:logback-access.xml") } returns resourceMock + ReactiveWebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ReactorNettyAccessLogFactoryAutoConfiguration::class.java)) + .withBean("resourceLoader", ResourceLoader::class.java, { resourceLoaderMock }) + .run { context -> + val factory = context.getBean() + factory.accessContext.name shouldBe "default" + } + } + + @Test + fun `should load configuration from default filename configuration classpath resource`() { + val resourceLoaderMock = mockk() + val filenameResourceMock = mockk() + val classpathResourceMock = mockk() + val urlMock = mockk(relaxed = true) + val urlConnectionMock = mockk(relaxed = true) + every { filenameResourceMock.exists() } returns false + every { classpathResourceMock.exists() } returns true + every { classpathResourceMock.url } returns urlMock + every { urlMock.file } returns "default" + every { urlMock.openConnection() } returns urlConnectionMock + every { urlConnectionMock.inputStream } returns ByteArrayInputStream("".toByteArray()) + every { resourceLoaderMock.getResource("file:logback-access.xml") } returns filenameResourceMock + every { resourceLoaderMock.getResource("classpath:logback-access.xml") } returns classpathResourceMock + ReactiveWebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ReactorNettyAccessLogFactoryAutoConfiguration::class.java)) + .withBean("resourceLoader", ResourceLoader::class.java, { resourceLoaderMock }) + .run { context -> + val factory = context.getBean() + factory.accessContext.name shouldBe "default" + } + } + + @Test + fun `should load configuration from default configuration file`() { + val resourceLoaderMock = mockk() + val filenameResourceMock = mockk() + val classpathResourceMock = mockk() + val defaultResourceMock = mockk() + every { filenameResourceMock.exists() } returns false + every { classpathResourceMock.exists() } returns false + every { defaultResourceMock.url } returns + ResourceUtils.getURL("classpath:logback-access-reactor-netty/logback-access-default-config.xml") + every { resourceLoaderMock.getResource("file:logback-access.xml") } returns filenameResourceMock + every { resourceLoaderMock.getResource("classpath:logback-access.xml") } returns classpathResourceMock + every { resourceLoaderMock.getResource("classpath:logback-access-reactor-netty/logback-access-default-config.xml") } returns + defaultResourceMock + ReactiveWebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ReactorNettyAccessLogFactoryAutoConfiguration::class.java)) + .withBean("resourceLoader", ResourceLoader::class.java, { resourceLoaderMock }) + .run { context -> + val factory = context.getBean() + factory.accessContext.name shouldEndWith "logback-access-reactor-netty/logback-access-default-config.xml" + } + } + + @Configuration(proxyBeanMethods = false) + class CustomReactorNettyAccessLogFactoryConfiguration { + @Bean + fun customReactorNettyAccessLogFactory() = ReactorNettyAccessLogFactory() + + @Bean + fun customReactorNettyAccessLogWebServerFactoryCustomizer(customReactorNettyAccessLogFactory: ReactorNettyAccessLogFactory) = + ReactorNettyAccessLogWebServerFactoryCustomizer(true, customReactorNettyAccessLogFactory) + } + + @Configuration(proxyBeanMethods = false) + class MockNettyAccessLogFactoryConfiguration { + private val mockReactorNettyAccessLogWebServerFactoryCustomizer = + mockk(relaxed = true) + + @Bean + fun mockReactorNettyAccessLogWebServerFactoryCustomizer() = mockReactorNettyAccessLogWebServerFactoryCustomizer + } +} diff --git a/logback-access-reactor-netty-spring-boot-starter/src/test/kotlin/io/github/dmitrysulman/logback/access/reactor/netty/autoconfigure/integration/EchoController.kt b/logback-access-reactor-netty-spring-boot-starter/src/test/kotlin/io/github/dmitrysulman/logback/access/reactor/netty/autoconfigure/integration/EchoController.kt new file mode 100644 index 0000000..ae8bb3c --- /dev/null +++ b/logback-access-reactor-netty-spring-boot-starter/src/test/kotlin/io/github/dmitrysulman/logback/access/reactor/netty/autoconfigure/integration/EchoController.kt @@ -0,0 +1,14 @@ +package io.github.dmitrysulman.logback.access.reactor.netty.autoconfigure.integration + +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController + +@RestController +class EchoController { + @GetMapping("/get") + fun getRequest( + @RequestParam param: String, + ): ResponseEntity = ResponseEntity.status(200).header("response_header", "response_header_value").body(param) +} diff --git a/logback-access-reactor-netty-spring-boot-starter/src/test/kotlin/io/github/dmitrysulman/logback/access/reactor/netty/autoconfigure/integration/IntegrationTests.kt b/logback-access-reactor-netty-spring-boot-starter/src/test/kotlin/io/github/dmitrysulman/logback/access/reactor/netty/autoconfigure/integration/IntegrationTests.kt new file mode 100644 index 0000000..8b8323a --- /dev/null +++ b/logback-access-reactor-netty-spring-boot-starter/src/test/kotlin/io/github/dmitrysulman/logback/access/reactor/netty/autoconfigure/integration/IntegrationTests.kt @@ -0,0 +1,57 @@ +package io.github.dmitrysulman.logback.access.reactor.netty.autoconfigure.integration + +import io.github.dmitrysulman.logback.access.reactor.netty.EventCaptureAppender +import io.github.dmitrysulman.logback.access.reactor.netty.ReactorNettyAccessLogFactory +import io.kotest.matchers.comparables.shouldBeGreaterThan +import io.kotest.matchers.longs.shouldBeGreaterThanOrEqual +import io.kotest.matchers.shouldBe +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.web.server.LocalServerPort +import org.springframework.test.web.reactive.server.WebTestClient + +@SpringBootTest( + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = ["logback.access.reactor.netty.config=classpath:logback-access-stdout.xml"], +) +class IntegrationTests( + @Autowired private val webTestClient: WebTestClient, + @Autowired private val reactorNettyAccessLogFactory: ReactorNettyAccessLogFactory, + @LocalServerPort private val localServerPort: Int, +) { + @Test + fun `smoke test`() { + val now = System.currentTimeMillis() + val value = "test" + webTestClient + .get() + .uri("/get?param={param}", value) + .header("request_header", "request_header_value") + .exchange() + .expectStatus() + .isOk + .expectBody() + .returnResult() + .responseBodyContent shouldBe value.toByteArray() + + val eventCaptureAppender = + reactorNettyAccessLogFactory.accessContext.getAppender("CAPTURE") as EventCaptureAppender + + eventCaptureAppender.list.size shouldBe 1 + val accessEvent = eventCaptureAppender.list.first() + accessEvent.requestURL shouldBe "GET /get?param=$value HTTP/1.1" + accessEvent.contentLength shouldBe value.length + accessEvent.localPort shouldBe localServerPort + accessEvent.requestURI shouldBe "/get" + accessEvent.queryString shouldBe "?param=$value" + accessEvent.protocol shouldBe "HTTP/1.1" + accessEvent.method shouldBe "GET" + accessEvent.statusCode shouldBe 200 + accessEvent.elapsedTime shouldBeGreaterThanOrEqual 0 + accessEvent.timeStamp shouldBeGreaterThan now + accessEvent.remoteUser shouldBe "-" + accessEvent.getRequestHeader("request_header") shouldBe "request_header_value" + accessEvent.getResponseHeader("response_header") shouldBe "response_header_value" + } +} diff --git a/logback-access-reactor-netty-spring-boot-starter/src/test/kotlin/io/github/dmitrysulman/logback/access/reactor/netty/autoconfigure/integration/TestSpringBootConfiguration.kt b/logback-access-reactor-netty-spring-boot-starter/src/test/kotlin/io/github/dmitrysulman/logback/access/reactor/netty/autoconfigure/integration/TestSpringBootConfiguration.kt new file mode 100644 index 0000000..3ede559 --- /dev/null +++ b/logback-access-reactor-netty-spring-boot-starter/src/test/kotlin/io/github/dmitrysulman/logback/access/reactor/netty/autoconfigure/integration/TestSpringBootConfiguration.kt @@ -0,0 +1,6 @@ +package io.github.dmitrysulman.logback.access.reactor.netty.autoconfigure.integration + +import org.springframework.boot.autoconfigure.SpringBootApplication + +@SpringBootApplication +class TestSpringBootConfiguration diff --git a/logback-access-reactor-netty-spring-boot-starter/src/test/kotlin/io/github/dmitrysulman/logback/access/reactor/netty/joran/LogbackAccessJoranConfiguratorTests.kt b/logback-access-reactor-netty-spring-boot-starter/src/test/kotlin/io/github/dmitrysulman/logback/access/reactor/netty/joran/LogbackAccessJoranConfiguratorTests.kt new file mode 100644 index 0000000..f0b8ff3 --- /dev/null +++ b/logback-access-reactor-netty-spring-boot-starter/src/test/kotlin/io/github/dmitrysulman/logback/access/reactor/netty/joran/LogbackAccessJoranConfiguratorTests.kt @@ -0,0 +1,83 @@ +package io.github.dmitrysulman.logback.access.reactor.netty.joran + +import io.github.dmitrysulman.logback.access.reactor.netty.EventCaptureAppender +import io.github.dmitrysulman.logback.access.reactor.netty.ReactorNettyAccessLogFactory +import io.github.dmitrysulman.logback.access.reactor.netty.autoconfigure.ReactorNettyAccessLogFactoryAutoConfiguration +import io.kotest.matchers.nulls.shouldBeNull +import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.mockk +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource +import org.junit.jupiter.params.provider.ValueSource +import org.springframework.beans.factory.getBean +import org.springframework.boot.autoconfigure.AutoConfigurations +import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner +import reactor.netty.http.server.logging.AccessLogArgProvider + +class LogbackAccessJoranConfiguratorTests { + @ParameterizedTest + @CsvSource( + "dev, logback-access-springprofile-dev.xml", + "'dev,prod', logback-access-springprofile-dev.xml", + "dev, logback-access-springprofile-included.xml", + "'dev,prod', logback-access-springprofile-included.xml", + "dev, logback-access-springprofile-dev-prod.xml", + "prod, logback-access-springprofile-dev-prod.xml", + "'dev,prod', logback-access-springprofile-dev-prod.xml", + "'dev,prod,stg',logback-access-springprofile-dev-prod.xml", + "'dev,stg', logback-access-springprofile-dev-prod.xml", + "'prod,stg', logback-access-springprofile-dev-prod.xml", + ) + fun `should log event with springProfile configuration`( + profile: String, + filename: String, + ) { + ReactiveWebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ReactorNettyAccessLogFactoryAutoConfiguration::class.java)) + .withPropertyValues("spring.profiles.active=$profile") + .withPropertyValues("logback.access.reactor.netty.config=classpath:$filename") + .run { context -> + val factory = context.getBean() + val mockArgProvider = mockk(relaxed = true) + val uri = "/test" + every { mockArgProvider.uri() } returns uri + factory.apply(mockArgProvider).log() + val eventCaptureAppender = factory.accessContext.getAppender("CAPTURE") as EventCaptureAppender + eventCaptureAppender.list.size shouldBe 1 + eventCaptureAppender.list.first().requestURI shouldBe uri + } + } + + @ParameterizedTest + @ValueSource( + strings = [ + "logback-access-springprofile-dev.xml", + "logback-access-springprofile-included.xml", + "logback-access-springprofile-dev-prod.xml", + ], + ) + fun `should not log event with springProfile configuration`(filename: String) { + ReactiveWebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ReactorNettyAccessLogFactoryAutoConfiguration::class.java)) + .withPropertyValues("spring.profiles.active=stg") + .withPropertyValues("logback.access.reactor.netty.config=classpath:$filename") + .run { context -> + val factory = context.getBean() + factory.accessContext.getAppender("CAPTURE").shouldBeNull() + } + } + + @Test + fun `should not log event with empty springProfile configuration`() { + ReactiveWebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ReactorNettyAccessLogFactoryAutoConfiguration::class.java)) + .withPropertyValues("spring.profiles.active=dev") + .withPropertyValues("logback.access.reactor.netty.config=classpath:logback-access-springprofile-empty.xml") + .run { context -> + val factory = context.getBean() + factory.accessContext.getAppender("CAPTURE").shouldBeNull() + } + } +} diff --git a/logback-access-reactor-netty-spring-boot-starter/src/test/kotlin/io/github/dmitrysulman/logback/access/reactor/netty/joran/LogbackAccessSpringProfileWithinSecondPhaseElementSanityCheckerTests.kt b/logback-access-reactor-netty-spring-boot-starter/src/test/kotlin/io/github/dmitrysulman/logback/access/reactor/netty/joran/LogbackAccessSpringProfileWithinSecondPhaseElementSanityCheckerTests.kt new file mode 100644 index 0000000..fe07aec --- /dev/null +++ b/logback-access-reactor-netty-spring-boot-starter/src/test/kotlin/io/github/dmitrysulman/logback/access/reactor/netty/joran/LogbackAccessSpringProfileWithinSecondPhaseElementSanityCheckerTests.kt @@ -0,0 +1,33 @@ +package io.github.dmitrysulman.logback.access.reactor.netty.joran + +import io.github.dmitrysulman.logback.access.reactor.netty.ReactorNettyAccessLogFactory +import io.github.dmitrysulman.logback.access.reactor.netty.autoconfigure.ReactorNettyAccessLogFactoryAutoConfiguration +import io.kotest.matchers.booleans.shouldBeTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.springframework.beans.factory.getBean +import org.springframework.boot.autoconfigure.AutoConfigurations +import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner + +class LogbackAccessSpringProfileWithinSecondPhaseElementSanityCheckerTests { + @Test + fun `should not fail when model is null`() { + val sanityChecker = LogbackAccessSpringProfileWithinSecondPhaseElementSanityChecker() + assertDoesNotThrow { sanityChecker.check(null) } + } + + @Test + fun `should add warning status on nested springProfile element within appender element`() { + ReactiveWebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ReactorNettyAccessLogFactoryAutoConfiguration::class.java)) + .withPropertyValues("logback.access.reactor.netty.config=classpath:logback-access-springprofile-in-appender.xml") + .run { context -> + val factory = context.getBean() + factory.accessContext.statusManager.copyOfStatusList + .any { + it.message == + " elements cannot be nested within an element" + }.shouldBeTrue() + } + } +} diff --git a/logback-access-reactor-netty-spring-boot-starter/src/test/resources/file/logback-access-file.xml b/logback-access-reactor-netty-spring-boot-starter/src/test/resources/file/logback-access-file.xml new file mode 100644 index 0000000..0f5a524 --- /dev/null +++ b/logback-access-reactor-netty-spring-boot-starter/src/test/resources/file/logback-access-file.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/logback-access-reactor-netty-spring-boot-starter/src/test/resources/logback-access-springprofile-dev-prod.xml b/logback-access-reactor-netty-spring-boot-starter/src/test/resources/logback-access-springprofile-dev-prod.xml new file mode 100644 index 0000000..8b25604 --- /dev/null +++ b/logback-access-reactor-netty-spring-boot-starter/src/test/resources/logback-access-springprofile-dev-prod.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/logback-access-reactor-netty-spring-boot-starter/src/test/resources/logback-access-springprofile-dev.xml b/logback-access-reactor-netty-spring-boot-starter/src/test/resources/logback-access-springprofile-dev.xml new file mode 100644 index 0000000..452dad5 --- /dev/null +++ b/logback-access-reactor-netty-spring-boot-starter/src/test/resources/logback-access-springprofile-dev.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/logback-access-reactor-netty-spring-boot-starter/src/test/resources/logback-access-springprofile-empty.xml b/logback-access-reactor-netty-spring-boot-starter/src/test/resources/logback-access-springprofile-empty.xml new file mode 100644 index 0000000..61d9bd1 --- /dev/null +++ b/logback-access-reactor-netty-spring-boot-starter/src/test/resources/logback-access-springprofile-empty.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/logback-access-reactor-netty-spring-boot-starter/src/test/resources/logback-access-springprofile-in-appender.xml b/logback-access-reactor-netty-spring-boot-starter/src/test/resources/logback-access-springprofile-in-appender.xml new file mode 100644 index 0000000..c41e358 --- /dev/null +++ b/logback-access-reactor-netty-spring-boot-starter/src/test/resources/logback-access-springprofile-in-appender.xml @@ -0,0 +1,11 @@ + + + + + common + + + + + + \ No newline at end of file diff --git a/logback-access-reactor-netty-spring-boot-starter/src/test/resources/logback-access-springprofile-included.xml b/logback-access-reactor-netty-spring-boot-starter/src/test/resources/logback-access-springprofile-included.xml new file mode 100644 index 0000000..059c348 --- /dev/null +++ b/logback-access-reactor-netty-spring-boot-starter/src/test/resources/logback-access-springprofile-included.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/logback-access-reactor-netty-spring-boot-starter/src/test/resources/logback-access-stdout.xml b/logback-access-reactor-netty-spring-boot-starter/src/test/resources/logback-access-stdout.xml new file mode 100644 index 0000000..77b275a --- /dev/null +++ b/logback-access-reactor-netty-spring-boot-starter/src/test/resources/logback-access-stdout.xml @@ -0,0 +1,11 @@ + + + + + common + + + + + + \ No newline at end of file diff --git a/logback-access-reactor-netty/build.gradle.kts b/logback-access-reactor-netty/build.gradle.kts index f284ad2..a9e356f 100644 --- a/logback-access-reactor-netty/build.gradle.kts +++ b/logback-access-reactor-netty/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - id("conventions") + alias(libs.plugins.conventions) } description = "Logback Access integration with Reactor Netty" diff --git a/logback-access-reactor-netty/src/test/kotlin/io/github/dmitrysulman/logback/access/reactor/netty/ReactorNettyAccessLogFactoryTests.kt b/logback-access-reactor-netty/src/test/kotlin/io/github/dmitrysulman/logback/access/reactor/netty/ReactorNettyAccessLogFactoryTests.kt index 8c6a1a0..b04f711 100644 --- a/logback-access-reactor-netty/src/test/kotlin/io/github/dmitrysulman/logback/access/reactor/netty/ReactorNettyAccessLogFactoryTests.kt +++ b/logback-access-reactor-netty/src/test/kotlin/io/github/dmitrysulman/logback/access/reactor/netty/ReactorNettyAccessLogFactoryTests.kt @@ -4,8 +4,6 @@ import ch.qos.logback.access.common.joran.JoranConfigurator import ch.qos.logback.core.joran.spi.JoranException import ch.qos.logback.core.status.OnConsoleStatusListener import io.github.dmitrysulman.logback.access.reactor.netty.ReactorNettyAccessLogFactory.Companion.CONFIG_FILE_NAME_PROPERTY -import io.github.dmitrysulman.logback.access.reactor.netty.ReactorNettyAccessLogFactory.Companion.DEFAULT_CONFIGURATION -import io.github.dmitrysulman.logback.access.reactor.netty.ReactorNettyAccessLogFactory.Companion.DEFAULT_CONFIG_FILE_NAME import io.github.dmitrysulman.logback.access.reactor.netty.integration.EventCaptureAppender import io.kotest.assertions.throwables.shouldThrowExactly import io.kotest.matchers.booleans.shouldBeTrue @@ -124,11 +122,11 @@ class ReactorNettyAccessLogFactoryTests { @Test fun `test not existing default config file fallback to default configuration`() { val reactorNettyAccessLogFactory = spyk(recordPrivateCalls = true) - every { reactorNettyAccessLogFactory["getConfigFromFileName"](DEFAULT_CONFIG_FILE_NAME) } throws FileNotFoundException() + every { reactorNettyAccessLogFactory["getConfigFromFileName"]("logback-access.xml") } throws FileNotFoundException() val getDefaultConfigMethod = reactorNettyAccessLogFactory::class.java.getDeclaredMethod("getDefaultConfig") getDefaultConfigMethod.trySetAccessible() val defaultConfigUrl = getDefaultConfigMethod.invoke(reactorNettyAccessLogFactory) as URL - defaultConfigUrl.toString() shouldEndWith DEFAULT_CONFIGURATION + defaultConfigUrl.toString() shouldEndWith "logback-access-reactor-netty/logback-access-default-config.xml" defaultConfigUrl.openStream().reader().use { it.readText() shouldBe """ diff --git a/settings.gradle.kts b/settings.gradle.kts index 08a8685..2c5482c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -4,4 +4,4 @@ plugins { id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" } -include("logback-access-reactor-netty") +include("logback-access-reactor-netty", "logback-access-reactor-netty-spring-boot-starter") \ No newline at end of file