Skip to content

Introduce Spring Boot starter #11

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 18 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ group = "io.github.dmitrysulman"

dependencies {
dokka(project("logback-access-reactor-netty"))
dokka(project("logback-access-reactor-netty-spring-boot-starter"))
}

tasks.jreleaserFullRelease {
Expand Down
4 changes: 4 additions & 0 deletions buildSrc/src/main/kotlin/conventions.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ dokka {
url("https://javadoc.io/doc/ch.qos.logback/logback-core/${libs.versions.logbackClassic.get()}/")
packageListUrl("https://javadoc.io/doc/ch.qos.logback/logback-core/${libs.versions.logbackClassic.get()}/element-list")
}
register("spring-boot-docs") {
url("https://docs.spring.io/spring-boot/${libs.versions.springBoot.get()}/api/java/")
packageListUrl("https://docs.spring.io/spring-boot/${libs.versions.springBoot.get()}/api/java/element-list")
}
}
}
}
Expand Down
5 changes: 3 additions & 2 deletions gradle.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
version=1.0.6-SNAPSHOT
version=1.1.0-SNAPSHOT
org.gradle.caching=true
org.gradle.configuration-cache=false
org.gradle.jvmargs=-Xmx2g
org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled
org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true
org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true
kapt.use.k2=true
13 changes: 12 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
[versions]
assertj = "3.27.3"
dokka = "2.0.0"
jackson = "2.19.0"
java = "17"
Expand All @@ -14,8 +15,10 @@ logbackClassic = "1.5.18"
mockk = "1.14.2"
reactorNetty = "1.2.6"
slf4j = "2.0.17"
springBoot = "3.4.6"

