Info
- log4j2로 기본 템플릿 사용
- filter로 http request/response logging
Log4j
src/resources/log4j2.xml파일로 관리
<?xml version="1.0" encoding="UTF-8" ?>
<Configuration status="INFO">
<Properties>
<Property name="LOG_DATEFORMAT_PATTERN">{yyyy-MM-dd HH:mm:ss.SSS}</Property>
<Property name="LOG_LEVEL_PATTERN">{-%5p}{FATAL=red blink, ERROR=red, WARN=yellow bold, INFO=green, DEBUG=green bold, TRACE=blue} [traceId=%X{traceId}] [userId=%X{userId}] %style{${sys:PID}}{magenta} %style{%F}{blue}:%style{%L}{white} : %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}</Property>
</Properties>
<Appenders>
<Console name="ConsoleAppender" target="SYSTEM_OUT">
<PatternLayout disableAnsi="false" pattern="✈️ %d${LOG_DATEFORMAT_PATTERN} %highlight${LOG_LEVEL_PATTERN}"/>
</Console>
<RollingFile name="FileAppender"
fileName="logs/spring.log"
filePattern="logs/spring-%d{yyyy-MM-dd}-%i.log">
<JsonLayout complete="false" compact="false" charset="UTF-8">
<KeyValuePair key="service" value="simple" />
</JsonLayout>
<Policies>
<TimeBasedTriggeringPolicy interval="1" />
<SizeBasedTriggeringPolicy size="10000KB" />
</Policies>
<DefaultRolloverStrategy max="20" fileIndex="min" />
</RollingFile>
</Appenders>
<Loggers>
<logger name="org.hibernate.type.descriptor.sql.BasicBinder" level="TRACE" additivity="false">
<AppenderRef ref="ConsoleAppender" />
</logger>
<logger name="org.hibernate.SQL" level="DEBUG" additivity="false" >
<AppenderRef ref="ConsoleAppender" />
</logger>
<Logger name="org.springframework.jdbc.datasource.init.ScriptUtils" level="DEBUG" additivity="false">
<AppenderRef ref="ConsoleAppender" />
</Logger>
<Root level="INFO">
<AppenderRef ref="ConsoleAppender" />
</Root>
</Loggers>
</Configuration>LoggingFilter
src/main/kotlin/com/example/kotlinapiserverguide/commen/filter/LoggingFilter.kt파일로 관리- http request/response 로깅 시, 사용.
- traceId를 UUID로 생성하여 request에 대한 서비스 로직 추적 (MDC에 추가)
- request를 받고 response를 수행하기 까지의 startTime/endTime 기록
- response에 대해서는 json 형태만 로깅. (excludeList 변수 참고)
package com.example.kotlinapiserverguide.common.filter
import com.example.kotlinapiserverguide.common.filter.domain.HttpLog
import com.example.kotlinapiserverguide.common.function.logger
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import jakarta.servlet.Filter
import jakarta.servlet.FilterChain
import jakarta.servlet.ServletRequest
import jakarta.servlet.ServletResponse
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.slf4j.MDC
import org.springframework.web.util.ContentCachingRequestWrapper
import org.springframework.web.util.ContentCachingResponseWrapper
import org.springframework.web.util.WebUtils
import java.io.UnsupportedEncodingException
import java.util.*
class LoggingFilter : Filter {
private val log = logger()
private val objectMapper: ObjectMapper = ObjectMapper()
private val excludeList: List<String> = listOf(
"application/javascript",
"text/html",
"image/x-icon",
)
override fun doFilter(request: ServletRequest?, response: ServletResponse?, chain: FilterChain?) {
MDC.put("traceId", UUID.randomUUID().toString())
val startTime = System.currentTimeMillis()
val requestWrapper = ContentCachingRequestWrapper((request as HttpServletRequest))
val responseWrapper = ContentCachingResponseWrapper((response as HttpServletResponse?)!!)
chain?.doFilter(requestWrapper, responseWrapper)
val endTime = System.currentTimeMillis()
val elapsedTime = (endTime - startTime) / 1000.0
val httpLog = objectMapper.writeValueAsString(createHttpLog(requestWrapper, elapsedTime, responseWrapper))
log.info("[HTTP] - $httpLog")
MDC.clear()
}
private fun createHttpLog(
requestWrapper: ContentCachingRequestWrapper,
elapsedTime: Double,
responseWrapper: ContentCachingResponseWrapper
): HttpLog {
return HttpLog(
HttpLog.Request(
requestWrapper.requestURL.toString(),
requestWrapper.method,
requestWrapper.servletPath,
elapsedTime,
getRequestHeaders(requestWrapper),
requestWrapper.queryString,
getRequestBody(requestWrapper)
),
HttpLog.Response(
responseWrapper.status.toString(),
getResponseHeaders(responseWrapper),
getResponseBody(responseWrapper)
)
)
}
private fun getRequestHeaders(request: HttpServletRequest): Map<String, Any?> {
val headers: MutableMap<String, Any?> = mutableMapOf()
val headerNames: Enumeration<String> = request.headerNames
while (headerNames.hasMoreElements()) {
val headerName: String = headerNames.nextElement()
headers[headerName] = request.getHeader(headerName)
}
return headers
}
private fun getRequestBody(request: ContentCachingRequestWrapper): Map<String, Any?>? {
val wrapper = WebUtils.getNativeRequest(
request,
ContentCachingRequestWrapper::class.java
)
try {
if (wrapper != null) {
val buf = wrapper.contentAsByteArray
if (buf.isNotEmpty()) {
val jsonString = String(buf, 0, buf.size, charset(wrapper.characterEncoding))
return objectMapper.readValue<Map<String, Any?>>(jsonString)
}
}
} catch (e: UnsupportedEncodingException) {
return null
} catch (e: Exception) {
return null
}
return null
}
private fun getResponseHeaders(response: HttpServletResponse): MutableMap<String, Any> {
val headers: MutableMap<String, Any> = mutableMapOf()
val headerNames: MutableCollection<String> = response.headerNames
headerNames.forEach { headerName ->
headers[headerName] = response.getHeader(headerName)
}
return headers
}
private fun getResponseBody(response: HttpServletResponse): Map<String, Any?>? {
var payload: String? = null
val wrapper = WebUtils.getNativeResponse(response, ContentCachingResponseWrapper::class.java)
if (wrapper != null) {
val buffer: ByteArray = wrapper.contentAsByteArray
if (buffer.isNotEmpty()) {
payload = buffer.toString(Charsets.UTF_8)
wrapper.copyBodyToResponse()
if (response.contentType != null) {
val isMatch: Boolean = excludeList.stream()
.anyMatch { exclude -> response.contentType.contains(exclude) }
if (isMatch) return null
}
return objectMapper.readValue<Map<String, Any?>>(payload)
}
}
return null
}
}Sample
- 실제 로깅 시에는 제외 대상을 선별할 계획
✈️ 2025-02-21 16:06:06.501 - INFO [traceId=4989cc04-2499-4030-b344-3321f5cf334d] [userId=] 45860 LoggingFilter.kt:40 : [HTTP] - {"request":{"url":"http://127.0.0.1:8000/api/user/login","method":"POST","path":"/api/user/login","elapsedTime":0.577,"headers":{"content-type":"application/json","content-length":"46","user-agent":"IntelliJ HTTP Client/IntelliJ IDEA 2024.3.3","accept-encoding":"br, deflate, gzip, x-gzip","accept":"*/*","host":"127.0.0.1:8000"},"queryString":null,"body":{"username":"test","password":"test"}},"response":{"status":"200","headers":{"Vary":"Origin","X-Content-Type-Options":"nosniff","X-XSS-Protection":"0","Cache-Control":"no-cache, no-store, max-age=0, must-revalidate","Pragma":"no-cache","Expires":"0","X-Frame-Options":"DENY"},"body":{"resultCode":"SUCCESS","resultMessage":"success.","body":{"accessToken":"eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIxIiwiaWF0IjoxNzQwMTIxNTY2LCJleHAiOjE3NDE0MTc1NjZ9.Xqf8O4jKezwSw1GacyEpxVVgnt9tTJl7-9SOXfIC89aRTsM3ixOzE0TdK_KLwWa_avO8USK-fvDpcgz8YCL85Q","refreshToken":"eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIxIiwiaWF0IjoxNzQwMTIxNTY2LCJleHAiOjE3NDI3MTM1NjZ9.vyYrQuAEflGLT_cKCWqjf5Zllk-ZO5c4RvLzm8AedDsCN-rsSmrbl9pA7I00N3L0S6vcYEmv_thM1coVXu0Oqg"}}}}