Merge pull request #44 from beaniejoy/feature/43
Http Request, Response 관련 logging filter 적용
This commit is contained in:
@@ -67,6 +67,8 @@ subprojects {
|
||||
|
||||
// Logging
|
||||
implementation("io.github.microutils:kotlin-logging:${Version.Deps.KOTLIN_LOGGING}")
|
||||
implementation("com.google.code.gson:gson")
|
||||
|
||||
|
||||
// Test
|
||||
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package io.beaniejoy.dongnecafe.common.config
|
||||
|
||||
import io.beaniejoy.dongnecafe.security.utils.JwtTokenUtils
|
||||
import io.beaniejoy.dongnecafe.security.config.JwtAuthenticationConfigurer
|
||||
import io.beaniejoy.dongnecafe.security.handler.CustomAccessDeniedHandler
|
||||
import io.beaniejoy.dongnecafe.security.handler.CustomAuthenticationEntryPoint
|
||||
import io.beaniejoy.dongnecafe.utils.security.JwtTokenUtils
|
||||
import io.beaniejoy.dongnecafe.infra.security.config.JwtAuthenticationConfigurer
|
||||
import io.beaniejoy.dongnecafe.infra.security.handler.CustomAccessDeniedHandler
|
||||
import io.beaniejoy.dongnecafe.infra.security.handler.CustomAuthenticationEntryPoint
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.autoconfigure.security.servlet.PathRequest
|
||||
import org.springframework.context.annotation.Bean
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package io.beaniejoy.dongnecafe.controller
|
||||
|
||||
import io.beaniejoy.dongnecafe.common.response.ApplicationResponse
|
||||
import io.beaniejoy.dongnecafe.security.utils.JwtTokenUtils
|
||||
import io.beaniejoy.dongnecafe.utils.security.JwtTokenUtils
|
||||
import io.beaniejoy.dongnecafe.domain.member.model.request.SignInRequest
|
||||
import io.beaniejoy.dongnecafe.model.TokenResponse
|
||||
import io.beaniejoy.dongnecafe.service.AuthService
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package io.beaniejoy.dongnecafe.security
|
||||
|
||||
import io.beaniejoy.dongnecafe.common.error.constant.ErrorCode
|
||||
import io.beaniejoy.dongnecafe.infra.security.SecurityUser
|
||||
import mu.KLogging
|
||||
import org.springframework.security.authentication.AuthenticationProvider
|
||||
import org.springframework.security.authentication.BadCredentialsException
|
||||
|
||||
@@ -4,6 +4,7 @@ import io.beaniejoy.dongnecafe.common.error.constant.ErrorCode
|
||||
import io.beaniejoy.dongnecafe.common.error.exception.BusinessException
|
||||
import io.beaniejoy.dongnecafe.domain.member.entity.Member
|
||||
import io.beaniejoy.dongnecafe.domain.member.repository.MemberRepository
|
||||
import io.beaniejoy.dongnecafe.infra.security.SecurityUser
|
||||
import mu.KLogging
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority
|
||||
import org.springframework.security.core.userdetails.UserDetailsService
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package io.beaniejoy.dongnecafe.common.config
|
||||
|
||||
import io.beaniejoy.dongnecafe.security.utils.getAuthPrincipal
|
||||
import io.beaniejoy.dongnecafe.utils.security.getAuthPrincipal
|
||||
import mu.KLogging
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package io.beaniejoy.dongnecafe.infra.logging
|
||||
|
||||
import io.beaniejoy.dongnecafe.utils.logging.*
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.web.util.ContentCachingRequestWrapper
|
||||
import org.springframework.web.util.ContentCachingResponseWrapper
|
||||
|
||||
data class HttpLogMessage(
|
||||
val httpMethod: String,
|
||||
val requestUri: String,
|
||||
val httpStatus: HttpStatus,
|
||||
val clientIp: String,
|
||||
val elapsedTime: Double,
|
||||
val headers: String?,
|
||||
val requestParam: String?,
|
||||
val requestBody: String?,
|
||||
val responseBody: String?,
|
||||
) {
|
||||
companion object {
|
||||
fun createInstance(
|
||||
requestWrapper: ContentCachingRequestWrapper,
|
||||
responseWrapper: ContentCachingResponseWrapper,
|
||||
elapsedTime: Double
|
||||
): HttpLogMessage {
|
||||
return HttpLogMessage(
|
||||
httpMethod = requestWrapper.method,
|
||||
requestUri = requestWrapper.requestURI,
|
||||
httpStatus = HttpStatus.valueOf(responseWrapper.status),
|
||||
clientIp = requestWrapper.getClientIp(),
|
||||
elapsedTime = elapsedTime,
|
||||
headers = requestWrapper.getRequestHeaders(),
|
||||
requestParam = requestWrapper.getRequestParams(),
|
||||
requestBody = requestWrapper.getRequestBody(),
|
||||
responseBody = responseWrapper.getResponseBody(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun toPrettierLog(): String {
|
||||
return """
|
||||
|
|
||||
|[REQUEST] ${this.httpMethod} ${this.requestUri} ${this.httpStatus} (${this.elapsedTime})
|
||||
|>> CLIENT_IP: ${this.clientIp}
|
||||
|>> HEADERS: ${this.headers}
|
||||
|>> REQUEST_PARAM: ${this.requestParam}
|
||||
|>> REQUEST_BODY: ${this.requestBody}
|
||||
|>> RESPONSE_BODY: ${this.responseBody}
|
||||
""".trimMargin()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package io.beaniejoy.dongnecafe.infra.logging
|
||||
|
||||
import mu.KotlinLogging
|
||||
import org.slf4j.MDC
|
||||
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
|
||||
import java.util.*
|
||||
import javax.servlet.FilterChain
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
|
||||
@Component
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
class ReqResLoggingFilter : OncePerRequestFilter() {
|
||||
private val log = KotlinLogging.logger {}
|
||||
|
||||
companion object {
|
||||
const val REQUEST_ID = "request_id"
|
||||
}
|
||||
|
||||
override fun doFilterInternal(
|
||||
request: HttpServletRequest,
|
||||
response: HttpServletResponse,
|
||||
filterChain: FilterChain,
|
||||
) {
|
||||
val cachingRequestWrapper = ContentCachingRequestWrapper(request)
|
||||
val cachingResponseWrapper = ContentCachingResponseWrapper(response)
|
||||
|
||||
val requestId = UUID.randomUUID().toString().substring(0, 8)
|
||||
|
||||
MDC.put(REQUEST_ID, requestId)
|
||||
|
||||
val startTime = System.currentTimeMillis()
|
||||
filterChain.doFilter(cachingRequestWrapper, cachingResponseWrapper)
|
||||
val end = System.currentTimeMillis()
|
||||
|
||||
try {
|
||||
log.info {
|
||||
HttpLogMessage.createInstance(
|
||||
requestWrapper = cachingRequestWrapper,
|
||||
responseWrapper = cachingResponseWrapper,
|
||||
elapsedTime = (end - startTime) / 1000.0
|
||||
).toPrettierLog()
|
||||
}
|
||||
|
||||
cachingResponseWrapper.copyBodyToResponse()
|
||||
} catch (e: Exception) {
|
||||
log.error(e) { "[${this::class.simpleName}] Logging 실패" }
|
||||
}
|
||||
|
||||
MDC.remove(REQUEST_ID)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package io.beaniejoy.dongnecafe.infra.logging.constant
|
||||
|
||||
/**
|
||||
* http request client ip possible enum list
|
||||
* (ref. https://blog.yevgnenll.me/posts/find-client-ip-from-http-request-header)
|
||||
* @property headerName String client ip header name
|
||||
*/
|
||||
enum class HttpClientIp(
|
||||
val headerName: String,
|
||||
) {
|
||||
X_FORWARDED_FOR("X-Forwarded-For"),
|
||||
PROXY_CLIENT_IP("Proxy-Client-IP"),
|
||||
WL_PROXY_CLIENT_IP("WL-Proxy-Client-IP"),
|
||||
HTTP_X_FORWARDED("HTTP_X_FORWARDED"),
|
||||
HTTP_X_FORWARDED_FOR("HTTP_X_FORWARDED_FOR"),
|
||||
HTTP_CLIENT_IP("HTTP_CLIENT_IP")
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package io.beaniejoy.dongnecafe.security
|
||||
package io.beaniejoy.dongnecafe.infra.security
|
||||
|
||||
import io.beaniejoy.dongnecafe.domain.member.entity.Member
|
||||
import org.springframework.security.core.GrantedAuthority
|
||||
@@ -1,7 +1,7 @@
|
||||
package io.beaniejoy.dongnecafe.security.config
|
||||
package io.beaniejoy.dongnecafe.infra.security.config
|
||||
|
||||
import io.beaniejoy.dongnecafe.security.filter.JwtAuthenticationFilter
|
||||
import io.beaniejoy.dongnecafe.security.utils.JwtTokenUtils
|
||||
import io.beaniejoy.dongnecafe.infra.security.filter.JwtAuthenticationFilter
|
||||
import io.beaniejoy.dongnecafe.utils.security.JwtTokenUtils
|
||||
import org.springframework.security.config.annotation.SecurityConfigurerAdapter
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||
import org.springframework.security.web.DefaultSecurityFilterChain
|
||||
@@ -1,4 +1,4 @@
|
||||
package io.beaniejoy.dongnecafe.security.constant
|
||||
package io.beaniejoy.dongnecafe.infra.security.constant
|
||||
|
||||
object SecurityConstant {
|
||||
const val BEARER = "Bearer"
|
||||
@@ -1,8 +1,8 @@
|
||||
package io.beaniejoy.dongnecafe.security.filter
|
||||
package io.beaniejoy.dongnecafe.infra.security.filter
|
||||
|
||||
import io.beaniejoy.dongnecafe.security.utils.JwtTokenUtils
|
||||
import io.beaniejoy.dongnecafe.security.constant.SecurityConstant.BEARER
|
||||
import io.beaniejoy.dongnecafe.security.constant.SecurityConstant.WHITESPACE
|
||||
import io.beaniejoy.dongnecafe.utils.security.JwtTokenUtils
|
||||
import io.beaniejoy.dongnecafe.infra.security.constant.SecurityConstant.BEARER
|
||||
import io.beaniejoy.dongnecafe.infra.security.constant.SecurityConstant.WHITESPACE
|
||||
import mu.KotlinLogging
|
||||
import org.springframework.http.HttpHeaders
|
||||
import org.springframework.security.core.context.SecurityContextHolder
|
||||
@@ -1,4 +1,4 @@
|
||||
package io.beaniejoy.dongnecafe.security.handler
|
||||
package io.beaniejoy.dongnecafe.infra.security.handler
|
||||
|
||||
import mu.KLogging
|
||||
import org.springframework.security.access.AccessDeniedException
|
||||
@@ -1,4 +1,4 @@
|
||||
package io.beaniejoy.dongnecafe.security.handler
|
||||
package io.beaniejoy.dongnecafe.infra.security.handler
|
||||
|
||||
import mu.KLogging
|
||||
import org.springframework.security.core.AuthenticationException
|
||||
@@ -0,0 +1,45 @@
|
||||
package io.beaniejoy.dongnecafe.utils.logging
|
||||
|
||||
import com.google.gson.Gson
|
||||
import io.beaniejoy.dongnecafe.infra.logging.constant.HttpClientIp
|
||||
import org.springframework.web.util.ContentCachingRequestWrapper
|
||||
import org.springframework.web.util.ContentCachingResponseWrapper
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
|
||||
fun HttpServletRequest.getRequestHeaders(): String? {
|
||||
val request = this
|
||||
return Gson().toJson(
|
||||
mutableMapOf<String, String?>().apply {
|
||||
request.headerNames.toList().forEach {
|
||||
this[it] = request.getHeader(it)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun HttpServletRequest.getRequestParams(): String {
|
||||
return this.parameterMap.mapValues {
|
||||
it.value.joinToString(",")
|
||||
}.entries.joinToString("&")
|
||||
}
|
||||
|
||||
fun HttpServletRequest.getClientIp(): String {
|
||||
HttpClientIp.values().forEach { clientIpHeader ->
|
||||
this.getHeader(clientIpHeader.headerName).also {
|
||||
if (it.isNullOrBlank().not() && "unknown".equals(it, true).not()) {
|
||||
return it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this.remoteAddr
|
||||
}
|
||||
|
||||
fun ContentCachingRequestWrapper.getRequestBody(): String {
|
||||
return this.contentAsByteArray.toString(Charsets.UTF_8)
|
||||
}
|
||||
|
||||
// TODO: logging response body maximum size 고려
|
||||
fun ContentCachingResponseWrapper.getResponseBody(): String {
|
||||
return this.contentAsByteArray.toString(Charsets.UTF_8)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package io.beaniejoy.dongnecafe.security.utils
|
||||
package io.beaniejoy.dongnecafe.utils.security
|
||||
|
||||
import io.beaniejoy.dongnecafe.security.SecurityUser
|
||||
import io.beaniejoy.dongnecafe.security.constant.SecurityConstant.JWT_AUTHORITIES_KEY
|
||||
import io.beaniejoy.dongnecafe.infra.security.SecurityUser
|
||||
import io.beaniejoy.dongnecafe.infra.security.constant.SecurityConstant.JWT_AUTHORITIES_KEY
|
||||
import io.jsonwebtoken.Claims
|
||||
import io.jsonwebtoken.ExpiredJwtException
|
||||
import io.jsonwebtoken.Jwts
|
||||
@@ -1,7 +1,7 @@
|
||||
package io.beaniejoy.dongnecafe.security.utils
|
||||
package io.beaniejoy.dongnecafe.utils.security
|
||||
|
||||
import io.beaniejoy.dongnecafe.security.constant.SecurityConstant.ANONYMOUS_USER
|
||||
import io.beaniejoy.dongnecafe.security.constant.SecurityConstant.ROLE_ANONYMOUS
|
||||
import io.beaniejoy.dongnecafe.infra.security.constant.SecurityConstant.ANONYMOUS_USER
|
||||
import io.beaniejoy.dongnecafe.infra.security.constant.SecurityConstant.ROLE_ANONYMOUS
|
||||
import org.springframework.security.core.Authentication
|
||||
|
||||
fun Authentication.getAuthPrincipal() : String? {
|
||||
@@ -1,9 +1,9 @@
|
||||
package io.beaniejoy.dongnecafe.common.config
|
||||
|
||||
import io.beaniejoy.dongnecafe.security.config.JwtAuthenticationConfigurer
|
||||
import io.beaniejoy.dongnecafe.security.utils.JwtTokenUtils
|
||||
import io.beaniejoy.dongnecafe.security.handler.CustomAccessDeniedHandler
|
||||
import io.beaniejoy.dongnecafe.security.handler.CustomAuthenticationEntryPoint
|
||||
import io.beaniejoy.dongnecafe.infra.security.config.JwtAuthenticationConfigurer
|
||||
import io.beaniejoy.dongnecafe.utils.security.JwtTokenUtils
|
||||
import io.beaniejoy.dongnecafe.infra.security.handler.CustomAccessDeniedHandler
|
||||
import io.beaniejoy.dongnecafe.infra.security.handler.CustomAuthenticationEntryPoint
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.autoconfigure.security.servlet.PathRequest
|
||||
import org.springframework.context.annotation.Bean
|
||||
|
||||
@@ -14,6 +14,9 @@ spring:
|
||||
devtools:
|
||||
livereload:
|
||||
enabled: false # no use devtools' LiveReload Server
|
||||
security:
|
||||
filter:
|
||||
order: 10
|
||||
|
||||
logging:
|
||||
level:
|
||||
|
||||
29
dongne-service-api/src/main/resources/logback-spring.xml
Normal file
29
dongne-service-api/src/main/resources/logback-spring.xml
Normal file
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
|
||||
<include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
|
||||
|
||||
<!-- Pattern -->
|
||||
<property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %clr(%5level) [%15.15t] [%X{request_id}] %clr(%-40.40logger{39}){cyan} : %m%n%wEx"/>
|
||||
<!-- Request Thread Console Appender -->
|
||||
<appender name="THREAD_CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
|
||||
<pattern>${LOG_PATTERN}</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<springProfile name="local">
|
||||
<logger additivity="false" level="INFO" name="io.beaniejoy.dongnecafe">
|
||||
<appender-ref ref="THREAD_CONSOLE"/>
|
||||
</logger>
|
||||
|
||||
<!-- Bootstrap class file -->
|
||||
<logger additivity="false" level="INFO" name="io.beaniejoy.dongnecafe.DongneServiceApiApplicationKt">
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
</logger>
|
||||
|
||||
<root level="INFO">
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
</root>
|
||||
</springProfile>
|
||||
</configuration>
|
||||
Reference in New Issue
Block a user