Skip to content
Merged
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
7 changes: 1 addition & 6 deletions buildSrc/src/main/kotlin/conventions.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jlleitschuh.gradle.ktlint.KtlintExtension

plugins {
Expand All @@ -23,14 +22,10 @@ repositories {
}

kotlin {
compilerOptions {
jvmTarget = JvmTarget.JVM_17
}
jvmToolchain(21)
jvmToolchain(17)
}

java {
targetCompatibility = JavaVersion.VERSION_17
withSourcesJar()
}

Expand Down
3 changes: 3 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ jreleaser = "1.18.0"
junit = "5.12.2"
kotest = "5.9.1"
kotlin = "2.1.20"
kotlinxCoroutines = "1.10.2"
ktlint = "12.2.0"
logbackAccess = "2.0.6"
logbackClassic = "1.5.18"
Expand All @@ -18,6 +19,8 @@ junit-jupiter = { group = "org.junit.jupiter", name = "junit-jupiter", version.r
junit-platformLauncher = { group = "org.junit.platform", name = "junit-platform-launcher" }
kotest-assertions-core-jvm = { group = "io.kotest", name = "kotest-assertions-core-jvm", version.ref = "kotest" }
kotlin-gradlePlugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" }
kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinxCoroutines" }
kotlinx-coroutines-reactor = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-reactor", version.ref = "kotlinxCoroutines" }
ktlintPlugin = { group = "org.jlleitschuh.gradle.ktlint", name = "org.jlleitschuh.gradle.ktlint.gradle.plugin", version.ref = "ktlint" }
logback-access-common = { group = "ch.qos.logback.access", name = "logback-access-common", version.ref = "logbackAccess" }
logback-classic = { group = "ch.qos.logback", name = "logback-classic", version.ref = "logbackClassic" }
Expand Down
2 changes: 2 additions & 0 deletions logback-access-reactor-netty/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ dependencies {

testImplementation(libs.junit.jupiter)
testImplementation(libs.kotest.assertions.core.jvm)
testImplementation(libs.kotlinx.coroutines.core)
testImplementation(libs.kotlinx.coroutines.reactor)
testImplementation(libs.logback.classic)
testImplementation(libs.logback.core)
testImplementation(libs.mockk)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

import ch.qos.logback.access.common.spi.AccessContext
import ch.qos.logback.access.common.spi.IAccessEvent
import jakarta.servlet.http.Cookie
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import reactor.netty.http.server.logging.AccessLogArgProvider
import java.io.Serializable
import java.net.InetSocketAddress
Expand Down Expand Up @@ -37,7 +40,7 @@
private val _sequenceNumber = context.sequenceNumberGenerator?.nextSequenceNumber() ?: 0
private val _elapsedTime = argProvider.duration()
private val _elapsedTimeSeconds = _elapsedTime / 1000
private val _requestUri by lazy { argProvider.uri()?.toString()?.substringBefore("?") ?: NA }
private val _requestPath by lazy { argProvider.uri()?.toString()?.substringBefore("?") ?: NA }
private val _queryString by lazy {
argProvider.uri()?.let { uri ->
uri
Expand All @@ -47,6 +50,7 @@
.orEmpty()
} ?: NA
}
private val _requestUrl by lazy { "$_method ${argProvider.uri()?.toString() ?: NA} $_protocol" }
private val _remoteHost by lazy {
val remoteAddress = argProvider.connectionInformation()?.connectionRemoteAddress()
if (remoteAddress is InetSocketAddress) {
Expand All @@ -58,22 +62,19 @@
private val _remoteUser by lazy { argProvider.user() ?: NA }
private val _protocol by lazy { argProvider.protocol() ?: NA }
private val _method by lazy { argProvider.method()?.toString() ?: NA }
private lateinit var _threadName: String
private var _threadName: String? = null
private val _requestParameterMap by lazy {
_queryString
.takeIf { it.isNotEmpty() && it != NA }
.takeIf { it.length > 1 }
?.substring(1)
?.split("&")
?.asSequence()
?.mapNotNull {
val index = it.indexOf("=")
if (index in 1..it.length - 2) {
it.substring(0, index) to it.substring(index + 1)
} else {
null
}
}?.groupBy({ URLDecoder.decode(it.first, StandardCharsets.UTF_8) }) {
URLDecoder.decode(it.second, StandardCharsets.UTF_8)
}?.mapValues { it.value.toTypedArray() }
if (index !in 1..it.length - 2) return@mapNotNull null
it.substring(0, index) to it.substring(index + 1)
}?.groupBy({ it.first.decodeCatching() }) { it.second.decodeCatching() }
?.mapValues { it.value.toTypedArray() }
?: emptyMap()
}
private val _remoteAddr by lazy {
Expand All @@ -85,9 +86,27 @@
} ?: NA
}
private val _cookieMap by lazy {
argProvider.cookies()?.entries?.associate {
it.key.toString() to (it.value.firstOrNull()?.value() ?: NA)
} ?: emptyMap()
argProvider
.cookies()
?.asSequence()
?.mapNotNull { (name, values) ->
if (name.isNullOrBlank()) return@mapNotNull null
val value = values.firstOrNull()?.value() ?: return@mapNotNull null
name.toString() to value
}?.toMap() ?: emptyMap()
}
private val _cookieList by lazy {
argProvider
.cookies()
?.mapNotNull { (name, values) ->
if (name.isNullOrBlank()) return@mapNotNull null
val value = values.firstOrNull()?.value() ?: return@mapNotNull null
try {
Cookie(name.toString(), value)
} catch (_: Exception) {

Check warning on line 106 in logback-access-reactor-netty/src/main/kotlin/io/github/dmitrysulman/logback/access/reactor/netty/AccessEvent.kt

View check run for this annotation

Codecov / codecov/patch

logback-access-reactor-netty/src/main/kotlin/io/github/dmitrysulman/logback/access/reactor/netty/AccessEvent.kt#L106

Added line #L106 was not covered by tests
null
}
} ?: emptyList()
}
private val _contentLength by lazy { _serverAdapter.contentLength }
private val _statusCode by lazy { _serverAdapter.statusCode }
Expand All @@ -97,15 +116,26 @@
argProvider
.requestHeaderIterator()
?.asSequence()
?.associate { it.key.toString() to it.value.toString() }
?.mapNotNull { (name, value) ->
if (name.isNullOrEmpty()) return@mapNotNull null
if (value == null) return@mapNotNull null
name.toString() to value.toString()
}?.toMap()
?: emptyMap()
}

@Transient
private val _serverAdapter = ReactorNettyServerAdapter(argProvider)
private val _serverAdapter by lazy { ReactorNettyServerAdapter(argProvider) }

private fun String.decodeCatching() =
try {
URLDecoder.decode(this, StandardCharsets.UTF_8)
} catch (_: Exception) {
this
}

override fun prepareForDeferredProcessing() {
requestURI
requestURL
queryString
remoteHost
remoteUser
Expand All @@ -114,16 +144,19 @@
requestParameterMap
remoteAddr
getCookieMap()
cookies
contentLength
statusCode
localPort
responseHeaderMap
requestHeaderMap
threadName
serverAdapter.requestTimestamp
}

override fun getRequest() = null
override fun getRequest(): HttpServletRequest? = null

override fun getResponse() = null
override fun getResponse(): HttpServletResponse? = null

override fun getTimeStamp() = _timeStamp

Expand All @@ -133,9 +166,9 @@

override fun getElapsedSeconds() = _elapsedTimeSeconds

override fun getRequestURI() = _requestUri
override fun getRequestURI() = _requestPath

override fun getRequestURL() = "$_method $_requestUri$_queryString $_protocol"
override fun getRequestURL() = _requestUrl

override fun getRemoteHost() = _remoteHost

Expand Down Expand Up @@ -171,6 +204,8 @@

override fun getRequestParameter(key: String) = _requestParameterMap[key] ?: NA_ARRAY

override fun getCookies() = _cookieList

private fun getCookieMap() = _cookieMap

override fun getCookie(key: String) = getCookieMap()[key] ?: NA
Expand All @@ -189,7 +224,7 @@

override fun getResponseHeader(key: String) = _responseHeaderMap[key] ?: NA

override fun getResponseHeaderMap(): Map<String, String> = _responseHeaderMap
override fun getResponseHeaderMap() = _responseHeaderMap

override fun getResponseHeaderNameList() = _responseHeaderMap.keys.toList()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class AccessLog(
public override fun log() {
try {
val accessEvent = AccessEvent(argProvider, accessContext)
accessEvent.threadName = Thread.currentThread().name
accessEvent.setThreadName(Thread.currentThread().name)
if (accessContext.getFilterChainDecision(accessEvent) != FilterReply.DENY) {
accessContext.callAppenders(accessEvent)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package io.github.dmitrysulman.logback.access.reactor.netty

import reactor.netty.http.server.HttpServer

/**
* Extension for [HttpServer] providing [HttpServer.accessLog] method alternative.
*
* @param reactorNettyAccessLogFactory The [ReactorNettyAccessLogFactory] instance for access log configuration.
*/
fun HttpServer.enableLogbackAccess(reactorNettyAccessLogFactory: ReactorNettyAccessLogFactory): HttpServer =
accessLog(true, reactorNettyAccessLogFactory)
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,33 @@ package io.github.dmitrysulman.logback.access.reactor.netty

import ch.qos.logback.access.common.spi.ServerAdapter
import reactor.netty.http.server.logging.AccessLogArgProvider
import java.io.Serializable

class ReactorNettyServerAdapter(
@Transient
private val argProvider: AccessLogArgProvider,
) : ServerAdapter {
override fun getRequestTimestamp() = argProvider.accessDateTime()?.toInstant()?.toEpochMilli() ?: 0

override fun getContentLength() = argProvider.contentLength()

override fun getStatusCode() = argProvider.status()?.toString()?.toIntOrNull() ?: -1

override fun buildResponseHeaderMap() =
) : ServerAdapter,
Serializable {
private val _requestTimestamp by lazy { argProvider.accessDateTime()?.toInstant()?.toEpochMilli() ?: 0 }
private val _contentLength by lazy { argProvider.contentLength() }
private val _statusCode by lazy { argProvider.status()?.toString()?.toIntOrNull() ?: -1 }
private val _responseHeaderMap by lazy {
argProvider
.responseHeaderIterator()
?.asSequence()
?.associate { it.key.toString() to it.value.toString() }
?.mapNotNull { (name, value) ->
if (name.isNullOrEmpty()) return@mapNotNull null
if (value == null) return@mapNotNull null
name.toString() to value.toString()
}?.toMap()
?: emptyMap()
}

override fun getRequestTimestamp() = _requestTimestamp

override fun getContentLength() = _contentLength

override fun getStatusCode() = _statusCode

override fun buildResponseHeaderMap() = _responseHeaderMap
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package io.github.dmitrysulman.logback.access.reactor.netty.integration;

import ch.qos.logback.access.common.joran.JoranConfigurator;
import io.github.dmitrysulman.logback.access.reactor.netty.ReactorNettyAccessLogFactory;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import reactor.netty.http.client.HttpClient;
import reactor.netty.http.server.HttpServer;

import java.time.Duration;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

public class JavaIntegrationTests {
@Test
public void smokeTest() throws InterruptedException {
var accessLogFactory =
new ReactorNettyAccessLogFactory("logback-access-stdout.xml", new JoranConfigurator(), true);
var eventCaptureAppender = (EventCaptureAppender) accessLogFactory.getAccessContext().getAppender("CAPTURE");
var responseBody = "test";
var server = HttpServer
.create()
.accessLog(true, accessLogFactory)
.handle((request, response) -> response.sendByteArray(Mono.just(responseBody.getBytes())))
.bindNow();
var uri = "/test";
var response = HttpClient
.create()
.port(server.port())
.get()
.uri(uri)
.response()
.block(Duration.ofSeconds(10));

assertNotNull(response);
assertEquals(200, response.status().code());

Thread.sleep(100);
assertEquals(1, eventCaptureAppender.getList().size());
assertEquals(uri, eventCaptureAppender.getList().get(0).getRequestURI());
assertEquals(responseBody.length(), eventCaptureAppender.getList().get(0).getContentLength());

server.disposeNow();
}
}
Loading
Loading