LoggingCallback.kt

package com.hexagonkt.http.server.callbacks

import com.hexagonkt.core.logging.Logger
import com.hexagonkt.http.model.*
import com.hexagonkt.http.handlers.HttpContext
import java.lang.System.Logger.Level
import kotlin.system.measureNanoTime

/**
 * Callback that logs server requests and responses.
 */
class LoggingCallback(
    private val level: Level = Level.INFO,
    private val logger: Logger = Logger(LoggingCallback::class),
    private val includeHeaders: Boolean = false,
    private val includeBody: Boolean = true,
) : (HttpContext) -> HttpContext {

    override fun invoke(context: HttpContext): HttpContext {
        var result: HttpContext

        logger.log(level) { details(context.request) }
        val ns = measureNanoTime { result = context.next() }
        logger.log(level) { details(context.request, result.response, ns) }

        return result
    }

    internal fun details(m: HttpRequestPort): String {
        val headers = if (includeHeaders) {
            val accept = Header("accept", m.accept.joinToString(", ") { it.text })
            val contentType = Header("content-type", m.contentType?.text ?: "")
            (m.headers - "accept" - "content-type" + accept + contentType).format()
        }
        else {
            ""
        }

        val body = m.formatBody()
        return "${m.method} ${m.path}$headers$body".trim()
    }

    internal fun details(n: HttpRequestPort, m: HttpResponsePort, ns: Long): String {
        val headers = if (includeHeaders) {
            val contentType = Header("content-type", m.contentType?.text ?: "")
            (m.headers - "content-type" + contentType).format()
        } else {
            ""
        }

        val path = "${n.method} ${n.path}"
        val result = "${m.status.type}(${m.status.code})"
        val time = "(${ns / 10e5} ms)"
        val body = m.formatBody()
        return "$path -> $result $time$headers$body".trim()
    }

    private fun HttpMessage.formatBody(): String =
        if (includeBody) "\n\n${bodyString()}" else ""

    private fun Headers.format(): String =
        httpFields
            .filter { (_, v) -> v.strings().any { it.isNotBlank() } }
            .map { (k, v) -> "$k: ${v.strings().joinToString(", ")}" }
            .joinToString("\n", prefix = "\n\n")
}