Skip to content

Commit cdd06e2

Browse files
committed
feat: Add support for enhanced response logging via HttpFormatter
- Enhanced response lifecycle methods across multiple classes to support verbose logging with chunk details. - Updated `ResponseDefinition` and its builders to integrate `HttpFormatter` for better structured output.
1 parent 36f7647 commit cdd06e2

File tree

13 files changed

+179
-34
lines changed

13 files changed

+179
-34
lines changed

ai-mocks-gemini/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ kotlin {
1717
commonMain {
1818
dependencies {
1919
api(libs.ktor.serialization.kotlinx.json)
20+
api(libs.ktor.sse)
2021
api(project(":ai-mocks-core"))
2122
api(project.dependencies.platform(libs.ktor.bom))
2223
}

ai-mocks-gemini/src/commonMain/kotlin/me/kpavlov/aimocks/gemini/content/GeminiStreamingContentBuildingStep.kt

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package me.kpavlov.aimocks.gemini.content
22

3+
import io.ktor.sse.TypedServerSentEvent
4+
import io.ktor.utils.io.InternalAPI
35
import kotlinx.coroutines.flow.Flow
46
import kotlinx.coroutines.flow.asFlow
57
import kotlinx.coroutines.flow.map
@@ -84,22 +86,22 @@ public class GeminiStreamingContentBuildingStep(
8486
}
8587
}
8688

89+
@OptIn(InternalAPI::class)
8790
private fun encodeChunk(
8891
chunk: GenerateContentResponse,
8992
sse: Boolean,
9093
lastChunk: Boolean = false,
9194
): String {
92-
val json =
93-
Json.encodeToString(
94-
value = chunk,
95-
serializer = GenerateContentResponse.serializer(),
96-
)
9795
return if (sse) {
98-
"data: $json\r\n\r\n"
96+
TypedServerSentEvent(
97+
data = chunk,
98+
).toString {
99+
Json.encodeToString(it)
100+
}
99101
} else if (lastChunk) {
100-
json
102+
Json.encodeToString(value = chunk)
101103
} else {
102-
"$json,\r\n"
104+
"${Json.encodeToString(value = chunk)},\r\n"
103105
}
104106
}
105107

gradle/libs.versions.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ ktor-server-core = { module = "io.ktor:ktor-server-core" }
6464
ktor-server-double-receive = { module = "io.ktor:ktor-server-double-receive" }
6565
ktor-server-netty = { module = "io.ktor:ktor-server-netty" }
6666
ktor-server-sse = { module = "io.ktor:ktor-server-sse" }
67+
ktor-sse = { module = "io.ktor:ktor-sse" }
6768
langchain4j-anthropic = { group = "dev.langchain4j", name = "langchain4j-anthropic" }
6869
langchain4j-bom = { group = "dev.langchain4j", name = "langchain4j-bom", version.ref = "langchain4j" }
6970
langchain4j-gemini = { group = "dev.langchain4j", name = "langchain4j-google-ai-gemini" }

mokksy/src/commonMain/kotlin/me/kpavlov/mokksy/BuildingStep.kt

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import io.ktor.sse.ServerSentEventMetadata
55
import me.kpavlov.mokksy.request.RequestSpecification
66
import me.kpavlov.mokksy.response.ResponseDefinitionBuilder
77
import me.kpavlov.mokksy.response.StreamingResponseDefinitionBuilder
8+
import me.kpavlov.mokksy.utils.logger.HttpFormatter
89
import java.io.IOException
910
import kotlin.reflect.KClass
1011

@@ -25,6 +26,7 @@ public class BuildingStep<P : Any> internal constructor(
2526
private val configuration: StubConfiguration,
2627
private val requestSpecification: RequestSpecification<P>,
2728
private val registerStub: (Stub<*, *>) -> Unit,
29+
private val formatter: HttpFormatter,
2830
) {
2931
/**
3032
* @param P The type of the request payload.
@@ -37,11 +39,13 @@ public class BuildingStep<P : Any> internal constructor(
3739
name: String?,
3840
requestSpecification: RequestSpecification<P>,
3941
registerStub: (Stub<*, *>) -> Unit,
42+
formatter: HttpFormatter,
4043
) : this(
4144
requestType = requestType,
4245
configuration = StubConfiguration(name),
4346
requestSpecification = requestSpecification,
4447
registerStub = registerStub,
48+
formatter = formatter,
4549
)
4650

4751
/**
@@ -62,8 +66,10 @@ public class BuildingStep<P : Any> internal constructor(
6266
val req = CapturedRequest(call.request, requestType)
6367
@SuppressWarnings("TooGenericExceptionCaught")
6468
try {
65-
ResponseDefinitionBuilder<P, T>(request = req)
66-
.apply(block)
69+
ResponseDefinitionBuilder<P, T>(
70+
request = req,
71+
formatter = formatter,
72+
).apply(block)
6773
.build()
6874
} catch (e: Exception) {
6975
if (e as? IOException == null) {
@@ -91,15 +97,19 @@ public class BuildingStep<P : Any> internal constructor(
9197
* @param block A lambda function applied to a [me.kpavlov.mokksy.response.StreamingResponseDefinitionBuilder],
9298
* used to configure the streaming response definition.
9399
*/
94-
public infix fun <T : Any> respondsWithStream(block: StreamingResponseDefinitionBuilder<P, T>.() -> Unit) {
100+
public infix fun <T : Any> respondsWithStream(
101+
block: StreamingResponseDefinitionBuilder<P, T>.() -> Unit,
102+
) {
95103
val stub =
96104
Stub(
97105
configuration = configuration,
98106
requestSpecification = requestSpecification,
99107
) { call ->
100108
val req = CapturedRequest(call.request, requestType)
101-
StreamingResponseDefinitionBuilder<P, T>(request = req)
102-
.apply(block)
109+
StreamingResponseDefinitionBuilder<P, T>(
110+
request = req,
111+
formatter = formatter,
112+
).apply(block)
103113
.build()
104114
}
105115

mokksy/src/commonMain/kotlin/me/kpavlov/mokksy/MokksyServer.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ public open class MokksyServer
197197
requestSpecification = requestSpec,
198198
registerStub = this::registerStub,
199199
requestType = requestType,
200+
formatter = httpFormatter,
200201
)
201202
}
202203

mokksy/src/commonMain/kotlin/me/kpavlov/mokksy/response/AbstractResponseDefinition.kt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,20 @@ import kotlin.time.Duration
99
internal typealias ResponseDefinitionSupplier<T> = (
1010
ApplicationCall,
1111
) -> AbstractResponseDefinition<T>
12-
1312
/**
1413
* Represents the base definition of an HTTP response in a mapping between a request and its corresponding response.
1514
* Provides the required attributes and behavior for configuring HTTP responses, including status code, headers,
1615
* and content type. This class serves as the foundation for more specialized response definitions.
1716
*
1817
* @param T The type of the response data.
19-
* @property contentType The MIME type of the response content. Defaults to `null`.
18+
* @property contentType The MIME type of the response content.
2019
* @property httpStatusCode The HTTP status code of the response as Int, defaulting to 200.
2120
* @property httpStatus The HTTP status code of the response. Defaults to [HttpStatusCode.OK].
2221
* @property headers A lambda function for configuring the response headers. Defaults to `null`.
2322
* @property headerList A list of header key-value pairs to populate the response headers. Defaults to an empty list.
2423
*/
2524
public abstract class AbstractResponseDefinition<T>(
26-
public val contentType: ContentType? = null,
25+
public val contentType: ContentType,
2726
public val httpStatusCode: Int = 200,
2827
public val httpStatus: HttpStatusCode = HttpStatusCode.fromValue(httpStatusCode),
2928
public val headers: (ResponseHeaders.() -> Unit)? = null,

mokksy/src/commonMain/kotlin/me/kpavlov/mokksy/response/ResponseDefinition.kt

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ import io.ktor.http.ContentType
44
import io.ktor.http.HttpStatusCode
55
import io.ktor.server.application.ApplicationCall
66
import io.ktor.server.application.log
7+
import io.ktor.server.request.httpVersion
78
import io.ktor.server.response.ResponseHeaders
89
import io.ktor.server.response.respond
910
import io.ktor.util.cio.ChannelWriteException
1011
import kotlinx.coroutines.delay
12+
import me.kpavlov.mokksy.utils.logger.HttpFormatter
1113
import kotlin.time.Duration
1214

1315
/**
@@ -33,6 +35,7 @@ public open class ResponseDefinition<P, T>(
3335
headers: (ResponseHeaders.() -> Unit)? = null,
3436
headerList: List<Pair<String, String>> = emptyList<Pair<String, String>>(),
3537
delay: Duration,
38+
private val formatter: HttpFormatter,
3639
) : AbstractResponseDefinition<T>(
3740
contentType = contentType,
3841
httpStatusCode = httpStatusCode,
@@ -49,7 +52,17 @@ public open class ResponseDefinition<P, T>(
4952
delay(delay)
5053
}
5154
if (verbose) {
52-
call.application.log.debug("Sending {}: {}", httpStatus, body)
55+
call.application.log.debug(
56+
"Sending:\n---\n${
57+
formatter.formatResponse(
58+
httpVersion = call.request.httpVersion,
59+
headers = call.response.headers,
60+
contentType = this.contentType,
61+
status = httpStatus,
62+
body = body?.toString(),
63+
)
64+
}---\n",
65+
)
5366
}
5467
try {
5568
call.respond(

mokksy/src/commonMain/kotlin/me/kpavlov/mokksy/response/ResponseDefinitionBuilders.kt

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import io.ktor.http.HttpStatusCode
55
import io.ktor.server.response.ResponseHeaders
66
import kotlinx.coroutines.flow.Flow
77
import me.kpavlov.mokksy.CapturedRequest
8+
import me.kpavlov.mokksy.utils.logger.HttpFormatter
89
import java.util.Collections
910
import kotlin.time.Duration
1011
import kotlin.time.Duration.Companion.milliseconds
@@ -66,8 +67,7 @@ public abstract class AbstractResponseDefinitionBuilder<P, T>(
6667
*
6768
* @param P The type of the request body.
6869
* @param T The type of the response body.
69-
* @property contentType Optional MIME type of the response.
70-
* Defaults to `ContentType.Application.Json` if not specified.
70+
* @property contentType Optional MIME type of the response. Defaults to `null` if not specified.
7171
* @property body The body of the response. Can be null.
7272
* @property httpStatusCode The HTTP status code of the response as Int, defaulting to 200.
7373
* @property httpStatus The HTTP status code of the response, defaulting to [HttpStatusCode.OK].
@@ -83,6 +83,7 @@ public open class ResponseDefinitionBuilder<P : Any, T : Any>(
8383
httpStatusCode: Int = 200,
8484
httpStatus: HttpStatusCode = HttpStatusCode.fromValue(httpStatusCode),
8585
headers: MutableList<Pair<String, String>> = mutableListOf(),
86+
private val formatter: HttpFormatter,
8687
) : AbstractResponseDefinitionBuilder<P, T>(
8788
httpStatusCode = httpStatusCode,
8889
httpStatus = httpStatus,
@@ -97,6 +98,7 @@ public open class ResponseDefinitionBuilder<P : Any, T : Any>(
9798
headers = headersLambda,
9899
headerList = Collections.unmodifiableList(headers),
99100
delay = delay,
101+
formatter = formatter,
100102
)
101103
}
102104

@@ -120,7 +122,12 @@ public open class StreamingResponseDefinitionBuilder<P : Any, T>(
120122
public var delayBetweenChunks: Duration = Duration.ZERO,
121123
httpStatus: HttpStatusCode = HttpStatusCode.OK,
122124
headers: MutableList<Pair<String, String>> = mutableListOf(),
123-
) : AbstractResponseDefinitionBuilder<P, T>(httpStatus = httpStatus, headers = headers) {
125+
public val chunkContentType: ContentType? = null,
126+
private val formatter: HttpFormatter,
127+
) : AbstractResponseDefinitionBuilder<P, T>(
128+
httpStatus = httpStatus,
129+
headers = headers,
130+
) {
124131
/**
125132
* Builds an instance of `StreamResponseDefinition`.
126133
*
@@ -142,5 +149,7 @@ public open class StreamingResponseDefinitionBuilder<P : Any, T>(
142149
headerList = Collections.unmodifiableList(headers),
143150
delayBetweenChunks = delayBetweenChunks,
144151
delay = delay,
152+
formatter = formatter,
153+
chunkContentType = chunkContentType
145154
)
146155
}

mokksy/src/commonMain/kotlin/me/kpavlov/mokksy/response/SseStreamResponseDefinition.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package me.kpavlov.mokksy.response
22

3+
import io.ktor.http.ContentType
34
import io.ktor.http.HttpHeaders
45
import io.ktor.http.HttpStatusCode
56
import io.ktor.server.application.ApplicationCall
@@ -14,12 +15,15 @@ import kotlinx.coroutines.flow.buffer
1415
import kotlinx.coroutines.flow.cancellable
1516
import kotlinx.coroutines.flow.catch
1617
import kotlinx.coroutines.flow.emptyFlow
18+
import me.kpavlov.mokksy.utils.logger.HttpFormatter
1719
import kotlin.time.Duration
1820

1921
public open class SseStreamResponseDefinition<P>(
2022
override val chunkFlow: Flow<ServerSentEvent>? = null,
23+
private val chunkContentType: ContentType? = null,
2124
delay: Duration = Duration.ZERO,
22-
) : StreamResponseDefinition<P, ServerSentEvent>(delay = delay) {
25+
formatter: HttpFormatter,
26+
) : StreamResponseDefinition<P, ServerSentEvent>(delay = delay, formatter = formatter) {
2327
override suspend fun writeResponse(
2428
call: ApplicationCall,
2529
verbose: Boolean,

0 commit comments

Comments
 (0)