LoggingCallback.kt

  1. package com.hexagonkt.http.server.callbacks

  2. import com.hexagonkt.core.logging.Logger
  3. import com.hexagonkt.http.model.*
  4. import com.hexagonkt.http.handlers.HttpContext
  5. import java.lang.System.Logger.Level
  6. import kotlin.system.measureNanoTime

  7. /**
  8.  * Callback that logs server requests and responses.
  9.  */
  10. class LoggingCallback(
  11.     private val level: Level = Level.INFO,
  12.     private val logger: Logger = Logger(LoggingCallback::class),
  13.     private val includeHeaders: Boolean = false,
  14.     private val includeBody: Boolean = true,
  15. ) : (HttpContext) -> HttpContext {

  16.     override fun invoke(context: HttpContext): HttpContext {
  17.         var result: HttpContext

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

  21.         return result
  22.     }

  23.     internal fun details(m: HttpRequestPort): String {
  24.         val headers = if (includeHeaders) {
  25.             val accept = Header("accept", m.accept.joinToString(", ") { it.text })
  26.             val contentType = Header("content-type", m.contentType?.text ?: "")
  27.             (m.headers - "accept" - "content-type" + accept + contentType).format()
  28.         }
  29.         else {
  30.             ""
  31.         }

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

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

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

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

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