diff --git a/README.md b/README.md index 133d448..9869048 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ [![Build Status](https://github.com/linked-planet/ktor-plugins/actions/workflows/default.yml/badge.svg)](https://github.com/linked-planet/ktor-plugins/actions/workflows/default.yml) [![GitHub License](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg?style=flat)](http://www.apache.org/licenses/LICENSE-2.0) +[![ktor Version](https://img.shields.io/badge/ktor-2.3.6-blue)](https://ktor.io/) Contains several useful plugins extending the [ktor][ktor] framework. @@ -9,13 +10,13 @@ Contains several useful plugins extending the [ktor][ktor] framework. ### ktor-server-onelogin-saml -Integrates [ktor][ktor] with onelogin's [java-saml][java-saml] library. +Integrates ktor-server with onelogin's [java-saml][java-saml] library. ## ktor-client ### ktor-client-awesome-logging -Awesome logging experience for [ktor][ktor] HttpClient. +Awesome logging experience for ktor-client. [ktor]: https://ktor.io diff --git a/build.gradle.kts b/build.gradle.kts index e612ecf..8e34b8e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,8 +1,8 @@ plugins { - kotlin("jvm") version "1.9.20" apply (false) + kotlin("jvm") version "1.9.21" apply (false) // derive gradle version from git tag - id("pl.allegro.tech.build.axion-release") version "1.15.5" + id("pl.allegro.tech.build.axion-release") version "1.16.0" // publishing id("io.github.gradle-nexus.publish-plugin") version "1.3.0" @@ -14,7 +14,7 @@ plugins { id("se.ascp.gradle.gradle-versions-filter") version "0.1.16" } -ext.set("kotlinVersion", "1.9.20") +ext.set("kotlinVersion", "1.9.21") ext.set("ktorVersion", "2.3.6") val libVersion: String = scmVersion.version diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7f93135..d64cd49 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 3fa8f86..1af9e09 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/ktor-client-awesome-logging/README.md b/ktor-client-awesome-logging/README.md index b10fa7e..3345f49 100644 --- a/ktor-client-awesome-logging/README.md +++ b/ktor-client-awesome-logging/README.md @@ -2,6 +2,7 @@ [![Maven Central](https://img.shields.io/maven-central/v/com.linked-planet/ktor-client-awesome-logging.svg?label=central)](https://central.sonatype.com/search?q=ktor-client-awesome-logging&namespace=com.linked-planet) [![GitHub License](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg?style=flat)](http://www.apache.org/licenses/LICENSE-2.0) +[![ktor Version](https://img.shields.io/badge/ktor-2.3.6-blue)](https://ktor.io/) Awesome logging experience for [ktor][ktor] HttpClient. diff --git a/ktor-client-awesome-logging/src/main/kotlin/com/linkedplanet/ktor/client/logging/AwesomeClientLogging.kt b/ktor-client-awesome-logging/src/main/kotlin/com/linkedplanet/ktor/client/logging/AwesomeClientLogging.kt index 1ad69b9..8639340 100644 --- a/ktor-client-awesome-logging/src/main/kotlin/com/linkedplanet/ktor/client/logging/AwesomeClientLogging.kt +++ b/ktor-client-awesome-logging/src/main/kotlin/com/linkedplanet/ktor/client/logging/AwesomeClientLogging.kt @@ -23,19 +23,16 @@ package com.linkedplanet.ktor.client.logging import io.ktor.client.* import io.ktor.client.plugins.api.* -import io.ktor.client.plugins.observer.* import io.ktor.client.request.* import io.ktor.client.statement.* import io.ktor.client.utils.* import io.ktor.http.* import io.ktor.http.content.* import io.ktor.util.* +import io.ktor.util.pipeline.* import io.ktor.utils.io.* import io.ktor.utils.io.core.* -import kotlinx.coroutines.slf4j.MDCContext -import kotlinx.coroutines.withContext import org.slf4j.LoggerFactory -import org.slf4j.MDC import org.slf4j.event.Level import java.nio.charset.Charset import java.util.* @@ -70,10 +67,16 @@ interface Logger { private val traceIdKey = AttributeKey("traceId") -@OptIn(InternalAPI::class) val AwesomeClientLogging = createClientPlugin("AwesomeClientLogging", ::AwesomeClientLoggingConfig) { val config = pluginConfig + /* + * The following line is needed so we can read the response body multiple times. + * Without this treatment, we are "consuming" the response body, making it unavailable to the application. + * -> Should not be needed anymore starting from ktor 3.0.0 (SaveBodyPlugin should provide this functionality). + */ + client.receivePipeline.intercept(HttpReceivePipeline.State) { response -> interceptReceive(config, response) } + on(SendingRequest) { request, content -> val body = if (!config.requestConfig.logBody) null @@ -86,20 +89,21 @@ val AwesomeClientLogging = createClientPlugin("AwesomeClientLogging", ::AwesomeC } logRequest(config, request, body) } +} - onResponse { response -> - val body = - if (response.status.isError() && config.responseConfig.logBodyOnError) { - val wrapped = response.call.wrapWithContent(response.content) - wrapped.response.contentType()?.let { contentType -> - val charset = contentType.charset() ?: Charsets.UTF_8 - wrapped.response.content.tryReadText(charset) ?: "[response body unavailable]" - } - } else { - null - } - logResponse(config, response, body) - } +private suspend fun PipelineContext.interceptReceive( + config: AwesomeClientLoggingConfig, + response: HttpResponse, +) { + val responseData = CachedHttpResponseData.create(response) + + val body = + if (response.status.isError() && config.responseConfig.logBodyOnError) + responseData.body.ifEmpty { "[response body unavailable]" } + else null + logResponse(config, response, body) + + proceedWith(CachedHttpResponse(response.call, responseData, response.coroutineContext)) } private suspend fun logRequest(config: AwesomeClientLoggingConfig, request: HttpRequestBuilder, body: String? = null) { @@ -186,23 +190,3 @@ class AwesomeClientLoggingConfig { ) } - -suspend fun withMdc(vararg infos: Pair, func: suspend () -> T): T = - withMdc(MDC.getCopyOfContextMap() ?: emptyMap(), infos.toMap(), func) - -private suspend fun withMdc(oldState: Map, newState: Map, func: suspend () -> T): T { - newState.entries.forEach { (key, value) -> - MDC.put(key, value.toString()) - } - return try { - withContext(MDCContext()) { - func() - } - } finally { - // the MDC context does not reliably capture the old context before switching into the new context - // this leads to values getting lost when restoring the old context, so we take care of it ourselves - oldState.entries.forEach { (key, value) -> - MDC.put(key, value) - } - } -} diff --git a/ktor-client-awesome-logging/src/main/kotlin/com/linkedplanet/ktor/client/logging/CachedHttpResponse.kt b/ktor-client-awesome-logging/src/main/kotlin/com/linkedplanet/ktor/client/logging/CachedHttpResponse.kt new file mode 100644 index 0000000..97108ce --- /dev/null +++ b/ktor-client-awesome-logging/src/main/kotlin/com/linkedplanet/ktor/client/logging/CachedHttpResponse.kt @@ -0,0 +1,51 @@ +package com.linkedplanet.ktor.client.logging + +import io.ktor.client.call.* +import io.ktor.client.statement.* +import io.ktor.http.* +import io.ktor.util.* +import io.ktor.util.date.* +import io.ktor.utils.io.* +import kotlin.coroutines.CoroutineContext + +class CachedHttpResponse( + override val call: HttpClientCall, + response: CachedHttpResponseData, + override val coroutineContext: CoroutineContext +) : HttpResponse() { + @InternalAPI + override val content: ByteReadChannel = ByteReadChannel(response.body) + override val headers: Headers = response.headers.headers.filterNot { it.name == "content-encoding" }.toHeaders() + override val requestTime: GMTDate = GMTDate(response.headers.timestamp) + override val responseTime: GMTDate = GMTDate() + override val status: HttpStatusCode = HttpStatusCode.fromValue(response.headers.statusCode) + override val version: HttpProtocolVersion = HttpProtocolVersion.HTTP_1_1 +} + +private fun List.toHeaders(): Headers = HeadersBuilder().apply { + forEach { header -> append(header.name, header.value) } +}.build() + +data class CachedHttpResponseData(val headers: CachedHttpHeaders, val body: String) { + companion object { + suspend fun create(response: HttpResponse): CachedHttpResponseData { + val body = try { + response.bodyAsText() + } catch (e: Exception) { + "" + } + return CachedHttpResponseData(CachedHttpHeaders(response), body) + } + } +} + +data class CachedHttpHeaders(val statusCode: Int, val timestamp: Long, val headers: List) { + constructor(response: HttpResponse) : this( + response.status.value, response.responseTime.timestamp, + response.headers.entries().flatMap { (name, values) -> + values.map { CachedHttpHeaderValue(name, it) } + } + ) +} + +data class CachedHttpHeaderValue(val name: String, val value: String) diff --git a/ktor-client-awesome-logging/src/main/kotlin/com/linkedplanet/ktor/client/logging/Mdc.kt b/ktor-client-awesome-logging/src/main/kotlin/com/linkedplanet/ktor/client/logging/Mdc.kt new file mode 100644 index 0000000..22d0bfa --- /dev/null +++ b/ktor-client-awesome-logging/src/main/kotlin/com/linkedplanet/ktor/client/logging/Mdc.kt @@ -0,0 +1,25 @@ +package com.linkedplanet.ktor.client.logging + +import kotlinx.coroutines.slf4j.MDCContext +import kotlinx.coroutines.withContext +import org.slf4j.MDC + +suspend fun withMdc(vararg infos: Pair, func: suspend () -> T): T = + withMdc(MDC.getCopyOfContextMap() ?: emptyMap(), infos.toMap(), func) + +private suspend fun withMdc(oldState: Map, newState: Map, func: suspend () -> T): T { + newState.entries.forEach { (key, value) -> + MDC.put(key, value.toString()) + } + return try { + withContext(MDCContext()) { + func() + } + } finally { + // the MDC context does not reliably capture the old context before switching into the new context + // this leads to values getting lost when restoring the old context, so we take care of it ourselves + oldState.entries.forEach { (key, value) -> + MDC.put(key, value) + } + } +} diff --git a/ktor-server-onelogin-saml/README.md b/ktor-server-onelogin-saml/README.md index b28a15e..49534e3 100644 --- a/ktor-server-onelogin-saml/README.md +++ b/ktor-server-onelogin-saml/README.md @@ -2,6 +2,7 @@ [![Maven Central](https://img.shields.io/maven-central/v/com.linked-planet/ktor-client-awesome-logging.svg?label=central)](https://central.sonatype.com/search?q=ktor-server-onelogin-saml&namespace=com.linked-planet) [![GitHub License](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg?style=flat)](http://www.apache.org/licenses/LICENSE-2.0) +[![ktor Version](https://img.shields.io/badge/ktor-2.3.6-blue)](https://ktor.io/) Integrates [ktor](ktor.io) with onelogin's [java-saml](https://github.com/onelogin/java-saml) library.