diff --git a/src/main/kotlin/roomescape/common/log/LoggingFilter.kt b/src/main/kotlin/roomescape/common/log/LoggingFilter.kt new file mode 100644 index 00000000..ea155306 --- /dev/null +++ b/src/main/kotlin/roomescape/common/log/LoggingFilter.kt @@ -0,0 +1,65 @@ +package roomescape.common.log + +import com.fasterxml.jackson.databind.ObjectMapper +import io.github.oshai.kotlinlogging.KotlinLogging +import jakarta.servlet.FilterChain +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.springframework.core.Ordered +import org.springframework.core.annotation.Order +import org.springframework.stereotype.Component +import org.springframework.web.filter.OncePerRequestFilter +import org.springframework.web.util.ContentCachingRequestWrapper +import org.springframework.web.util.ContentCachingResponseWrapper + +private val log = KotlinLogging.logger {} + +@Component +@Order(Ordered.HIGHEST_PRECEDENCE) +class LoggingFilter( + private val objectMapper: ObjectMapper +) : OncePerRequestFilter() { + + override fun doFilterInternal( + request: HttpServletRequest, + response: HttpServletResponse, + filterChain: FilterChain + ) { + val cachedRequest = ContentCachingRequestWrapper(request) + val cachedResponse = ContentCachingResponseWrapper(response) + + val startTime = System.currentTimeMillis() + filterChain.doFilter(cachedRequest, cachedResponse) + val duration = System.currentTimeMillis() - startTime + + logAPISummary(cachedRequest, cachedResponse, duration) + cachedResponse.copyBodyToResponse() + + } + + private fun logAPISummary( + request: ContentCachingRequestWrapper, + response: ContentCachingResponseWrapper, + duration: Long + ) { + val payload = linkedMapOf( + "type" to "API_LOG", + "method" to request.method, + "url" to request.requestURL.toString(), + ) + request.queryString?.let { payload["query_params"] = it } + payload["remote_ip"] = request.remoteAddr + payload["status_code"] = response.status + payload["duration_ms"] = duration + + if (log.isDebugEnabled()) { + request.contentAsByteArray.takeIf { it.isNotEmpty() } + ?.let { payload["request_body"] = objectMapper.readValue(it, Map::class.java) } + + response.contentAsByteArray.takeIf { it.isNotEmpty() } + ?.let { payload["response_body"] = objectMapper.readValue(it, Map::class.java) } + } + + log.info { objectMapper.writeValueAsString(payload) } + } +}