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

Content type이 multipart/form-data인 요청에 대해 데이터의 상세 정보를 로그에 출력할 수 있도록 기능 추가 #95

Merged
merged 1 commit into from
Sep 18, 2024
Merged
Show file tree
Hide file tree
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 @@ -8,51 +8,62 @@ import jakarta.servlet.ServletInputStream
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletRequestWrapper
import jakarta.servlet.http.HttpServletResponse
import jakarta.servlet.http.Part
import org.springframework.http.MediaType
import org.springframework.util.ObjectUtils
import org.springframework.util.StreamUtils
import org.springframework.util.StringUtils
import org.springframework.web.filter.OncePerRequestFilter
import org.springframework.web.util.ContentCachingResponseWrapper
import java.io.BufferedReader
import java.io.ByteArrayInputStream
import java.io.IOException
import java.io.InputStream
import java.util.Arrays
import java.io.InputStreamReader
import java.util.stream.Collectors

class LogApiInfoFilter : OncePerRequestFilter() {
companion object {
val LOG_BLACK_LIST = arrayOf(
"/swagger",
"/v3/api-docs",
"/actuator",
)

private val VISIBLE_TYPES: Set<MediaType> = setOf(
MediaType.valueOf("text/*"),
MediaType.APPLICATION_FORM_URLENCODED,
MediaType.APPLICATION_JSON,
MediaType.APPLICATION_XML,
MediaType.MULTIPART_FORM_DATA,
)
}

override fun doFilterInternal(
request: HttpServletRequest,
response: HttpServletResponse,
filterChain: FilterChain,
) {
if (!StringUtils.hasText(MDCLogTraceManager.logTraceId)) {
MDCLogTraceManager.setLogTraceId()
}
MDCLogTraceManager.setLogTraceIdIfAbsent()

try {
if (isAsyncDispatch(request)) {
filterChain.doFilter(request, response)
} else {
val doLog = Arrays.stream(LOG_BLACK_LIST).noneMatch { s: String? ->
request.requestURI.contains(
s!!,
)
}
val doLog = LOG_BLACK_LIST.none { request.requestURI.startsWith(it) }
val responseWrapper = ResponseWrapper(response)
try {
if (isMultipartFormData(request.contentType)) {
if (!isMultipartFormData(request.contentType)) {
val requestWrapper = RequestWrapper(request)
if (doLog) {
logRequest(requestWrapper)
}
filterChain.doFilter(requestWrapper, responseWrapper)
} else {
val payload = request.parts.map { part -> processPart(part) }
Logger.info(
String.format(
"Request: [%s] uri=%s, payload=multipart/form-data",
request.method,
request.requestURI,
),
"Request: [${request.method}] uri=${request.requestURI}, " +
"content-type=multipart/form-data, payload={${payload.joinToString(", ") { it }}}",
)
filterChain.doFilter(request, responseWrapper)
} else {
val requestWrapper = RequestWrapper(request)
if (doLog) logRequest(requestWrapper)
filterChain.doFilter(requestWrapper, responseWrapper)
}
} finally {
if (doLog) logResponse(responseWrapper)
Expand All @@ -64,7 +75,6 @@ class LogApiInfoFilter : OncePerRequestFilter() {
}
}

@Throws(IOException::class)
private fun logRequest(request: RequestWrapper) {
var uri = request.requestURI
val queryString = request.queryString
Expand All @@ -74,42 +84,36 @@ class LogApiInfoFilter : OncePerRequestFilter() {

val content = StreamUtils.copyToByteArray(request.inputStream)
if (ObjectUtils.isEmpty(content)) {
Logger.info(java.lang.String.format("Request: [%s] uri=%s", request.method, uri))
Logger.info("Request: [${request.method}] uri=$uri")
} else {
val payloadInfo = getPayloadInfo(request.contentType, content)
Logger.info(java.lang.String.format("Request: [%s] uri=%s, %s", request.method, uri, payloadInfo))
Logger.info("Request: [$uri] uri=$uri, $payloadInfo")
}
}

private fun logResponse(response: ContentCachingResponseWrapper) {
Logger.info(String.format("Response: status=%d", response.status))
Logger.info("Response: status=${response.status}")
}

private fun getPayloadInfo(contentType: String?, content: ByteArray): String {
var type: String? = contentType
var payloadInfo = "content-type=$type, payload="
val mediaType: MediaType =
if (!contentType.isNullOrBlank()) MediaType.valueOf(contentType) else MediaType.APPLICATION_JSON
var payloadInfo = "content-type=$mediaType, payload="

if (type == null) {
type = MediaType.APPLICATION_JSON_VALUE
if (mediaType.isCompatibleWith(MediaType.TEXT_HTML)) {
return "${payloadInfo}HTML Content"
}

if (MediaType.valueOf(type) == MediaType.valueOf("text/html") ||
MediaType.valueOf(
type,
) == MediaType.valueOf("text/css")
) {
return payloadInfo + "HTML/CSS Content"
}
if (!isVisible(MediaType.valueOf(type))) {
return payloadInfo + "Binary Content"
if (!isMediaTypeVisible(mediaType)) {
return "${payloadInfo}Binary Content"
}

if (content.size >= 10000) {
return payloadInfo + "too many data."
return "${payloadInfo}too many data."
}

val contentString = String(content)
payloadInfo += if (type == MediaType.APPLICATION_JSON_VALUE) {
payloadInfo += if (mediaType == MediaType.APPLICATION_JSON) {
contentString.replace("\n *".toRegex(), "").replace(",".toRegex(), ", ")
} else {
contentString
Expand All @@ -121,52 +125,51 @@ class LogApiInfoFilter : OncePerRequestFilter() {
private fun isMultipartFormData(contentType: String?): Boolean =
contentType != null && contentType.contains(MediaType.MULTIPART_FORM_DATA_VALUE)

private fun isVisible(mediaType: MediaType): Boolean = VISIBLE_TYPES.stream()
.anyMatch { visibleType: MediaType ->
visibleType.includes(
mediaType,
)
/**
* @param part
* @return 전달받은 `part`의 내용을 담은 문자열
*/
private fun processPart(part: Part): String =
if (part.contentType == null) {
// 파일이 아닌, 텍스트 데이터인 경우
val value = BufferedReader(InputStreamReader(part.inputStream))
.lines()
.collect(Collectors.joining(","))
"${part.name}:$value"
} else {
// 파일 데이터인 경우
val fileSize = String.format("%.2f", convertByteToKB(part.size))
"${part.name}:${part.submittedFileName}(${fileSize}KB)"
}

companion object {
private val LOG_BLACK_LIST = arrayOf(
"/swagger",
"/v3/api-docs",
"/actuator",
)
private fun convertByteToKB(sizeInByte: Long): Double = sizeInByte / 1024.0

private val VISIBLE_TYPES: List<MediaType> = listOf(
MediaType.valueOf("text/*"),
MediaType.APPLICATION_FORM_URLENCODED,
MediaType.APPLICATION_JSON,
MediaType.APPLICATION_XML,
MediaType.MULTIPART_FORM_DATA,
)
}
private fun isMediaTypeVisible(mediaType: MediaType): Boolean =
VISIBLE_TYPES.any { visibleType -> visibleType.includes(mediaType) }
}

class RequestWrapper(request: HttpServletRequest) : HttpServletRequestWrapper(request) {
private val cachedInputStream = StreamUtils.copyToByteArray(request.inputStream)
private val cachedInputStream: ByteArray? by lazy {
StreamUtils.copyToByteArray(request.inputStream)
}

override fun getInputStream(): ServletInputStream {
val byteArray = cachedInputStream ?: return super.getInputStream()
return object : ServletInputStream() {
private val cachedBodyInputStream: InputStream = ByteArrayInputStream(cachedInputStream)
private val inputStream = ByteArrayInputStream(byteArray)

override fun isFinished(): Boolean {
try {
return cachedBodyInputStream.available() == 0
} catch (e: IOException) {
e.printStackTrace()
}
return false
override fun isFinished(): Boolean = try {
inputStream.available() == 0
} catch (e: IOException) {
Logger.error("Raised IOException at LogApiInfoFilter.RequestWrapper.isFinished(). Error=$e")
true // safe finish
}

override fun isReady(): Boolean = true

override fun setReadListener(listener: ReadListener): Unit = throw UnsupportedOperationException()

@Throws(IOException::class)
override fun read(): Int = cachedBodyInputStream.read()
override fun read(): Int = inputStream.read()
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.routebox.routebox.logger

import org.slf4j.MDC
import org.springframework.util.StringUtils
import java.util.UUID

object MDCLogTraceManager {
Expand All @@ -10,7 +11,13 @@ object MDCLogTraceManager {
val logTraceId: String
get() = MDC.get(LOG_TRACE_ID_MDC_KEY) ?: ""

fun setLogTraceId() {
fun setLogTraceIdIfAbsent() {
if (!StringUtils.hasText(logTraceId)) {
setLogTraceId()
}
}

private fun setLogTraceId() {
MDC.put(LOG_TRACE_ID_MDC_KEY, UUID.randomUUID().toString().substring(0, 8))
}

Expand Down
Loading