diff --git a/README.md b/README.md
index 8ef2a98..eefabdc 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,26 @@
[](https://github.com/dmitrysulman/logback-access-reactor-netty/actions/workflows/codeql.yml)
[](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