diff --git a/src/main/kotlin/com/wire/bots/polls/setup/HttpClient.kt b/src/main/kotlin/com/wire/bots/polls/setup/HttpClient.kt index aa459b4..7af6d94 100644 --- a/src/main/kotlin/com/wire/bots/polls/setup/HttpClient.kt +++ b/src/main/kotlin/com/wire/bots/polls/setup/HttpClient.kt @@ -1,5 +1,6 @@ package com.wire.bots.polls.setup +import com.wire.bots.polls.utils.ClientRequestMetric import com.wire.bots.polls.utils.createLogger import com.wire.bots.polls.utils.httpCall import io.ktor.client.HttpClient @@ -9,7 +10,6 @@ import io.ktor.client.features.json.JsonFeature import io.ktor.client.features.logging.LogLevel import io.ktor.client.features.logging.Logger import io.ktor.client.features.logging.Logging -import io.ktor.client.features.observer.ResponseObserver import io.micrometer.core.instrument.MeterRegistry @@ -22,14 +22,8 @@ fun createHttpClient(meterRegistry: MeterRegistry) = serializer = JacksonSerializer() } - // TODO check https://github.com/ktorio/ktor/issues/1813 - @Suppress("ConstantConditionIf") // temporary disabled until https://github.com/ktorio/ktor/issues/1813 is resolved - if (false) { - install(ResponseObserver) { - onResponse { - meterRegistry.httpCall(it) - } - } + install(ClientRequestMetric) { + onResponse { meterRegistry.httpCall(it) } } install(Logging) { diff --git a/src/main/kotlin/com/wire/bots/polls/utils/ClientRequestMetric.kt b/src/main/kotlin/com/wire/bots/polls/utils/ClientRequestMetric.kt new file mode 100644 index 0000000..56576fb --- /dev/null +++ b/src/main/kotlin/com/wire/bots/polls/utils/ClientRequestMetric.kt @@ -0,0 +1,72 @@ +package com.wire.bots.polls.utils + +import io.ktor.client.HttpClient +import io.ktor.client.features.HttpClientFeature +import io.ktor.client.statement.HttpReceivePipeline +import io.ktor.client.statement.HttpResponse +import io.ktor.client.statement.request +import io.ktor.util.AttributeKey +import mu.KLogging + +/** + * [ClientRequestMetric] callback. + */ +typealias RequestMetricHandler = suspend (RequestMetric) -> Unit + + +/** + * Enables callback after HttpClient sends a request with [RequestMetric]. + */ +class ClientRequestMetric(private val metricHandler: RequestMetricHandler) { + + class Config { + internal var metricHandler: RequestMetricHandler = {} + + /** + * Set [RequestMetricHandler] called at the end of the request. + */ + fun onResponse(block: RequestMetricHandler) { + metricHandler = block + } + } + + companion object Feature : HttpClientFeature, KLogging() { + + override val key: AttributeKey = + AttributeKey("ClientRequestMetric") + + override fun prepare(block: Config.() -> Unit) = + ClientRequestMetric(Config().apply(block).metricHandler) + + override fun install(feature: ClientRequestMetric, scope: HttpClient) { + // synchronous response pipeline hook + // instead of ResponseObserver - which spawns a new coroutine + scope.receivePipeline.intercept(HttpReceivePipeline.After) { response -> + // WARNING: Do not consume HttpResponse.content here, + // or you will corrupt the client response. + runCatching { feature.metricHandler(response.toRequestMetric()) } + .onFailure { logger.error(it) { "Error during metering!" } } + } + } + + // does not touch the content + private fun HttpResponse.toRequestMetric() = RequestMetric( + requestTime = requestTime.timestamp, + responseTime = responseTime.timestamp, + method = request.method.value, + url = request.url.toString(), + responseCode = status.value + ) + } +} + + +data class RequestMetric( + // number of epoch milliseconds + val requestTime: Long, + val responseTime: Long, + val method: String, + val url: String, + val responseCode: Int +) + diff --git a/src/main/kotlin/com/wire/bots/polls/utils/PrometheusExtensions.kt b/src/main/kotlin/com/wire/bots/polls/utils/PrometheusExtensions.kt index 6cbe45a..95cc3b7 100644 --- a/src/main/kotlin/com/wire/bots/polls/utils/PrometheusExtensions.kt +++ b/src/main/kotlin/com/wire/bots/polls/utils/PrometheusExtensions.kt @@ -1,7 +1,5 @@ package com.wire.bots.polls.utils -import io.ktor.client.statement.HttpResponse -import io.ktor.client.statement.request import io.micrometer.core.instrument.MeterRegistry import io.micrometer.core.instrument.Tag import java.util.concurrent.TimeUnit @@ -18,18 +16,17 @@ fun MeterRegistry.countException(exception: Throwable, additionalTags: Map