Skip to content

Commit

Permalink
logbook-ktor
Browse files Browse the repository at this point in the history
  • Loading branch information
sokomishalov committed Aug 16, 2021
1 parent 5fc9079 commit b9e5537
Show file tree
Hide file tree
Showing 23 changed files with 1,336 additions and 1 deletion.
49 changes: 48 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Logbook is ready to use out of the box for most common setups. Even for uncommon
- OkHttp 2.x **or 3.x** (optional)
- Spring 4.x **or 5.x** (optional)
- Spring Boot 1.x **or 2.x** (optional)
- Ktor (optional)
- logstash-logback-encoder 5.x (optional)

## Installation
Expand Down Expand Up @@ -70,7 +71,8 @@ Alternatively, you can import our *bill of materials*...
</dependencyManagement>
```

... which allows you to omit versions:
<details>
<summary>... which allows you to omit versions:</summary>

```xml
<dependency>
Expand Down Expand Up @@ -109,11 +111,28 @@ Alternatively, you can import our *bill of materials*...
<groupId>org.zalando</groupId>
<artifactId>logbook-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.zalando</groupId>
<artifactId>logbook-ktor-common</artifactId>
</dependency>
<dependency>
<groupId>org.zalando</groupId>
<artifactId>logbook-ktor-client</artifactId>
</dependency>
<dependency>
<groupId>org.zalando</groupId>
<artifactId>logbook-ktor-server</artifactId>
</dependency>
<dependency>
<groupId>org.zalando</groupId>
<artifactId>logbook-ktor</artifactId>
</dependency>
<dependency>
<groupId>org.zalando</groupId>
<artifactId>logbook-logstash</artifactId>
</dependency>
```
</details>

The logbook logger must be configured to trace level in order to log the requests and responses. With Spring Boot 2 (using Logback) this can be accomplised by adding the following line to your `application.properties`

Expand Down Expand Up @@ -736,6 +755,34 @@ OkHttpClient client = new OkHttpClient.Builder()
.build();
```

### Ktor

The `logbook-ktor-client` module contains:

A `LogbookClient` to be used with an `HttpClient`:

```kotlin
private val client = HttpClient(CIO) {
install(LogbookClient) {
logbook = logbook
}
}
```

The `logbook-ktor-server` module contains:

A `LogbookServer` to be used with an `Application`:

```kotlin
private val server = embeddedServer(CIO) {
install(LogbookServer) {
logbook = logbook
}
}
```

Alternatively, you can use `logbook-ktor`, which ships both `logbook-ktor-client` and `logbook-ktor-server` modules.

### Spring
The `logbook-spring` module contains a `ClientHttpRequestInterceptor` to use with `RestTemplate`:

Expand Down
20 changes: 20 additions & 0 deletions logbook-bom/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,26 @@
<artifactId>logbook-servlet</artifactId>
<version>2.12.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.zalando</groupId>
<artifactId>logbook-ktor</artifactId>
<version>2.12.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.zalando</groupId>
<artifactId>logbook-ktor-common</artifactId>
<version>2.12.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.zalando</groupId>
<artifactId>logbook-ktor-client</artifactId>
<version>2.12.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.zalando</groupId>
<artifactId>logbook-ktor-server</artifactId>
<version>2.12.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.zalando</groupId>
<artifactId>logbook-spring-boot-autoconfigure</artifactId>
Expand Down
98 changes: 98 additions & 0 deletions logbook-ktor-client/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<?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-client</artifactId>


<dependencies>
<dependency>
<groupId>org.zalando</groupId>
<artifactId>logbook-api</artifactId>
</dependency>
<dependency>
<groupId>org.zalando</groupId>
<artifactId>logbook-ktor-common</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
<version>1.5.21</version>
</dependency>
<dependency>
<groupId>io.ktor</groupId>
<artifactId>ktor-client-core-jvm</artifactId>
<version>1.6.2</version>
</dependency>
<!-- testing -->
<dependency>
<groupId>io.ktor</groupId>
<artifactId>ktor-client-cio-jvm</artifactId>
<version>1.6.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ktor</groupId>
<artifactId>ktor-server-cio</artifactId>
<version>1.6.2</version>
<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>

<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>1.5.21</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>
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
@file:Suppress(
"SimpleRedundantLet" // jacoco workaround
)

package org.zalando.logbook.client

import io.ktor.client.request.*
import io.ktor.http.*
import io.ktor.http.HttpProtocolVersion.Companion.HTTP_1_1
import io.ktor.util.*
import org.zalando.logbook.HttpHeaders
import org.zalando.logbook.HttpRequest
import org.zalando.logbook.Origin
import org.zalando.logbook.common.State
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.toString()
override fun getOrigin(): Origin = Origin.LOCAL
override fun getHeaders(): HttpHeaders = HttpHeaders.of(request.headers.build().toMap())
override fun getContentType(): String? = request.contentType()?.let { it.toString().substringBefore(";") }
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 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
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
@file:Suppress(
"SimpleRedundantLet" // jacoco workaround
)

package org.zalando.logbook.client

import io.ktor.http.*
import io.ktor.util.*
import org.zalando.logbook.HttpHeaders
import org.zalando.logbook.HttpResponse
import org.zalando.logbook.Origin
import org.zalando.logbook.common.State
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()?.let { it.toString().substringBefore(";") }
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
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
@file:Suppress(
"BlockingMethodInNonBlockingContext"
)

package org.zalando.logbook.client

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 org.apiguardian.api.API
import org.apiguardian.api.API.Status.EXPERIMENTAL
import org.zalando.logbook.Logbook
import org.zalando.logbook.Logbook.ResponseProcessingStage
import org.zalando.logbook.common.ExperimentalLogbookKtorApi
import org.zalando.logbook.common.readBytes


@API(status = EXPERIMENTAL)
@ExperimentalLogbookKtorApi
class LogbookClient(
val logbook: Logbook
) {

class Config {
var logbook: Logbook = Logbook.create()
}

companion object : HttpClientFeature<Config, LogbookClient> {
private val responseProcessingStageKey: AttributeKey<ResponseProcessingStage> = AttributeKey("Logbook.ResponseProcessingStage")
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)
if (request.shouldBuffer()) {
val content = (context.body as OutgoingContent).readBytes(scope)
request.buffer(content)
}
val responseStage = requestWritingStage.write()
context.attributes.put(responseProcessingStageKey, responseStage)
proceed()
}

scope.receivePipeline.intercept(HttpReceivePipeline.After) {
val (loggingContent, responseContent) = it.content.split(it)

val responseProcessingStage = it.call.attributes[responseProcessingStageKey]
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)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.zalando.logbook.client

import io.ktor.client.request.*
import io.ktor.http.*
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import kotlin.text.Charsets.US_ASCII


internal class ClientRequestUnitTest {

@Test
fun `ClientRequest unit test`() {
val req = HttpRequestBuilder().apply {
headers.append(HttpHeaders.ContentType, "application/json; charset=us-ascii")
}
val request = ClientRequest(req)
assertThat(request.contentType).isEqualTo("application/json")
assertThat(request.charset).isEqualTo(US_ASCII)
}
}
Loading

0 comments on commit b9e5537

Please sign in to comment.