-
Notifications
You must be signed in to change notification settings - Fork 264
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
ee4ad1f
commit 5c8d2cc
Showing
16 changed files
with
767 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<project xmlns="http://maven.apache.org/POM/4.0.0" | ||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||
<modelVersion>4.0.0</modelVersion> | ||
|
||
<parent> | ||
<groupId>org.zalando</groupId> | ||
<artifactId>logbook-parent</artifactId> | ||
<version>2.12.0-SNAPSHOT</version> | ||
</parent> | ||
|
||
<artifactId>logbook-ktor</artifactId> | ||
|
||
<properties> | ||
<kotlin.version>1.5.21</kotlin.version> | ||
<ktor.version>1.6.2</ktor.version> | ||
</properties> | ||
|
||
<dependencies> | ||
<dependency> | ||
<groupId>org.zalando</groupId> | ||
<artifactId>logbook-api</artifactId> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.jetbrains.kotlin</groupId> | ||
<artifactId>kotlin-stdlib</artifactId> | ||
</dependency> | ||
<dependency> | ||
<groupId>io.ktor</groupId> | ||
<artifactId>ktor-client-core-jvm</artifactId> | ||
</dependency> | ||
<dependency> | ||
<groupId>io.ktor</groupId> | ||
<artifactId>ktor-server-core</artifactId> | ||
</dependency> | ||
<!-- testing --> | ||
<dependency> | ||
<groupId>io.ktor</groupId> | ||
<artifactId>ktor-client-cio-jvm</artifactId> | ||
<scope>test</scope> | ||
</dependency> | ||
<dependency> | ||
<groupId>io.ktor</groupId> | ||
<artifactId>ktor-client-logging-jvm</artifactId> | ||
<scope>test</scope> | ||
</dependency> | ||
<dependency> | ||
<groupId>io.ktor</groupId> | ||
<artifactId>ktor-server-cio</artifactId> | ||
<scope>test</scope> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.jetbrains.kotlin</groupId> | ||
<artifactId>kotlin-test-junit5</artifactId> | ||
<scope>test</scope> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.zalando</groupId> | ||
<artifactId>logbook-core</artifactId> | ||
<scope>test</scope> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.zalando</groupId> | ||
<artifactId>logbook-test</artifactId> | ||
<scope>test</scope> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.slf4j</groupId> | ||
<artifactId>slf4j-nop</artifactId> | ||
<scope>test</scope> | ||
</dependency> | ||
</dependencies> | ||
|
||
<dependencyManagement> | ||
<dependencies> | ||
<dependency> | ||
<groupId>io.ktor</groupId> | ||
<artifactId>ktor-bom</artifactId> | ||
<version>${ktor.version}</version> | ||
<type>pom</type> | ||
<scope>import</scope> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.jetbrains.kotlin</groupId> | ||
<artifactId>kotlin-bom</artifactId> | ||
<version>${kotlin.version}</version> | ||
<type>pom</type> | ||
<scope>import</scope> | ||
</dependency> | ||
</dependencies> | ||
</dependencyManagement> | ||
|
||
<build> | ||
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory> | ||
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory> | ||
<plugins> | ||
<plugin> | ||
<groupId>org.jetbrains.kotlin</groupId> | ||
<artifactId>kotlin-maven-plugin</artifactId> | ||
<version>${kotlin.version}</version> | ||
<configuration> | ||
<jvmTarget>${java.version}</jvmTarget> | ||
<args> | ||
<arg>-Xopt-in=kotlin.RequiresOptIn</arg> | ||
</args> | ||
</configuration> | ||
<executions> | ||
<execution> | ||
<id>compile</id> | ||
<phase>compile</phase> | ||
<goals> | ||
<goal>compile</goal> | ||
</goals> | ||
</execution> | ||
<execution> | ||
<id>test-compile</id> | ||
<phase>test-compile</phase> | ||
<goals> | ||
<goal>test-compile</goal> | ||
</goals> | ||
</execution> | ||
</executions> | ||
</plugin> | ||
</plugins> | ||
</build> | ||
</project> |
38 changes: 38 additions & 0 deletions
38
logbook-ktor/src/main/kotlin/org/zalando/logbook/ktor/ClientRequest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
package org.zalando.logbook.ktor | ||
|
||
import io.ktor.client.request.* | ||
import io.ktor.http.* | ||
import io.ktor.util.* | ||
import org.zalando.logbook.HttpHeaders | ||
import org.zalando.logbook.HttpRequest | ||
import org.zalando.logbook.Origin | ||
import java.nio.charset.Charset | ||
import java.util.* | ||
import java.util.concurrent.atomic.AtomicReference | ||
import kotlin.text.Charsets.UTF_8 | ||
|
||
|
||
internal class ClientRequest( | ||
private val request: HttpRequestBuilder | ||
) : HttpRequest { | ||
private val state: AtomicReference<State> = AtomicReference(State.Unbuffered) | ||
|
||
override fun getProtocolVersion(): String = "HTTP/1.1" // fixme extract the real one | ||
override fun getOrigin(): Origin = Origin.LOCAL | ||
override fun getHeaders(): HttpHeaders = HttpHeaders.of(request.headers.build().toMap()) | ||
override fun getContentType(): String? = request.contentType()?.contentType | ||
override fun getCharset(): Charset = request.charset() ?: UTF_8 | ||
override fun getRemote(): String = "localhost" | ||
override fun getMethod(): String = request.method.value | ||
override fun getScheme(): String = request.url.protocol.name | ||
override fun getHost(): String = request.host | ||
override fun getPort(): Optional<Int> = Optional.of(request.port) | ||
override fun getPath(): String = request.url.encodedPath | ||
override fun getQuery(): String = request.url.buildString().substringAfter("?", "") | ||
override fun getRequestUri(): String = request.url.buildString() | ||
override fun withBody(): HttpRequest = apply { state.updateAndGet { it.with() } } | ||
override fun withoutBody(): HttpRequest = apply { state.updateAndGet { it.without() } } | ||
override fun getBody(): ByteArray = state.get().body | ||
internal fun buffer(bytes: ByteArray): State = state.updateAndGet { it.buffer(bytes) } | ||
internal fun shouldBuffer(): Boolean = state.get() is State.Offering | ||
} |
30 changes: 30 additions & 0 deletions
30
logbook-ktor/src/main/kotlin/org/zalando/logbook/ktor/ClientResponse.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
package org.zalando.logbook.ktor | ||
|
||
import io.ktor.http.* | ||
import io.ktor.util.* | ||
import org.zalando.logbook.HttpHeaders | ||
import org.zalando.logbook.HttpResponse | ||
import org.zalando.logbook.Origin | ||
import java.nio.charset.Charset | ||
import java.util.concurrent.atomic.AtomicReference | ||
import kotlin.text.Charsets.UTF_8 | ||
import io.ktor.client.statement.HttpResponse as KtorResponse | ||
|
||
|
||
internal class ClientResponse( | ||
private val response: KtorResponse | ||
) : HttpResponse { | ||
private val state: AtomicReference<State> = AtomicReference(State.Unbuffered) | ||
|
||
override fun getProtocolVersion(): String = response.version.toString() | ||
override fun getOrigin(): Origin = Origin.REMOTE | ||
override fun getHeaders(): HttpHeaders = HttpHeaders.of(response.headers.toMap()) | ||
override fun getContentType(): String? = response.contentType()?.contentType | ||
override fun getCharset(): Charset = response.charset() ?: UTF_8 | ||
override fun getStatus(): Int = response.status.value | ||
override fun withBody(): HttpResponse = apply { state.updateAndGet { it.with() } } | ||
override fun withoutBody(): HttpResponse = apply { state.updateAndGet { it.without() } } | ||
override fun getBody(): ByteArray = state.get().body | ||
internal fun buffer(bytes: ByteArray) = state.updateAndGet { it.buffer(bytes) } | ||
internal fun shouldBuffer(): Boolean = state.get() is State.Offering | ||
} |
28 changes: 28 additions & 0 deletions
28
logbook-ktor/src/main/kotlin/org/zalando/logbook/ktor/ContentUtils.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
package org.zalando.logbook.ktor | ||
|
||
import io.ktor.http.content.* | ||
import io.ktor.util.* | ||
import io.ktor.utils.io.* | ||
import io.ktor.utils.io.core.* | ||
import kotlinx.coroutines.CoroutineScope | ||
import kotlinx.coroutines.Dispatchers | ||
|
||
@JvmField | ||
internal val EMPTY_BODY = ByteArray(0) | ||
|
||
internal suspend fun OutgoingContent.readBytes(scope: CoroutineScope): ByteArray = runCatching { | ||
when (this) { | ||
is OutgoingContent.ByteArrayContent -> bytes() | ||
is OutgoingContent.ReadChannelContent -> readFrom().toByteArray() | ||
is OutgoingContent.WriteChannelContent -> scope.writer(Dispatchers.Unconfined) { writeTo(channel) }.channel.toByteArray() | ||
else -> EMPTY_BODY | ||
} | ||
}.getOrElse { | ||
EMPTY_BODY | ||
} | ||
|
||
internal suspend fun ByteReadChannel.readBytes(): ByteArray = runCatching { | ||
toByteArray() | ||
}.getOrElse { | ||
EMPTY_BODY | ||
} |
30 changes: 30 additions & 0 deletions
30
logbook-ktor/src/main/kotlin/org/zalando/logbook/ktor/ExperimentalLogbookKtorApi.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
package org.zalando.logbook.ktor | ||
|
||
import kotlin.RequiresOptIn.Level.WARNING | ||
import kotlin.annotation.AnnotationRetention.BINARY | ||
import kotlin.annotation.AnnotationTarget.* | ||
|
||
|
||
/** | ||
* This annotation marks the API is considered experimental and the behavior of such API may be changed or the API may be removed completely in any further release. | ||
* | ||
* Any usage of a declaration annotated with `@ExperimentalLogbookKtorApi` must be accepted either by | ||
* annotating that usage with the [OptIn] annotation, e.g. `@OptIn(ExperimentalLogbookKtorApi::class)`, | ||
* or by using the compiler argument `-Xopt-in=org.zalando.logbook.ktor.ExperimentalLogbookKtorApi`. | ||
*/ | ||
@RequiresOptIn(level = WARNING) | ||
@Retention(BINARY) | ||
@Target( | ||
CLASS, | ||
ANNOTATION_CLASS, | ||
PROPERTY, | ||
FIELD, | ||
LOCAL_VARIABLE, | ||
VALUE_PARAMETER, | ||
CONSTRUCTOR, | ||
FUNCTION, | ||
PROPERTY_GETTER, | ||
PROPERTY_SETTER, | ||
TYPEALIAS | ||
) | ||
annotation class ExperimentalLogbookKtorApi |
69 changes: 69 additions & 0 deletions
69
logbook-ktor/src/main/kotlin/org/zalando/logbook/ktor/LogbookClient.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
@file:Suppress( | ||
"BlockingMethodInNonBlockingContext" | ||
) | ||
|
||
package org.zalando.logbook.ktor | ||
|
||
import io.ktor.client.* | ||
import io.ktor.client.features.* | ||
import io.ktor.client.features.observer.* | ||
import io.ktor.client.request.* | ||
import io.ktor.client.statement.* | ||
import io.ktor.http.content.* | ||
import io.ktor.util.* | ||
import io.ktor.utils.io.* | ||
import org.apiguardian.api.API | ||
import org.apiguardian.api.API.Status.EXPERIMENTAL | ||
import org.zalando.logbook.Logbook | ||
import org.zalando.logbook.Logbook.ResponseProcessingStage | ||
|
||
|
||
@API(status = EXPERIMENTAL) | ||
@ExperimentalLogbookKtorApi | ||
class LogbookClient( | ||
val logbook: Logbook | ||
) { | ||
|
||
class Config { | ||
var logbook: Logbook = Logbook.create() | ||
} | ||
|
||
companion object : HttpClientFeature<Config, LogbookClient> { | ||
private val stageKey: AttributeKey<ResponseProcessingStage> = AttributeKey("Logbook.Stage") | ||
override val key: AttributeKey<LogbookClient> = AttributeKey("LogbookFeature") | ||
override fun prepare(block: Config.() -> Unit): LogbookClient = LogbookClient(Config().apply(block).logbook) | ||
|
||
override fun install(feature: LogbookClient, scope: HttpClient) { | ||
scope.sendPipeline.intercept(HttpSendPipeline.Monitoring) { | ||
val request = ClientRequest(context) | ||
val requestWritingStage = feature.logbook.process(request) | ||
val body = context.body | ||
if (request.shouldBuffer() && body is OutgoingContent) { | ||
val content = body.readBytes(scope) | ||
request.buffer(content) | ||
} | ||
val responseStage = requestWritingStage.write() | ||
context.attributes.put(stageKey, responseStage) | ||
proceed() | ||
} | ||
|
||
scope.receivePipeline.intercept(HttpReceivePipeline.After) { | ||
val (loggingContent, responseContent) = it.content.split(it) | ||
|
||
val responseProcessingStage = it.call.attributes.getOrNull(stageKey) | ||
if (responseProcessingStage != null) { | ||
val response = ClientResponse(it) | ||
val responseWritingStage = responseProcessingStage.process(response) | ||
if (response.shouldBuffer() && !loggingContent.isClosedForRead) { | ||
val content = loggingContent.readBytes() | ||
response.buffer(content) | ||
} | ||
responseWritingStage.write() | ||
} | ||
|
||
val newClientCall = context.wrapWithContent(responseContent) | ||
proceedWith(newClientCall.response) | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.