[libraries]
assertj-core = { group = "org.assertj", name = "assertj-core", version.ref = "assertj" }
dokka-plugin = { group = "org.jetbrains.dokka", name = "org.jetbrains.dokka.gradle.plugin", version.ref = "dokka" }
dokka-javadoc-plugin = { group = "org.jetbrains.dokka-javadoc", name = "org.jetbrains.dokka-javadoc.gradle.plugin", version.ref = "dokka" }
jackson-bom = { group = "com.fasterxml.jackson", name = "jackson-bom", version.ref = "jackson" }
Expand All @@ -31,7 +34,15 @@ logback-classic = { group = "ch.qos.logback", name = "logback-classic", version.
mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" }
reactorNetty-http = { group = "io.projectreactor.netty", name = "reactor-netty-http", version.ref = "reactorNetty" }
slf4j-api = { group = "org.slf4j", name = "slf4j-api", version.ref = "slf4j" }
spring-boot-autoconfigureProcessor = { group = "org.springframework.boot", name = "spring-boot-autoconfigure-processor", version.ref = "springBoot" }
spring-boot-configurationProcessor = { group = "org.springframework.boot", name = "spring-boot-configuration-processor", version.ref = "springBoot" }
spring-boot-starter = { group = "org.springframework.boot", name = "spring-boot-starter", version.ref = "springBoot" }
spring-boot-starter-reactorNetty = { group = "org.springframework.boot", name = "spring-boot-starter-reactor-netty", version.ref = "springBoot" }
spring-boot-starter-test = { group = "org.springframework.boot", name = "spring-boot-starter-test", version.ref = "springBoot" }
spring-boot-starter-webflux = { group = "org.springframework.boot", name = "spring-boot-starter-webflux", version.ref = "springBoot" }

[plugins]
dokka = { id = "org.jetbrains.dokka" }
jreleaser = { id = "org.jreleaser", version.ref = "jreleaser" }
conventions = { id = "conventions" }
jreleaser = { id = "org.jreleaser", version.ref = "jreleaser" }
kotlin-springPlugin = { id = "org.jetbrains.kotlin.plugin.spring", version.ref = "kotlin" }
25 changes: 25 additions & 0 deletions logback-access-reactor-netty-spring-boot-starter/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
plugins {
alias(libs.plugins.conventions)
alias(libs.plugins.kotlin.springPlugin)
kotlin("kapt")
}

description = "Spring Boot Starter for Logback Access integration with Reactor Netty"

dependencies {
api(project(":logback-access-reactor-netty"))

implementation(libs.spring.boot.starter)
implementation(libs.slf4j.api)

provided(libs.spring.boot.starter.reactorNetty)

kapt(libs.spring.boot.autoconfigureProcessor)
kapt(libs.spring.boot.configurationProcessor)

testImplementation(libs.assertj.core)
testImplementation(libs.kotest.assertions.core.jvm)
testImplementation(libs.mockk)
testImplementation(libs.spring.boot.starter.test)
testImplementation(libs.spring.boot.starter.webflux)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package io.github.dmitrysulman.logback.access.reactor.netty.autoconfigure

import io.github.dmitrysulman.logback.access.reactor.netty.ReactorNettyAccessLogFactory
import io.github.dmitrysulman.logback.access.reactor.netty.joran.LogbackAccessJoranConfigurator
import org.springframework.boot.autoconfigure.AutoConfiguration
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Bean
import org.springframework.core.env.Environment
import org.springframework.core.io.ResourceLoader
import org.springframework.util.ResourceUtils
import reactor.netty.http.server.HttpServer

/**
* [Auto-configuration][EnableAutoConfiguration] for the Logback Access integration with Reactor Netty.
*/
@AutoConfiguration
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
@ConditionalOnClass(HttpServer::class)
@ConditionalOnProperty(prefix = "logback.access.reactor.netty", name = ["enabled"], havingValue = "true", matchIfMissing = true)
@EnableConfigurationProperties(ReactorNettyAccessLogProperties::class)
class ReactorNettyAccessLogFactoryAutoConfiguration {
@Bean
@ConditionalOnMissingBean
fun reactorNettyAccessLogFactory(
properties: ReactorNettyAccessLogProperties,
resourceLoader: ResourceLoader,
environment: Environment,
) = ReactorNettyAccessLogFactory(
getConfigUrl(properties, resourceLoader),
LogbackAccessJoranConfigurator(environment),
properties.debug ?: false,
)

@Bean
@ConditionalOnMissingBean
fun reactorNettyAccessLogWebServerFactoryCustomizer(reactorNettyAccessLogFactory: ReactorNettyAccessLogFactory) =
ReactorNettyAccessLogWebServerFactoryCustomizer(true, reactorNettyAccessLogFactory)

private fun getConfigUrl(
properties: ReactorNettyAccessLogProperties,
resourceLoader: ResourceLoader,
) = properties.config?.let { ResourceUtils.getURL(it) }
?: getDefaultConfigurationResource(resourceLoader).url

private fun getDefaultConfigurationResource(resourceLoader: ResourceLoader) =
resourceLoader
.getResource("${ResourceUtils.FILE_URL_PREFIX}${ReactorNettyAccessLogFactory.DEFAULT_CONFIG_FILE_NAME}")
.takeIf { it.exists() }
?: resourceLoader
.getResource("${ResourceUtils.CLASSPATH_URL_PREFIX}${ReactorNettyAccessLogFactory.DEFAULT_CONFIG_FILE_NAME}")
.takeIf { it.exists() }
?: resourceLoader.getResource("${ResourceUtils.CLASSPATH_URL_PREFIX}${ReactorNettyAccessLogFactory.DEFAULT_CONFIGURATION}")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package io.github.dmitrysulman.logback.access.reactor.netty.autoconfigure

import org.springframework.boot.context.properties.ConfigurationProperties

/**
* [@ConfigurationProperties][ConfigurationProperties] for the Logback Access integration with Reactor Netty.
*/
@ConfigurationProperties("logback.access.reactor.netty")
class ReactorNettyAccessLogProperties {
/**
* Enable Logback Access Reactor Netty auto-configuration.
*/
var enabled: Boolean? = null

/**
* Config file name.
*/
var config: String? = null

/**
* Enable debug mode.
*/
var debug: Boolean? = null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package io.github.dmitrysulman.logback.access.reactor.netty.autoconfigure

import io.github.dmitrysulman.logback.access.reactor.netty.ReactorNettyAccessLogFactory
import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory
import org.springframework.boot.web.server.WebServerFactoryCustomizer

/**
* [WebServerFactoryCustomizer] of the [NettyReactiveWebServerFactory] for the Logback Access integration.
*/
class ReactorNettyAccessLogWebServerFactoryCustomizer(
private val enableAccessLog: Boolean,
private val reactorNettyAccessLogFactory: ReactorNettyAccessLogFactory,
) : WebServerFactoryCustomizer<NettyReactiveWebServerFactory> {
override fun customize(factory: NettyReactiveWebServerFactory) {
factory.addServerCustomizers(
{ server ->
server.accessLog(
enableAccessLog,
reactorNettyAccessLogFactory,
)
},
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package io.github.dmitrysulman.logback.access.reactor.netty.joran

import ch.qos.logback.access.common.joran.JoranConfigurator
import ch.qos.logback.core.joran.spi.ElementSelector
import ch.qos.logback.core.joran.spi.RuleStore
import ch.qos.logback.core.model.Model
import ch.qos.logback.core.model.processor.DefaultProcessor
import org.springframework.core.env.Environment
import java.util.function.Supplier

/**
* Extended version of the Logback Access [JoranConfigurator] that adds support of `<springProfile>` tags.
*
* See [SpringBootJoranConfigurator](https://github.com/spring-projects/spring-boot/blob/main/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/SpringBootJoranConfigurator.java).
*/
class LogbackAccessJoranConfigurator(
private val environment: Environment,
) : JoranConfigurator() {
override fun addElementSelectorAndActionAssociations(rs: RuleStore) {
super.addElementSelectorAndActionAssociations(rs)
rs.addRule(ElementSelector("*/springProfile"), ::LogbackAccessSpringProfileAction)
rs.addTransparentPathPart("springProfile")
}

override fun sanityCheck(topModel: Model) {
super.sanityCheck(topModel)
performCheck(LogbackAccessSpringProfileWithinSecondPhaseElementSanityChecker(), topModel)
}

override fun addModelHandlerAssociations(defaultProcessor: DefaultProcessor) {
defaultProcessor.addHandler(LogbackAccessSpringProfileModel::class.java) { _, _ ->
LogbackAccessSpringProfileModelHandler(context, environment)
}
super.addModelHandlerAssociations(defaultProcessor)
}

override fun buildModelInterpretationContext() {
super.buildModelInterpretationContext()
modelInterpretationContext.configuratorSupplier =
Supplier {
LogbackAccessJoranConfigurator(environment).also { it.context = this.context }
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package io.github.dmitrysulman.logback.access.reactor.netty.joran

import ch.qos.logback.core.joran.action.BaseModelAction
import ch.qos.logback.core.joran.spi.SaxEventInterpretationContext
import ch.qos.logback.core.model.Model
import org.xml.sax.Attributes

/**
* Logback Access [BaseModelAction] for `<springProfile>` tags. Allows a section of a
* Logback Access configuration to only be enabled when a specific profile is active.
*
* See [SpringProfileAction](https://github.com/spring-projects/spring-boot/blob/main/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/SpringProfileAction.java).
*
* @see [LogbackAccessSpringProfileModel]
* @see [LogbackAccessSpringProfileModelHandler]
*/
class LogbackAccessSpringProfileAction : BaseModelAction() {
override fun buildCurrentModel(
interpretationContext: SaxEventInterpretationContext,
name: String,
attributes: Attributes,
): Model =
LogbackAccessSpringProfileModel().apply {
this.name = attributes.getValue(NAME_ATTRIBUTE)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.github.dmitrysulman.logback.access.reactor.netty.joran

import ch.qos.logback.core.model.NamedModel

/**
* Logback Access [NamedModel] to support `<springProfile>` tags.
*
* See [SpringProfileModel](https://github.com/spring-projects/spring-boot/blob/main/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/SpringProfileModel.java).
*
* @see [LogbackAccessSpringProfileAction]
* @see [LogbackAccessSpringProfileModelHandler]
*/
class LogbackAccessSpringProfileModel : NamedModel()
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package io.github.dmitrysulman.logback.access.reactor.netty.joran

import ch.qos.logback.core.Context
import ch.qos.logback.core.model.Model
import ch.qos.logback.core.model.processor.ModelHandlerBase
import ch.qos.logback.core.model.processor.ModelInterpretationContext
import ch.qos.logback.core.util.OptionHelper
import org.springframework.core.env.Environment

/**
* Logback Access [ModelHandlerBase] model handler to support `<springProfile>` tags.
*
* See [SpringProfileModelHandler](https://github.com/spring-projects/spring-boot/blob/main/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/SpringProfileModelHandler.java).
*
* @see [LogbackAccessSpringProfileModel]
* @see [LogbackAccessSpringProfileAction]
*/
class LogbackAccessSpringProfileModelHandler(
context: Context,
private val environment: Environment,
) : ModelHandlerBase(context) {
override fun handle(
mic: ModelInterpretationContext,
model: Model,
) {
val profiles =
(model as LogbackAccessSpringProfileModel)
.name
?.split(",")
?.map { OptionHelper.substVars(it.trim(), mic, context) }
?: emptyList()
if (profiles.isEmpty() || !environment.matchesProfiles(*profiles.toTypedArray())) {
model.deepMarkAsSkipped()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package io.github.dmitrysulman.logback.access.reactor.netty.joran

import ch.qos.logback.core.joran.sanity.SanityChecker
import ch.qos.logback.core.model.AppenderModel
import ch.qos.logback.core.model.Model
import ch.qos.logback.core.spi.ContextAwareBase

/**
* [SanityChecker] to ensure that `springProfile` elements are not nested
* within second-phase elements.
*
* See [SpringProfileIfNestedWithinSecondPhaseElementSanityChecker](https://github.com/spring-projects/spring-boot/blob/main/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/SpringProfileIfNestedWithinSecondPhaseElementSanityChecker.java).
*/
class LogbackAccessSpringProfileWithinSecondPhaseElementSanityChecker :
ContextAwareBase(),
SanityChecker {
override fun check(model: Model?) {
if (model == null) return

val secondsPhaseModels = mutableListOf<Model>()

SECOND_PHASE_TYPES.forEach {
deepFindAllModelsOfType(it, secondsPhaseModels, model)
}

deepFindNestedSubModelsOfType(LogbackAccessSpringProfileModel::class.java, secondsPhaseModels)
.takeIf { it.isNotEmpty() }
?.also {
addWarn("<springProfile> elements cannot be nested within an <appender> element")
}?.forEach {
val first = it.first
val second = it.second
addWarn(
"Element <${first.tag}> at line ${first.lineNumber} contains a nested <${second.tag}> element at line ${second.lineNumber}",
)
}
}

companion object {
private val SECOND_PHASE_TYPES =
listOf(
AppenderModel::class.java,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
io.github.dmitrysulman.logback.access.reactor.netty.autoconfigure.ReactorNettyAccessLogFactoryAutoConfiguration
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Module logback-access-reactor-netty-spring-boot-starter

Spring Boot Starter for using Logback Access integration with Reactor Netty.

# Package io.github.dmitrysulman.logback.access.reactor.netty.autoconfigure

This package contains the auto-configuration and configuration properties classes.

# Package io.github.dmitrysulman.logback.access.reactor.netty.joran

This package contains the custom Logback Access Joran configurator.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>common</pattern>
</encoder>
</appender>

<appender-ref ref="STDOUT" />
</configuration>
Loading
Loading