├── .github ├── CODEOWNERS ├── FUNDING.yml ├── issue_template.md ├── pull_request_template.md ├── ISSUE_TEMPLATE │ ├── question.md │ ├── feature_request.md │ └── bug_report.md ├── dependabot.yml └── workflows │ ├── deploy.yml │ ├── release.yml │ └── build.yml ├── src ├── main │ ├── resources │ │ ├── META-INF │ │ │ ├── spring │ │ │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports │ │ │ └── spring-configuration-metadata.json │ │ └── dev │ │ │ └── akkinoc │ │ │ └── spring │ │ │ └── boot │ │ │ └── logback │ │ │ └── access │ │ │ └── logback-access-spring.xml │ └── kotlin │ │ └── dev │ │ └── akkinoc │ │ └── spring │ │ └── boot │ │ └── logback │ │ └── access │ │ ├── module.md │ │ ├── package.md │ │ ├── joran │ │ ├── LogbackAccessJoranSpringProfileModel.kt │ │ ├── LogbackAccessJoranSpringPropertyModel.kt │ │ ├── LogbackAccessJoranSpringProfileAction.kt │ │ ├── LogbackAccessJoranSpringPropertyAction.kt │ │ ├── LogbackAccessJoranSpringProfileModelHandler.kt │ │ ├── LogbackAccessJoranSpringPropertyModelHandler.kt │ │ └── LogbackAccessJoranConfigurator.kt │ │ ├── value │ │ └── LogbackAccessLocalPortStrategy.kt │ │ ├── security │ │ ├── LogbackAccessSecurityServletFilter.kt │ │ └── LogbackAccessSecurityServletFilterConfiguration.kt │ │ ├── LogbackAccessHackyLoggingOverrides.kt │ │ ├── tomcat │ │ ├── LogbackAccessTomcatWebServerFactoryCustomizer.kt │ │ ├── LogbackAccessTomcatConfiguration.kt │ │ └── LogbackAccessTomcatValve.kt │ │ ├── jetty │ │ ├── LogbackAccessJettyRequestLog.kt │ │ ├── LogbackAccessJettyWebServerFactoryCustomizer.kt │ │ └── LogbackAccessJettyConfiguration.kt │ │ ├── undertow │ │ ├── LogbackAccessUndertowWebServerFactoryCustomizer.kt │ │ ├── LogbackAccessUndertowConfiguration.kt │ │ ├── LogbackAccessUndertowServletConfiguration.kt │ │ ├── LogbackAccessUndertowHttpHandler.kt │ │ ├── LogbackAccessUndertowServletWebServerFactoryCustomizer.kt │ │ ├── LogbackAccessUndertowReactiveConfiguration.kt │ │ └── LogbackAccessUndertowReactiveWebServerFactory.kt │ │ ├── tee │ │ └── LogbackAccessTeeServletFilterConfiguration.kt │ │ ├── LogbackAccessContext.kt │ │ ├── LogbackAccessAutoConfiguration.kt │ │ ├── LogbackAccessProperties.kt │ │ └── LogbackAccessEvent.kt └── test │ ├── resources │ ├── application.yml │ ├── dev │ │ └── akkinoc │ │ │ └── spring │ │ │ └── boot │ │ │ └── logback │ │ │ └── access │ │ │ ├── TestConfigurationFileDetectionTest │ │ │ ├── logback-access.xml │ │ │ ├── logback-access-spring.xml │ │ │ ├── logback-access-test-spring.xml │ │ │ └── logback-access-test.xml │ │ │ ├── MainConfigurationFileDetectionTest │ │ │ ├── logback-access-spring.xml │ │ │ ├── logback-access-test-spring.xml │ │ │ └── logback-access.xml │ │ │ ├── TestSpringConfigurationFileDetectionTest │ │ │ ├── logback-access-spring.xml │ │ │ └── logback-access-test-spring.xml │ │ │ └── MainSpringConfigurationFileDetectionTest │ │ │ └── logback-access-spring.xml │ ├── META-INF │ │ └── spring.factories │ ├── logback-access-test-spring.property-as-include.xml │ ├── static │ │ └── mock-static │ │ │ └── image.svg │ ├── logback-access-test.capture.xml │ ├── logback-access-test.filter.capture.xml │ ├── logback-access-test.sequence-number.capture.xml │ ├── logback-access-test-spring.property.xml │ ├── logback-access-test-spring.property-as-include.included.xml │ └── logback-access-test-spring.profile.xml │ └── kotlin │ └── dev │ └── akkinoc │ └── spring │ └── boot │ └── logback │ └── access │ ├── test │ ├── extension │ │ ├── EventsCapture.kt │ │ ├── EventsCaptureAppender.kt │ │ └── EventsCaptureExtension.kt │ ├── type │ │ ├── NonWebTest.kt │ │ └── WebTest.kt │ ├── mock │ │ ├── MockEventFilter.kt │ │ ├── MockReactiveController.kt │ │ ├── MockServletController.kt │ │ └── MockController.kt │ ├── assertion │ │ └── Assertions.kt │ └── configuration │ │ ├── TestContextClassLoaderCustomizer.kt │ │ ├── WebTestConfiguration.kt │ │ └── TestContextClassLoaderCustomizerFactory.kt │ ├── TestContextConfiguration.kt │ ├── undertow │ └── UndertowNoRequestStartTimeTest.kt │ ├── InactiveTest.kt │ ├── SequenceNumberTest.kt │ ├── tomcat │ ├── TomcatRequestAttributesEnabledTest.kt │ └── TomcatRequestAttributesDisabledTest.kt │ ├── joran │ ├── JoranSpringPropertyTest.kt │ ├── JoranSpringPropertyAsIncludeTest.kt │ └── JoranSpringProfileTest.kt │ ├── InvalidAccessEventTest.kt │ ├── BasicTest.kt │ ├── EventFilterTest.kt │ ├── SecurityAttributesTest.kt │ ├── LocalPortNoRewriteTest.kt │ ├── ConfigurationFileLocationTest.kt │ └── ForwardHeadersSupportTest.kt ├── SECURITY.md ├── examples ├── webflux-jetty-java │ ├── src │ │ └── main │ │ │ ├── resources │ │ │ ├── static │ │ │ │ └── index.html │ │ │ └── logback-access.xml │ │ │ └── java │ │ │ └── example │ │ │ └── Application.java │ └── pom.xml ├── webflux-tomcat-java │ ├── src │ │ └── main │ │ │ ├── resources │ │ │ ├── static │ │ │ │ └── index.html │ │ │ └── logback-access.xml │ │ │ └── java │ │ │ └── example │ │ │ └── Application.java │ └── pom.xml ├── webflux-undertow-java │ ├── src │ │ └── main │ │ │ ├── resources │ │ │ ├── static │ │ │ │ └── index.html │ │ │ └── logback-access.xml │ │ │ └── java │ │ │ └── example │ │ │ └── Application.java │ └── pom.xml ├── webmvc-jetty-java │ ├── src │ │ └── main │ │ │ ├── resources │ │ │ ├── static │ │ │ │ └── index.html │ │ │ └── logback-access.xml │ │ │ └── java │ │ │ └── example │ │ │ └── Application.java │ └── pom.xml ├── webmvc-tomcat-java │ ├── src │ │ └── main │ │ │ ├── resources │ │ │ ├── static │ │ │ │ └── index.html │ │ │ └── logback-access.xml │ │ │ └── java │ │ │ └── example │ │ │ └── Application.java │ └── pom.xml ├── webmvc-tomcat-kotlin │ ├── src │ │ └── main │ │ │ ├── resources │ │ │ ├── static │ │ │ │ └── index.html │ │ │ └── logback-access.xml │ │ │ └── kotlin │ │ │ └── example │ │ │ └── Application.kt │ └── pom.xml └── webmvc-undertow-java │ ├── src │ └── main │ │ ├── resources │ │ ├── static │ │ │ └── index.html │ │ └── logback-access.xml │ │ └── java │ │ └── example │ │ └── Application.java │ └── pom.xml ├── .detekt └── config.yml ├── CONTRIBUTING.md └── CODE_OF_CONDUCT.md /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @akkinoc 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: akkinoc 2 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | ## Describe the issue 2 | 3 | 6 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Describe the changes 2 | 3 | 6 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports: -------------------------------------------------------------------------------- 1 | dev.akkinoc.spring.boot.logback.access.LogbackAccessAutoConfiguration 2 | -------------------------------------------------------------------------------- /src/test/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | main: 3 | banner-mode: off 4 | logging: 5 | level: 6 | root: info 7 | dev.akkinoc.spring.boot.logback.access: debug 8 | -------------------------------------------------------------------------------- /src/test/resources/dev/akkinoc/spring/boot/logback/access/TestConfigurationFileDetectionTest/logback-access.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/test/resources/dev/akkinoc/spring/boot/logback/access/MainConfigurationFileDetectionTest/logback-access-spring.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/test/resources/dev/akkinoc/spring/boot/logback/access/TestConfigurationFileDetectionTest/logback-access-spring.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/test/resources/dev/akkinoc/spring/boot/logback/access/MainConfigurationFileDetectionTest/logback-access-test-spring.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/test/resources/dev/akkinoc/spring/boot/logback/access/TestConfigurationFileDetectionTest/logback-access-test-spring.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/test/resources/dev/akkinoc/spring/boot/logback/access/TestSpringConfigurationFileDetectionTest/logback-access-spring.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/test/resources/META-INF/spring.factories: -------------------------------------------------------------------------------- 1 | org.springframework.test.context.ContextCustomizerFactory=\ 2 | dev.akkinoc.spring.boot.logback.access.test.configuration.TestContextClassLoaderCustomizerFactory 3 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Only the latest stable release is supported. 6 | 7 | ## Reporting a Vulnerability 8 | 9 | To report a vulnerability, please email . 10 | -------------------------------------------------------------------------------- /src/test/resources/logback-access-test-spring.property-as-include.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: ❓ Question 3 | about: Ask a question about usage, support, internal behavior, etc. 4 | labels: type:question 5 | --- 6 | 7 | ## Question 8 | 9 | 12 | -------------------------------------------------------------------------------- /src/test/resources/static/mock-static/image.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/akkinoc/spring/boot/logback/access/module.md: -------------------------------------------------------------------------------- 1 | # Module logback-access-spring-boot-starter 2 | 3 | [Spring Boot] Starter for [Logback-access]. 4 | 5 | [Spring Boot]: https://spring.io/projects/spring-boot 6 | [Logback-access]: https://logback.qos.ch/access.html 7 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/akkinoc/spring/boot/logback/access/package.md: -------------------------------------------------------------------------------- 1 | # Package dev.akkinoc.spring.boot.logback.access 2 | 3 | [Spring Boot] Starter for [Logback-access]. 4 | 5 | [Spring Boot]: https://spring.io/projects/spring-boot 6 | [Logback-access]: https://logback.qos.ch/access.html 7 | -------------------------------------------------------------------------------- /src/test/resources/logback-access-test.capture.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /examples/webflux-jetty-java/src/main/resources/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @project.artifactId@ 5 | 6 | 7 |

@project.artifactId@

8 |

This is an example of an application using logback-access-spring-boot-starter.

9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/webflux-tomcat-java/src/main/resources/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @project.artifactId@ 5 | 6 | 7 |

@project.artifactId@

8 |

This is an example of an application using logback-access-spring-boot-starter.

9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/webflux-undertow-java/src/main/resources/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @project.artifactId@ 5 | 6 | 7 |

@project.artifactId@

8 |

This is an example of an application using logback-access-spring-boot-starter.

9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/webmvc-jetty-java/src/main/resources/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @project.artifactId@ 5 | 6 | 7 |

@project.artifactId@

8 |

This is an example of an application using logback-access-spring-boot-starter.

9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/webmvc-tomcat-java/src/main/resources/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @project.artifactId@ 5 | 6 | 7 |

@project.artifactId@

8 |

This is an example of an application using logback-access-spring-boot-starter.

9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/webmvc-tomcat-kotlin/src/main/resources/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @project.artifactId@ 5 | 6 | 7 |

@project.artifactId@

8 |

This is an example of an application using logback-access-spring-boot-starter.

9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/webmvc-undertow-java/src/main/resources/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @project.artifactId@ 5 | 6 | 7 |

@project.artifactId@

8 |

This is an example of an application using logback-access-spring-boot-starter.

