Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve ktor-client request/response logging #1728

Closed
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,13 @@ import kotlinx.coroutines.*
class Logging(
val logger: Logger,
var level: LogLevel,
var filters: List<(HttpRequestBuilder) -> Boolean>
val filters: List<(HttpClientCall) -> Boolean>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if it's ok to replace the request builder with the call here. The call will be created only after the network request, but we should print the request log before any network activity.
Could you tell me if you have an idea of fixing this?

val logContentType: LogContentType
) {
/**
* Constructor
*/
constructor(logger: Logger, level: LogLevel) : this(logger, level, emptyList())
constructor(logger: Logger, level: LogLevel) : this(logger, level, emptyList(), LogContentType.BOTH)

/**
* [Logging] feature configuration
Expand All @@ -38,7 +39,13 @@ class Logging(
/**
* filters
*/
internal var filters = mutableListOf<(HttpRequestBuilder) -> Boolean>()
internal var filters = mutableListOf<(HttpClientCall) -> Boolean>()

/**
* Log request/response/both
*/
internal var logContentType = LogContentType.BOTH

/**
* [Logger] instance to use
*/
Expand All @@ -52,20 +59,19 @@ class Logging(
/**
* Log messages for calls matching a [predicate]
*/
fun filter(predicate: (HttpRequestBuilder) -> Boolean) {
fun filter(predicate: (HttpClientCall) -> Boolean) {
filters.add(predicate)
}
}

private suspend fun logRequest(request: HttpRequestBuilder): OutgoingContent? {
private suspend fun logRequest(request: HttpRequest): OutgoingContent? {
if (level.info) {
logger.log("REQUEST: ${Url(request.url)}")
logger.log("REQUEST: ${request.url}")
logger.log("METHOD: ${request.method}")
}
val content = request.body as OutgoingContent
if (level.headers) logHeaders(request.headers.entries(), content.headers)
if (level.headers) logHeaders(request.headers.entries(), request.content.headers)
return if (level.body) {
logRequestBody(content)
logRequestBody(request.content)
} else null
}

Expand Down Expand Up @@ -146,23 +152,13 @@ class Logging(

override fun prepare(block: Config.() -> Unit): Logging {
val config = Config().apply(block)
return Logging(config.logger, config.level, config.filters)
return Logging(config.logger, config.level, config.filters, config.logContentType)
}

override fun install(feature: Logging, scope: HttpClient) {
scope.sendPipeline.intercept(HttpSendPipeline.Monitoring) {
val response = try {
if (feature.filters.isEmpty() || feature.filters.any { it(context) }) {
feature.logRequest(context)
} else {
null
}
} catch (_: Throwable) {
null
} ?: subject

try {
proceedWith(response)
proceed()
} catch (cause: Throwable) {
feature.logRequestException(context, cause)
throw cause
Expand All @@ -171,7 +167,14 @@ class Logging(

scope.responsePipeline.intercept(HttpResponsePipeline.Receive) {
try {
feature.logResponse(context.response)
if (feature.filters.isEmpty() || feature.filters.any { it(context) }) {
if (feature.logContentType.shouldLogRequest()) {
feature.logRequest(context.request)
}
if (feature.logContentType.shouldLogResponse()) {
feature.logResponse(context.response)
}
}
proceedWith(subject)
} catch (cause: Throwable) {
feature.logResponseException(context, cause)
Expand Down Expand Up @@ -204,3 +207,12 @@ fun HttpClientConfig<*>.Logging(block: Logging.Config.() -> Unit = {}) {

private suspend inline fun ByteReadChannel.readText(charset: Charset): String =
readRemaining().readText(charset = charset)

enum class LogContentType {
REQUEST_ONLY,
RESPONSE_ONLY,
BOTH
}

fun LogContentType.shouldLogRequest() = this == LogContentType.BOTH || this == LogContentType.REQUEST_ONLY
fun LogContentType.shouldLogResponse() = this == LogContentType.BOTH || this == LogContentType.RESPONSE_ONLY