.
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 |
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 |
--------------------------------------------------------------------------------