9 | 10 | 11 | -------------------------------------------------------------------------------- /src/test/resources/dev/akkinoc/spring/boot/logback/access/MainConfigurationFileDetectionTest/logback-access.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/test/resources/dev/akkinoc/spring/boot/logback/access/TestConfigurationFileDetectionTest/logback-access-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/test/resources/dev/akkinoc/spring/boot/logback/access/MainSpringConfigurationFileDetectionTest/logback-access-spring.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /examples/webmvc-tomcat-kotlin/src/main/kotlin/example/Application.kt: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import org.springframework.boot.autoconfigure.SpringBootApplication 4 | import org.springframework.boot.runApplication 5 | 6 | @SpringBootApplication 7 | class Application 8 | 9 | fun main(vararg args: String) { 10 | runApplication(*args) 11 | } 12 | -------------------------------------------------------------------------------- /src/test/resources/dev/akkinoc/spring/boot/logback/access/TestSpringConfigurationFileDetectionTest/logback-access-test-spring.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/test/resources/logback-access-test.filter.capture.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/main/resources/dev/akkinoc/spring/boot/logback/access/logback-access-spring.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | common 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/test/resources/logback-access-test.sequence-number.capture.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /examples/webflux-jetty-java/src/main/java/example/Application.java: -------------------------------------------------------------------------------- 1 | package example; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class Application { 8 | 9 | public static void main(String... args) { 10 | SpringApplication.run(Application.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /examples/webmvc-jetty-java/src/main/java/example/Application.java: -------------------------------------------------------------------------------- 1 | package example; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class Application { 8 | 9 | public static void main(String... args) { 10 | SpringApplication.run(Application.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /examples/webmvc-tomcat-java/src/main/java/example/Application.java: -------------------------------------------------------------------------------- 1 | package example; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class Application { 8 | 9 | public static void main(String... args) { 10 | SpringApplication.run(Application.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /examples/webflux-tomcat-java/src/main/java/example/Application.java: -------------------------------------------------------------------------------- 1 | package example; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class Application { 8 | 9 | public static void main(String... args) { 10 | SpringApplication.run(Application.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /examples/webflux-undertow-java/src/main/java/example/Application.java: -------------------------------------------------------------------------------- 1 | package example; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class Application { 8 | 9 | public static void main(String... args) { 10 | SpringApplication.run(Application.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /examples/webmvc-undertow-java/src/main/java/example/Application.java: -------------------------------------------------------------------------------- 1 | package example; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class Application { 8 | 9 | public static void main(String... args) { 10 | SpringApplication.run(Application.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/test/kotlin/dev/akkinoc/spring/boot/logback/access/test/extension/EventsCapture.kt: -------------------------------------------------------------------------------- 1 | package dev.akkinoc.spring.boot.logback.access.test.extension 2 | 3 | import dev.akkinoc.spring.boot.logback.access.LogbackAccessEvent 4 | import java.util.concurrent.CopyOnWriteArrayList 5 | 6 | /** 7 | * The Logback-access events capture. 8 | */ 9 | class EventsCapture : MutableList by CopyOnWriteArrayList() 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 💡 Feature request 3 | about: Suggest an idea or a feature. 4 | labels: type:enhancement 5 | --- 6 | 7 | ## Describe the problem you'd like to have solved 8 | 9 | 12 | 13 | ## Describe the solution you'd like 14 | 15 | 18 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/akkinoc/spring/boot/logback/access/joran/LogbackAccessJoranSpringProfileModel.kt: -------------------------------------------------------------------------------- 1 | package dev.akkinoc.spring.boot.logback.access.joran 2 | 3 | import ch.qos.logback.core.model.Model 4 | import ch.qos.logback.core.model.NamedModel 5 | 6 | /** 7 | * The Joran [Model] to support `` tags. 8 | * 9 | * @see org.springframework.boot.logging.logback.SpringProfileModel 10 | */ 11 | class LogbackAccessJoranSpringProfileModel : NamedModel() 12 | -------------------------------------------------------------------------------- /src/test/kotlin/dev/akkinoc/spring/boot/logback/access/test/type/NonWebTest.kt: -------------------------------------------------------------------------------- 1 | package dev.akkinoc.spring.boot.logback.access.test.type 2 | 3 | import org.springframework.boot.test.context.SpringBootTest 4 | 5 | /** 6 | * Indicates a Spring Boot based test that does not use a web server. 7 | */ 8 | @Target(AnnotationTarget.CLASS) 9 | @Retention(AnnotationRetention.RUNTIME) 10 | @MustBeDocumented 11 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) 12 | annotation class NonWebTest 13 | -------------------------------------------------------------------------------- /examples/webflux-jetty-java/src/main/resources/logback-access.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | common 6 | 7 | 8 | 9 | access.log 10 | 11 | combined 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /examples/webflux-tomcat-java/src/main/resources/logback-access.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | common 6 | 7 | 8 | 9 | access.log 10 | 11 | combined 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /examples/webmvc-jetty-java/src/main/resources/logback-access.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | common 6 | 7 | 8 | 9 | access.log 10 | 11 | combined 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /examples/webmvc-tomcat-java/src/main/resources/logback-access.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | common 6 | 7 | 8 | 9 | access.log 10 | 11 | combined 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /examples/webmvc-tomcat-kotlin/src/main/resources/logback-access.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | common 6 | 7 | 8 | 9 | access.log 10 | 11 | combined 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /examples/webmvc-undertow-java/src/main/resources/logback-access.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | common 6 | 7 | 8 | 9 | access.log 10 | 11 | combined 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /examples/webflux-undertow-java/src/main/resources/logback-access.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | common 6 | 7 | 8 | 9 | access.log 10 | 11 | combined 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/test/kotlin/dev/akkinoc/spring/boot/logback/access/TestContextConfiguration.kt: -------------------------------------------------------------------------------- 1 | package dev.akkinoc.spring.boot.logback.access 2 | 3 | import org.springframework.boot.SpringBootConfiguration 4 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration 5 | import org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration 6 | import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration 7 | 8 | /** 9 | * The test context configuration. 10 | */ 11 | @SpringBootConfiguration(proxyBeanMethods = false) 12 | @EnableAutoConfiguration(exclude = [SecurityAutoConfiguration::class, ReactiveSecurityAutoConfiguration::class]) 13 | class TestContextConfiguration 14 | -------------------------------------------------------------------------------- /src/test/kotlin/dev/akkinoc/spring/boot/logback/access/test/mock/MockEventFilter.kt: -------------------------------------------------------------------------------- 1 | package dev.akkinoc.spring.boot.logback.access.test.mock 2 | 3 | import ch.qos.logback.access.common.spi.IAccessEvent 4 | import ch.qos.logback.core.filter.Filter 5 | import ch.qos.logback.core.spi.FilterReply 6 | 7 | /** 8 | * The mock event filter. 9 | * The filter reply can be overridden by request header. 10 | */ 11 | class MockEventFilter : Filter() { 12 | 13 | override fun decide(event: IAccessEvent): FilterReply { 14 | val value = event.getRequestHeader("mock-event-filter-reply") 15 | return FilterReply.entries.find { it.name.equals(value, ignoreCase = true) } ?: FilterReply.NEUTRAL 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐛 Bug report 3 | about: Create a bug report. 4 | labels: type:bug 5 | --- 6 | 7 | ## Describe the bug 8 | 9 | 12 | 13 | ## To Reproduce 14 | 15 | 18 | 19 | ## Expected behavior 20 | 21 | 24 | 25 | ### Environment 26 | 27 | 30 | 31 | * **Version of this library used**: 32 | * **Version of Java used**: 33 | * **Version of Spring Boot used**: 34 | * **Web server used (Tomcat, Jetty, Undertow or Netty)**: 35 | * **Web application type used (Servlet Stack or Reactive Stack)**: 36 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/akkinoc/spring/boot/logback/access/joran/LogbackAccessJoranSpringPropertyModel.kt: -------------------------------------------------------------------------------- 1 | package dev.akkinoc.spring.boot.logback.access.joran 2 | 3 | import ch.qos.logback.core.model.Model 4 | import ch.qos.logback.core.model.NamedModel 5 | 6 | /** 7 | * The Joran [Model] to support `` tags. 8 | * 9 | * @see org.springframework.boot.logging.logback.SpringPropertyModel 10 | */ 11 | class LogbackAccessJoranSpringPropertyModel : NamedModel() { 12 | 13 | /** 14 | * The property name in the Spring environment. 15 | */ 16 | var source: String? = null 17 | 18 | /** 19 | * The fallback value if [source] can't be found in the Spring environment. 20 | */ 21 | var defaultValue: String? = null 22 | 23 | /** 24 | * The Logback property scope. 25 | */ 26 | var scope: String? = null 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/akkinoc/spring/boot/logback/access/value/LogbackAccessLocalPortStrategy.kt: -------------------------------------------------------------------------------- 1 | package dev.akkinoc.spring.boot.logback.access.value 2 | 3 | import ch.qos.logback.access.common.spi.IAccessEvent 4 | import jakarta.servlet.ServletRequest 5 | 6 | /** 7 | * The strategy to change the behavior of [IAccessEvent.getLocalPort]. 8 | */ 9 | enum class LogbackAccessLocalPortStrategy { 10 | 11 | /** 12 | * Returns the port number of the interface on which the request was received. 13 | * Equivalent to [ServletRequest.getLocalPort] when using a servlet web server. 14 | */ 15 | LOCAL, 16 | 17 | /** 18 | * Returns the port number to which the request was sent. 19 | * Equivalent to [ServletRequest.getServerPort] when using a servlet web server. 20 | * Helps to identify the destination port number used by the client when forward headers are enabled. 21 | */ 22 | SERVER, 23 | 24 | } 25 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: cron 7 | cronjob: 0 12 * * * 8 | assignees: 9 | - akkinoc 10 | labels: 11 | - type:dependencies 12 | milestone: 4 13 | - package-ecosystem: maven 14 | directories: 15 | - / 16 | - /examples/webflux-jetty-java 17 | - /examples/webflux-tomcat-java 18 | - /examples/webflux-undertow-java 19 | - /examples/webmvc-jetty-java 20 | - /examples/webmvc-tomcat-java 21 | - /examples/webmvc-tomcat-kotlin 22 | - /examples/webmvc-undertow-java 23 | schedule: 24 | interval: cron 25 | cronjob: 0 12 * * * 26 | assignees: 27 | - akkinoc 28 | labels: 29 | - type:dependencies 30 | milestone: 4 31 | groups: 32 | spring-boot: 33 | patterns: 34 | - org.springframework.boot:* 35 | logback-access: 36 | patterns: 37 | - ch.qos.logback.access:* 38 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/akkinoc/spring/boot/logback/access/joran/LogbackAccessJoranSpringProfileAction.kt: -------------------------------------------------------------------------------- 1 | package dev.akkinoc.spring.boot.logback.access.joran 2 | 3 | import ch.qos.logback.core.joran.action.Action 4 | import ch.qos.logback.core.joran.action.BaseModelAction 5 | import ch.qos.logback.core.joran.spi.SaxEventInterpretationContext 6 | import ch.qos.logback.core.model.Model 7 | import org.xml.sax.Attributes 8 | 9 | /** 10 | * The Joran [Action] to support `` tags. 11 | * Allows a section of a Logback configuration to only be enabled when a specific Spring profile is active. 12 | * 13 | * @see org.springframework.boot.logging.logback.SpringProfileAction 14 | */ 15 | class LogbackAccessJoranSpringProfileAction : BaseModelAction() { 16 | 17 | override fun buildCurrentModel(ic: SaxEventInterpretationContext, name: String, attrs: Attributes): Model { 18 | val model = LogbackAccessJoranSpringProfileModel() 19 | model.name = attrs.getValue(NAME_ATTRIBUTE) 20 | return model 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /.detekt/config.yml: -------------------------------------------------------------------------------- 1 | comments: 2 | UndocumentedPublicClass: 3 | active: true 4 | searchInInnerObject: false 5 | UndocumentedPublicFunction: 6 | active: true 7 | UndocumentedPublicProperty: 8 | active: true 9 | complexity: 10 | TooManyFunctions: 11 | ignoreDeprecated: true 12 | ignoreOverridden: true 13 | ignorePrivate: true 14 | formatting: 15 | ImportOrdering: 16 | active: true 17 | NoBlankLineBeforeRbrace: 18 | active: false 19 | ParameterListWrapping: 20 | active: false 21 | performance: 22 | SpreadOperator: 23 | active: false 24 | style: 25 | ClassOrdering: 26 | active: true 27 | ForbiddenComment: 28 | active: false 29 | FunctionOnlyReturningConstant: 30 | active: false 31 | NoTabs: 32 | active: true 33 | ReturnCount: 34 | excludeLabeled: true 35 | excludeGuardClauses: true 36 | SerialVersionUIDInSerializableClass: 37 | active: false 38 | UnusedPrivateMember: 39 | allowedNames: (readObject|readObjectNoData|readResolve|serialVersionUID|writeObject|writeReplace) 40 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/akkinoc/spring/boot/logback/access/security/LogbackAccessSecurityServletFilter.kt: -------------------------------------------------------------------------------- 1 | package dev.akkinoc.spring.boot.logback.access.security 2 | 3 | import jakarta.servlet.Filter 4 | import jakarta.servlet.FilterChain 5 | import jakarta.servlet.ServletRequest 6 | import jakarta.servlet.ServletResponse 7 | import jakarta.servlet.http.HttpServletRequest 8 | 9 | /** 10 | * The security filter for the servlet web server. 11 | */ 12 | class LogbackAccessSecurityServletFilter : Filter { 13 | 14 | override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) { 15 | request as HttpServletRequest 16 | request.setAttribute(REMOTE_USER_ATTRIBUTE, request.remoteUser) 17 | chain.doFilter(request, response) 18 | } 19 | 20 | companion object { 21 | 22 | /** 23 | * The attribute name for the remote user. 24 | */ 25 | @JvmField 26 | val REMOTE_USER_ATTRIBUTE: String = "${LogbackAccessSecurityServletFilter::class.qualifiedName}.remoteUser" 27 | 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | [![issues badge]][issues] 4 | [![pulls badge]][pulls] 5 | [![license badge]][license] 6 | 7 | [issues]: https://github.com/akkinoc/logback-access-spring-boot-starter/issues 8 | [issues badge]: https://img.shields.io/github/issues/akkinoc/logback-access-spring-boot-starter 9 | [pulls]: https://github.com/akkinoc/logback-access-spring-boot-starter/pulls 10 | [pulls badge]: https://img.shields.io/github/issues-pr/akkinoc/logback-access-spring-boot-starter 11 | [license]: LICENSE.txt 12 | [license badge]: https://img.shields.io/github/license/akkinoc/logback-access-spring-boot-starter?color=blue 13 | 14 | [Bug reports][issues] and [pull requests][pulls] are welcome :) 15 | 16 | ## Setup 17 | 18 | To set up, run: 19 | 20 | ```console 21 | $ git clone https://github.com/akkinoc/logback-access-spring-boot-starter.git 22 | $ cd logback-access-spring-boot-starter 23 | ``` 24 | 25 | ## Building and Testing 26 | 27 | To build and test, run: 28 | 29 | ```console 30 | $ mvn clean install 31 | ``` 32 | 33 | ## License 34 | 35 | By contributing, you agree that your contributions will be licensed under the [Apache License, Version 2.0][license]. 36 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/akkinoc/spring/boot/logback/access/joran/LogbackAccessJoranSpringPropertyAction.kt: -------------------------------------------------------------------------------- 1 | package dev.akkinoc.spring.boot.logback.access.joran 2 | 3 | import ch.qos.logback.core.joran.action.Action 4 | import ch.qos.logback.core.joran.action.BaseModelAction 5 | import ch.qos.logback.core.joran.spi.SaxEventInterpretationContext 6 | import ch.qos.logback.core.model.Model 7 | import org.xml.sax.Attributes 8 | 9 | /** 10 | * The Joran [Action] to support `` tags. 11 | * Allows Logback properties to be sourced from the Spring environment. 12 | * 13 | * @see org.springframework.boot.logging.logback.SpringPropertyAction 14 | */ 15 | class LogbackAccessJoranSpringPropertyAction : BaseModelAction() { 16 | 17 | override fun buildCurrentModel(ic: SaxEventInterpretationContext, name: String, attrs: Attributes): Model { 18 | val model = LogbackAccessJoranSpringPropertyModel() 19 | model.name = attrs.getValue(NAME_ATTRIBUTE) 20 | model.source = attrs.getValue("source") 21 | model.defaultValue = attrs.getValue("defaultValue") 22 | model.scope = attrs.getValue(SCOPE_ATTRIBUTE) 23 | return model 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/test/resources/logback-access-test-spring.property.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 10 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | ${console_pattern_prefix}${console_pattern_body}${console_pattern_suffix} 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/test/resources/logback-access-test-spring.property-as-include.included.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 10 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | ${console_pattern_prefix}${console_pattern_body}${console_pattern_suffix} 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/akkinoc/spring/boot/logback/access/LogbackAccessHackyLoggingOverrides.kt: -------------------------------------------------------------------------------- 1 | package dev.akkinoc.spring.boot.logback.access 2 | 3 | import jakarta.servlet.http.HttpServletRequest 4 | import jakarta.servlet.http.HttpServletResponse 5 | 6 | /** 7 | * This is a hacky workaround for https://github.com/akkinoc/logback-access-spring-boot-starter/issues/98. 8 | * A user who wants to modify this logic should copy this class and modify it, 9 | * and make sure the modified class has a higher classloader priority. 10 | * Please note that this is a temporary solution. 11 | */ 12 | object LogbackAccessHackyLoggingOverrides { 13 | 14 | /** 15 | * Returning null means no override. 16 | * Returning non-null value will result in that value being logged instead of request body. 17 | */ 18 | @JvmStatic 19 | @Suppress("UNUSED_PARAMETER") 20 | fun overriddenRequestBody(request: HttpServletRequest?): String? = null 21 | 22 | /** 23 | * Returning null means no override. 24 | * Returning non-null value will result in that value being logged instead of response body. 25 | */ 26 | @JvmStatic 27 | @Suppress("UNUSED_PARAMETER") 28 | fun overriddenResponseBody(request: HttpServletRequest?, response: HttpServletResponse?): String? = null 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/test/kotlin/dev/akkinoc/spring/boot/logback/access/test/extension/EventsCaptureAppender.kt: -------------------------------------------------------------------------------- 1 | package dev.akkinoc.spring.boot.logback.access.test.extension 2 | 3 | import ch.qos.logback.core.AppenderBase 4 | import dev.akkinoc.spring.boot.logback.access.LogbackAccessEvent 5 | import org.slf4j.Logger 6 | import org.slf4j.LoggerFactory.getLogger 7 | import org.springframework.util.SerializationUtils.clone 8 | import java.util.concurrent.ConcurrentHashMap 9 | 10 | /** 11 | * The Logback-access appender to capture Logback-access events that will be appended. 12 | */ 13 | class EventsCaptureAppender : AppenderBase() { 14 | 15 | override fun append(event: LogbackAccessEvent) { 16 | event.prepareForDeferredProcessing() 17 | val cloned = clone(event) 18 | captures.forEach { (id, capture) -> 19 | capture += cloned 20 | log.debug("Captured the {}: {} @{}", LogbackAccessEvent::class.simpleName, event, id) 21 | } 22 | } 23 | 24 | companion object { 25 | 26 | /** 27 | * The logger. 28 | */ 29 | private val log: Logger = getLogger(EventsCaptureAppender::class.java) 30 | 31 | /** 32 | * The Logback-access events captures. 33 | */ 34 | val captures: MutableMap = ConcurrentHashMap() 35 | 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | schedule: 7 | - cron: 0 12 * * * 8 | workflow_dispatch: 9 | concurrency: 10 | group: deploy 11 | cancel-in-progress: false 12 | jobs: 13 | deploy: 14 | name: Deploy 15 | runs-on: ubuntu-24.04 16 | env: 17 | MAVEN_ARGS: -B --color always 18 | steps: 19 | - name: Checkout the Git repository 20 | uses: actions/checkout@v6.0.1 21 | - name: Setup Java 22 | uses: actions/setup-java@v5.1.0 23 | with: 24 | distribution: temurin 25 | java-version: 21 26 | cache: maven 27 | server-id: central 28 | server-username: MAVEN_CENTRAL_USERNAME 29 | server-password: MAVEN_CENTRAL_PASSWORD 30 | - name: Import GPG key 31 | uses: crazy-max/ghaction-import-gpg@v6.3.0 32 | with: 33 | gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} 34 | passphrase: ${{ secrets.GPG_PASSPHRASE }} 35 | - name: Export the project version 36 | run: echo PROJECT_VERSION=$(mvn -Dexpression=project.version -q help:evaluate) | tee -a $GITHUB_ENV 37 | - name: Deploy the project 38 | run: mvn clean deploy 39 | if: ${{ startsWith(github.ref, 'refs/tags/') || endsWith(env.PROJECT_VERSION, '-SNAPSHOT') }} 40 | env: 41 | MAVEN_CENTRAL_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }} 42 | MAVEN_CENTRAL_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} 43 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | version: 6 | description: Version 7 | type: string 8 | required: true 9 | concurrency: 10 | group: release 11 | cancel-in-progress: false 12 | jobs: 13 | release: 14 | name: Release 15 | runs-on: ubuntu-24.04 16 | env: 17 | MAVEN_ARGS: -B --color always 18 | steps: 19 | - name: Checkout the Git repository 20 | uses: actions/checkout@v6.0.1 21 | with: 22 | ssh-key: ${{ secrets.GIT_PRIVATE_KEY }} 23 | - name: Setup Java 24 | uses: actions/setup-java@v5.1.0 25 | with: 26 | distribution: temurin 27 | java-version: 21 28 | cache: maven 29 | server-id: central 30 | server-username: MAVEN_CENTRAL_USERNAME 31 | server-password: MAVEN_CENTRAL_PASSWORD 32 | - name: Import GPG key 33 | uses: crazy-max/ghaction-import-gpg@v6.3.0 34 | with: 35 | gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} 36 | passphrase: ${{ secrets.GPG_PASSPHRASE }} 37 | git_user_signingkey: true 38 | git_commit_gpgsign: true 39 | git_tag_gpgsign: true 40 | - name: Release the project 41 | run: mvn -DreleaseVersion=${{ inputs.version }} release:clean release:prepare 42 | env: 43 | MAVEN_CENTRAL_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }} 44 | MAVEN_CENTRAL_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} 45 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/akkinoc/spring/boot/logback/access/joran/LogbackAccessJoranSpringProfileModelHandler.kt: -------------------------------------------------------------------------------- 1 | package dev.akkinoc.spring.boot.logback.access.joran 2 | 3 | import ch.qos.logback.core.Context 4 | import ch.qos.logback.core.model.Model 5 | import ch.qos.logback.core.model.processor.ModelInterpretationContext 6 | import ch.qos.logback.core.util.OptionHelper.substVars 7 | import org.springframework.core.env.Environment 8 | import org.springframework.core.env.Profiles 9 | import org.springframework.util.StringUtils.commaDelimitedListToStringArray 10 | import org.springframework.util.StringUtils.trimArrayElements 11 | import ch.qos.logback.core.model.processor.ModelHandlerBase as ModelHandler 12 | 13 | /** 14 | * The Joran [ModelHandler] to support `` tags. 15 | * 16 | * @param context The Logback context. 17 | * @property environment The environment. 18 | * @see org.springframework.boot.logging.logback.SpringProfileModelHandler 19 | */ 20 | class LogbackAccessJoranSpringProfileModelHandler( 21 | context: Context, 22 | private val environment: Environment, 23 | ) : ModelHandler(context) { 24 | 25 | override fun handle(ic: ModelInterpretationContext, model: Model) { 26 | model as LogbackAccessJoranSpringProfileModel 27 | val name = trimArrayElements(commaDelimitedListToStringArray(model.name)) 28 | name.indices.forEach { name[it] = substVars(name[it], ic, context) } 29 | if (name.isEmpty() || !environment.acceptsProfiles(Profiles.of(*name))) model.deepMarkAsSkipped() 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/akkinoc/spring/boot/logback/access/tomcat/LogbackAccessTomcatWebServerFactoryCustomizer.kt: -------------------------------------------------------------------------------- 1 | package dev.akkinoc.spring.boot.logback.access.tomcat 2 | 3 | import dev.akkinoc.spring.boot.logback.access.LogbackAccessContext 4 | import org.slf4j.Logger 5 | import org.slf4j.LoggerFactory.getLogger 6 | import org.springframework.boot.web.embedded.tomcat.ConfigurableTomcatWebServerFactory 7 | import org.springframework.boot.web.server.WebServerFactoryCustomizer 8 | 9 | /** 10 | * The [WebServerFactoryCustomizer] for the Tomcat web server. 11 | * 12 | * @property logbackAccessContext The Logback-access context. 13 | * @see org.springframework.boot.autoconfigure.web.embedded.TomcatWebServerFactoryCustomizer 14 | */ 15 | class LogbackAccessTomcatWebServerFactoryCustomizer( 16 | private val logbackAccessContext: LogbackAccessContext, 17 | ) : WebServerFactoryCustomizer { 18 | 19 | override fun customize(factory: ConfigurableTomcatWebServerFactory) { 20 | val valve = LogbackAccessTomcatValve(logbackAccessContext) 21 | factory.addEngineValves(valve) 22 | log.debug( 23 | "Customized the {}: {} @{}", 24 | ConfigurableTomcatWebServerFactory::class.simpleName, 25 | factory, 26 | logbackAccessContext, 27 | ) 28 | } 29 | 30 | companion object { 31 | 32 | /** 33 | * The logger. 34 | */ 35 | private val log: Logger = getLogger(LogbackAccessTomcatWebServerFactoryCustomizer::class.java) 36 | 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/akkinoc/spring/boot/logback/access/joran/LogbackAccessJoranSpringPropertyModelHandler.kt: -------------------------------------------------------------------------------- 1 | package dev.akkinoc.spring.boot.logback.access.joran 2 | 3 | import ch.qos.logback.core.Context 4 | import ch.qos.logback.core.joran.action.ActionUtil.stringToScope 5 | import ch.qos.logback.core.model.Model 6 | import ch.qos.logback.core.model.processor.ModelInterpretationContext 7 | import ch.qos.logback.core.model.util.PropertyModelHandlerHelper.setProperty 8 | import org.springframework.core.env.Environment 9 | import ch.qos.logback.core.model.processor.ModelHandlerBase as ModelHandler 10 | 11 | /** 12 | * The Joran [ModelHandler] to support `` tags. 13 | * 14 | * @param context The Logback context. 15 | * @property environment The environment. 16 | * @see org.springframework.boot.logging.logback.SpringPropertyModelHandler 17 | */ 18 | class LogbackAccessJoranSpringPropertyModelHandler( 19 | context: Context, 20 | private val environment: Environment, 21 | ) : ModelHandler(context) { 22 | 23 | override fun handle(ic: ModelInterpretationContext, model: Model) { 24 | model as LogbackAccessJoranSpringPropertyModel 25 | val name = model.name 26 | val source = model.source 27 | val defaultValue = model.defaultValue.orEmpty() 28 | val scope = stringToScope(model.scope) 29 | if (name.isNullOrBlank() || source.isNullOrBlank()) { 30 | addError("""The "name" and "source" attributes of must be set""") 31 | return 32 | } 33 | val value = environment.getProperty(source, defaultValue) 34 | setProperty(ic, name, value, scope) 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/akkinoc/spring/boot/logback/access/jetty/LogbackAccessJettyRequestLog.kt: -------------------------------------------------------------------------------- 1 | package dev.akkinoc.spring.boot.logback.access.jetty 2 | 3 | import dev.akkinoc.spring.boot.logback.access.LogbackAccessContext 4 | import dev.akkinoc.spring.boot.logback.access.LogbackAccessEvent 5 | import org.eclipse.jetty.server.Request 6 | import org.eclipse.jetty.server.RequestLog 7 | import org.eclipse.jetty.server.Response 8 | import org.slf4j.Logger 9 | import org.slf4j.LoggerFactory.getLogger 10 | 11 | /** 12 | * The Jetty [RequestLog] to emit Logback-access events. 13 | * 14 | * @property logbackAccessContext The Logback-access context. 15 | * @see org.eclipse.jetty.server.CustomRequestLog 16 | * @see ch.qos.logback.access.jetty.RequestLogImpl 17 | */ 18 | class LogbackAccessJettyRequestLog( 19 | private val logbackAccessContext: LogbackAccessContext, 20 | ) : RequestLog { 21 | 22 | override fun log(request: Request, response: Response) { 23 | log.debug( 24 | "Logging the {}/{}: {} => {} @{}", 25 | Request::class.simpleName, 26 | Response::class.simpleName, 27 | request, 28 | response, 29 | logbackAccessContext, 30 | ) 31 | val source = LogbackAccessJettyEventSource( 32 | logbackAccessContext = logbackAccessContext, 33 | rawRequest = request, 34 | rawResponse = response, 35 | ) 36 | val event = LogbackAccessEvent(source) 37 | logbackAccessContext.emit(event) 38 | } 39 | 40 | companion object { 41 | 42 | /** 43 | * The logger. 44 | */ 45 | private val log: Logger = getLogger(LogbackAccessJettyRequestLog::class.java) 46 | 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /examples/webmvc-tomcat-java/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 4.0.0 5 | 6 | dev.akkinoc.spring.boot 7 | logback-access-spring-boot-starter.examples.webmvc-tomcat-java 8 | 1.0.0-SNAPSHOT 9 | 10 | 11 | org.springframework.boot 12 | spring-boot-starter-parent 13 | 3.5.7 14 | 15 | 16 | 17 | [4.0.0,) 18 | 19 | 20 | 21 | 22 | org.springframework.boot 23 | spring-boot-starter-web 24 | 25 | 26 | dev.akkinoc.spring.boot 27 | logback-access-spring-boot-starter 28 | ${logback-access-spring-boot-starter.version} 29 | 30 | 31 | ch.qos.logback.access 32 | logback-access-tomcat 33 | 2.0.7 34 | 35 | 36 | 37 | 38 | 39 | 40 | src/main/resources 41 | true 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/akkinoc/spring/boot/logback/access/jetty/LogbackAccessJettyWebServerFactoryCustomizer.kt: -------------------------------------------------------------------------------- 1 | package dev.akkinoc.spring.boot.logback.access.jetty 2 | 3 | import dev.akkinoc.spring.boot.logback.access.LogbackAccessContext 4 | import org.eclipse.jetty.server.Server 5 | import org.slf4j.Logger 6 | import org.slf4j.LoggerFactory.getLogger 7 | import org.springframework.boot.web.embedded.jetty.ConfigurableJettyWebServerFactory 8 | import org.springframework.boot.web.server.WebServerFactoryCustomizer 9 | 10 | /** 11 | * The [WebServerFactoryCustomizer] for the Jetty web server. 12 | * 13 | * @property logbackAccessContext The Logback-access context. 14 | * @see org.springframework.boot.autoconfigure.web.embedded.JettyWebServerFactoryCustomizer 15 | */ 16 | class LogbackAccessJettyWebServerFactoryCustomizer( 17 | private val logbackAccessContext: LogbackAccessContext, 18 | ) : WebServerFactoryCustomizer { 19 | 20 | override fun customize(factory: ConfigurableJettyWebServerFactory) { 21 | factory.addServerCustomizers(::customize) 22 | log.debug( 23 | "Customized the {}: {} @{}", 24 | ConfigurableJettyWebServerFactory::class.simpleName, 25 | factory, 26 | logbackAccessContext, 27 | ) 28 | } 29 | 30 | /** 31 | * Customizes the [Server]. 32 | * 33 | * @param server The [Server]. 34 | */ 35 | private fun customize(server: Server) { 36 | server.requestLog = LogbackAccessJettyRequestLog(logbackAccessContext) 37 | log.debug( 38 | "Customized the {}: {} @{}", 39 | Server::class.simpleName, 40 | server, 41 | logbackAccessContext, 42 | ) 43 | } 44 | 45 | companion object { 46 | 47 | /** 48 | * The logger. 49 | */ 50 | private val log: Logger = getLogger(LogbackAccessJettyWebServerFactoryCustomizer::class.java) 51 | 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/test/kotlin/dev/akkinoc/spring/boot/logback/access/test/assertion/Assertions.kt: -------------------------------------------------------------------------------- 1 | package dev.akkinoc.spring.boot.logback.access.test.assertion 2 | 3 | import io.kotest.assertions.nondeterministic.continually 4 | import io.kotest.assertions.nondeterministic.continuallyConfig 5 | import io.kotest.assertions.nondeterministic.eventually 6 | import io.kotest.assertions.nondeterministic.eventuallyConfig 7 | import kotlinx.coroutines.runBlocking 8 | import kotlin.time.Duration.Companion.milliseconds 9 | import kotlin.time.Duration.Companion.seconds 10 | 11 | /** 12 | * The utility methods to support test assertions. 13 | */ 14 | object Assertions { 15 | 16 | /** 17 | * Asserts that the assertion function will eventually pass within a short time. 18 | * Used to assert Logback-access events that may be appended late. 19 | * 20 | * @param T The return value type of the assertion function. 21 | * @param assert The assertion function that is called repeatedly. 22 | * @return The return value of the assertion function. 23 | */ 24 | fun assertLogbackAccessEventsEventually(assert: () -> T): T = runBlocking { 25 | val config = eventuallyConfig { 26 | duration = 2.seconds 27 | interval = 100.milliseconds 28 | } 29 | eventually(config) { assert() } 30 | } 31 | 32 | /** 33 | * Asserts that the assertion function will continually pass within a short time. 34 | * Used to assert Logback-access events that may be appended late. 35 | * 36 | * @param T The return value type of the assertion function. 37 | * @param assert The assertion function that is called repeatedly. 38 | * @return The return value of the assertion function. 39 | */ 40 | fun assertLogbackAccessEventsContinually(assert: () -> T): T = runBlocking { 41 | val config = continuallyConfig { 42 | duration = 2.seconds 43 | interval = 100.milliseconds 44 | } 45 | continually(config) { assert() } 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/test/kotlin/dev/akkinoc/spring/boot/logback/access/test/mock/MockReactiveController.kt: -------------------------------------------------------------------------------- 1 | package dev.akkinoc.spring.boot.logback.access.test.mock 2 | 3 | import org.slf4j.Logger 4 | import org.slf4j.LoggerFactory.getLogger 5 | import org.springframework.web.bind.annotation.GetMapping 6 | import org.springframework.web.bind.annotation.RequestMapping 7 | import org.springframework.web.bind.annotation.RestController 8 | import org.springframework.web.server.ServerWebExchange 9 | import org.springframework.web.server.WebSession 10 | 11 | /** 12 | * The mock controller for testing using the reactive web server. 13 | */ 14 | @RestController 15 | @RequestMapping("/mock-controller") 16 | class MockReactiveController { 17 | 18 | /** 19 | * Gets a text with request attributes. 20 | * 21 | * @param exchange The request/response exchange to set attributes. 22 | * @return A text. 23 | */ 24 | @GetMapping("/text-with-request-attributes") 25 | fun getTextWithRequestAttributes(exchange: ServerWebExchange): String { 26 | exchange.attributes["a"] = "value @a" 27 | exchange.attributes["b"] = listOf("value1 @b", "value2 @b") 28 | exchange.attributes["c"] = "" 29 | exchange.attributes["d"] = Any() 30 | val response = "mock-text" 31 | log.debug("Getting a text with request attributes: {}; {}", response, exchange) 32 | return response 33 | } 34 | 35 | /** 36 | * Gets a text with a session. 37 | * 38 | * @param session The session. 39 | * @return A text. 40 | */ 41 | @GetMapping("/text-with-session") 42 | fun getTextWithSession(session: WebSession): String { 43 | session.start() 44 | val response = "mock-text" 45 | log.debug("Getting a text with a session: {}; {}", response, session) 46 | return response 47 | } 48 | 49 | companion object { 50 | 51 | /** 52 | * The logger. 53 | */ 54 | private val log: Logger = getLogger(MockReactiveController::class.java) 55 | 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/akkinoc/spring/boot/logback/access/joran/LogbackAccessJoranConfigurator.kt: -------------------------------------------------------------------------------- 1 | package dev.akkinoc.spring.boot.logback.access.joran 2 | 3 | import ch.qos.logback.access.common.joran.JoranConfigurator 4 | import ch.qos.logback.core.joran.spi.ElementSelector 5 | import ch.qos.logback.core.joran.spi.RuleStore 6 | import ch.qos.logback.core.model.processor.DefaultProcessor 7 | import org.springframework.core.env.Environment 8 | import java.util.function.Supplier 9 | 10 | /** 11 | * The [JoranConfigurator] to support additional rules. 12 | * 13 | * @property environment The environment. 14 | * @see org.springframework.boot.logging.logback.SpringBootJoranConfigurator 15 | */ 16 | class LogbackAccessJoranConfigurator(private val environment: Environment) : JoranConfigurator() { 17 | 18 | override fun addElementSelectorAndActionAssociations(store: RuleStore) { 19 | super.addElementSelectorAndActionAssociations(store) 20 | store.addRule(ElementSelector("configuration/springProperty")) { LogbackAccessJoranSpringPropertyAction() } 21 | store.addRule(ElementSelector("*/springProfile")) { LogbackAccessJoranSpringProfileAction() } 22 | store.addTransparentPathPart("springProfile") 23 | } 24 | 25 | override fun addModelHandlerAssociations(processor: DefaultProcessor) { 26 | processor.addHandler(LogbackAccessJoranSpringPropertyModel::class.java) { _, _ -> 27 | LogbackAccessJoranSpringPropertyModelHandler(context, environment) 28 | } 29 | processor.addHandler(LogbackAccessJoranSpringProfileModel::class.java) { _, _ -> 30 | LogbackAccessJoranSpringProfileModelHandler(context, environment) 31 | } 32 | super.addModelHandlerAssociations(processor) 33 | } 34 | 35 | override fun buildModelInterpretationContext() { 36 | super.buildModelInterpretationContext() 37 | modelInterpretationContext.configuratorSupplier = Supplier { 38 | LogbackAccessJoranConfigurator(environment).also { it.context = context } 39 | } 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /examples/webmvc-undertow-java/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 4.0.0 5 | 6 | dev.akkinoc.spring.boot 7 | logback-access-spring-boot-starter.examples.webmvc-undertow-java 8 | 1.0.0-SNAPSHOT 9 | 10 | 11 | org.springframework.boot 12 | spring-boot-starter-parent 13 | 3.5.7 14 | 15 | 16 | 17 | [4.0.0,) 18 | 19 | 20 | 21 | 22 | org.springframework.boot 23 | spring-boot-starter-web 24 | 25 | 26 | org.springframework.boot 27 | spring-boot-starter-tomcat 28 | 29 | 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-starter-undertow 34 | 35 | 36 | dev.akkinoc.spring.boot 37 | logback-access-spring-boot-starter 38 | ${logback-access-spring-boot-starter.version} 39 | 40 | 41 | 42 | 43 | 44 | 45 | src/main/resources 46 | true 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/akkinoc/spring/boot/logback/access/security/LogbackAccessSecurityServletFilterConfiguration.kt: -------------------------------------------------------------------------------- 1 | package dev.akkinoc.spring.boot.logback.access.security 2 | 3 | import org.slf4j.Logger 4 | import org.slf4j.LoggerFactory.getLogger 5 | import org.springframework.boot.autoconfigure.condition.ConditionalOnClass 6 | import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication 7 | import org.springframework.boot.autoconfigure.web.servlet.ConditionalOnMissingFilterBean 8 | import org.springframework.boot.web.servlet.FilterRegistrationBean 9 | import org.springframework.context.annotation.Bean 10 | import org.springframework.context.annotation.Configuration 11 | import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer 12 | 13 | /** 14 | * The configuration to register the security filter for the servlet web server. 15 | */ 16 | @Configuration(proxyBeanMethods = false) 17 | @ConditionalOnClass(AbstractSecurityWebApplicationInitializer::class) 18 | @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) 19 | class LogbackAccessSecurityServletFilterConfiguration { 20 | 21 | /** 22 | * Provides the security filter for the servlet web server. 23 | * 24 | * @return The security filter for the servlet web server. 25 | */ 26 | @Bean 27 | @ConditionalOnMissingFilterBean 28 | fun logbackAccessSecurityServletFilter(): FilterRegistrationBean { 29 | val logbackAccessSecurityServletFilter = FilterRegistrationBean(LogbackAccessSecurityServletFilter()) 30 | log.debug( 31 | "Providing the {}: {}", 32 | LogbackAccessSecurityServletFilter::class.simpleName, 33 | logbackAccessSecurityServletFilter, 34 | ) 35 | return logbackAccessSecurityServletFilter 36 | } 37 | 38 | companion object { 39 | 40 | /** 41 | * The logger. 42 | */ 43 | private val log: Logger = getLogger(LogbackAccessSecurityServletFilterConfiguration::class.java) 44 | 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /examples/webflux-undertow-java/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 4.0.0 5 | 6 | dev.akkinoc.spring.boot 7 | logback-access-spring-boot-starter.examples.webflux-undertow-java 8 | 1.0.0-SNAPSHOT 9 | 10 | 11 | org.springframework.boot 12 | spring-boot-starter-parent 13 | 3.5.7 14 | 15 | 16 | 17 | [4.0.0,) 18 | 19 | 20 | 21 | 22 | org.springframework.boot 23 | spring-boot-starter-webflux 24 | 25 | 26 | org.springframework.boot 27 | spring-boot-starter-reactor-netty 28 | 29 | 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-starter-undertow 34 | 35 | 36 | dev.akkinoc.spring.boot 37 | logback-access-spring-boot-starter 38 | ${logback-access-spring-boot-starter.version} 39 | 40 | 41 | 42 | 43 | 44 | 45 | src/main/resources 46 | true 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/akkinoc/spring/boot/logback/access/jetty/LogbackAccessJettyConfiguration.kt: -------------------------------------------------------------------------------- 1 | package dev.akkinoc.spring.boot.logback.access.jetty 2 | 3 | import dev.akkinoc.spring.boot.logback.access.LogbackAccessContext 4 | import org.eclipse.jetty.server.Server 5 | import org.slf4j.Logger 6 | import org.slf4j.LoggerFactory.getLogger 7 | import org.springframework.boot.autoconfigure.condition.ConditionalOnClass 8 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean 9 | import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication 10 | import org.springframework.boot.web.server.WebServerFactoryCustomizer 11 | import org.springframework.context.annotation.Bean 12 | import org.springframework.context.annotation.Configuration 13 | 14 | /** 15 | * The configuration for the Jetty web server. 16 | */ 17 | @Configuration(proxyBeanMethods = false) 18 | @ConditionalOnClass(Server::class) 19 | @ConditionalOnWebApplication 20 | class LogbackAccessJettyConfiguration { 21 | 22 | /** 23 | * Provides the [WebServerFactoryCustomizer] for the Jetty web server. 24 | * 25 | * @param logbackAccessContext The Logback-access context. 26 | * @return The [WebServerFactoryCustomizer] for the Jetty web server. 27 | */ 28 | @Bean 29 | @ConditionalOnMissingBean 30 | fun logbackAccessJettyWebServerFactoryCustomizer( 31 | logbackAccessContext: LogbackAccessContext, 32 | ): LogbackAccessJettyWebServerFactoryCustomizer { 33 | val logbackAccessJettyWebServerFactoryCustomizer = 34 | LogbackAccessJettyWebServerFactoryCustomizer(logbackAccessContext) 35 | log.debug( 36 | "Providing the {}: {}", 37 | LogbackAccessJettyWebServerFactoryCustomizer::class.simpleName, 38 | logbackAccessJettyWebServerFactoryCustomizer, 39 | ) 40 | return logbackAccessJettyWebServerFactoryCustomizer 41 | } 42 | 43 | companion object { 44 | 45 | /** 46 | * The logger. 47 | */ 48 | private val log: Logger = getLogger(LogbackAccessJettyConfiguration::class.java) 49 | 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/akkinoc/spring/boot/logback/access/undertow/LogbackAccessUndertowWebServerFactoryCustomizer.kt: -------------------------------------------------------------------------------- 1 | package dev.akkinoc.spring.boot.logback.access.undertow 2 | 3 | import dev.akkinoc.spring.boot.logback.access.LogbackAccessContext 4 | import io.undertow.Undertow.Builder 5 | import io.undertow.UndertowOptions 6 | import org.slf4j.Logger 7 | import org.slf4j.LoggerFactory.getLogger 8 | import org.springframework.boot.web.embedded.undertow.ConfigurableUndertowWebServerFactory 9 | import org.springframework.boot.web.server.WebServerFactoryCustomizer 10 | 11 | /** 12 | * The [WebServerFactoryCustomizer] for the Undertow web server. 13 | * 14 | * @property logbackAccessContext The Logback-access context. 15 | * @see org.springframework.boot.autoconfigure.web.embedded.UndertowWebServerFactoryCustomizer 16 | */ 17 | class LogbackAccessUndertowWebServerFactoryCustomizer( 18 | private val logbackAccessContext: LogbackAccessContext, 19 | ) : WebServerFactoryCustomizer { 20 | 21 | override fun customize(factory: ConfigurableUndertowWebServerFactory) { 22 | factory.addBuilderCustomizers(::customize) 23 | log.debug( 24 | "Customized the {}: {} @{}", 25 | ConfigurableUndertowWebServerFactory::class.simpleName, 26 | factory, 27 | logbackAccessContext, 28 | ) 29 | } 30 | 31 | /** 32 | * Customizes the [Builder]. 33 | * 34 | * @param builder The [Builder]. 35 | */ 36 | private fun customize(builder: Builder) { 37 | val props = logbackAccessContext.properties.undertow 38 | builder.setServerOption(UndertowOptions.RECORD_REQUEST_START_TIME, props.recordRequestStartTime) 39 | log.debug( 40 | "Customized the {}: {} @{}", 41 | Builder::class.simpleName, 42 | builder, 43 | logbackAccessContext, 44 | ) 45 | } 46 | 47 | companion object { 48 | 49 | /** 50 | * The logger. 51 | */ 52 | private val log: Logger = getLogger(LogbackAccessUndertowWebServerFactoryCustomizer::class.java) 53 | 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/akkinoc/spring/boot/logback/access/tomcat/LogbackAccessTomcatConfiguration.kt: -------------------------------------------------------------------------------- 1 | package dev.akkinoc.spring.boot.logback.access.tomcat 2 | 3 | import dev.akkinoc.spring.boot.logback.access.LogbackAccessContext 4 | import org.apache.catalina.startup.Tomcat 5 | import org.slf4j.Logger 6 | import org.slf4j.LoggerFactory.getLogger 7 | import org.springframework.boot.autoconfigure.condition.ConditionalOnClass 8 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean 9 | import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication 10 | import org.springframework.boot.web.server.WebServerFactoryCustomizer 11 | import org.springframework.context.annotation.Bean 12 | import org.springframework.context.annotation.Configuration 13 | 14 | /** 15 | * The configuration for the Tomcat web server. 16 | */ 17 | @Configuration(proxyBeanMethods = false) 18 | @ConditionalOnClass(Tomcat::class) 19 | @ConditionalOnWebApplication 20 | class LogbackAccessTomcatConfiguration { 21 | 22 | /** 23 | * Provides the [WebServerFactoryCustomizer] for the Tomcat web server. 24 | * 25 | * @param logbackAccessContext The Logback-access context. 26 | * @return The [WebServerFactoryCustomizer] for the Tomcat web server. 27 | */ 28 | @Bean 29 | @ConditionalOnMissingBean 30 | fun logbackAccessTomcatWebServerFactoryCustomizer( 31 | logbackAccessContext: LogbackAccessContext, 32 | ): LogbackAccessTomcatWebServerFactoryCustomizer { 33 | val logbackAccessTomcatWebServerFactoryCustomizer = 34 | LogbackAccessTomcatWebServerFactoryCustomizer(logbackAccessContext) 35 | log.debug( 36 | "Providing the {}: {}", 37 | LogbackAccessTomcatWebServerFactoryCustomizer::class.simpleName, 38 | logbackAccessTomcatWebServerFactoryCustomizer, 39 | ) 40 | return logbackAccessTomcatWebServerFactoryCustomizer 41 | } 42 | 43 | companion object { 44 | 45 | /** 46 | * The logger. 47 | */ 48 | private val log: Logger = getLogger(LogbackAccessTomcatConfiguration::class.java) 49 | 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/akkinoc/spring/boot/logback/access/undertow/LogbackAccessUndertowConfiguration.kt: -------------------------------------------------------------------------------- 1 | package dev.akkinoc.spring.boot.logback.access.undertow 2 | 3 | import dev.akkinoc.spring.boot.logback.access.LogbackAccessContext 4 | import io.undertow.Undertow 5 | import org.slf4j.Logger 6 | import org.slf4j.LoggerFactory.getLogger 7 | import org.springframework.boot.autoconfigure.condition.ConditionalOnClass 8 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean 9 | import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication 10 | import org.springframework.boot.web.server.WebServerFactoryCustomizer 11 | import org.springframework.context.annotation.Bean 12 | import org.springframework.context.annotation.Configuration 13 | 14 | /** 15 | * The configuration for the Undertow web server. 16 | */ 17 | @Configuration(proxyBeanMethods = false) 18 | @ConditionalOnClass(Undertow::class) 19 | @ConditionalOnWebApplication 20 | class LogbackAccessUndertowConfiguration { 21 | 22 | /** 23 | * Provides the [WebServerFactoryCustomizer] for the Undertow web server. 24 | * 25 | * @param logbackAccessContext The Logback-access context. 26 | * @return The [WebServerFactoryCustomizer] for the Undertow web server. 27 | */ 28 | @Bean 29 | @ConditionalOnMissingBean 30 | fun logbackAccessUndertowWebServerFactoryCustomizer( 31 | logbackAccessContext: LogbackAccessContext, 32 | ): LogbackAccessUndertowWebServerFactoryCustomizer { 33 | val logbackAccessUndertowWebServerFactoryCustomizer = 34 | LogbackAccessUndertowWebServerFactoryCustomizer(logbackAccessContext) 35 | log.debug( 36 | "Providing the {}: {}", 37 | LogbackAccessUndertowWebServerFactoryCustomizer::class.simpleName, 38 | logbackAccessUndertowWebServerFactoryCustomizer, 39 | ) 40 | return logbackAccessUndertowWebServerFactoryCustomizer 41 | } 42 | 43 | companion object { 44 | 45 | /** 46 | * The logger. 47 | */ 48 | private val log: Logger = getLogger(LogbackAccessUndertowConfiguration::class.java) 49 | 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /examples/webmvc-jetty-java/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 4.0.0 5 | 6 | dev.akkinoc.spring.boot 7 | logback-access-spring-boot-starter.examples.webmvc-jetty-java 8 | 1.0.0-SNAPSHOT 9 | 10 | 11 | org.springframework.boot 12 | spring-boot-starter-parent 13 | 3.5.7 14 | 15 | 16 | 17 | [4.0.0,) 18 | 19 | 20 | 21 | 22 | org.springframework.boot 23 | spring-boot-starter-web 24 | 25 | 26 | org.springframework.boot 27 | spring-boot-starter-tomcat 28 | 29 | 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-starter-jetty 34 | 35 | 36 | dev.akkinoc.spring.boot 37 | logback-access-spring-boot-starter 38 | ${logback-access-spring-boot-starter.version} 39 | 40 | 41 | ch.qos.logback.access 42 | logback-access-jetty12 43 | 2.0.7 44 | 45 | 46 | 47 | 48 | 49 | 50 | src/main/resources 51 | true 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /examples/webflux-jetty-java/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 4.0.0 5 | 6 | dev.akkinoc.spring.boot 7 | logback-access-spring-boot-starter.examples.webflux-jetty-java 8 | 1.0.0-SNAPSHOT 9 | 10 | 11 | org.springframework.boot 12 | spring-boot-starter-parent 13 | 3.5.7 14 | 15 | 16 | 17 | [4.0.0,) 18 | 19 | 20 | 21 | 22 | org.springframework.boot 23 | spring-boot-starter-webflux 24 | 25 | 26 | org.springframework.boot 27 | spring-boot-starter-reactor-netty 28 | 29 | 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-starter-jetty 34 | 35 | 36 | dev.akkinoc.spring.boot 37 | logback-access-spring-boot-starter 38 | ${logback-access-spring-boot-starter.version} 39 | 40 | 41 | ch.qos.logback.access 42 | logback-access-jetty12 43 | 2.0.7 44 | 45 | 46 | 47 | 48 | 49 | 50 | src/main/resources 51 | true 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /examples/webflux-tomcat-java/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 4.0.0 5 | 6 | dev.akkinoc.spring.boot 7 | logback-access-spring-boot-starter.examples.webflux-tomcat-java 8 | 1.0.0-SNAPSHOT 9 | 10 | 11 | org.springframework.boot 12 | spring-boot-starter-parent 13 | 3.5.7 14 | 15 | 16 | 17 | [4.0.0,) 18 | 19 | 20 | 21 | 22 | org.springframework.boot 23 | spring-boot-starter-webflux 24 | 25 | 26 | org.springframework.boot 27 | spring-boot-starter-reactor-netty 28 | 29 | 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-starter-tomcat 34 | 35 | 36 | dev.akkinoc.spring.boot 37 | logback-access-spring-boot-starter 38 | ${logback-access-spring-boot-starter.version} 39 | 40 | 41 | ch.qos.logback.access 42 | logback-access-tomcat 43 | 2.0.7 44 | 45 | 46 | 47 | 48 | 49 | 50 | src/main/resources 51 | true 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/akkinoc/spring/boot/logback/access/undertow/LogbackAccessUndertowServletConfiguration.kt: -------------------------------------------------------------------------------- 1 | package dev.akkinoc.spring.boot.logback.access.undertow 2 | 3 | import dev.akkinoc.spring.boot.logback.access.LogbackAccessContext 4 | import io.undertow.Undertow 5 | import org.slf4j.Logger 6 | import org.slf4j.LoggerFactory.getLogger 7 | import org.springframework.boot.autoconfigure.condition.ConditionalOnClass 8 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean 9 | import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication 10 | import org.springframework.boot.web.server.WebServerFactoryCustomizer 11 | import org.springframework.context.annotation.Bean 12 | import org.springframework.context.annotation.Configuration 13 | 14 | /** 15 | * The configuration for the Undertow servlet web server. 16 | */ 17 | @Configuration(proxyBeanMethods = false) 18 | @ConditionalOnClass(Undertow::class) 19 | @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) 20 | class LogbackAccessUndertowServletConfiguration { 21 | 22 | /** 23 | * Provides the [WebServerFactoryCustomizer] for the Undertow servlet web server. 24 | * 25 | * @param logbackAccessContext The Logback-access context. 26 | * @return The [WebServerFactoryCustomizer] for the Undertow servlet web server. 27 | */ 28 | @Bean 29 | @ConditionalOnMissingBean 30 | fun logbackAccessUndertowServletWebServerFactoryCustomizer( 31 | logbackAccessContext: LogbackAccessContext, 32 | ): LogbackAccessUndertowServletWebServerFactoryCustomizer { 33 | val logbackAccessUndertowServletWebServerFactoryCustomizer = 34 | LogbackAccessUndertowServletWebServerFactoryCustomizer(logbackAccessContext) 35 | log.debug( 36 | "Providing the {}: {}", 37 | LogbackAccessUndertowServletWebServerFactoryCustomizer::class.simpleName, 38 | logbackAccessUndertowServletWebServerFactoryCustomizer, 39 | ) 40 | return logbackAccessUndertowServletWebServerFactoryCustomizer 41 | } 42 | 43 | companion object { 44 | 45 | /** 46 | * The logger. 47 | */ 48 | private val log: Logger = getLogger(LogbackAccessUndertowServletConfiguration::class.java) 49 | 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/test/resources/logback-access-test-spring.profile.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | default_console: %h %l %u [%t] "%r" %s %b 7 | 8 | 9 | 10 | 11 | 12 | 13 | default_nested_console: %h %l %u [%t] "%r" %s %b 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | additional_console: %h %l %u [%t] "%r" %s %b 23 | 24 | 25 | 26 | 27 | 28 | 29 | additional_nested_console: %h %l %u [%t] "%r" %s %b 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | ignored_console: %h %l %u [%t] "%r" %s %b 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/akkinoc/spring/boot/logback/access/undertow/LogbackAccessUndertowHttpHandler.kt: -------------------------------------------------------------------------------- 1 | package dev.akkinoc.spring.boot.logback.access.undertow 2 | 3 | import dev.akkinoc.spring.boot.logback.access.LogbackAccessContext 4 | import dev.akkinoc.spring.boot.logback.access.LogbackAccessEvent 5 | import io.undertow.server.ExchangeCompletionListener.NextListener 6 | import io.undertow.server.HttpHandler 7 | import io.undertow.server.HttpServerExchange 8 | import org.slf4j.Logger 9 | import org.slf4j.LoggerFactory.getLogger 10 | 11 | /** 12 | * The Undertow [HttpHandler] to emit Logback-access events. 13 | * 14 | * @property logbackAccessContext The Logback-access context. 15 | * @property next The next [HttpHandler]. 16 | * @see io.undertow.server.handlers.accesslog.AccessLogHandler 17 | */ 18 | class LogbackAccessUndertowHttpHandler( 19 | private val logbackAccessContext: LogbackAccessContext, 20 | private val next: HttpHandler, 21 | ) : HttpHandler { 22 | 23 | override fun handleRequest(exchange: HttpServerExchange) { 24 | log.debug( 25 | "Handling the {}: {} @{}", 26 | HttpServerExchange::class.simpleName, 27 | exchange, 28 | logbackAccessContext, 29 | ) 30 | exchange.addExchangeCompleteListener(::log) 31 | next.handleRequest(exchange) 32 | } 33 | 34 | /** 35 | * Logs the request/response exchange. 36 | * 37 | * @param exchange The request/response exchange. 38 | * @param next The next listener. 39 | */ 40 | private fun log(exchange: HttpServerExchange, next: NextListener) { 41 | log.debug( 42 | "Logging the {}: {} @{}", 43 | HttpServerExchange::class.simpleName, 44 | exchange, 45 | logbackAccessContext, 46 | ) 47 | val source = LogbackAccessUndertowEventSource( 48 | logbackAccessContext = logbackAccessContext, 49 | exchange = exchange, 50 | ) 51 | val event = LogbackAccessEvent(source) 52 | logbackAccessContext.emit(event) 53 | next.proceed() 54 | } 55 | 56 | companion object { 57 | 58 | /** 59 | * The logger. 60 | */ 61 | private val log: Logger = getLogger(LogbackAccessUndertowHttpHandler::class.java) 62 | 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/akkinoc/spring/boot/logback/access/undertow/LogbackAccessUndertowServletWebServerFactoryCustomizer.kt: -------------------------------------------------------------------------------- 1 | package dev.akkinoc.spring.boot.logback.access.undertow 2 | 3 | import dev.akkinoc.spring.boot.logback.access.LogbackAccessContext 4 | import io.undertow.servlet.api.DeploymentInfo 5 | import org.slf4j.Logger 6 | import org.slf4j.LoggerFactory.getLogger 7 | import org.springframework.boot.web.embedded.undertow.UndertowServletWebServerFactory 8 | import org.springframework.boot.web.server.WebServerFactoryCustomizer 9 | 10 | /** 11 | * The [WebServerFactoryCustomizer] for the Undertow servlet web server. 12 | * 13 | * @property logbackAccessContext The Logback-access context. 14 | * @see org.springframework.boot.autoconfigure.web.embedded.UndertowWebServerFactoryCustomizer 15 | * @see org.springframework.boot.web.embedded.undertow.UndertowServletWebServerFactory 16 | * @see org.springframework.boot.web.embedded.undertow.UndertowWebServerFactoryDelegate 17 | * @see org.springframework.boot.web.embedded.undertow.AccessLogHttpHandlerFactory 18 | */ 19 | class LogbackAccessUndertowServletWebServerFactoryCustomizer( 20 | private val logbackAccessContext: LogbackAccessContext, 21 | ) : WebServerFactoryCustomizer { 22 | 23 | override fun customize(factory: UndertowServletWebServerFactory) { 24 | factory.addDeploymentInfoCustomizers(::customize) 25 | log.debug( 26 | "Customized the {}: {} @{}", 27 | UndertowServletWebServerFactory::class.simpleName, 28 | factory, 29 | logbackAccessContext, 30 | ) 31 | } 32 | 33 | /** 34 | * Customizes the [DeploymentInfo]. 35 | * 36 | * @param info The [DeploymentInfo]. 37 | */ 38 | private fun customize(info: DeploymentInfo) { 39 | info.addInitialHandlerChainWrapper { 40 | LogbackAccessUndertowHttpHandler(logbackAccessContext, it) 41 | } 42 | log.debug( 43 | "Customized the {}: {} @{}", 44 | DeploymentInfo::class.simpleName, 45 | info, 46 | logbackAccessContext, 47 | ) 48 | } 49 | 50 | companion object { 51 | 52 | /** 53 | * The logger. 54 | */ 55 | private val log: Logger = getLogger(LogbackAccessUndertowServletWebServerFactoryCustomizer::class.java) 56 | 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/test/kotlin/dev/akkinoc/spring/boot/logback/access/test/configuration/TestContextClassLoaderCustomizer.kt: -------------------------------------------------------------------------------- 1 | package dev.akkinoc.spring.boot.logback.access.test.configuration 2 | 3 | import org.slf4j.Logger 4 | import org.slf4j.LoggerFactory.getLogger 5 | import org.springframework.boot.test.context.FilteredClassLoader 6 | import org.springframework.context.ConfigurableApplicationContext 7 | import org.springframework.test.context.ContextCustomizer 8 | import org.springframework.test.context.MergedContextConfiguration 9 | import java.net.URL 10 | import java.net.URLClassLoader 11 | 12 | /** 13 | * The test context customizer to configure the class loader. 14 | * 15 | * @property hiddenClasses The classes to hide. 16 | * @property additionalClassPaths The class paths to add. 17 | */ 18 | data class TestContextClassLoaderCustomizer( 19 | private val hiddenClasses: Set> = emptySet(), 20 | private val additionalClassPaths: List = emptyList(), 21 | ) : ContextCustomizer { 22 | 23 | override fun customizeContext(context: ConfigurableApplicationContext, config: MergedContextConfiguration) { 24 | hideClasses(context) 25 | addClassPaths(context) 26 | log.debug("Customized the {}: {}", ConfigurableApplicationContext::class.simpleName, context) 27 | } 28 | 29 | /** 30 | * Hides the specified classes from the class loader. 31 | * 32 | * @param context The test context. 33 | */ 34 | private fun hideClasses(context: ConfigurableApplicationContext) { 35 | if (hiddenClasses.isEmpty()) return 36 | context.classLoader = FilteredClassLoader(*hiddenClasses.toTypedArray()) 37 | } 38 | 39 | /** 40 | * Adds the specified class paths to the class loader. 41 | * 42 | * @param context The test context. 43 | */ 44 | private fun addClassPaths(context: ConfigurableApplicationContext) { 45 | if (additionalClassPaths.isEmpty()) return 46 | val urls = additionalClassPaths.toTypedArray() 47 | val parent = context.classLoader 48 | checkNotNull(parent) { "Failed to get the current class loader: $context" } 49 | context.classLoader = URLClassLoader(urls, parent) 50 | } 51 | 52 | companion object { 53 | 54 | /** 55 | * The logger. 56 | */ 57 | private val log: Logger = getLogger(TestContextClassLoaderCustomizer::class.java) 58 | 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/test/kotlin/dev/akkinoc/spring/boot/logback/access/test/mock/MockServletController.kt: -------------------------------------------------------------------------------- 1 | package dev.akkinoc.spring.boot.logback.access.test.mock 2 | 3 | import jakarta.servlet.http.HttpServletRequest 4 | import jakarta.servlet.http.HttpSession 5 | import org.slf4j.Logger 6 | import org.slf4j.LoggerFactory.getLogger 7 | import org.springframework.web.bind.annotation.GetMapping 8 | import org.springframework.web.bind.annotation.RequestMapping 9 | import org.springframework.web.bind.annotation.RestController 10 | import org.springframework.web.servlet.ModelAndView 11 | 12 | /** 13 | * The mock controller for testing using the servlet web server. 14 | */ 15 | @RestController 16 | @RequestMapping("/mock-controller") 17 | class MockServletController { 18 | 19 | /** 20 | * Gets a text with request attributes. 21 | * 22 | * @param request The request to set attributes. 23 | * @return A text. 24 | */ 25 | @GetMapping("/text-with-request-attributes") 26 | fun getTextWithRequestAttributes(request: HttpServletRequest): String { 27 | request.setAttribute("a", "value @a") 28 | request.setAttribute("b", listOf("value1 @b", "value2 @b")) 29 | request.setAttribute("c", "") 30 | request.setAttribute("d", Any()) 31 | val response = "mock-text" 32 | log.debug("Getting a text with request attributes: {}; {}", response, request) 33 | return response 34 | } 35 | 36 | /** 37 | * Gets a text with a session. 38 | * 39 | * @param session The session. 40 | * @return A text. 41 | */ 42 | @GetMapping("/text-with-session") 43 | fun getTextWithSession(session: HttpSession): String { 44 | val response = "mock-text" 45 | log.debug("Getting a text with a session: {}; {}", response, session) 46 | return response 47 | } 48 | 49 | /** 50 | * Gets a text with a forward response. 51 | * 52 | * @return A [ModelAndView] to return a text with a forward response. 53 | */ 54 | @GetMapping("/text-with-forward") 55 | fun getTextWithForward(): ModelAndView { 56 | val response = ModelAndView("forward:text?forwarded=true") 57 | log.debug("Getting a text with a forward response: {}", response) 58 | return response 59 | } 60 | 61 | companion object { 62 | 63 | /** 64 | * The logger. 65 | */ 66 | private val log: Logger = getLogger(MockServletController::class.java) 67 | 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | push: 4 | pull_request: 5 | workflow_dispatch: 6 | concurrency: 7 | group: build-${{ github.ref }} 8 | cancel-in-progress: true 9 | jobs: 10 | build: 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | java: [17, 21] 15 | name: Build with Java ${{ matrix.java }} 16 | runs-on: ubuntu-24.04 17 | env: 18 | MAVEN_ARGS: -B --color always 19 | steps: 20 | - name: Checkout the Git repository 21 | uses: actions/checkout@v6.0.1 22 | - name: Setup Java 23 | uses: actions/setup-java@v5.1.0 24 | with: 25 | distribution: temurin 26 | java-version: ${{ matrix.java }} 27 | cache: maven 28 | - name: Export the project version 29 | run: echo PROJECT_VERSION=$(mvn -Dexpression=project.version -q help:evaluate) | tee -a $GITHUB_ENV 30 | - name: Build the project 31 | run: mvn clean install 32 | - name: Build the "examples/webmvc-tomcat-java" project 33 | run: mvn -Dlogback-access-spring-boot-starter.version=$PROJECT_VERSION clean install 34 | working-directory: examples/webmvc-tomcat-java 35 | - name: Build the "examples/webmvc-tomcat-kotlin" project 36 | run: mvn -Dlogback-access-spring-boot-starter.version=$PROJECT_VERSION clean install 37 | working-directory: examples/webmvc-tomcat-kotlin 38 | - name: Build the "examples/webmvc-jetty-java" project 39 | run: mvn -Dlogback-access-spring-boot-starter.version=$PROJECT_VERSION clean install 40 | working-directory: examples/webmvc-jetty-java 41 | - name: Build the "examples/webmvc-undertow-java" project 42 | run: mvn -Dlogback-access-spring-boot-starter.version=$PROJECT_VERSION clean install 43 | working-directory: examples/webmvc-undertow-java 44 | - name: Build the "examples/webflux-tomcat-java" project 45 | run: mvn -Dlogback-access-spring-boot-starter.version=$PROJECT_VERSION clean install 46 | working-directory: examples/webflux-tomcat-java 47 | - name: Build the "examples/webflux-jetty-java" project 48 | run: mvn -Dlogback-access-spring-boot-starter.version=$PROJECT_VERSION clean install 49 | working-directory: examples/webflux-jetty-java 50 | - name: Build the "examples/webflux-undertow-java" project 51 | run: mvn -Dlogback-access-spring-boot-starter.version=$PROJECT_VERSION clean install 52 | working-directory: examples/webflux-undertow-java 53 | - name: Upload the coverage report to Codecov 54 | uses: codecov/codecov-action@v5.5.2 55 | with: 56 | token: ${{ secrets.CODECOV_TOKEN }} 57 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/akkinoc/spring/boot/logback/access/tee/LogbackAccessTeeServletFilterConfiguration.kt: -------------------------------------------------------------------------------- 1 | package dev.akkinoc.spring.boot.logback.access.tee 2 | 3 | import ch.qos.logback.access.common.AccessConstants.TEE_FILTER_EXCLUDES_PARAM 4 | import ch.qos.logback.access.common.AccessConstants.TEE_FILTER_INCLUDES_PARAM 5 | import ch.qos.logback.access.common.servlet.TeeFilter 6 | import dev.akkinoc.spring.boot.logback.access.LogbackAccessProperties 7 | import org.slf4j.Logger 8 | import org.slf4j.LoggerFactory.getLogger 9 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty 10 | import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication 11 | import org.springframework.boot.autoconfigure.web.servlet.ConditionalOnMissingFilterBean 12 | import org.springframework.boot.web.servlet.FilterRegistrationBean 13 | import org.springframework.context.annotation.Bean 14 | import org.springframework.context.annotation.Configuration 15 | 16 | /** 17 | * The configuration to register the tee filter for the servlet web server. 18 | */ 19 | @Configuration(proxyBeanMethods = false) 20 | @ConditionalOnProperty( 21 | prefix = "logback.access.tee-filter", 22 | name = ["enabled"], 23 | havingValue = "true", 24 | matchIfMissing = false, 25 | ) 26 | @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) 27 | class LogbackAccessTeeServletFilterConfiguration { 28 | 29 | /** 30 | * Provides the tee filter for the servlet web server. 31 | * 32 | * @param logbackAccessProperties The configuration properties for Logback-access. 33 | * @return The tee filter for the servlet web server. 34 | */ 35 | @Bean 36 | @ConditionalOnMissingFilterBean 37 | fun logbackAccessTeeServletFilter( 38 | logbackAccessProperties: LogbackAccessProperties, 39 | ): FilterRegistrationBean { 40 | val props = logbackAccessProperties.teeFilter 41 | val logbackAccessTeeServletFilter = FilterRegistrationBean(TeeFilter()) 42 | props.includes?.also { logbackAccessTeeServletFilter.addInitParameter(TEE_FILTER_INCLUDES_PARAM, it) } 43 | props.excludes?.also { logbackAccessTeeServletFilter.addInitParameter(TEE_FILTER_EXCLUDES_PARAM, it) } 44 | log.debug( 45 | "Providing the {}: {}", 46 | TeeFilter::class.simpleName, 47 | logbackAccessTeeServletFilter, 48 | ) 49 | return logbackAccessTeeServletFilter 50 | } 51 | 52 | companion object { 53 | 54 | /** 55 | * The logger. 56 | */ 57 | private val log: Logger = getLogger(LogbackAccessTeeServletFilterConfiguration::class.java) 58 | 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/test/kotlin/dev/akkinoc/spring/boot/logback/access/undertow/UndertowNoRequestStartTimeTest.kt: -------------------------------------------------------------------------------- 1 | package dev.akkinoc.spring.boot.logback.access.undertow 2 | 3 | import dev.akkinoc.spring.boot.logback.access.test.assertion.Assertions.assertLogbackAccessEventsEventually 4 | import dev.akkinoc.spring.boot.logback.access.test.extension.EventsCapture 5 | import dev.akkinoc.spring.boot.logback.access.test.extension.EventsCaptureExtension 6 | import dev.akkinoc.spring.boot.logback.access.test.type.UndertowReactiveWebTest 7 | import dev.akkinoc.spring.boot.logback.access.test.type.UndertowServletWebTest 8 | import io.kotest.matchers.collections.shouldBeSingleton 9 | import io.kotest.matchers.longs.shouldBeBetween 10 | import io.kotest.matchers.shouldBe 11 | import org.junit.jupiter.api.Test 12 | import org.junit.jupiter.api.extension.ExtendWith 13 | import org.springframework.beans.factory.annotation.Autowired 14 | import org.springframework.boot.test.web.client.TestRestTemplate 15 | import org.springframework.boot.test.web.client.exchange 16 | import org.springframework.http.RequestEntity 17 | import org.springframework.test.context.TestPropertySource 18 | import java.lang.System.currentTimeMillis 19 | 20 | /** 21 | * Tests the case where the Undertow request start time is not recorded. 22 | */ 23 | @ExtendWith(EventsCaptureExtension::class) 24 | @TestPropertySource( 25 | properties = [ 26 | "logback.access.config=classpath:logback-access-test.capture.xml", 27 | "logback.access.undertow.record-request-start-time=false", 28 | ], 29 | ) 30 | sealed class UndertowNoRequestStartTimeTest { 31 | 32 | @Test 33 | fun `Does not return the elapsed time of the appended Logback-access event`( 34 | @Autowired rest: TestRestTemplate, 35 | capture: EventsCapture, 36 | ) { 37 | val request = RequestEntity.get("/mock-controller/text").build() 38 | val started = currentTimeMillis() 39 | val response = rest.exchange(request) 40 | response.statusCode.value().shouldBe(200) 41 | val event = assertLogbackAccessEventsEventually { capture.shouldBeSingleton().single() } 42 | val finished = currentTimeMillis() 43 | event.timeStamp.shouldBeBetween(started, finished) 44 | event.elapsedTime.shouldBe(-1L) 45 | event.elapsedSeconds.shouldBe(-1L) 46 | } 47 | 48 | } 49 | 50 | /** 51 | * Tests the [UndertowNoRequestStartTimeTest] using the Undertow servlet web server. 52 | */ 53 | @UndertowServletWebTest 54 | class UndertowServletWebNoRequestStartTimeTest : UndertowNoRequestStartTimeTest() 55 | 56 | /** 57 | * Tests the [UndertowNoRequestStartTimeTest] using the Undertow reactive web server. 58 | */ 59 | @UndertowReactiveWebTest 60 | class UndertowReactiveWebNoRequestStartTimeTest : UndertowNoRequestStartTimeTest() 61 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/akkinoc/spring/boot/logback/access/undertow/LogbackAccessUndertowReactiveConfiguration.kt: -------------------------------------------------------------------------------- 1 | package dev.akkinoc.spring.boot.logback.access.undertow 2 | 3 | import dev.akkinoc.spring.boot.logback.access.LogbackAccessContext 4 | import io.undertow.Undertow 5 | import org.slf4j.Logger 6 | import org.slf4j.LoggerFactory.getLogger 7 | import org.springframework.boot.autoconfigure.condition.ConditionalOnClass 8 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean 9 | import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication 10 | import org.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerFactoryAutoConfiguration 11 | import org.springframework.boot.web.embedded.undertow.UndertowBuilderCustomizer 12 | import org.springframework.boot.web.reactive.server.ReactiveWebServerFactory 13 | import org.springframework.context.annotation.Bean 14 | import org.springframework.context.annotation.Configuration 15 | 16 | /** 17 | * The configuration for the Undertow reactive web server. 18 | */ 19 | @Configuration(proxyBeanMethods = false) 20 | @ConditionalOnClass(Undertow::class) 21 | @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) 22 | class LogbackAccessUndertowReactiveConfiguration { 23 | 24 | /** 25 | * Provides the [ReactiveWebServerFactory] for the Undertow reactive web server. 26 | * Overrides the [ReactiveWebServerFactory] provided by [ReactiveWebServerFactoryAutoConfiguration]. 27 | * 28 | * @param logbackAccessContext The Logback-access context. 29 | * @param undertowBuilderCustomizers The Undertow builder customizers. 30 | * @return The [ReactiveWebServerFactory] for the Undertow reactive web server. 31 | */ 32 | @Bean 33 | @ConditionalOnMissingBean(ReactiveWebServerFactory::class) 34 | fun logbackAccessUndertowReactiveWebServerFactory( 35 | logbackAccessContext: LogbackAccessContext, 36 | undertowBuilderCustomizers: List, 37 | ): LogbackAccessUndertowReactiveWebServerFactory { 38 | val logbackAccessUndertowReactiveWebServerFactory = 39 | LogbackAccessUndertowReactiveWebServerFactory(logbackAccessContext) 40 | logbackAccessUndertowReactiveWebServerFactory.builderCustomizers.addAll(undertowBuilderCustomizers) 41 | log.debug( 42 | "Providing the {}: {}", 43 | LogbackAccessUndertowReactiveWebServerFactory::class.simpleName, 44 | logbackAccessUndertowReactiveWebServerFactory, 45 | ) 46 | return logbackAccessUndertowReactiveWebServerFactory 47 | } 48 | 49 | companion object { 50 | 51 | /** 52 | * The logger. 53 | */ 54 | private val log: Logger = getLogger(LogbackAccessUndertowReactiveConfiguration::class.java) 55 | 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/test/kotlin/dev/akkinoc/spring/boot/logback/access/InactiveTest.kt: -------------------------------------------------------------------------------- 1 | package dev.akkinoc.spring.boot.logback.access 2 | 3 | import dev.akkinoc.spring.boot.logback.access.test.type.JettyReactiveWebTest 4 | import dev.akkinoc.spring.boot.logback.access.test.type.JettyServletWebTest 5 | import dev.akkinoc.spring.boot.logback.access.test.type.NonWebTest 6 | import dev.akkinoc.spring.boot.logback.access.test.type.TomcatReactiveWebTest 7 | import dev.akkinoc.spring.boot.logback.access.test.type.TomcatServletWebTest 8 | import dev.akkinoc.spring.boot.logback.access.test.type.UndertowReactiveWebTest 9 | import dev.akkinoc.spring.boot.logback.access.test.type.UndertowServletWebTest 10 | import io.kotest.matchers.nulls.shouldBeNull 11 | import org.junit.jupiter.api.Test 12 | import org.springframework.beans.factory.annotation.Autowired 13 | import org.springframework.test.context.TestPropertySource 14 | 15 | /** 16 | * Tests the case where auto-configuration is inactive. 17 | */ 18 | sealed class InactiveTest { 19 | 20 | @Test 21 | fun `Does not provide the configuration properties for Logback-access`( 22 | @Autowired logbackAccessProperties: LogbackAccessProperties?, 23 | ) { 24 | logbackAccessProperties.shouldBeNull() 25 | } 26 | 27 | @Test 28 | fun `Does not provide the Logback-access context`( 29 | @Autowired logbackAccessContext: LogbackAccessContext?, 30 | ) { 31 | logbackAccessContext.shouldBeNull() 32 | } 33 | 34 | } 35 | 36 | /** 37 | * Tests the case where the web server is not used. 38 | */ 39 | @NonWebTest 40 | class NonWebInactiveTest : InactiveTest() 41 | 42 | /** 43 | * Tests the case where auto-configuration is disabled. 44 | */ 45 | @TestPropertySource(properties = ["logback.access.enabled=false"]) 46 | sealed class DisabledTest : InactiveTest() 47 | 48 | /** 49 | * Tests the [DisabledTest] using the Tomcat servlet web server. 50 | */ 51 | @TomcatServletWebTest 52 | class TomcatServletWebDisabledTest : DisabledTest() 53 | 54 | /** 55 | * Tests the [DisabledTest] using the Tomcat reactive web server. 56 | */ 57 | @TomcatReactiveWebTest 58 | class TomcatReactiveWebDisabledTest : DisabledTest() 59 | 60 | /** 61 | * Tests the [DisabledTest] using the Jetty servlet web server. 62 | */ 63 | @JettyServletWebTest 64 | class JettyServletWebDisabledTest : DisabledTest() 65 | 66 | /** 67 | * Tests the [DisabledTest] using the Jetty reactive web server. 68 | */ 69 | @JettyReactiveWebTest 70 | class JettyReactiveWebDisabledTest : DisabledTest() 71 | 72 | /** 73 | * Tests the [DisabledTest] using the Undertow servlet web server. 74 | */ 75 | @UndertowServletWebTest 76 | class UndertowServletWebDisabledTest : DisabledTest() 77 | 78 | /** 79 | * Tests the [DisabledTest] using the Undertow reactive web server. 80 | */ 81 | @UndertowReactiveWebTest 82 | class UndertowReactiveWebDisabledTest : DisabledTest() 83 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/akkinoc/spring/boot/logback/access/tomcat/LogbackAccessTomcatValve.kt: -------------------------------------------------------------------------------- 1 | package dev.akkinoc.spring.boot.logback.access.tomcat 2 | 3 | import dev.akkinoc.spring.boot.logback.access.LogbackAccessContext 4 | import dev.akkinoc.spring.boot.logback.access.LogbackAccessEvent 5 | import org.apache.catalina.AccessLog 6 | import org.apache.catalina.Valve 7 | import org.apache.catalina.connector.Request 8 | import org.apache.catalina.connector.Response 9 | import org.apache.catalina.valves.RemoteIpValve 10 | import org.apache.catalina.valves.ValveBase 11 | import org.slf4j.Logger 12 | import org.slf4j.LoggerFactory.getLogger 13 | 14 | /** 15 | * The Tomcat [Valve] to emit Logback-access events. 16 | * 17 | * @property logbackAccessContext The Logback-access context. 18 | * @see org.apache.catalina.valves.AccessLogValve 19 | * @see ch.qos.logback.access.tomcat.LogbackValve 20 | */ 21 | class LogbackAccessTomcatValve( 22 | private val logbackAccessContext: LogbackAccessContext, 23 | ) : ValveBase(true), AccessLog { 24 | 25 | /** 26 | * The value of [getRequestAttributesEnabled] and [setRequestAttributesEnabled]. 27 | */ 28 | private var requestAttributesEnabledValue: Boolean = false 29 | 30 | override fun getRequestAttributesEnabled(): Boolean { 31 | return requestAttributesEnabledValue 32 | } 33 | 34 | override fun setRequestAttributesEnabled(value: Boolean) { 35 | requestAttributesEnabledValue = value 36 | } 37 | 38 | override fun initInternal() { 39 | super.initInternal() 40 | val props = logbackAccessContext.properties.tomcat 41 | requestAttributesEnabled = props.requestAttributesEnabled 42 | ?: container.pipeline.valves.any { it is RemoteIpValve } 43 | log.debug("Initialized the {}: {}", LogbackAccessTomcatValve::class.simpleName, this) 44 | } 45 | 46 | override fun invoke(request: Request, response: Response) { 47 | log.debug( 48 | "Handling the {}/{}: {} => {} @{}", 49 | Request::class.simpleName, 50 | Response::class.simpleName, 51 | request, 52 | response, 53 | logbackAccessContext, 54 | ) 55 | getNext().invoke(request, response) 56 | } 57 | 58 | override fun log(request: Request, response: Response, time: Long) { 59 | log.debug( 60 | "Logging the {}/{}: {} => {} ({}ms) @{}", 61 | Request::class.simpleName, 62 | Response::class.simpleName, 63 | request, 64 | response, 65 | time, 66 | logbackAccessContext, 67 | ) 68 | val source = LogbackAccessTomcatEventSource( 69 | logbackAccessContext = logbackAccessContext, 70 | requestAttributesEnabled = requestAttributesEnabled, 71 | request = request, 72 | response = response, 73 | ) 74 | val event = LogbackAccessEvent(source) 75 | logbackAccessContext.emit(event) 76 | } 77 | 78 | companion object { 79 | 80 | /** 81 | * The logger. 82 | */ 83 | private val log: Logger = getLogger(LogbackAccessTomcatValve::class.java) 84 | 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /src/test/kotlin/dev/akkinoc/spring/boot/logback/access/test/type/WebTest.kt: -------------------------------------------------------------------------------- 1 | package dev.akkinoc.spring.boot.logback.access.test.type 2 | 3 | import dev.akkinoc.spring.boot.logback.access.test.configuration.WebTestConfiguration 4 | import org.springframework.boot.test.context.SpringBootTest 5 | import org.springframework.context.annotation.Import 6 | import org.springframework.test.context.TestPropertySource 7 | 8 | /** 9 | * Indicates a Spring Boot based test using the Tomcat servlet web server. 10 | */ 11 | @Target(AnnotationTarget.CLASS) 12 | @Retention(AnnotationRetention.RUNTIME) 13 | @MustBeDocumented 14 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 15 | @TestPropertySource(properties = ["spring.main.web-application-type=servlet"]) 16 | @Import(WebTestConfiguration::class) 17 | annotation class TomcatServletWebTest 18 | 19 | /** 20 | * Indicates a Spring Boot based test using the Tomcat reactive web server. 21 | */ 22 | @Target(AnnotationTarget.CLASS) 23 | @Retention(AnnotationRetention.RUNTIME) 24 | @MustBeDocumented 25 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 26 | @TestPropertySource(properties = ["spring.main.web-application-type=reactive"]) 27 | @Import(WebTestConfiguration::class) 28 | annotation class TomcatReactiveWebTest 29 | 30 | /** 31 | * Indicates a Spring Boot based test using the Jetty servlet web server. 32 | */ 33 | @Target(AnnotationTarget.CLASS) 34 | @Retention(AnnotationRetention.RUNTIME) 35 | @MustBeDocumented 36 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 37 | @TestPropertySource(properties = ["spring.main.web-application-type=servlet"]) 38 | @Import(WebTestConfiguration::class) 39 | annotation class JettyServletWebTest 40 | 41 | /** 42 | * Indicates a Spring Boot based test using the Jetty reactive web server. 43 | */ 44 | @Target(AnnotationTarget.CLASS) 45 | @Retention(AnnotationRetention.RUNTIME) 46 | @MustBeDocumented 47 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 48 | @TestPropertySource(properties = ["spring.main.web-application-type=reactive"]) 49 | @Import(WebTestConfiguration::class) 50 | annotation class JettyReactiveWebTest 51 | 52 | /** 53 | * Indicates a Spring Boot based test using the Undertow servlet web server. 54 | */ 55 | @Target(AnnotationTarget.CLASS) 56 | @Retention(AnnotationRetention.RUNTIME) 57 | @MustBeDocumented 58 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 59 | @TestPropertySource(properties = ["spring.main.web-application-type=servlet"]) 60 | @Import(WebTestConfiguration::class) 61 | annotation class UndertowServletWebTest 62 | 63 | /** 64 | * Indicates a Spring Boot based test using the Undertow reactive web server. 65 | */ 66 | @Target(AnnotationTarget.CLASS) 67 | @Retention(AnnotationRetention.RUNTIME) 68 | @MustBeDocumented 69 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 70 | @TestPropertySource(properties = ["spring.main.web-application-type=reactive"]) 71 | @Import(WebTestConfiguration::class) 72 | annotation class UndertowReactiveWebTest 73 | 74 | /** 75 | * Indicates a Spring Boot based test using the Netty reactive web server. 76 | */ 77 | @Target(AnnotationTarget.CLASS) 78 | @Retention(AnnotationRetention.RUNTIME) 79 | @MustBeDocumented 80 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 81 | @TestPropertySource(properties = ["spring.main.web-application-type=reactive"]) 82 | @Import(WebTestConfiguration::class) 83 | annotation class NettyReactiveWebTest 84 | -------------------------------------------------------------------------------- /src/test/kotlin/dev/akkinoc/spring/boot/logback/access/test/extension/EventsCaptureExtension.kt: -------------------------------------------------------------------------------- 1 | package dev.akkinoc.spring.boot.logback.access.test.extension 2 | 3 | import dev.akkinoc.spring.boot.logback.access.test.extension.EventsCaptureAppender.Companion.captures 4 | import org.junit.jupiter.api.extension.AfterAllCallback 5 | import org.junit.jupiter.api.extension.AfterEachCallback 6 | import org.junit.jupiter.api.extension.BeforeAllCallback 7 | import org.junit.jupiter.api.extension.BeforeEachCallback 8 | import org.junit.jupiter.api.extension.ExtensionContext 9 | import org.junit.jupiter.api.extension.ExtensionContext.Namespace 10 | import org.junit.jupiter.api.extension.ParameterContext 11 | import org.junit.jupiter.api.extension.ParameterResolver 12 | import org.slf4j.Logger 13 | import org.slf4j.LoggerFactory.getLogger 14 | 15 | /** 16 | * The test extension to capture Logback-access events that will be appended. 17 | * To use it, include [EventsCaptureAppender] in the Logback-access configuration. 18 | * The captured Logback-access events can be obtained from a [EventsCapture] test parameter. 19 | */ 20 | class EventsCaptureExtension : 21 | BeforeAllCallback, AfterAllCallback, BeforeEachCallback, AfterEachCallback, ParameterResolver { 22 | 23 | override fun beforeAll(extensionContext: ExtensionContext) { 24 | val id = extensionContext.uniqueId 25 | captures[id] = extensionContext.getAccessEventsCapture() 26 | log.debug("Started the {}: {} @{}", EventsCapture::class.simpleName, id, extensionContext) 27 | } 28 | 29 | override fun afterAll(extensionContext: ExtensionContext) { 30 | val id = extensionContext.uniqueId 31 | log.debug("Finishing the {}: {} @{}", EventsCapture::class.simpleName, id, extensionContext) 32 | captures -= id 33 | } 34 | 35 | override fun beforeEach(extensionContext: ExtensionContext) { 36 | val id = extensionContext.uniqueId 37 | captures[id] = extensionContext.getAccessEventsCapture() 38 | log.debug("Started the {}: {} @{}", EventsCapture::class.simpleName, id, extensionContext) 39 | } 40 | 41 | override fun afterEach(extensionContext: ExtensionContext) { 42 | val id = extensionContext.uniqueId 43 | log.debug("Finishing the {}: {} @{}", EventsCapture::class.simpleName, id, extensionContext) 44 | captures -= id 45 | } 46 | 47 | override fun supportsParameter(parameterContext: ParameterContext, extensionContext: ExtensionContext): Boolean { 48 | return parameterContext.parameter.type == EventsCapture::class.java 49 | } 50 | 51 | override fun resolveParameter(parameterContext: ParameterContext, extensionContext: ExtensionContext): Any { 52 | return extensionContext.getAccessEventsCapture() 53 | } 54 | 55 | /** 56 | * Retrieves the Logback-access events capture that is present in the extension context. 57 | * If it is not present, creates, saves, and returns it. 58 | * 59 | * @receiver The extension context. 60 | * @return The Logback-access events capture. 61 | */ 62 | private fun ExtensionContext.getAccessEventsCapture(): EventsCapture { 63 | val namespace = Namespace.create(this::class) 64 | val store = getStore(namespace) 65 | return store.getOrComputeIfAbsent(EventsCapture::class.java) 66 | } 67 | 68 | companion object { 69 | 70 | /** 71 | * The logger. 72 | */ 73 | private val log: Logger = getLogger(EventsCaptureExtension::class.java) 74 | 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/akkinoc/spring/boot/logback/access/LogbackAccessContext.kt: -------------------------------------------------------------------------------- 1 | package dev.akkinoc.spring.boot.logback.access 2 | 3 | import ch.qos.logback.access.common.spi.AccessContext 4 | import ch.qos.logback.core.spi.FilterReply 5 | import ch.qos.logback.core.status.Status 6 | import dev.akkinoc.spring.boot.logback.access.LogbackAccessProperties.Companion.DEFAULT_CONFIGS 7 | import dev.akkinoc.spring.boot.logback.access.LogbackAccessProperties.Companion.FALLBACK_CONFIG 8 | import dev.akkinoc.spring.boot.logback.access.joran.LogbackAccessJoranConfigurator 9 | import org.slf4j.Logger 10 | import org.slf4j.LoggerFactory.getLogger 11 | import org.springframework.core.env.Environment 12 | import org.springframework.core.io.ResourceLoader 13 | import org.springframework.util.ResourceUtils.getURL 14 | 15 | /** 16 | * The Logback-access context. 17 | * 18 | * @property properties The configuration properties for Logback-access. 19 | * @param resourceLoader The resource loader. 20 | * @param environment The environment. 21 | */ 22 | class LogbackAccessContext( 23 | val properties: LogbackAccessProperties, 24 | resourceLoader: ResourceLoader, 25 | environment: Environment, 26 | ) : AutoCloseable { 27 | 28 | /** 29 | * The raw Logback-access context. 30 | */ 31 | val raw: AccessContext = AccessContext() 32 | 33 | init { 34 | val (name, resource) = run { 35 | properties.config 36 | ?.also { return@run it to resourceLoader.getResource("${getURL(it)}") } 37 | DEFAULT_CONFIGS.asSequence() 38 | .map { it to resourceLoader.getResource(it) } 39 | .firstOrNull { (_, resource) -> resource.exists() } 40 | ?.also { return@run it } 41 | return@run FALLBACK_CONFIG.let { it to resourceLoader.getResource(it) } 42 | } 43 | raw.name = name 44 | raw.statusManager.add(::log) 45 | val configurator = LogbackAccessJoranConfigurator(environment) 46 | configurator.context = raw 47 | configurator.doConfigure(resource.url) 48 | raw.start() 49 | log.debug("Initialized the {}: {}", LogbackAccessContext::class.simpleName, this) 50 | } 51 | 52 | /** 53 | * Logs the Logback-access status. 54 | * 55 | * @param status The Logback-access status. 56 | */ 57 | private fun log(status: Status) { 58 | log.debug("Added the {}: {} @{}", Status::class.simpleName, status, this, status.throwable) 59 | } 60 | 61 | /** 62 | * Emits the Logback-access event. 63 | * 64 | * @param event The Logback-access event. 65 | */ 66 | fun emit(event: LogbackAccessEvent) { 67 | val filterReply = raw.getFilterChainDecision(event) 68 | log.debug("Emitting the {}: {} {} @{}", LogbackAccessEvent::class.simpleName, filterReply, event, this) 69 | if (filterReply != FilterReply.DENY) raw.callAppenders(event) 70 | } 71 | 72 | override fun close() { 73 | log.debug("Closing the {}: {}", LogbackAccessContext::class.simpleName, this) 74 | raw.stop() 75 | raw.reset() 76 | raw.detachAndStopAllAppenders() 77 | raw.copyOfAttachedFiltersList.forEach { it.stop() } 78 | raw.clearAllFilters() 79 | } 80 | 81 | override fun toString(): String = "${this::class.simpleName}(${raw.name})" 82 | 83 | companion object { 84 | 85 | /** 86 | * The logger. 87 | */ 88 | private val log: Logger = getLogger(LogbackAccessContext::class.java) 89 | 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/akkinoc/spring/boot/logback/access/undertow/LogbackAccessUndertowReactiveWebServerFactory.kt: -------------------------------------------------------------------------------- 1 | package dev.akkinoc.spring.boot.logback.access.undertow 2 | 3 | import dev.akkinoc.spring.boot.logback.access.LogbackAccessContext 4 | import io.undertow.Undertow.Builder 5 | import org.slf4j.Logger 6 | import org.slf4j.LoggerFactory.getLogger 7 | import org.springframework.boot.web.embedded.undertow.HttpHandlerFactory 8 | import org.springframework.boot.web.embedded.undertow.UndertowReactiveWebServerFactory 9 | import org.springframework.boot.web.embedded.undertow.UndertowWebServer 10 | import org.springframework.boot.web.reactive.server.ReactiveWebServerFactory 11 | import org.springframework.boot.web.server.WebServer 12 | import org.springframework.http.server.reactive.HttpHandler 13 | import org.springframework.util.ReflectionUtils.findField 14 | import org.springframework.util.ReflectionUtils.getField 15 | import org.springframework.util.ReflectionUtils.makeAccessible 16 | 17 | /** 18 | * The [ReactiveWebServerFactory] for the Undertow reactive web server. 19 | * 20 | * @property logbackAccessContext The Logback-access context. 21 | * @see org.springframework.boot.autoconfigure.web.embedded.UndertowWebServerFactoryCustomizer 22 | * @see org.springframework.boot.web.embedded.undertow.UndertowReactiveWebServerFactory 23 | * @see org.springframework.boot.web.embedded.undertow.UndertowWebServerFactoryDelegate 24 | * @see org.springframework.boot.web.embedded.undertow.AccessLogHttpHandlerFactory 25 | */ 26 | class LogbackAccessUndertowReactiveWebServerFactory( 27 | private val logbackAccessContext: LogbackAccessContext, 28 | ) : UndertowReactiveWebServerFactory() { 29 | 30 | // FIXME: I'd like to fix this class if there is a better way. 31 | // I couldn't find an extension point for Undertow HttpHandler, so I'm using reflection to customize it. 32 | 33 | override fun getWebServer(httpHandler: HttpHandler): WebServer { 34 | val originalServer = super.getWebServer(httpHandler) as UndertowWebServer 35 | val builder = originalServer.extractField("builder") 36 | val httpHandlerFactories = buildList { 37 | addAll(originalServer.extractField>("httpHandlerFactories")) 38 | add { LogbackAccessUndertowHttpHandler(logbackAccessContext, it) } 39 | } 40 | val autoStart = originalServer.extractField("autoStart") 41 | val server = UndertowWebServer(builder, httpHandlerFactories, autoStart) 42 | log.debug( 43 | "Customized the {}: {} @{}", 44 | WebServer::class.simpleName, 45 | server, 46 | logbackAccessContext, 47 | ) 48 | return server 49 | } 50 | 51 | /** 52 | * Extracts the field value from the [UndertowWebServer]. 53 | * 54 | * @receiver The [UndertowWebServer]. 55 | * @param T The field type. 56 | * @param name The field name. 57 | * @return The field value. 58 | */ 59 | private inline fun UndertowWebServer.extractField(name: String): T { 60 | val field = findField(UndertowWebServer::class.java, name) 61 | checkNotNull(field) { "Failed to extract the field: UndertowWebServer.$name" } 62 | makeAccessible(field) 63 | return getField(field, this) as T 64 | } 65 | 66 | companion object { 67 | 68 | /** 69 | * The logger. 70 | */ 71 | private val log: Logger = getLogger(LogbackAccessUndertowReactiveWebServerFactory::class.java) 72 | 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /examples/webmvc-tomcat-kotlin/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 4.0.0 5 | 6 | dev.akkinoc.spring.boot 7 | logback-access-spring-boot-starter.examples.webmvc-tomcat-kotlin 8 | 1.0.0-SNAPSHOT 9 | 10 | 11 | org.springframework.boot 12 | spring-boot-starter-parent 13 | 3.5.7 14 | 15 | 16 | 17 | [4.0.0,) 18 | 19 | 20 | 21 | 22 | org.jetbrains.kotlin 23 | kotlin-stdlib-jdk8 24 | 25 | 26 | org.jetbrains.kotlin 27 | kotlin-reflect 28 | 29 | 30 | org.springframework.boot 31 | spring-boot-starter-web 32 | 33 | 34 | dev.akkinoc.spring.boot 35 | logback-access-spring-boot-starter 36 | ${logback-access-spring-boot-starter.version} 37 | 38 | 39 | ch.qos.logback.access 40 | logback-access-tomcat 41 | 2.0.7 42 | 43 | 44 | 45 | 46 | src/main/kotlin 47 | src/test/kotlin 48 | 49 | 50 | src/main/resources 51 | true 52 | 53 | 54 | 55 | 56 | org.jetbrains.kotlin 57 | kotlin-maven-plugin 58 | 59 | 60 | org.jetbrains.kotlin 61 | kotlin-maven-allopen 62 | ${kotlin.version} 63 | 64 | 65 | 66 | 67 | spring 68 | 69 | 70 | 71 | 72 | maven-compiler-plugin 73 | 74 | 75 | default-compile 76 | none 77 | 78 | 79 | default-testCompile 80 | none 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /src/test/kotlin/dev/akkinoc/spring/boot/logback/access/SequenceNumberTest.kt: -------------------------------------------------------------------------------- 1 | package dev.akkinoc.spring.boot.logback.access 2 | 3 | import dev.akkinoc.spring.boot.logback.access.test.assertion.Assertions.assertLogbackAccessEventsEventually 4 | import dev.akkinoc.spring.boot.logback.access.test.extension.EventsCapture 5 | import dev.akkinoc.spring.boot.logback.access.test.extension.EventsCaptureExtension 6 | import dev.akkinoc.spring.boot.logback.access.test.type.JettyReactiveWebTest 7 | import dev.akkinoc.spring.boot.logback.access.test.type.JettyServletWebTest 8 | import dev.akkinoc.spring.boot.logback.access.test.type.TomcatReactiveWebTest 9 | import dev.akkinoc.spring.boot.logback.access.test.type.TomcatServletWebTest 10 | import dev.akkinoc.spring.boot.logback.access.test.type.UndertowReactiveWebTest 11 | import dev.akkinoc.spring.boot.logback.access.test.type.UndertowServletWebTest 12 | import io.kotest.matchers.collections.shouldBeSorted 13 | import io.kotest.matchers.collections.shouldBeUnique 14 | import io.kotest.matchers.collections.shouldHaveSize 15 | import io.kotest.matchers.longs.shouldBeGreaterThan 16 | import io.kotest.matchers.shouldBe 17 | import org.junit.jupiter.api.Test 18 | import org.junit.jupiter.api.extension.ExtendWith 19 | import org.springframework.beans.factory.annotation.Autowired 20 | import org.springframework.boot.test.web.client.TestRestTemplate 21 | import org.springframework.boot.test.web.client.exchange 22 | import org.springframework.http.RequestEntity 23 | import org.springframework.test.context.TestPropertySource 24 | 25 | /** 26 | * Tests the case where sequence numbers are used in the configuration file. 27 | */ 28 | @ExtendWith(EventsCaptureExtension::class) 29 | @TestPropertySource(properties = ["logback.access.config=classpath:logback-access-test.sequence-number.capture.xml"]) 30 | sealed class SequenceNumberTest { 31 | 32 | @Test 33 | fun `Generates unique sequence numbers`( 34 | @Autowired rest: TestRestTemplate, 35 | capture: EventsCapture, 36 | ) { 37 | for (n in 1..3) { 38 | val request = RequestEntity.get("/mock-controller/text").build() 39 | val response = rest.exchange(request) 40 | response.statusCode.value().shouldBe(200) 41 | val event = assertLogbackAccessEventsEventually { capture.shouldHaveSize(n).last() } 42 | event.sequenceNumber.shouldBeGreaterThan(0L) 43 | } 44 | capture.map { it.sequenceNumber }.shouldBeUnique().shouldBeSorted() 45 | } 46 | 47 | } 48 | 49 | /** 50 | * Tests the [SequenceNumberTest] using the Tomcat servlet web server. 51 | */ 52 | @TomcatServletWebTest 53 | class TomcatServletWebSequenceNumberTest : SequenceNumberTest() 54 | 55 | /** 56 | * Tests the [SequenceNumberTest] using the Tomcat reactive web server. 57 | */ 58 | @TomcatReactiveWebTest 59 | class TomcatReactiveWebSequenceNumberTest : SequenceNumberTest() 60 | 61 | /** 62 | * Tests the [SequenceNumberTest] using the Jetty servlet web server. 63 | */ 64 | @JettyServletWebTest 65 | class JettyServletWebSequenceNumberTest : SequenceNumberTest() 66 | 67 | /** 68 | * Tests the [SequenceNumberTest] using the Jetty reactive web server. 69 | */ 70 | @JettyReactiveWebTest 71 | class JettyReactiveWebSequenceNumberTest : SequenceNumberTest() 72 | 73 | /** 74 | * Tests the [SequenceNumberTest] using the Undertow servlet web server. 75 | */ 76 | @UndertowServletWebTest 77 | class UndertowServletWebSequenceNumberTest : SequenceNumberTest() 78 | 79 | /** 80 | * Tests the [SequenceNumberTest] using the Undertow reactive web server. 81 | */ 82 | @UndertowReactiveWebTest 83 | class UndertowReactiveWebSequenceNumberTest : SequenceNumberTest() 84 | -------------------------------------------------------------------------------- /src/test/kotlin/dev/akkinoc/spring/boot/logback/access/tomcat/TomcatRequestAttributesEnabledTest.kt: -------------------------------------------------------------------------------- 1 | package dev.akkinoc.spring.boot.logback.access.tomcat 2 | 3 | import dev.akkinoc.spring.boot.logback.access.test.assertion.Assertions.assertLogbackAccessEventsEventually 4 | import dev.akkinoc.spring.boot.logback.access.test.extension.EventsCapture 5 | import dev.akkinoc.spring.boot.logback.access.test.extension.EventsCaptureExtension 6 | import dev.akkinoc.spring.boot.logback.access.test.type.TomcatReactiveWebTest 7 | import dev.akkinoc.spring.boot.logback.access.test.type.TomcatServletWebTest 8 | import io.kotest.matchers.collections.shouldBeSingleton 9 | import io.kotest.matchers.shouldBe 10 | import org.junit.jupiter.api.Test 11 | import org.junit.jupiter.api.extension.ExtendWith 12 | import org.springframework.beans.factory.annotation.Autowired 13 | import org.springframework.boot.test.web.client.TestRestTemplate 14 | import org.springframework.boot.test.web.client.exchange 15 | import org.springframework.boot.test.web.server.LocalServerPort 16 | import org.springframework.http.RequestEntity 17 | import org.springframework.test.context.TestPropertySource 18 | 19 | /** 20 | * Tests the case where the Tomcat request attributes are enabled and forward headers are not supported. 21 | */ 22 | @ExtendWith(EventsCaptureExtension::class) 23 | @TestPropertySource( 24 | properties = [ 25 | "server.forward-headers-strategy=none", 26 | "logback.access.config=classpath:logback-access-test.capture.xml", 27 | "logback.access.tomcat.request-attributes-enabled=true", 28 | ], 29 | ) 30 | sealed class TomcatRequestAttributesEnabledTest { 31 | 32 | @Test 33 | fun `Does not rewrite some attributes of the appended Logback-access event with forward headers`( 34 | @Autowired rest: TestRestTemplate, 35 | @LocalServerPort port: Int, 36 | capture: EventsCapture, 37 | ) { 38 | val request = RequestEntity.get("/mock-controller/text").build() 39 | val response = rest.exchange(request) 40 | response.statusCode.value().shouldBe(200) 41 | val event = assertLogbackAccessEventsEventually { capture.shouldBeSingleton().single() } 42 | event.serverName.shouldBe("localhost") 43 | event.localPort.shouldBe(port) 44 | event.remoteAddr.shouldBe("127.0.0.1") 45 | event.remoteHost.shouldBe("127.0.0.1") 46 | event.protocol.shouldBe("HTTP/1.1") 47 | } 48 | 49 | @Test 50 | fun `Does not rewrite some attributes of the appended Logback-access event without forward headers`( 51 | @Autowired rest: TestRestTemplate, 52 | @LocalServerPort port: Int, 53 | capture: EventsCapture, 54 | ) { 55 | val request = RequestEntity.get("/mock-controller/text").build() 56 | val response = rest.exchange(request) 57 | response.statusCode.value().shouldBe(200) 58 | val event = assertLogbackAccessEventsEventually { capture.shouldBeSingleton().single() } 59 | event.serverName.shouldBe("localhost") 60 | event.localPort.shouldBe(port) 61 | event.remoteAddr.shouldBe("127.0.0.1") 62 | event.remoteHost.shouldBe("127.0.0.1") 63 | event.protocol.shouldBe("HTTP/1.1") 64 | } 65 | 66 | } 67 | 68 | /** 69 | * Tests the [TomcatRequestAttributesEnabledTest] using the Tomcat servlet web server. 70 | */ 71 | @TomcatServletWebTest 72 | class TomcatServletWebRequestAttributesEnabledTest : TomcatRequestAttributesEnabledTest() 73 | 74 | /** 75 | * Tests the [TomcatRequestAttributesEnabledTest] using the Tomcat reactive web server. 76 | */ 77 | @TomcatReactiveWebTest 78 | class TomcatReactiveWebRequestAttributesEnabledTest : TomcatRequestAttributesEnabledTest() 79 | -------------------------------------------------------------------------------- /src/test/kotlin/dev/akkinoc/spring/boot/logback/access/tomcat/TomcatRequestAttributesDisabledTest.kt: -------------------------------------------------------------------------------- 1 | package dev.akkinoc.spring.boot.logback.access.tomcat 2 | 3 | import dev.akkinoc.spring.boot.logback.access.test.assertion.Assertions.assertLogbackAccessEventsEventually 4 | import dev.akkinoc.spring.boot.logback.access.test.extension.EventsCapture 5 | import dev.akkinoc.spring.boot.logback.access.test.extension.EventsCaptureExtension 6 | import dev.akkinoc.spring.boot.logback.access.test.type.TomcatReactiveWebTest 7 | import dev.akkinoc.spring.boot.logback.access.test.type.TomcatServletWebTest 8 | import io.kotest.matchers.collections.shouldBeSingleton 9 | import io.kotest.matchers.shouldBe 10 | import org.junit.jupiter.api.Test 11 | import org.junit.jupiter.api.extension.ExtendWith 12 | import org.springframework.beans.factory.annotation.Autowired 13 | import org.springframework.boot.test.web.client.TestRestTemplate 14 | import org.springframework.boot.test.web.client.exchange 15 | import org.springframework.boot.test.web.server.LocalServerPort 16 | import org.springframework.http.RequestEntity 17 | import org.springframework.test.context.TestPropertySource 18 | 19 | /** 20 | * Tests the case where the Tomcat request attributes are disabled and forward headers are supported. 21 | */ 22 | @ExtendWith(EventsCaptureExtension::class) 23 | @TestPropertySource( 24 | properties = [ 25 | "server.forward-headers-strategy=native", 26 | "logback.access.config=classpath:logback-access-test.capture.xml", 27 | "logback.access.tomcat.request-attributes-enabled=false", 28 | ], 29 | ) 30 | sealed class TomcatRequestAttributesDisabledTest { 31 | 32 | @Test 33 | fun `Does not rewrite some attributes of the appended Logback-access event with forward headers`( 34 | @Autowired rest: TestRestTemplate, 35 | @LocalServerPort port: Int, 36 | capture: EventsCapture, 37 | ) { 38 | val request = RequestEntity.get("/mock-controller/text").build() 39 | val response = rest.exchange(request) 40 | response.statusCode.value().shouldBe(200) 41 | val event = assertLogbackAccessEventsEventually { capture.shouldBeSingleton().single() } 42 | event.serverName.shouldBe("localhost") 43 | event.localPort.shouldBe(port) 44 | event.remoteAddr.shouldBe("127.0.0.1") 45 | event.remoteHost.shouldBe("127.0.0.1") 46 | event.protocol.shouldBe("HTTP/1.1") 47 | } 48 | 49 | @Test 50 | fun `Does not rewrite some attributes of the appended Logback-access event without forward headers`( 51 | @Autowired rest: TestRestTemplate, 52 | @LocalServerPort port: Int, 53 | capture: EventsCapture, 54 | ) { 55 | val request = RequestEntity.get("/mock-controller/text").build() 56 | val response = rest.exchange(request) 57 | response.statusCode.value().shouldBe(200) 58 | val event = assertLogbackAccessEventsEventually { capture.shouldBeSingleton().single() } 59 | event.serverName.shouldBe("localhost") 60 | event.localPort.shouldBe(port) 61 | event.remoteAddr.shouldBe("127.0.0.1") 62 | event.remoteHost.shouldBe("127.0.0.1") 63 | event.protocol.shouldBe("HTTP/1.1") 64 | } 65 | 66 | } 67 | 68 | /** 69 | * Tests the [TomcatRequestAttributesDisabledTest] using the Tomcat servlet web server. 70 | */ 71 | @TomcatServletWebTest 72 | class TomcatServletWebRequestAttributesDisabledTest : TomcatRequestAttributesDisabledTest() 73 | 74 | /** 75 | * Tests the [TomcatRequestAttributesDisabledTest] using the Tomcat reactive web server. 76 | */ 77 | @TomcatReactiveWebTest 78 | class TomcatReactiveWebRequestAttributesDisabledTest : TomcatRequestAttributesDisabledTest() 79 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/akkinoc/spring/boot/logback/access/LogbackAccessAutoConfiguration.kt: -------------------------------------------------------------------------------- 1 | package dev.akkinoc.spring.boot.logback.access 2 | 3 | import dev.akkinoc.spring.boot.logback.access.jetty.LogbackAccessJettyConfiguration 4 | import dev.akkinoc.spring.boot.logback.access.security.LogbackAccessSecurityServletFilterConfiguration 5 | import dev.akkinoc.spring.boot.logback.access.tee.LogbackAccessTeeServletFilterConfiguration 6 | import dev.akkinoc.spring.boot.logback.access.tomcat.LogbackAccessTomcatConfiguration 7 | import dev.akkinoc.spring.boot.logback.access.undertow.LogbackAccessUndertowConfiguration 8 | import dev.akkinoc.spring.boot.logback.access.undertow.LogbackAccessUndertowReactiveConfiguration 9 | import dev.akkinoc.spring.boot.logback.access.undertow.LogbackAccessUndertowServletConfiguration 10 | import org.slf4j.Logger 11 | import org.slf4j.LoggerFactory.getLogger 12 | import org.springframework.boot.autoconfigure.AutoConfigureBefore 13 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean 14 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty 15 | import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication 16 | import org.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerFactoryAutoConfiguration 17 | import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration 18 | import org.springframework.boot.context.properties.EnableConfigurationProperties 19 | import org.springframework.context.annotation.Bean 20 | import org.springframework.context.annotation.Configuration 21 | import org.springframework.context.annotation.Import 22 | import org.springframework.core.env.Environment 23 | import org.springframework.core.io.ResourceLoader 24 | 25 | /** 26 | * The auto-configuration for Logback-access. 27 | */ 28 | @Configuration(proxyBeanMethods = false) 29 | @AutoConfigureBefore(ServletWebServerFactoryAutoConfiguration::class, ReactiveWebServerFactoryAutoConfiguration::class) 30 | @ConditionalOnProperty(prefix = "logback.access", name = ["enabled"], havingValue = "true", matchIfMissing = true) 31 | @ConditionalOnWebApplication 32 | @EnableConfigurationProperties(LogbackAccessProperties::class) 33 | @Import( 34 | LogbackAccessTomcatConfiguration::class, 35 | LogbackAccessJettyConfiguration::class, 36 | LogbackAccessUndertowConfiguration::class, 37 | LogbackAccessUndertowServletConfiguration::class, 38 | LogbackAccessUndertowReactiveConfiguration::class, 39 | LogbackAccessSecurityServletFilterConfiguration::class, 40 | LogbackAccessTeeServletFilterConfiguration::class, 41 | ) 42 | class LogbackAccessAutoConfiguration { 43 | 44 | /** 45 | * Provides the Logback-access context. 46 | * 47 | * @param logbackAccessProperties The configuration properties for Logback-access. 48 | * @param resourceLoader The resource loader. 49 | * @param environment The environment. 50 | * @return The Logback-access context. 51 | */ 52 | @Bean 53 | @ConditionalOnMissingBean 54 | fun logbackAccessContext( 55 | logbackAccessProperties: LogbackAccessProperties, 56 | resourceLoader: ResourceLoader, 57 | environment: Environment, 58 | ): LogbackAccessContext { 59 | val logbackAccessContext = LogbackAccessContext(logbackAccessProperties, resourceLoader, environment) 60 | log.debug("Providing the {}: {}", LogbackAccessContext::class.simpleName, logbackAccessContext) 61 | return logbackAccessContext 62 | } 63 | 64 | companion object { 65 | 66 | /** 67 | * The logger. 68 | */ 69 | private val log: Logger = getLogger(LogbackAccessAutoConfiguration::class.java) 70 | 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/test/kotlin/dev/akkinoc/spring/boot/logback/access/joran/JoranSpringPropertyTest.kt: -------------------------------------------------------------------------------- 1 | package dev.akkinoc.spring.boot.logback.access.joran 2 | 3 | import dev.akkinoc.spring.boot.logback.access.test.assertion.Assertions.assertLogbackAccessEventsEventually 4 | import dev.akkinoc.spring.boot.logback.access.test.type.JettyReactiveWebTest 5 | import dev.akkinoc.spring.boot.logback.access.test.type.JettyServletWebTest 6 | import dev.akkinoc.spring.boot.logback.access.test.type.TomcatReactiveWebTest 7 | import dev.akkinoc.spring.boot.logback.access.test.type.TomcatServletWebTest 8 | import dev.akkinoc.spring.boot.logback.access.test.type.UndertowReactiveWebTest 9 | import dev.akkinoc.spring.boot.logback.access.test.type.UndertowServletWebTest 10 | import io.kotest.matchers.collections.shouldHaveSingleElement 11 | import io.kotest.matchers.shouldBe 12 | import org.junit.jupiter.api.Test 13 | import org.junit.jupiter.api.extension.ExtendWith 14 | import org.springframework.beans.factory.annotation.Autowired 15 | import org.springframework.boot.test.system.CapturedOutput 16 | import org.springframework.boot.test.system.OutputCaptureExtension 17 | import org.springframework.boot.test.web.client.TestRestTemplate 18 | import org.springframework.boot.test.web.client.exchange 19 | import org.springframework.http.RequestEntity 20 | import org.springframework.test.context.TestPropertySource 21 | 22 | /** 23 | * Tests the case where `` tags are used in the configuration file. 24 | */ 25 | @ExtendWith(OutputCaptureExtension::class) 26 | @TestPropertySource( 27 | properties = [ 28 | "logback.access.config=classpath:logback-access-test-spring.property.xml", 29 | "logback.access.test.console.pattern.prefix=>>>", 30 | "logback.access.test.console.pattern.suffix=<<<", 31 | ], 32 | ) 33 | sealed class JoranSpringPropertyTest { 34 | 35 | @Test 36 | fun `Appends a Logback-access event according to the configuration file that contains springProperty tags`( 37 | @Autowired rest: TestRestTemplate, 38 | capture: CapturedOutput, 39 | ) { 40 | val request = RequestEntity.get("/mock-controller/text").build() 41 | val response = rest.exchange(request) 42 | response.statusCode.value().shouldBe(200) 43 | response.body.shouldBe("mock-text") 44 | assertLogbackAccessEventsEventually { 45 | capture.out.lines().shouldHaveSingleElement { it.startsWith(">>>") && it.endsWith("<<<") } 46 | } 47 | } 48 | 49 | } 50 | 51 | /** 52 | * Tests the [JoranSpringPropertyTest] using the Tomcat servlet web server. 53 | */ 54 | @TomcatServletWebTest 55 | class TomcatServletWebJoranSpringPropertyTest : JoranSpringPropertyTest() 56 | 57 | /** 58 | * Tests the [JoranSpringPropertyTest] using the Tomcat reactive web server. 59 | */ 60 | @TomcatReactiveWebTest 61 | class TomcatReactiveWebJoranSpringPropertyTest : JoranSpringPropertyTest() 62 | 63 | /** 64 | * Tests the [JoranSpringPropertyTest] using the Jetty servlet web server. 65 | */ 66 | @JettyServletWebTest 67 | class JettyServletWebJoranSpringPropertyTest : JoranSpringPropertyTest() 68 | 69 | /** 70 | * Tests the [JoranSpringPropertyTest] using the Jetty reactive web server. 71 | */ 72 | @JettyReactiveWebTest 73 | class JettyReactiveWebJoranSpringPropertyTest : JoranSpringPropertyTest() 74 | 75 | /** 76 | * Tests the [JoranSpringPropertyTest] using the Undertow servlet web server. 77 | */ 78 | @UndertowServletWebTest 79 | class UndertowServletWebJoranSpringPropertyTest : JoranSpringPropertyTest() 80 | 81 | /** 82 | * Tests the [JoranSpringPropertyTest] using the Undertow reactive web server. 83 | */ 84 | @UndertowReactiveWebTest 85 | class UndertowReactiveWebJoranSpringPropertyTest : JoranSpringPropertyTest() 86 | -------------------------------------------------------------------------------- /src/test/kotlin/dev/akkinoc/spring/boot/logback/access/test/configuration/WebTestConfiguration.kt: -------------------------------------------------------------------------------- 1 | package dev.akkinoc.spring.boot.logback.access.test.configuration 2 | 3 | import dev.akkinoc.spring.boot.logback.access.test.mock.MockController 4 | import dev.akkinoc.spring.boot.logback.access.test.mock.MockReactiveController 5 | import dev.akkinoc.spring.boot.logback.access.test.mock.MockServletController 6 | import org.slf4j.Logger 7 | import org.slf4j.LoggerFactory.getLogger 8 | import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication 9 | import org.springframework.boot.test.context.TestConfiguration 10 | import org.springframework.boot.test.web.client.LocalHostUriTemplateHandler 11 | import org.springframework.boot.test.web.client.TestRestTemplate 12 | import org.springframework.context.annotation.Bean 13 | import org.springframework.core.env.Environment 14 | import org.springframework.web.util.DefaultUriBuilderFactory 15 | 16 | /** 17 | * The test configuration for testing using the web server. 18 | */ 19 | @TestConfiguration(proxyBeanMethods = false) 20 | class WebTestConfiguration { 21 | 22 | /** 23 | * Provides the mock controller for testing using the web server. 24 | * 25 | * @return The mock controller for testing using the web server. 26 | */ 27 | @Bean 28 | fun mockController(): MockController { 29 | val mockController = MockController() 30 | log.debug("Providing the {}: {}", MockController::class.simpleName, mockController) 31 | return mockController 32 | } 33 | 34 | /** 35 | * Provides the mock controller for testing using the servlet web server. 36 | * 37 | * @return The mock controller for testing using the servlet web server. 38 | */ 39 | @Bean 40 | @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) 41 | fun mockServletController(): MockServletController { 42 | val mockServletController = MockServletController() 43 | log.debug("Providing the {}: {}", MockServletController::class.simpleName, mockServletController) 44 | return mockServletController 45 | } 46 | 47 | /** 48 | * Provides the mock controller for testing using the reactive web server. 49 | * 50 | * @return The mock controller for testing using the reactive web server. 51 | */ 52 | @Bean 53 | @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) 54 | fun mockReactiveController(): MockReactiveController { 55 | val mockReactiveController = MockReactiveController() 56 | log.debug("Providing the {}: {}", MockReactiveController::class.simpleName, mockReactiveController) 57 | return mockReactiveController 58 | } 59 | 60 | /** 61 | * Provides the [TestRestTemplate] for testing using the web server. 62 | * 63 | * @return The [TestRestTemplate] for testing using the web server. 64 | */ 65 | @Bean 66 | fun testRestTemplate(environment: Environment): TestRestTemplate { 67 | val uriBuilderFactory = DefaultUriBuilderFactory().apply { 68 | encodingMode = DefaultUriBuilderFactory.EncodingMode.VALUES_ONLY 69 | } 70 | val uriTemplateHandler = LocalHostUriTemplateHandler(environment, "http", uriBuilderFactory) 71 | val testRestTemplate = TestRestTemplate().apply { 72 | setUriTemplateHandler(uriTemplateHandler) 73 | } 74 | log.debug("Providing the {}: {}", TestRestTemplate::class.simpleName, testRestTemplate) 75 | return testRestTemplate 76 | } 77 | 78 | companion object { 79 | 80 | /** 81 | * The logger. 82 | */ 83 | private val log: Logger = getLogger(WebTestConfiguration::class.java) 84 | 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /src/test/kotlin/dev/akkinoc/spring/boot/logback/access/joran/JoranSpringPropertyAsIncludeTest.kt: -------------------------------------------------------------------------------- 1 | package dev.akkinoc.spring.boot.logback.access.joran 2 | 3 | import dev.akkinoc.spring.boot.logback.access.test.assertion.Assertions.assertLogbackAccessEventsEventually 4 | import dev.akkinoc.spring.boot.logback.access.test.type.JettyReactiveWebTest 5 | import dev.akkinoc.spring.boot.logback.access.test.type.JettyServletWebTest 6 | import dev.akkinoc.spring.boot.logback.access.test.type.TomcatReactiveWebTest 7 | import dev.akkinoc.spring.boot.logback.access.test.type.TomcatServletWebTest 8 | import dev.akkinoc.spring.boot.logback.access.test.type.UndertowReactiveWebTest 9 | import dev.akkinoc.spring.boot.logback.access.test.type.UndertowServletWebTest 10 | import io.kotest.matchers.collections.shouldHaveSingleElement 11 | import io.kotest.matchers.shouldBe 12 | import org.junit.jupiter.api.Test 13 | import org.junit.jupiter.api.extension.ExtendWith 14 | import org.springframework.beans.factory.annotation.Autowired 15 | import org.springframework.boot.test.system.CapturedOutput 16 | import org.springframework.boot.test.system.OutputCaptureExtension 17 | import org.springframework.boot.test.web.client.TestRestTemplate 18 | import org.springframework.boot.test.web.client.exchange 19 | import org.springframework.http.RequestEntity 20 | import org.springframework.test.context.TestPropertySource 21 | 22 | /** 23 | * Tests the case where `` tags are used in the configuration file. 24 | */ 25 | @ExtendWith(OutputCaptureExtension::class) 26 | @TestPropertySource( 27 | properties = [ 28 | "logback.access.config=classpath:logback-access-test-spring.property-as-include.xml", 29 | "logback.access.test.console.pattern.prefix=>>>", 30 | "logback.access.test.console.pattern.suffix=<<<", 31 | ], 32 | ) 33 | sealed class JoranSpringPropertyAsIncludeTest { 34 | 35 | @Test 36 | fun `Appends a Logback-access event according to the configuration file that contains springProperty tags`( 37 | @Autowired rest: TestRestTemplate, 38 | capture: CapturedOutput, 39 | ) { 40 | val request = RequestEntity.get("/mock-controller/text").build() 41 | val response = rest.exchange(request) 42 | response.statusCode.value().shouldBe(200) 43 | response.body.shouldBe("mock-text") 44 | assertLogbackAccessEventsEventually { 45 | capture.out.lines().shouldHaveSingleElement { it.startsWith(">>>") && it.endsWith("<<<") } 46 | } 47 | } 48 | 49 | } 50 | 51 | /** 52 | * Tests the [JoranSpringPropertyAsIncludeTest] using the Tomcat servlet web server. 53 | */ 54 | @TomcatServletWebTest 55 | class TomcatServletWebJoranSpringPropertyAsIncludeTest : JoranSpringPropertyAsIncludeTest() 56 | 57 | /** 58 | * Tests the [JoranSpringPropertyAsIncludeTest] using the Tomcat reactive web server. 59 | */ 60 | @TomcatReactiveWebTest 61 | class TomcatReactiveWebJoranSpringPropertyAsIncludeTest : JoranSpringPropertyAsIncludeTest() 62 | 63 | /** 64 | * Tests the [JoranSpringPropertyAsIncludeTest] using the Jetty servlet web server. 65 | */ 66 | @JettyServletWebTest 67 | class JettyServletWebJoranSpringPropertyAsIncludeTest : JoranSpringPropertyAsIncludeTest() 68 | 69 | /** 70 | * Tests the [JoranSpringPropertyAsIncludeTest] using the Jetty reactive web server. 71 | */ 72 | @JettyReactiveWebTest 73 | class JettyReactiveWebJoranSpringPropertyAsIncludeTest : JoranSpringPropertyAsIncludeTest() 74 | 75 | /** 76 | * Tests the [JoranSpringPropertyAsIncludeTest] using the Undertow servlet web server. 77 | */ 78 | @UndertowServletWebTest 79 | class UndertowServletWebJoranSpringPropertyAsIncludeTest : JoranSpringPropertyAsIncludeTest() 80 | 81 | /** 82 | * Tests the [JoranSpringPropertyAsIncludeTest] using the Undertow reactive web server. 83 | */ 84 | @UndertowReactiveWebTest 85 | class UndertowReactiveWebJoranSpringPropertyAsIncludeTest : JoranSpringPropertyAsIncludeTest() 86 | -------------------------------------------------------------------------------- /src/test/kotlin/dev/akkinoc/spring/boot/logback/access/joran/JoranSpringProfileTest.kt: -------------------------------------------------------------------------------- 1 | package dev.akkinoc.spring.boot.logback.access.joran 2 | 3 | import dev.akkinoc.spring.boot.logback.access.test.assertion.Assertions.assertLogbackAccessEventsEventually 4 | import dev.akkinoc.spring.boot.logback.access.test.type.JettyReactiveWebTest 5 | import dev.akkinoc.spring.boot.logback.access.test.type.JettyServletWebTest 6 | import dev.akkinoc.spring.boot.logback.access.test.type.TomcatReactiveWebTest 7 | import dev.akkinoc.spring.boot.logback.access.test.type.TomcatServletWebTest 8 | import dev.akkinoc.spring.boot.logback.access.test.type.UndertowReactiveWebTest 9 | import dev.akkinoc.spring.boot.logback.access.test.type.UndertowServletWebTest 10 | import io.kotest.matchers.collections.shouldHaveSingleElement 11 | import io.kotest.matchers.shouldBe 12 | import io.kotest.matchers.string.shouldNotContain 13 | import org.junit.jupiter.api.Test 14 | import org.junit.jupiter.api.extension.ExtendWith 15 | import org.springframework.beans.factory.annotation.Autowired 16 | import org.springframework.boot.test.system.CapturedOutput 17 | import org.springframework.boot.test.system.OutputCaptureExtension 18 | import org.springframework.boot.test.web.client.TestRestTemplate 19 | import org.springframework.boot.test.web.client.exchange 20 | import org.springframework.http.RequestEntity 21 | import org.springframework.test.context.ActiveProfiles 22 | import org.springframework.test.context.TestPropertySource 23 | 24 | /** 25 | * Tests the case where `` tags are used in the configuration file. 26 | */ 27 | @ExtendWith(OutputCaptureExtension::class) 28 | @ActiveProfiles( 29 | "logback-access-test-disable-default-console", 30 | "logback-access-test-enable-additional-console", 31 | "logback-access-test-enable-additional-nested-console", 32 | ) 33 | @TestPropertySource(properties = ["logback.access.config=classpath:logback-access-test-spring.profile.xml"]) 34 | sealed class JoranSpringProfileTest { 35 | 36 | @Test 37 | fun `Appends a Logback-access event according to the configuration file that contains springProfile tags`( 38 | @Autowired rest: TestRestTemplate, 39 | capture: CapturedOutput, 40 | ) { 41 | val request = RequestEntity.get("/mock-controller/text").build() 42 | val response = rest.exchange(request) 43 | response.statusCode.value().shouldBe(200) 44 | response.body.shouldBe("mock-text") 45 | assertLogbackAccessEventsEventually { 46 | capture.out.shouldNotContain("default_console:") 47 | capture.out.shouldNotContain("default_nested_console:") 48 | capture.out.lines().shouldHaveSingleElement { it.startsWith("additional_console:") } 49 | capture.out.lines().shouldHaveSingleElement { it.startsWith("additional_nested_console:") } 50 | capture.out.shouldNotContain("ignored_console:") 51 | } 52 | } 53 | 54 | } 55 | 56 | /** 57 | * Tests the [JoranSpringProfileTest] using the Tomcat servlet web server. 58 | */ 59 | @TomcatServletWebTest 60 | class TomcatServletWebJoranSpringProfileTest : JoranSpringProfileTest() 61 | 62 | /** 63 | * Tests the [JoranSpringProfileTest] using the Tomcat reactive web server. 64 | */ 65 | @TomcatReactiveWebTest 66 | class TomcatReactiveWebJoranSpringProfileTest : JoranSpringProfileTest() 67 | 68 | /** 69 | * Tests the [JoranSpringProfileTest] using the Jetty servlet web server. 70 | */ 71 | @JettyServletWebTest 72 | class JettyServletWebJoranSpringProfileTest : JoranSpringProfileTest() 73 | 74 | /** 75 | * Tests the [JoranSpringProfileTest] using the Jetty reactive web server. 76 | */ 77 | @JettyReactiveWebTest 78 | class JettyReactiveWebJoranSpringProfileTest : JoranSpringProfileTest() 79 | 80 | /** 81 | * Tests the [JoranSpringProfileTest] using the Undertow servlet web server. 82 | */ 83 | @UndertowServletWebTest 84 | class UndertowServletWebJoranSpringProfileTest : JoranSpringProfileTest() 85 | 86 | /** 87 | * Tests the [JoranSpringProfileTest] using the Undertow reactive web server. 88 | */ 89 | @UndertowReactiveWebTest 90 | class UndertowReactiveWebJoranSpringProfileTest : JoranSpringProfileTest() 91 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/akkinoc/spring/boot/logback/access/LogbackAccessProperties.kt: -------------------------------------------------------------------------------- 1 | package dev.akkinoc.spring.boot.logback.access 2 | 3 | import ch.qos.logback.access.common.spi.IAccessEvent 4 | import dev.akkinoc.spring.boot.logback.access.value.LogbackAccessLocalPortStrategy 5 | import io.undertow.UndertowOptions 6 | import org.apache.catalina.valves.RemoteIpValve 7 | import org.springframework.boot.context.properties.ConfigurationProperties 8 | 9 | /** 10 | * The configuration properties for Logback-access. 11 | * 12 | * @property enabled 13 | * Whether to enable auto-configuration. 14 | * @property config 15 | * The location of the configuration file. 16 | * Specify a URL that starts with "classpath:" or "file:". 17 | * Auto-detected by default: 18 | * 1. "classpath:logback-access-test.xml" 19 | * 2. "classpath:logback-access.xml" 20 | * 3. "classpath:logback-access-test-spring.xml" 21 | * 4. "classpath:logback-access-spring.xml" 22 | * 5. "classpath:dev/akkinoc/spring/boot/logback/access/logback-access-spring.xml" 23 | * @property localPortStrategy 24 | * The strategy to change the behavior of [IAccessEvent.getLocalPort]. 25 | * @property tomcat 26 | * The properties for the Tomcat web server. 27 | * @property undertow 28 | * The properties for the Undertow web server. 29 | * @property teeFilter 30 | * The properties for the tee filter. 31 | */ 32 | @ConfigurationProperties("logback.access") 33 | data class LogbackAccessProperties 34 | @JvmOverloads 35 | constructor( 36 | val enabled: Boolean = true, 37 | val config: String? = null, 38 | val localPortStrategy: LogbackAccessLocalPortStrategy = LogbackAccessLocalPortStrategy.SERVER, 39 | val tomcat: Tomcat = Tomcat(), 40 | val undertow: Undertow = Undertow(), 41 | val teeFilter: TeeFilter = TeeFilter(), 42 | ) { 43 | 44 | companion object { 45 | 46 | /** 47 | * The default locations of the configuration file. 48 | */ 49 | @JvmField 50 | val DEFAULT_CONFIGS: List = listOf( 51 | "classpath:logback-access-test.xml", 52 | "classpath:logback-access.xml", 53 | "classpath:logback-access-test-spring.xml", 54 | "classpath:logback-access-spring.xml", 55 | ) 56 | 57 | /** 58 | * The fallback location of the configuration file. 59 | */ 60 | const val FALLBACK_CONFIG: String = "classpath:dev/akkinoc/spring/boot/logback/access/logback-access-spring.xml" 61 | 62 | } 63 | 64 | /** 65 | * The properties for the Tomcat web server. 66 | * 67 | * @property requestAttributesEnabled 68 | * Whether to enable the request attributes to work with [RemoteIpValve]. 69 | * Defaults to the presence of [RemoteIpValve] enabled by the property "server.forward-headers-strategy=native". 70 | */ 71 | data class Tomcat 72 | @JvmOverloads 73 | constructor( 74 | val requestAttributesEnabled: Boolean? = null, 75 | ) 76 | 77 | /** 78 | * The properties for the Undertow web server. 79 | * 80 | * @property recordRequestStartTime 81 | * Whether to enable [UndertowOptions.RECORD_REQUEST_START_TIME]. 82 | * Used to measure [IAccessEvent.getElapsedTime] and [IAccessEvent.getElapsedSeconds]. 83 | */ 84 | data class Undertow 85 | @JvmOverloads 86 | constructor( 87 | val recordRequestStartTime: Boolean = true, 88 | ) 89 | 90 | /** 91 | * The properties for the tee filter. 92 | * 93 | * @property enabled 94 | * Whether to enable the tee filter. 95 | * @property includes 96 | * The host names to activate. 97 | * By default, all hosts are activated. 98 | * @property excludes 99 | * The host names to deactivate. 100 | * By default, all hosts are activated. 101 | */ 102 | data class TeeFilter 103 | @JvmOverloads 104 | constructor( 105 | val enabled: Boolean = false, 106 | val includes: String? = null, 107 | val excludes: String? = null, 108 | ) 109 | 110 | } 111 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/spring-configuration-metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "groups": [ 3 | { 4 | "name": "logback.access", 5 | "type": "dev.akkinoc.spring.boot.logback.access.LogbackAccessProperties", 6 | "sourceType": "dev.akkinoc.spring.boot.logback.access.LogbackAccessProperties", 7 | "description": "The configuration properties for Logback-access." 8 | }, 9 | { 10 | "name": "logback.access.tomcat", 11 | "type": "dev.akkinoc.spring.boot.logback.access.LogbackAccessProperties$Tomcat", 12 | "sourceType": "dev.akkinoc.spring.boot.logback.access.LogbackAccessProperties", 13 | "description": "The properties for the Tomcat web server." 14 | }, 15 | { 16 | "name": "logback.access.undertow", 17 | "type": "dev.akkinoc.spring.boot.logback.access.LogbackAccessProperties$Undertow", 18 | "sourceType": "dev.akkinoc.spring.boot.logback.access.LogbackAccessProperties", 19 | "description": "The properties for the Undertow web server." 20 | }, 21 | { 22 | "name": "logback.access.tee-filter", 23 | "type": "dev.akkinoc.spring.boot.logback.access.LogbackAccessProperties$TeeFilter", 24 | "sourceType": "dev.akkinoc.spring.boot.logback.access.LogbackAccessProperties", 25 | "description": "The properties for the tee filter." 26 | } 27 | ], 28 | "properties": [ 29 | { 30 | "name": "logback.access.enabled", 31 | "type": "java.lang.Boolean", 32 | "sourceType": "dev.akkinoc.spring.boot.logback.access.LogbackAccessProperties", 33 | "description": "Whether to enable auto-configuration.", 34 | "defaultValue": true 35 | }, 36 | { 37 | "name": "logback.access.config", 38 | "type": "java.lang.String", 39 | "sourceType": "dev.akkinoc.spring.boot.logback.access.LogbackAccessProperties", 40 | "description": "The location of the configuration file. Specify a URL that starts with \"classpath:\" or \"file:\". Auto-detected by default: 1. \"classpath:logback-access-test.xml\" 2. \"classpath:logback-access.xml\" 3. \"classpath:logback-access-test-spring.xml\" 4. \"classpath:logback-access-spring.xml\" 5. \"classpath:dev/akkinoc/spring/boot/logback/access/logback-access-spring.xml\"" 41 | }, 42 | { 43 | "name": "logback.access.local-port-strategy", 44 | "type": "dev.akkinoc.spring.boot.logback.access.value.LogbackAccessLocalPortStrategy", 45 | "sourceType": "dev.akkinoc.spring.boot.logback.access.LogbackAccessProperties", 46 | "defaultValue": "server", 47 | "description": "The strategy to change the behavior of IAccessEvent.getLocalPort." 48 | }, 49 | { 50 | "name": "logback.access.tomcat.request-attributes-enabled", 51 | "type": "java.lang.Boolean", 52 | "sourceType": "dev.akkinoc.spring.boot.logback.access.LogbackAccessProperties$Tomcat", 53 | "description": "Whether to enable the request attributes to work with RemoteIpValve. Defaults to the presence of RemoteIpValve enabled by the property \"server.forward-headers-strategy=native\"." 54 | }, 55 | { 56 | "name": "logback.access.undertow.record-request-start-time", 57 | "type": "java.lang.Boolean", 58 | "sourceType": "dev.akkinoc.spring.boot.logback.access.LogbackAccessProperties$Undertow", 59 | "description": "Whether to enable UndertowOptions.RECORD_REQUEST_START_TIME. Used to measure IAccessEvent.getElapsedTime and IAccessEvent.getElapsedSeconds." 60 | }, 61 | { 62 | "name": "logback.access.tee-filter.enabled", 63 | "type": "java.lang.Boolean", 64 | "sourceType": "dev.akkinoc.spring.boot.logback.access.LogbackAccessProperties$TeeFilter", 65 | "defaultValue": false, 66 | "description": "Whether to enable the tee filter." 67 | }, 68 | { 69 | "name": "logback.access.tee-filter.includes", 70 | "type": "java.lang.String", 71 | "sourceType": "dev.akkinoc.spring.boot.logback.access.LogbackAccessProperties$TeeFilter", 72 | "description": "The host names to activate. By default, all hosts are activated." 73 | }, 74 | { 75 | "name": "logback.access.tee-filter.excludes", 76 | "type": "java.lang.String", 77 | "sourceType": "dev.akkinoc.spring.boot.logback.access.LogbackAccessProperties$TeeFilter", 78 | "description": "The host names to deactivate. By default, all hosts are activated." 79 | } 80 | ], 81 | "hints": [ 82 | ] 83 | } 84 | -------------------------------------------------------------------------------- /src/test/kotlin/dev/akkinoc/spring/boot/logback/access/test/mock/MockController.kt: -------------------------------------------------------------------------------- 1 | package dev.akkinoc.spring.boot.logback.access.test.mock 2 | 3 | import org.slf4j.Logger 4 | import org.slf4j.LoggerFactory.getLogger 5 | import org.springframework.http.ResponseEntity 6 | import org.springframework.web.bind.annotation.GetMapping 7 | import org.springframework.web.bind.annotation.ModelAttribute 8 | import org.springframework.web.bind.annotation.PostMapping 9 | import org.springframework.web.bind.annotation.RequestBody 10 | import org.springframework.web.bind.annotation.RequestMapping 11 | import org.springframework.web.bind.annotation.RestController 12 | import reactor.core.publisher.Flux 13 | import java.util.concurrent.CompletableFuture 14 | 15 | /** 16 | * The mock controller for testing using the web server. 17 | */ 18 | @RestController 19 | @RequestMapping("/mock-controller") 20 | class MockController { 21 | 22 | /** 23 | * Gets a text. 24 | * 25 | * @return A text. 26 | */ 27 | @GetMapping("/text") 28 | fun getText(): String { 29 | val response = "mock-text" 30 | log.debug("Getting a text: {}", response) 31 | return response 32 | } 33 | 34 | /** 35 | * Gets a text with response headers. 36 | * 37 | * @return A [ResponseEntity] to return a text with response headers. 38 | */ 39 | @GetMapping("/text-with-response-headers") 40 | fun getTextWithResponseHeaders(): ResponseEntity { 41 | val response = ResponseEntity.ok() 42 | .header("a", "value @a") 43 | .header("b", "value1 @b", "value2 @b") 44 | .header("c", "") 45 | .body("mock-text") 46 | log.debug("Getting a text with response headers: {}", response) 47 | return response 48 | } 49 | 50 | /** 51 | * Gets an empty text. 52 | * 53 | * @return An empty text. 54 | */ 55 | @GetMapping("/empty-text") 56 | fun getEmptyText(): String { 57 | val response = "" 58 | log.debug("Getting an empty text: {}", response) 59 | return response 60 | } 61 | 62 | /** 63 | * Gets a text asynchronously. 64 | * 65 | * @return A [CompletableFuture] to return a text asynchronously. 66 | */ 67 | @GetMapping("/text-asynchronously") 68 | fun getTextAsynchronously(): CompletableFuture { 69 | val response = CompletableFuture.supplyAsync { "mock-text" } 70 | log.debug("Getting a text asynchronously: {}", response) 71 | return response 72 | } 73 | 74 | /** 75 | * Gets a text with chunked transfer encoding. 76 | * 77 | * @return A [Flux] to return a text with chunked transfer encoding. 78 | */ 79 | @GetMapping("/text-with-chunked-transfer-encoding") 80 | fun getTextWithChunkedTransferEncoding(): Flux { 81 | val response = Flux.just("mock-text") 82 | log.debug("Getting a text with chunked transfer encoding: {}", response) 83 | return response 84 | } 85 | 86 | /** 87 | * Posts the text. 88 | * 89 | * @param posted The posted text. 90 | * @return A text. 91 | */ 92 | @PostMapping("/text") 93 | fun postText(@RequestBody posted: String): String { 94 | val response = "mock-text" 95 | log.debug("Posting the text: {} => {}", posted, response) 96 | return response 97 | } 98 | 99 | /** 100 | * Posts the form data. 101 | * 102 | * @param posted The posted form data. 103 | * @return A text. 104 | */ 105 | @PostMapping("/form-data") 106 | fun postFormData(@ModelAttribute posted: FormData): String { 107 | val response = "mock-text" 108 | log.debug("Posting the form data: {} => {}", posted, response) 109 | return response 110 | } 111 | 112 | companion object { 113 | 114 | /** 115 | * The logger. 116 | */ 117 | private val log: Logger = getLogger(MockController::class.java) 118 | 119 | } 120 | 121 | /** 122 | * The form data. 123 | * 124 | * @property a The string value. 125 | * @property b The string values. 126 | * @property c The string value. 127 | */ 128 | data class FormData(val a: String?, val b: List?, val c: String?) 129 | 130 | } 131 | -------------------------------------------------------------------------------- /src/test/kotlin/dev/akkinoc/spring/boot/logback/access/InvalidAccessEventTest.kt: -------------------------------------------------------------------------------- 1 | package dev.akkinoc.spring.boot.logback.access 2 | 3 | import dev.akkinoc.spring.boot.logback.access.test.assertion.Assertions.assertLogbackAccessEventsEventually 4 | import dev.akkinoc.spring.boot.logback.access.test.extension.EventsCapture 5 | import dev.akkinoc.spring.boot.logback.access.test.extension.EventsCaptureExtension 6 | import dev.akkinoc.spring.boot.logback.access.test.type.JettyReactiveWebTest 7 | import dev.akkinoc.spring.boot.logback.access.test.type.JettyServletWebTest 8 | import dev.akkinoc.spring.boot.logback.access.test.type.TomcatReactiveWebTest 9 | import dev.akkinoc.spring.boot.logback.access.test.type.TomcatServletWebTest 10 | import dev.akkinoc.spring.boot.logback.access.test.type.UndertowReactiveWebTest 11 | import dev.akkinoc.spring.boot.logback.access.test.type.UndertowServletWebTest 12 | import io.kotest.matchers.collections.shouldBeSingleton 13 | import io.kotest.matchers.shouldBe 14 | import io.kotest.matchers.string.shouldBeEmpty 15 | import org.junit.jupiter.api.Test 16 | import org.junit.jupiter.api.extension.ExtendWith 17 | import org.springframework.beans.factory.annotation.Autowired 18 | import org.springframework.boot.test.web.client.TestRestTemplate 19 | import org.springframework.boot.test.web.client.exchange 20 | import org.springframework.http.RequestEntity 21 | import org.springframework.test.context.TestPropertySource 22 | 23 | /** 24 | * Tests the appended Logback-access event in the case where the access is invalid. 25 | * 26 | * @property supportsInvalidUrls Whether to support invalid URLs. 27 | */ 28 | @ExtendWith(EventsCaptureExtension::class) 29 | @TestPropertySource(properties = ["logback.access.config=classpath:logback-access-test.capture.xml"]) 30 | sealed class InvalidAccessEventTest( 31 | private val supportsInvalidUrls: Boolean, 32 | ) { 33 | 34 | @Test 35 | fun `Appends a Logback-access event with an invalid URL`( 36 | @Autowired rest: TestRestTemplate, 37 | capture: EventsCapture, 38 | ) { 39 | val request = RequestEntity.get("/mock-controller/text?[]").build() 40 | val response = rest.exchange(request) 41 | response.statusCode.value().shouldBe(if (supportsInvalidUrls) 200 else 400) 42 | val event = assertLogbackAccessEventsEventually { capture.shouldBeSingleton().single() } 43 | if (supportsInvalidUrls) { 44 | event.requestURI.shouldBe("/mock-controller/text") 45 | event.queryString.shouldBe("?[]") 46 | event.requestURL.shouldBe("GET /mock-controller/text?[] HTTP/1.1") 47 | } else { 48 | event.requestURI.shouldBe("-") 49 | event.queryString.shouldBeEmpty() 50 | event.requestURL.shouldBe("GET null HTTP/1.1") 51 | } 52 | } 53 | 54 | } 55 | 56 | /** 57 | * Tests the [InvalidAccessEventTest] using the Tomcat servlet web server. 58 | */ 59 | @TomcatServletWebTest 60 | class TomcatServletWebInvalidAccessEventTest : InvalidAccessEventTest( 61 | supportsInvalidUrls = false, 62 | ) 63 | 64 | /** 65 | * Tests the [InvalidAccessEventTest] using the Tomcat reactive web server. 66 | */ 67 | @TomcatReactiveWebTest 68 | class TomcatReactiveWebInvalidAccessEventTest : InvalidAccessEventTest( 69 | supportsInvalidUrls = false, 70 | ) 71 | 72 | /** 73 | * Tests the [InvalidAccessEventTest] using the Jetty servlet web server. 74 | */ 75 | @JettyServletWebTest 76 | class JettyServletWebInvalidAccessEventTest : InvalidAccessEventTest( 77 | supportsInvalidUrls = true, 78 | ) 79 | 80 | /** 81 | * Tests the [InvalidAccessEventTest] using the Jetty reactive web server. 82 | */ 83 | @JettyReactiveWebTest 84 | class JettyReactiveWebInvalidAccessEventTest : InvalidAccessEventTest( 85 | supportsInvalidUrls = true, 86 | ) 87 | 88 | /** 89 | * Tests the [InvalidAccessEventTest] using the Undertow servlet web server. 90 | */ 91 | @UndertowServletWebTest 92 | class UndertowServletWebInvalidAccessEventTest : InvalidAccessEventTest( 93 | supportsInvalidUrls = true, 94 | ) 95 | 96 | /** 97 | * Tests the [InvalidAccessEventTest] using the Undertow reactive web server. 98 | */ 99 | @UndertowReactiveWebTest 100 | class UndertowReactiveWebInvalidAccessEventTest : InvalidAccessEventTest( 101 | supportsInvalidUrls = true, 102 | ) 103 | -------------------------------------------------------------------------------- /src/test/kotlin/dev/akkinoc/spring/boot/logback/access/BasicTest.kt: -------------------------------------------------------------------------------- 1 | package dev.akkinoc.spring.boot.logback.access 2 | 3 | import dev.akkinoc.spring.boot.logback.access.test.assertion.Assertions.assertLogbackAccessEventsEventually 4 | import dev.akkinoc.spring.boot.logback.access.test.type.JettyReactiveWebTest 5 | import dev.akkinoc.spring.boot.logback.access.test.type.JettyServletWebTest 6 | import dev.akkinoc.spring.boot.logback.access.test.type.TomcatReactiveWebTest 7 | import dev.akkinoc.spring.boot.logback.access.test.type.TomcatServletWebTest 8 | import dev.akkinoc.spring.boot.logback.access.test.type.UndertowReactiveWebTest 9 | import dev.akkinoc.spring.boot.logback.access.test.type.UndertowServletWebTest 10 | import dev.akkinoc.spring.boot.logback.access.value.LogbackAccessLocalPortStrategy 11 | import io.kotest.matchers.booleans.shouldBeTrue 12 | import io.kotest.matchers.collections.shouldHaveSingleElement 13 | import io.kotest.matchers.nulls.shouldBeNull 14 | import io.kotest.matchers.nulls.shouldNotBeNull 15 | import io.kotest.matchers.shouldBe 16 | import io.kotest.matchers.types.shouldBeSameInstanceAs 17 | import org.junit.jupiter.api.Test 18 | import org.junit.jupiter.api.extension.ExtendWith 19 | import org.springframework.beans.factory.annotation.Autowired 20 | import org.springframework.boot.test.system.CapturedOutput 21 | import org.springframework.boot.test.system.OutputCaptureExtension 22 | import org.springframework.boot.test.web.client.TestRestTemplate 23 | import org.springframework.boot.test.web.client.exchange 24 | import org.springframework.http.RequestEntity 25 | 26 | /** 27 | * Tests the case where the configuration is the default. 28 | */ 29 | @ExtendWith(OutputCaptureExtension::class) 30 | sealed class BasicTest { 31 | 32 | @Test 33 | fun `Provides the configuration properties for Logback-access`( 34 | @Autowired logbackAccessProperties: LogbackAccessProperties?, 35 | ) { 36 | logbackAccessProperties.shouldNotBeNull() 37 | logbackAccessProperties.enabled.shouldBe(true) 38 | logbackAccessProperties.config.shouldBeNull() 39 | logbackAccessProperties.localPortStrategy.shouldBe(LogbackAccessLocalPortStrategy.SERVER) 40 | logbackAccessProperties.tomcat.requestAttributesEnabled.shouldBeNull() 41 | logbackAccessProperties.undertow.recordRequestStartTime.shouldBeTrue() 42 | logbackAccessProperties.teeFilter.enabled.shouldBe(false) 43 | logbackAccessProperties.teeFilter.includes.shouldBeNull() 44 | logbackAccessProperties.teeFilter.excludes.shouldBeNull() 45 | } 46 | 47 | @Test 48 | fun `Provides the Logback-access context`( 49 | @Autowired logbackAccessContext: LogbackAccessContext?, 50 | @Autowired logbackAccessProperties: LogbackAccessProperties?, 51 | ) { 52 | logbackAccessContext.shouldNotBeNull() 53 | logbackAccessContext.properties.shouldBeSameInstanceAs(logbackAccessProperties) 54 | } 55 | 56 | @Test 57 | fun `Appends a Logback-access event`( 58 | @Autowired rest: TestRestTemplate, 59 | capture: CapturedOutput, 60 | ) { 61 | val request = RequestEntity.get("/mock-controller/text").build() 62 | val response = rest.exchange(request) 63 | response.statusCode.value().shouldBe(200) 64 | response.body.shouldBe("mock-text") 65 | val regex = Regex("""^127\.0\.0\.1 - - \[.+] "GET /mock-controller/text HTTP/1\.1" 200 9$""") 66 | assertLogbackAccessEventsEventually { capture.out.lines().shouldHaveSingleElement { it matches regex } } 67 | } 68 | 69 | } 70 | 71 | /** 72 | * Tests the [BasicTest] using the Tomcat servlet web server. 73 | */ 74 | @TomcatServletWebTest 75 | class TomcatServletWebBasicTest : BasicTest() 76 | 77 | /** 78 | * Tests the [BasicTest] using the Tomcat reactive web server. 79 | */ 80 | @TomcatReactiveWebTest 81 | class TomcatReactiveWebBasicTest : BasicTest() 82 | 83 | /** 84 | * Tests the [BasicTest] using the Jetty servlet web server. 85 | */ 86 | @JettyServletWebTest 87 | class JettyServletWebBasicTest : BasicTest() 88 | 89 | /** 90 | * Tests the [BasicTest] using the Jetty reactive web server. 91 | */ 92 | @JettyReactiveWebTest 93 | class JettyReactiveWebBasicTest : BasicTest() 94 | 95 | /** 96 | * Tests the [BasicTest] using the Undertow servlet web server. 97 | */ 98 | @UndertowServletWebTest 99 | class UndertowServletWebBasicTest : BasicTest() 100 | 101 | /** 102 | * Tests the [BasicTest] using the Undertow reactive web server. 103 | */ 104 | @UndertowReactiveWebTest 105 | class UndertowReactiveWebBasicTest : BasicTest() 106 | -------------------------------------------------------------------------------- /src/test/kotlin/dev/akkinoc/spring/boot/logback/access/EventFilterTest.kt: -------------------------------------------------------------------------------- 1 | package dev.akkinoc.spring.boot.logback.access 2 | 3 | import dev.akkinoc.spring.boot.logback.access.test.assertion.Assertions.assertLogbackAccessEventsContinually 4 | import dev.akkinoc.spring.boot.logback.access.test.assertion.Assertions.assertLogbackAccessEventsEventually 5 | import dev.akkinoc.spring.boot.logback.access.test.extension.EventsCapture 6 | import dev.akkinoc.spring.boot.logback.access.test.extension.EventsCaptureExtension 7 | import dev.akkinoc.spring.boot.logback.access.test.type.JettyReactiveWebTest 8 | import dev.akkinoc.spring.boot.logback.access.test.type.JettyServletWebTest 9 | import dev.akkinoc.spring.boot.logback.access.test.type.TomcatReactiveWebTest 10 | import dev.akkinoc.spring.boot.logback.access.test.type.TomcatServletWebTest 11 | import dev.akkinoc.spring.boot.logback.access.test.type.UndertowReactiveWebTest 12 | import dev.akkinoc.spring.boot.logback.access.test.type.UndertowServletWebTest 13 | import io.kotest.matchers.collections.shouldBeEmpty 14 | import io.kotest.matchers.collections.shouldBeSingleton 15 | import io.kotest.matchers.shouldBe 16 | import org.junit.jupiter.api.Test 17 | import org.junit.jupiter.api.extension.ExtendWith 18 | import org.springframework.beans.factory.annotation.Autowired 19 | import org.springframework.boot.test.web.client.TestRestTemplate 20 | import org.springframework.boot.test.web.client.exchange 21 | import org.springframework.http.RequestEntity 22 | import org.springframework.test.context.TestPropertySource 23 | 24 | /** 25 | * Tests the case where event filters are used in the configuration file. 26 | */ 27 | @ExtendWith(EventsCaptureExtension::class) 28 | @TestPropertySource(properties = ["logback.access.config=classpath:logback-access-test.filter.capture.xml"]) 29 | sealed class EventFilterTest { 30 | 31 | @Test 32 | fun `If the filter accepts a Logback-access event, appends it`( 33 | @Autowired rest: TestRestTemplate, 34 | capture: EventsCapture, 35 | ) { 36 | val request = RequestEntity.get("/mock-controller/text") 37 | .header("mock-event-filter-reply", "accept") 38 | .build() 39 | val response = rest.exchange(request) 40 | response.statusCode.value().shouldBe(200) 41 | assertLogbackAccessEventsEventually { capture.shouldBeSingleton() } 42 | } 43 | 44 | @Test 45 | fun `If the filter passes a Logback-access event, appends it`( 46 | @Autowired rest: TestRestTemplate, 47 | capture: EventsCapture, 48 | ) { 49 | val request = RequestEntity.get("/mock-controller/text") 50 | .header("mock-event-filter-reply", "neutral") 51 | .build() 52 | val response = rest.exchange(request) 53 | response.statusCode.value().shouldBe(200) 54 | assertLogbackAccessEventsEventually { capture.shouldBeSingleton() } 55 | } 56 | 57 | @Test 58 | fun `If the filter denies a Logback-access event, does not append it`( 59 | @Autowired rest: TestRestTemplate, 60 | capture: EventsCapture, 61 | ) { 62 | val request = RequestEntity.get("/mock-controller/text") 63 | .header("mock-event-filter-reply", "deny") 64 | .build() 65 | val response = rest.exchange(request) 66 | response.statusCode.value().shouldBe(200) 67 | assertLogbackAccessEventsContinually { capture.shouldBeEmpty() } 68 | } 69 | 70 | } 71 | 72 | /** 73 | * Tests the [EventFilterTest] using the Tomcat servlet web server. 74 | */ 75 | @TomcatServletWebTest 76 | class TomcatServletWebEventFilterTest : EventFilterTest() 77 | 78 | /** 79 | * Tests the [EventFilterTest] using the Tomcat reactive web server. 80 | */ 81 | @TomcatReactiveWebTest 82 | class TomcatReactiveWebEventFilterTest : EventFilterTest() 83 | 84 | /** 85 | * Tests the [EventFilterTest] using the Jetty servlet web server. 86 | */ 87 | @JettyServletWebTest 88 | class JettyServletWebEventFilterTest : EventFilterTest() 89 | 90 | /** 91 | * Tests the [EventFilterTest] using the Jetty reactive web server. 92 | */ 93 | @JettyReactiveWebTest 94 | class JettyReactiveWebEventFilterTest : EventFilterTest() 95 | 96 | /** 97 | * Tests the [EventFilterTest] using the Undertow servlet web server. 98 | */ 99 | @UndertowServletWebTest 100 | class UndertowServletWebEventFilterTest : EventFilterTest() 101 | 102 | /** 103 | * Tests the [EventFilterTest] using the Undertow reactive web server. 104 | */ 105 | @UndertowReactiveWebTest 106 | class UndertowReactiveWebEventFilterTest : EventFilterTest() 107 | -------------------------------------------------------------------------------- /src/test/kotlin/dev/akkinoc/spring/boot/logback/access/SecurityAttributesTest.kt: -------------------------------------------------------------------------------- 1 | package dev.akkinoc.spring.boot.logback.access 2 | 3 | import dev.akkinoc.spring.boot.logback.access.test.assertion.Assertions.assertLogbackAccessEventsEventually 4 | import dev.akkinoc.spring.boot.logback.access.test.extension.EventsCapture 5 | import dev.akkinoc.spring.boot.logback.access.test.extension.EventsCaptureExtension 6 | import dev.akkinoc.spring.boot.logback.access.test.type.JettyReactiveWebTest 7 | import dev.akkinoc.spring.boot.logback.access.test.type.JettyServletWebTest 8 | import dev.akkinoc.spring.boot.logback.access.test.type.TomcatReactiveWebTest 9 | import dev.akkinoc.spring.boot.logback.access.test.type.TomcatServletWebTest 10 | import dev.akkinoc.spring.boot.logback.access.test.type.UndertowReactiveWebTest 11 | import dev.akkinoc.spring.boot.logback.access.test.type.UndertowServletWebTest 12 | import io.kotest.matchers.collections.shouldBeSingleton 13 | import io.kotest.matchers.shouldBe 14 | import org.junit.jupiter.api.Test 15 | import org.junit.jupiter.api.extension.ExtendWith 16 | import org.springframework.beans.factory.annotation.Autowired 17 | import org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration 18 | import org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration 19 | import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration 20 | import org.springframework.boot.test.web.client.TestRestTemplate 21 | import org.springframework.boot.test.web.client.exchange 22 | import org.springframework.context.annotation.Import 23 | import org.springframework.http.RequestEntity 24 | import org.springframework.test.context.TestPropertySource 25 | 26 | /** 27 | * Tests the case where Spring Security is enabled. 28 | * 29 | * @property supportsRemoteUsers Whether to support remote users. 30 | */ 31 | @ExtendWith(EventsCaptureExtension::class) 32 | @Import( 33 | ReactiveUserDetailsServiceAutoConfiguration::class, 34 | SecurityAutoConfiguration::class, 35 | ReactiveSecurityAutoConfiguration::class, 36 | ) 37 | @TestPropertySource( 38 | properties = [ 39 | "spring.security.user.name=test-user", 40 | "spring.security.user.password=test-password", 41 | "logback.access.config=classpath:logback-access-test.capture.xml", 42 | ], 43 | ) 44 | sealed class SecurityAttributesTest( 45 | private val supportsRemoteUsers: Boolean, 46 | ) { 47 | 48 | @Test 49 | fun `Rewrites the remote user of the appended Logback-access event with Spring Security`( 50 | @Autowired rest: TestRestTemplate, 51 | capture: EventsCapture, 52 | ) { 53 | val request = RequestEntity.get("/mock-controller/text").build() 54 | val response = rest.withBasicAuth("test-user", "test-password").exchange(request) 55 | response.statusCode.value().shouldBe(200) 56 | val event = assertLogbackAccessEventsEventually { capture.shouldBeSingleton().single() } 57 | if (supportsRemoteUsers) { 58 | event.remoteUser.shouldBe("test-user") 59 | } else { 60 | event.remoteUser.shouldBe("-") 61 | } 62 | } 63 | 64 | } 65 | 66 | /** 67 | * Tests the [SecurityAttributesTest] using the Tomcat servlet web server. 68 | */ 69 | @TomcatServletWebTest 70 | class TomcatServletWebSecurityAttributesTest : SecurityAttributesTest( 71 | supportsRemoteUsers = true, 72 | ) 73 | 74 | /** 75 | * Tests the [SecurityAttributesTest] using the Tomcat reactive web server. 76 | */ 77 | @TomcatReactiveWebTest 78 | class TomcatReactiveWebSecurityAttributesTest : SecurityAttributesTest( 79 | supportsRemoteUsers = false, 80 | ) 81 | 82 | /** 83 | * Tests the [SecurityAttributesTest] using the Jetty servlet web server. 84 | */ 85 | @JettyServletWebTest 86 | class JettyServletWebSecurityAttributesTest : SecurityAttributesTest( 87 | supportsRemoteUsers = true, 88 | ) 89 | 90 | /** 91 | * Tests the [SecurityAttributesTest] using the Jetty reactive web server. 92 | */ 93 | @JettyReactiveWebTest 94 | class JettyReactiveWebSecurityAttributesTest : SecurityAttributesTest( 95 | supportsRemoteUsers = false, 96 | ) 97 | 98 | /** 99 | * Tests the [SecurityAttributesTest] using the Undertow servlet web server. 100 | */ 101 | @UndertowServletWebTest 102 | class UndertowServletWebSecurityAttributesTest : SecurityAttributesTest( 103 | supportsRemoteUsers = true, 104 | ) 105 | 106 | /** 107 | * Tests the [SecurityAttributesTest] using the Undertow reactive web server. 108 | */ 109 | @UndertowReactiveWebTest 110 | class UndertowReactiveWebSecurityAttributesTest : SecurityAttributesTest( 111 | supportsRemoteUsers = false, 112 | ) 113 | -------------------------------------------------------------------------------- /src/test/kotlin/dev/akkinoc/spring/boot/logback/access/LocalPortNoRewriteTest.kt: -------------------------------------------------------------------------------- 1 | package dev.akkinoc.spring.boot.logback.access 2 | 3 | import dev.akkinoc.spring.boot.logback.access.test.assertion.Assertions.assertLogbackAccessEventsEventually 4 | import dev.akkinoc.spring.boot.logback.access.test.extension.EventsCapture 5 | import dev.akkinoc.spring.boot.logback.access.test.extension.EventsCaptureExtension 6 | import dev.akkinoc.spring.boot.logback.access.test.type.JettyReactiveWebTest 7 | import dev.akkinoc.spring.boot.logback.access.test.type.JettyServletWebTest 8 | import dev.akkinoc.spring.boot.logback.access.test.type.TomcatReactiveWebTest 9 | import dev.akkinoc.spring.boot.logback.access.test.type.TomcatServletWebTest 10 | import dev.akkinoc.spring.boot.logback.access.test.type.UndertowReactiveWebTest 11 | import dev.akkinoc.spring.boot.logback.access.test.type.UndertowServletWebTest 12 | import io.kotest.matchers.collections.shouldBeSingleton 13 | import io.kotest.matchers.shouldBe 14 | import org.junit.jupiter.api.Test 15 | import org.junit.jupiter.api.extension.ExtendWith 16 | import org.springframework.beans.factory.annotation.Autowired 17 | import org.springframework.boot.test.web.client.TestRestTemplate 18 | import org.springframework.boot.test.web.client.exchange 19 | import org.springframework.boot.test.web.server.LocalServerPort 20 | import org.springframework.http.RequestEntity 21 | import org.springframework.test.context.TestPropertySource 22 | 23 | /** 24 | * Tests the case where the local port is not rewritten. 25 | */ 26 | @ExtendWith(EventsCaptureExtension::class) 27 | @TestPropertySource( 28 | properties = [ 29 | "server.forward-headers-strategy=native", 30 | "logback.access.config=classpath:logback-access-test.capture.xml", 31 | "logback.access.local-port-strategy=local", 32 | ], 33 | ) 34 | sealed class LocalPortNoRewriteTest { 35 | 36 | @Test 37 | fun `Does not rewrite the local port of the appended Logback-access event with forward headers`( 38 | @Autowired rest: TestRestTemplate, 39 | @LocalServerPort port: Int, 40 | capture: EventsCapture, 41 | ) { 42 | val request = RequestEntity.get("/mock-controller/text") 43 | .header("x-forwarded-host", "forwarded-host") 44 | .header("x-forwarded-port", "12345") 45 | .header("x-forwarded-for", "1.2.3.4") 46 | .header("x-forwarded-proto", "https") 47 | .build() 48 | val response = rest.exchange(request) 49 | response.statusCode.value().shouldBe(200) 50 | val event = assertLogbackAccessEventsEventually { capture.shouldBeSingleton().single() } 51 | event.serverName.shouldBe("forwarded-host") 52 | event.localPort.shouldBe(port) 53 | event.remoteAddr.shouldBe("1.2.3.4") 54 | event.remoteHost.shouldBe("1.2.3.4") 55 | event.protocol.shouldBe("HTTP/1.1") 56 | } 57 | 58 | @Test 59 | fun `Does not rewrite the local port of the appended Logback-access event without forward headers`( 60 | @Autowired rest: TestRestTemplate, 61 | @LocalServerPort port: Int, 62 | capture: EventsCapture, 63 | ) { 64 | val request = RequestEntity.get("/mock-controller/text").build() 65 | val response = rest.exchange(request) 66 | response.statusCode.value().shouldBe(200) 67 | val event = assertLogbackAccessEventsEventually { capture.shouldBeSingleton().single() } 68 | event.serverName.shouldBe("localhost") 69 | event.localPort.shouldBe(port) 70 | event.remoteAddr.shouldBe("127.0.0.1") 71 | event.remoteHost.shouldBe("127.0.0.1") 72 | event.protocol.shouldBe("HTTP/1.1") 73 | } 74 | 75 | } 76 | 77 | /** 78 | * Tests the [LocalPortNoRewriteTest] using the Tomcat servlet web server. 79 | */ 80 | @TomcatServletWebTest 81 | class TomcatServletWebLocalPortNoRewriteTest : LocalPortNoRewriteTest() 82 | 83 | /** 84 | * Tests the [LocalPortNoRewriteTest] using the Tomcat reactive web server. 85 | */ 86 | @TomcatReactiveWebTest 87 | class TomcatReactiveWebLocalPortNoRewriteTest : LocalPortNoRewriteTest() 88 | 89 | /** 90 | * Tests the [LocalPortNoRewriteTest] using the Jetty servlet web server. 91 | */ 92 | @JettyServletWebTest 93 | class JettyServletWebLocalPortNoRewriteTest : LocalPortNoRewriteTest() 94 | 95 | /** 96 | * Tests the [LocalPortNoRewriteTest] using the Jetty reactive web server. 97 | */ 98 | @JettyReactiveWebTest 99 | class JettyReactiveWebLocalPortNoRewriteTest : LocalPortNoRewriteTest() 100 | 101 | /** 102 | * Tests the [LocalPortNoRewriteTest] using the Undertow servlet web server. 103 | */ 104 | @UndertowServletWebTest 105 | class UndertowServletWebLocalPortNoRewriteTest : LocalPortNoRewriteTest() 106 | 107 | /** 108 | * Tests the [LocalPortNoRewriteTest] using the Undertow reactive web server. 109 | */ 110 | @UndertowReactiveWebTest 111 | class UndertowReactiveWebLocalPortNoRewriteTest : LocalPortNoRewriteTest() 112 | -------------------------------------------------------------------------------- /src/test/kotlin/dev/akkinoc/spring/boot/logback/access/test/configuration/TestContextClassLoaderCustomizerFactory.kt: -------------------------------------------------------------------------------- 1 | package dev.akkinoc.spring.boot.logback.access.test.configuration 2 | 3 | import dev.akkinoc.spring.boot.logback.access.test.type.JettyReactiveWebTest 4 | import dev.akkinoc.spring.boot.logback.access.test.type.JettyServletWebTest 5 | import dev.akkinoc.spring.boot.logback.access.test.type.NettyReactiveWebTest 6 | import dev.akkinoc.spring.boot.logback.access.test.type.TomcatReactiveWebTest 7 | import dev.akkinoc.spring.boot.logback.access.test.type.TomcatServletWebTest 8 | import dev.akkinoc.spring.boot.logback.access.test.type.UndertowReactiveWebTest 9 | import dev.akkinoc.spring.boot.logback.access.test.type.UndertowServletWebTest 10 | import io.undertow.Undertow 11 | import org.apache.catalina.startup.Tomcat 12 | import org.slf4j.Logger 13 | import org.slf4j.LoggerFactory.getLogger 14 | import org.springframework.core.annotation.MergedAnnotations 15 | import org.springframework.core.io.ClassPathResource 16 | import org.springframework.test.context.ContextConfigurationAttributes 17 | import org.springframework.test.context.ContextCustomizerFactory 18 | import org.springframework.util.ClassUtils.convertClassNameToResourcePath 19 | import java.net.URL 20 | import io.undertow.websockets.jsr.Bootstrap as UndertowBootstrap 21 | import org.apache.tomcat.websocket.server.WsSci as TomcatWsSci 22 | import org.eclipse.jetty.ee10.websocket.jakarta.server.config.JakartaWebSocketServletContainerInitializer as JettyJakartaWebSocketServletContainerInitializer 23 | import org.eclipse.jetty.server.Server as JettyServer 24 | import reactor.netty.http.server.HttpServer as NettyHttpServer 25 | 26 | /** 27 | * The test context customizer factory to configure the class loader. 28 | */ 29 | class TestContextClassLoaderCustomizerFactory : ContextCustomizerFactory { 30 | 31 | override fun createContextCustomizer( 32 | testClass: Class<*>, 33 | attributes: MutableList, 34 | ): TestContextClassLoaderCustomizer { 35 | val testContextClassLoaderCustomizer = TestContextClassLoaderCustomizer( 36 | hiddenClasses = getHiddenClasses(testClass), 37 | additionalClassPaths = getAdditionalClassPaths(testClass), 38 | ) 39 | log.debug( 40 | "Creating the {}: {}", 41 | TestContextClassLoaderCustomizer::class.simpleName, 42 | testContextClassLoaderCustomizer, 43 | ) 44 | return testContextClassLoaderCustomizer 45 | } 46 | 47 | /** 48 | * Gets the classes to hide from the class loader. 49 | * Returns unused web server classes to prevent detection by Spring Boot auto-configuration. 50 | * 51 | * @param testClass The test class. 52 | * @return The classes to hide from the class loader. 53 | */ 54 | private fun getHiddenClasses(testClass: Class<*>): Set> { 55 | val annotations = MergedAnnotations.from(testClass, MergedAnnotations.SearchStrategy.TYPE_HIERARCHY) 56 | val usesTomcat = annotations.isPresent(TomcatServletWebTest::class.java) || 57 | annotations.isPresent(TomcatReactiveWebTest::class.java) 58 | val usesJetty = annotations.isPresent(JettyServletWebTest::class.java) || 59 | annotations.isPresent(JettyReactiveWebTest::class.java) 60 | val usesUndertow = annotations.isPresent(UndertowServletWebTest::class.java) || 61 | annotations.isPresent(UndertowReactiveWebTest::class.java) 62 | val usesNetty = annotations.isPresent(NettyReactiveWebTest::class.java) 63 | return buildSet { 64 | if (!usesTomcat) add(Tomcat::class.java) 65 | if (!usesTomcat) add(TomcatWsSci::class.java) 66 | if (!usesJetty) add(JettyServer::class.java) 67 | if (!usesJetty) add(JettyJakartaWebSocketServletContainerInitializer::class.java) 68 | if (!usesUndertow) add(Undertow::class.java) 69 | if (!usesUndertow) add(UndertowBootstrap::class.java) 70 | if (!usesNetty) add(NettyHttpServer::class.java) 71 | } 72 | } 73 | 74 | /** 75 | * Gets the class paths to add to the class loader. 76 | * Returns a URL containing the test class name as the path so that resources can be prepared for each test class. 77 | * If the test class is inherited, returns URLs for all super classes as well. 78 | * 79 | * @param testClass The test class. 80 | * @return The class paths to add to the class loader. 81 | */ 82 | private fun getAdditionalClassPaths(testClass: Class<*>): List { 83 | return generateSequence(testClass) { it.superclass } 84 | .takeWhile { it != Any::class.java } 85 | .map { convertClassNameToResourcePath(it.name) } 86 | .map { ClassPathResource("$it/") } 87 | .filter { it.exists() } 88 | .map { it.url } 89 | .toList() 90 | } 91 | 92 | companion object { 93 | 94 | /** 95 | * The logger. 96 | */ 97 | private val log: Logger = getLogger(TestContextClassLoaderCustomizerFactory::class.java) 98 | 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /src/test/kotlin/dev/akkinoc/spring/boot/logback/access/ConfigurationFileLocationTest.kt: -------------------------------------------------------------------------------- 1 | package dev.akkinoc.spring.boot.logback.access 2 | 3 | import dev.akkinoc.spring.boot.logback.access.test.assertion.Assertions.assertLogbackAccessEventsEventually 4 | import dev.akkinoc.spring.boot.logback.access.test.extension.EventsCapture 5 | import dev.akkinoc.spring.boot.logback.access.test.extension.EventsCaptureExtension 6 | import dev.akkinoc.spring.boot.logback.access.test.type.JettyReactiveWebTest 7 | import dev.akkinoc.spring.boot.logback.access.test.type.JettyServletWebTest 8 | import dev.akkinoc.spring.boot.logback.access.test.type.TomcatReactiveWebTest 9 | import dev.akkinoc.spring.boot.logback.access.test.type.TomcatServletWebTest 10 | import dev.akkinoc.spring.boot.logback.access.test.type.UndertowReactiveWebTest 11 | import dev.akkinoc.spring.boot.logback.access.test.type.UndertowServletWebTest 12 | import io.kotest.matchers.collections.shouldBeSingleton 13 | import io.kotest.matchers.shouldBe 14 | import org.junit.jupiter.api.Test 15 | import org.junit.jupiter.api.extension.ExtendWith 16 | import org.springframework.beans.factory.annotation.Autowired 17 | import org.springframework.boot.test.web.client.TestRestTemplate 18 | import org.springframework.boot.test.web.client.exchange 19 | import org.springframework.http.RequestEntity 20 | import org.springframework.test.context.TestPropertySource 21 | 22 | /** 23 | * Tests the case where the location of the configuration file is specified. 24 | */ 25 | @ExtendWith(EventsCaptureExtension::class) 26 | sealed class ConfigurationFileLocationTest { 27 | 28 | @Test 29 | fun `Appends a Logback-access event according to the configuration file found`( 30 | @Autowired rest: TestRestTemplate, 31 | capture: EventsCapture, 32 | ) { 33 | val request = RequestEntity.get("/mock-controller/text").build() 34 | val response = rest.exchange(request) 35 | response.statusCode.value().shouldBe(200) 36 | assertLogbackAccessEventsEventually { capture.shouldBeSingleton() } 37 | } 38 | 39 | } 40 | 41 | /** 42 | * Tests the case where the location of the configuration file is specified as a file system path. 43 | */ 44 | @TestPropertySource(properties = ["logback.access.config=target/test-classes/logback-access-test.capture.xml"]) 45 | sealed class ConfigurationFilePathTest : ConfigurationFileLocationTest() 46 | 47 | /** 48 | * Tests the [ConfigurationFilePathTest] using the Tomcat servlet web server. 49 | */ 50 | @TomcatServletWebTest 51 | class TomcatServletWebConfigurationFilePathTest : ConfigurationFilePathTest() 52 | 53 | /** 54 | * Tests the [ConfigurationFilePathTest] using the Tomcat reactive web server. 55 | */ 56 | @TomcatReactiveWebTest 57 | class TomcatReactiveWebConfigurationFilePathTest : ConfigurationFilePathTest() 58 | 59 | /** 60 | * Tests the [ConfigurationFilePathTest] using the Jetty servlet web server. 61 | */ 62 | @JettyServletWebTest 63 | class JettyServletWebConfigurationFilePathTest : ConfigurationFilePathTest() 64 | 65 | /** 66 | * Tests the [ConfigurationFilePathTest] using the Jetty reactive web server. 67 | */ 68 | @JettyReactiveWebTest 69 | class JettyReactiveWebConfigurationFilePathTest : ConfigurationFilePathTest() 70 | 71 | /** 72 | * Tests the [ConfigurationFilePathTest] using the Undertow servlet web server. 73 | */ 74 | @UndertowServletWebTest 75 | class UndertowServletWebConfigurationFilePathTest : ConfigurationFilePathTest() 76 | 77 | /** 78 | * Tests the [ConfigurationFilePathTest] using the Undertow reactive web server. 79 | */ 80 | @UndertowReactiveWebTest 81 | class UndertowReactiveWebConfigurationFilePathTest : ConfigurationFilePathTest() 82 | 83 | /** 84 | * Tests the case where the location of the configuration file is specified as a file scheme URI. 85 | */ 86 | @TestPropertySource(properties = ["logback.access.config=file:target/test-classes/logback-access-test.capture.xml"]) 87 | sealed class ConfigurationFileUriTest : ConfigurationFileLocationTest() 88 | 89 | /** 90 | * Tests the [ConfigurationFileUriTest] using the Tomcat servlet web server. 91 | */ 92 | @TomcatServletWebTest 93 | class TomcatServletWebConfigurationFileUriTest : ConfigurationFileUriTest() 94 | 95 | /** 96 | * Tests the [ConfigurationFileUriTest] using the Tomcat reactive web server. 97 | */ 98 | @TomcatReactiveWebTest 99 | class TomcatReactiveWebConfigurationFileUriTest : ConfigurationFileUriTest() 100 | 101 | /** 102 | * Tests the [ConfigurationFileUriTest] using the Jetty servlet web server. 103 | */ 104 | @JettyServletWebTest 105 | class JettyServletWebConfigurationFileUriTest : ConfigurationFileUriTest() 106 | 107 | /** 108 | * Tests the [ConfigurationFileUriTest] using the Jetty reactive web server. 109 | */ 110 | @JettyReactiveWebTest 111 | class JettyReactiveWebConfigurationFileUriTest : ConfigurationFileUriTest() 112 | 113 | /** 114 | * Tests the [ConfigurationFileUriTest] using the Undertow servlet web server. 115 | */ 116 | @UndertowServletWebTest 117 | class UndertowServletWebConfigurationFileUriTest : ConfigurationFileUriTest() 118 | 119 | /** 120 | * Tests the [ConfigurationFileUriTest] using the Undertow reactive web server. 121 | */ 122 | @UndertowReactiveWebTest 123 | class UndertowReactiveWebConfigurationFileUriTest : ConfigurationFileUriTest() 124 | -------------------------------------------------------------------------------- /src/test/kotlin/dev/akkinoc/spring/boot/logback/access/ForwardHeadersSupportTest.kt: -------------------------------------------------------------------------------- 1 | package dev.akkinoc.spring.boot.logback.access 2 | 3 | import dev.akkinoc.spring.boot.logback.access.test.assertion.Assertions.assertLogbackAccessEventsEventually 4 | import dev.akkinoc.spring.boot.logback.access.test.extension.EventsCapture 5 | import dev.akkinoc.spring.boot.logback.access.test.extension.EventsCaptureExtension 6 | import dev.akkinoc.spring.boot.logback.access.test.type.JettyReactiveWebTest 7 | import dev.akkinoc.spring.boot.logback.access.test.type.JettyServletWebTest 8 | import dev.akkinoc.spring.boot.logback.access.test.type.TomcatReactiveWebTest 9 | import dev.akkinoc.spring.boot.logback.access.test.type.TomcatServletWebTest 10 | import dev.akkinoc.spring.boot.logback.access.test.type.UndertowReactiveWebTest 11 | import dev.akkinoc.spring.boot.logback.access.test.type.UndertowServletWebTest 12 | import io.kotest.matchers.collections.shouldBeSingleton 13 | import io.kotest.matchers.shouldBe 14 | import org.junit.jupiter.api.Test 15 | import org.junit.jupiter.api.extension.ExtendWith 16 | import org.springframework.beans.factory.annotation.Autowired 17 | import org.springframework.boot.test.web.client.TestRestTemplate 18 | import org.springframework.boot.test.web.client.exchange 19 | import org.springframework.boot.test.web.server.LocalServerPort 20 | import org.springframework.http.RequestEntity 21 | import org.springframework.test.context.TestPropertySource 22 | 23 | /** 24 | * Tests the case where forward headers are supported. 25 | */ 26 | @ExtendWith(EventsCaptureExtension::class) 27 | @TestPropertySource(properties = ["logback.access.config=classpath:logback-access-test.capture.xml"]) 28 | sealed class ForwardHeadersSupportTest { 29 | 30 | @Test 31 | fun `Rewrites some attributes of the appended Logback-access event with forward headers`( 32 | @Autowired rest: TestRestTemplate, 33 | capture: EventsCapture, 34 | ) { 35 | val request = RequestEntity.get("/mock-controller/text") 36 | .header("x-forwarded-host", "forwarded-host") 37 | .header("x-forwarded-port", "12345") 38 | .header("x-forwarded-for", "1.2.3.4") 39 | .header("x-forwarded-proto", "https") 40 | .build() 41 | val response = rest.exchange(request) 42 | response.statusCode.value().shouldBe(200) 43 | val event = assertLogbackAccessEventsEventually { capture.shouldBeSingleton().single() } 44 | event.serverName.shouldBe("forwarded-host") 45 | event.localPort.shouldBe(12345) 46 | event.remoteAddr.shouldBe("1.2.3.4") 47 | event.remoteHost.shouldBe("1.2.3.4") 48 | event.protocol.shouldBe("HTTP/1.1") 49 | } 50 | 51 | @Test 52 | fun `Does not rewrite some attributes of the appended Logback-access event without forward headers`( 53 | @Autowired rest: TestRestTemplate, 54 | @LocalServerPort port: Int, 55 | capture: EventsCapture, 56 | ) { 57 | val request = RequestEntity.get("/mock-controller/text").build() 58 | val response = rest.exchange(request) 59 | response.statusCode.value().shouldBe(200) 60 | val event = assertLogbackAccessEventsEventually { capture.shouldBeSingleton().single() } 61 | event.serverName.shouldBe("localhost") 62 | event.localPort.shouldBe(port) 63 | event.remoteAddr.shouldBe("127.0.0.1") 64 | event.remoteHost.shouldBe("127.0.0.1") 65 | event.protocol.shouldBe("HTTP/1.1") 66 | } 67 | 68 | } 69 | 70 | /** 71 | * Tests the case where forward headers are supported natively. 72 | */ 73 | @TestPropertySource(properties = ["server.forward-headers-strategy=native"]) 74 | sealed class NativeForwardHeadersSupportTest : ForwardHeadersSupportTest() 75 | 76 | /** 77 | * Tests the [NativeForwardHeadersSupportTest] using the Tomcat servlet web server. 78 | */ 79 | @TomcatServletWebTest 80 | class TomcatServletWebNativeForwardHeadersSupportTest : NativeForwardHeadersSupportTest() 81 | 82 | /** 83 | * Tests the [NativeForwardHeadersSupportTest] using the Tomcat reactive web server. 84 | */ 85 | @TomcatReactiveWebTest 86 | class TomcatReactiveWebNativeForwardHeadersSupportTest : NativeForwardHeadersSupportTest() 87 | 88 | /** 89 | * Tests the [NativeForwardHeadersSupportTest] using the Jetty servlet web server. 90 | */ 91 | @JettyServletWebTest 92 | class JettyServletWebNativeForwardHeadersSupportTest : NativeForwardHeadersSupportTest() 93 | 94 | /** 95 | * Tests the [NativeForwardHeadersSupportTest] using the Jetty reactive web server. 96 | */ 97 | @JettyReactiveWebTest 98 | class JettyReactiveWebNativeForwardHeadersSupportTest : NativeForwardHeadersSupportTest() 99 | 100 | /** 101 | * Tests the [NativeForwardHeadersSupportTest] using the Undertow servlet web server. 102 | */ 103 | @UndertowServletWebTest 104 | class UndertowServletWebNativeForwardHeadersSupportTest : NativeForwardHeadersSupportTest() 105 | 106 | /** 107 | * Tests the [NativeForwardHeadersSupportTest] using the Undertow reactive web server. 108 | */ 109 | @UndertowReactiveWebTest 110 | class UndertowReactiveWebNativeForwardHeadersSupportTest : NativeForwardHeadersSupportTest() 111 | 112 | // TODO: Add support for forward headers supported by framework. 113 | // /** 114 | // * Tests the case where forward headers are supported by framework. 115 | // */ 116 | // @TestPropertySource(properties = ["server.forward-headers-strategy=framework"]) 117 | // sealed class FrameworkForwardHeadersSupportTest : ForwardHeadersSupportTest() 118 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/akkinoc/spring/boot/logback/access/LogbackAccessEvent.kt: -------------------------------------------------------------------------------- 1 | package dev.akkinoc.spring.boot.logback.access 2 | 3 | import ch.qos.logback.access.common.spi.IAccessEvent 4 | import ch.qos.logback.access.common.spi.IAccessEvent.NA 5 | import ch.qos.logback.access.common.spi.IAccessEvent.SENTINEL 6 | import ch.qos.logback.access.common.spi.ServerAdapter 7 | import jakarta.servlet.http.HttpServletRequest 8 | import jakarta.servlet.http.HttpServletResponse 9 | import java.io.IOException 10 | import java.io.ObjectOutputStream 11 | import java.io.Serializable 12 | import java.util.Collections.enumeration 13 | import java.util.Enumeration 14 | import java.util.concurrent.TimeUnit.MILLISECONDS 15 | 16 | /** 17 | * The Logback-access event. 18 | * 19 | * @property source The Logback-access event source. 20 | * @see ch.qos.logback.access.common.spi.AccessEvent 21 | */ 22 | class LogbackAccessEvent(private var source: LogbackAccessEventSource) : IAccessEvent, Serializable { 23 | 24 | override fun getRequest(): HttpServletRequest? { 25 | return source.request 26 | } 27 | 28 | override fun getResponse(): HttpServletResponse? { 29 | return source.response 30 | } 31 | 32 | override fun getServerAdapter(): ServerAdapter? { 33 | return source.serverAdapter 34 | } 35 | 36 | override fun getTimeStamp(): Long { 37 | return source.timeStamp 38 | } 39 | 40 | override fun getElapsedTime(): Long { 41 | return source.elapsedTime ?: SENTINEL.toLong() 42 | } 43 | 44 | override fun getElapsedSeconds(): Long { 45 | val millis = source.elapsedTime ?: return SENTINEL.toLong() 46 | return MILLISECONDS.toSeconds(millis) 47 | } 48 | 49 | override fun getSequenceNumber(): Long { 50 | return source.sequenceNumber ?: 0L 51 | } 52 | 53 | override fun getThreadName(): String { 54 | return source.threadName 55 | } 56 | 57 | override fun setThreadName(value: String) { 58 | throw UnsupportedOperationException("Cannot change: $this") 59 | } 60 | 61 | override fun getServerName(): String { 62 | return source.serverName ?: NA 63 | } 64 | 65 | override fun getLocalPort(): Int { 66 | return source.localPort 67 | } 68 | 69 | override fun getRemoteAddr(): String { 70 | return source.remoteAddr 71 | } 72 | 73 | override fun getRemoteHost(): String { 74 | return source.remoteHost 75 | } 76 | 77 | override fun getRemoteUser(): String { 78 | return source.remoteUser ?: NA 79 | } 80 | 81 | override fun getProtocol(): String { 82 | return source.protocol 83 | } 84 | 85 | override fun getMethod(): String { 86 | return source.method 87 | } 88 | 89 | override fun getRequestURI(): String { 90 | return source.requestURI ?: NA 91 | } 92 | 93 | override fun getQueryString(): String { 94 | return source.queryString 95 | } 96 | 97 | override fun getRequestURL(): String { 98 | return source.requestURL 99 | } 100 | 101 | override fun getRequestHeaderMap(): Map { 102 | return source.requestHeaderMap 103 | } 104 | 105 | override fun getRequestHeaderNames(): Enumeration { 106 | return enumeration(source.requestHeaderMap.keys) 107 | } 108 | 109 | override fun getRequestHeader(key: String): String { 110 | return source.requestHeaderMap[key] ?: NA 111 | } 112 | 113 | override fun getCookie(key: String): String { 114 | return source.cookieMap[key] ?: NA 115 | } 116 | 117 | override fun getRequestParameterMap(): Map> { 118 | return source.requestParameterMap.mapValues { it.value.toTypedArray() } 119 | } 120 | 121 | override fun getRequestParameter(key: String): Array { 122 | val values = source.requestParameterMap[key] ?: return arrayOf(NA) 123 | return values.toTypedArray() 124 | } 125 | 126 | override fun getAttribute(key: String): String { 127 | return source.attributeMap[key] ?: NA 128 | } 129 | 130 | override fun getSessionID(): String { 131 | return source.sessionID ?: NA 132 | } 133 | 134 | override fun getRequestContent(): String { 135 | return source.requestContent.orEmpty() 136 | } 137 | 138 | override fun getStatusCode(): Int { 139 | return source.statusCode 140 | } 141 | 142 | override fun getResponseHeaderMap(): Map { 143 | return source.responseHeaderMap 144 | } 145 | 146 | override fun getResponseHeaderNameList(): List { 147 | return source.responseHeaderMap.keys.toList() 148 | } 149 | 150 | override fun getResponseHeader(key: String): String { 151 | return source.responseHeaderMap[key] ?: NA 152 | } 153 | 154 | override fun getContentLength(): Long { 155 | return source.contentLength 156 | } 157 | 158 | override fun getResponseContent(): String { 159 | return source.responseContent.orEmpty() 160 | } 161 | 162 | override fun prepareForDeferredProcessing() { 163 | source = source.fix() 164 | } 165 | 166 | override fun toString(): String = "${this::class.simpleName}($requestURL $statusCode)" 167 | 168 | /** 169 | * @see java.io.Serializable 170 | */ 171 | @Throws(IOException::class) 172 | private fun writeObject(out: ObjectOutputStream) { 173 | prepareForDeferredProcessing() 174 | out.defaultWriteObject() 175 | } 176 | 177 | } 178 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, caste, color, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | . 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.1, available at 119 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 120 | 121 | Community Impact Guidelines were inspired by 122 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 123 | 124 | For answers to common questions about this code of conduct, see the FAQ at 125 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available 126 | at [https://www.contributor-covenant.org/translations][translations]. 127 | 128 | [homepage]: https://www.contributor-covenant.org 129 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 130 | [Mozilla CoC]: https://github.com/mozilla/diversity 131 | [FAQ]: https://www.contributor-covenant.org/faq 132 | [translations]: https://www.contributor-covenant.org/translations 133 | --------------------------------------------------------------------------------