generated from pricelees/issue-pr-template
[#48] Tosspay mocking 서버 구현을 위한 멀티모듈 전환 #49
@ -1,77 +0,0 @@
|
|||||||
package com.sangdol.common.log.message
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
|
||||||
import com.sangdol.common.log.config.LogType
|
|
||||||
import com.sangdol.common.utils.MdcPrincipalIdUtil
|
|
||||||
import jakarta.servlet.http.HttpServletRequest
|
|
||||||
|
|
||||||
class ApiLogMessageConverter(
|
|
||||||
private val objectMapper: ObjectMapper
|
|
||||||
) {
|
|
||||||
fun convertToHttpRequestMessage(
|
|
||||||
request: HttpServletRequest
|
|
||||||
): String {
|
|
||||||
val payload: MutableMap<String, Any> = commonRequestPayload(LogType.INCOMING_HTTP_REQUEST, request)
|
|
||||||
|
|
||||||
request.queryString?.let { payload["query_params"] = it }
|
|
||||||
payload["client_ip"] = request.remoteAddr
|
|
||||||
payload["user_agent"] = request.getHeader("User-Agent")
|
|
||||||
|
|
||||||
return objectMapper.writeValueAsString(payload)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun convertToControllerInvokedMessage(
|
|
||||||
request: HttpServletRequest,
|
|
||||||
controllerPayload: Map<String, Any>,
|
|
||||||
): String {
|
|
||||||
val payload: MutableMap<String, Any> = commonRequestPayload(LogType.CONTROLLER_INVOKED, request)
|
|
||||||
val memberId: Long? = MdcPrincipalIdUtil.extractAsLongOrNull()
|
|
||||||
if (memberId != null) payload["principal_id"] = memberId else payload["principal_id"] = "NONE"
|
|
||||||
|
|
||||||
payload.putAll(controllerPayload)
|
|
||||||
|
|
||||||
return objectMapper.writeValueAsString(payload)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun convertToResponseMessage(request: ConvertResponseMessageRequest): String {
|
|
||||||
val payload: MutableMap<String, Any> = mutableMapOf()
|
|
||||||
payload["type"] = request.type
|
|
||||||
payload["endpoint"] = request.endpoint
|
|
||||||
payload["status_code"] = request.httpStatus
|
|
||||||
|
|
||||||
MdcPrincipalIdUtil.extractAsLongOrNull()
|
|
||||||
?.let { payload["principal_id"] = it }
|
|
||||||
?: run { payload["principal_id"] = "NONE" }
|
|
||||||
|
|
||||||
request.startTime?.let { payload["duration_ms"] = System.currentTimeMillis() - it }
|
|
||||||
request.body?.let { payload["response_body"] = it }
|
|
||||||
request.exception?.let {
|
|
||||||
payload["exception"] = mapOf(
|
|
||||||
"class" to it.javaClass.simpleName,
|
|
||||||
"message" to it.message
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return objectMapper.writeValueAsString(payload)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun commonRequestPayload(
|
|
||||||
logType: LogType,
|
|
||||||
request: HttpServletRequest
|
|
||||||
): MutableMap<String, Any> = mutableMapOf(
|
|
||||||
"type" to logType,
|
|
||||||
"method" to request.method,
|
|
||||||
"uri" to request.requestURI
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
data class ConvertResponseMessageRequest(
|
|
||||||
val type: LogType,
|
|
||||||
val endpoint: String,
|
|
||||||
val httpStatus: Int = 200,
|
|
||||||
val startTime: Long? = null,
|
|
||||||
val body: Any? = null,
|
|
||||||
val exception: Exception? = null
|
|
||||||
)
|
|
||||||
|
|
||||||
fun HttpServletRequest.getEndpoint(): String = "${this.method} ${this.requestURI}"
|
|
||||||
@ -1,69 +0,0 @@
|
|||||||
package com.sangdol.common.log
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
|
||||||
import com.sangdol.common.log.config.LogType
|
|
||||||
import com.sangdol.common.log.message.ApiLogMessageConverter
|
|
||||||
import com.sangdol.common.log.message.ConvertResponseMessageRequest
|
|
||||||
import io.kotest.core.spec.style.StringSpec
|
|
||||||
import io.kotest.matchers.shouldBe
|
|
||||||
import io.mockk.every
|
|
||||||
import io.mockk.mockk
|
|
||||||
import jakarta.servlet.http.HttpServletRequest
|
|
||||||
import org.slf4j.MDC
|
|
||||||
|
|
||||||
class ApiLogMessageConverterTest : StringSpec({
|
|
||||||
val converter = ApiLogMessageConverter(jacksonObjectMapper())
|
|
||||||
val request: HttpServletRequest = mockk()
|
|
||||||
|
|
||||||
beforeTest {
|
|
||||||
MDC.remove("principal_id")
|
|
||||||
MDC.put("principal_id", "1")
|
|
||||||
}
|
|
||||||
|
|
||||||
afterSpec {
|
|
||||||
MDC.remove("principal_id")
|
|
||||||
}
|
|
||||||
|
|
||||||
"HTTP 요청 메시지를 변환한다." {
|
|
||||||
val method = "POST".also { every { request.method } returns it }
|
|
||||||
val requestURI = "/test/sangdol".also { every { request.requestURI } returns it }
|
|
||||||
val clientIP = "127.0.0.1".also { every { request.remoteAddr } returns it }
|
|
||||||
val query = "key=value&key1=value1".also { every { request.queryString } returns it }
|
|
||||||
val userAgent = "Mozilla/5.".also { every { request.getHeader("User-Agent") } returns it }
|
|
||||||
|
|
||||||
converter.convertToHttpRequestMessage(request) shouldBe """
|
|
||||||
{"type":"INCOMING_HTTP_REQUEST","method":"$method","uri":"$requestURI","query_params":"$query","client_ip":"$clientIP","user_agent":"$userAgent"}
|
|
||||||
""".trimIndent()
|
|
||||||
}
|
|
||||||
|
|
||||||
"Controller 요청 메시지를 변환한다." {
|
|
||||||
val controllerPayload: Map<String, Any> = mapOf(
|
|
||||||
"controller_method" to "Controller 요청 메시지를 변환한다.",
|
|
||||||
"request_body" to mapOf("key1" to "value1")
|
|
||||||
)
|
|
||||||
val method = "POST".also { every { request.method } returns it }
|
|
||||||
val requestURI = "/test/sangdol".also { every { request.requestURI } returns it }
|
|
||||||
|
|
||||||
converter.convertToControllerInvokedMessage(request, controllerPayload) shouldBe """
|
|
||||||
{"type":"CONTROLLER_INVOKED","method":"$method","uri":"$requestURI","principal_id":1,"controller_method":"${
|
|
||||||
controllerPayload.get(
|
|
||||||
"controller_method"
|
|
||||||
)
|
|
||||||
}","request_body":{"key1":"value1"}}
|
|
||||||
""".trimIndent()
|
|
||||||
}
|
|
||||||
|
|
||||||
"Controller 응답 메시지를 반환한다." {
|
|
||||||
val endpoint = "POST /test/sangdol"
|
|
||||||
val request = ConvertResponseMessageRequest(
|
|
||||||
type = LogType.CONTROLLER_SUCCESS,
|
|
||||||
endpoint = endpoint,
|
|
||||||
httpStatus = 200,
|
|
||||||
exception = RuntimeException("테스트 메시지!")
|
|
||||||
)
|
|
||||||
|
|
||||||
converter.convertToResponseMessage(request) shouldBe """
|
|
||||||
{"type":"CONTROLLER_SUCCESS","endpoint":"$endpoint","status_code":200,"principal_id":1,"exception":{"class":"RuntimeException","message":"테스트 메시지!"}}
|
|
||||||
""".trimIndent()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@ -1,9 +1,9 @@
|
|||||||
package com.sangdol.common.log.config
|
package com.sangdol.common.web.config
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import com.sangdol.common.log.message.ApiLogMessageConverter
|
import com.sangdol.common.web.asepct.ControllerLoggingAspect
|
||||||
import com.sangdol.common.log.web.ControllerLoggingAspect
|
import com.sangdol.common.web.servlet.HttpRequestLoggingFilter
|
||||||
import com.sangdol.common.log.web.HttpRequestLoggingFilter
|
import com.sangdol.common.web.support.log.WebLogMessageConverter
|
||||||
import org.springframework.boot.web.servlet.FilterRegistrationBean
|
import org.springframework.boot.web.servlet.FilterRegistrationBean
|
||||||
import org.springframework.context.annotation.Bean
|
import org.springframework.context.annotation.Bean
|
||||||
import org.springframework.context.annotation.Configuration
|
import org.springframework.context.annotation.Configuration
|
||||||
@ -12,27 +12,27 @@ import org.springframework.core.Ordered
|
|||||||
import org.springframework.web.filter.OncePerRequestFilter
|
import org.springframework.web.filter.OncePerRequestFilter
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
class LogConfiguration {
|
class WebLoggingConfig {
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@DependsOn(value = ["apiLogMessageConverter"])
|
@DependsOn(value = ["webLogMessageConverter"])
|
||||||
fun filterRegistrationBean(
|
fun filterRegistrationBean(
|
||||||
apiLogMessageConverter: ApiLogMessageConverter
|
webLogMessageConverter: WebLogMessageConverter
|
||||||
): FilterRegistrationBean<OncePerRequestFilter> {
|
): FilterRegistrationBean<OncePerRequestFilter> {
|
||||||
val filter = HttpRequestLoggingFilter(apiLogMessageConverter)
|
val filter = HttpRequestLoggingFilter(webLogMessageConverter)
|
||||||
|
|
||||||
return FilterRegistrationBean<OncePerRequestFilter>(filter)
|
return FilterRegistrationBean<OncePerRequestFilter>(filter)
|
||||||
.apply { this.order = Ordered.HIGHEST_PRECEDENCE + 2 }
|
.apply { this.order = Ordered.HIGHEST_PRECEDENCE + 2 }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@DependsOn(value = ["apiLogMessageConverter"])
|
@DependsOn(value = ["webLogMessageConverter"])
|
||||||
fun apiLoggingAspect(apiLogMessageConverter: ApiLogMessageConverter): ControllerLoggingAspect {
|
fun apiLoggingAspect(webLogMessageConverter: WebLogMessageConverter): ControllerLoggingAspect {
|
||||||
return ControllerLoggingAspect(apiLogMessageConverter)
|
return ControllerLoggingAspect(webLogMessageConverter)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
fun apiLogMessageConverter(objectMapper: ObjectMapper): ApiLogMessageConverter {
|
fun webLogMessageConverter(objectMapper: ObjectMapper): WebLogMessageConverter {
|
||||||
return ApiLogMessageConverter(objectMapper)
|
return WebLogMessageConverter(objectMapper)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -0,0 +1,49 @@
|
|||||||
|
package com.sangdol.common.web.support.log
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import com.sangdol.common.log.constant.LogType
|
||||||
|
import jakarta.servlet.http.HttpServletRequest
|
||||||
|
|
||||||
|
class WebLogMessageConverter(
|
||||||
|
private val objectMapper: ObjectMapper
|
||||||
|
) {
|
||||||
|
fun convertToHttpRequestMessage(servletRequest: HttpServletRequest): String {
|
||||||
|
val payload = LogPayloadBuilder(type = LogType.INCOMING_HTTP_REQUEST, servletRequest = servletRequest)
|
||||||
|
.endpoint()
|
||||||
|
.queryString()
|
||||||
|
.clientIp()
|
||||||
|
.userAgent()
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return objectMapper.writeValueAsString(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun convertToControllerInvokedMessage(servletRequest: HttpServletRequest, controllerPayload: Map<String, Any>): String {
|
||||||
|
val payload = LogPayloadBuilder(type = LogType.CONTROLLER_INVOKED, servletRequest = servletRequest)
|
||||||
|
.endpoint()
|
||||||
|
.principalId()
|
||||||
|
.additionalPayloads(controllerPayload)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return objectMapper.writeValueAsString(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun convertToResponseMessage(
|
||||||
|
type: LogType,
|
||||||
|
servletRequest: HttpServletRequest,
|
||||||
|
httpStatusCode: Int,
|
||||||
|
responseBody: Any? = null,
|
||||||
|
exception: Exception? = null,
|
||||||
|
): String {
|
||||||
|
val payload = LogPayloadBuilder(type = type, servletRequest = servletRequest)
|
||||||
|
.endpoint()
|
||||||
|
.httpStatus(httpStatusCode)
|
||||||
|
.durationMs()
|
||||||
|
.principalId()
|
||||||
|
.responseBody(responseBody)
|
||||||
|
.exception(exception)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return objectMapper.writeValueAsString(payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,166 @@
|
|||||||
|
package com.sangdol.common.web.support.log
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
|
import com.sangdol.common.log.constant.LogType
|
||||||
|
import com.sangdol.common.types.web.HttpStatus
|
||||||
|
import com.sangdol.common.utils.MdcPrincipalIdUtil
|
||||||
|
import com.sangdol.common.utils.MdcStartTimeUtil
|
||||||
|
import io.kotest.assertions.assertSoftly
|
||||||
|
import io.kotest.core.spec.style.FunSpec
|
||||||
|
import io.kotest.matchers.nulls.shouldNotBeNull
|
||||||
|
import io.kotest.matchers.shouldBe
|
||||||
|
import io.mockk.clearMocks
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import jakarta.servlet.http.HttpServletRequest
|
||||||
|
|
||||||
|
class WebLogMessageConverterTest : FunSpec({
|
||||||
|
|
||||||
|
val objectMapper = jacksonObjectMapper()
|
||||||
|
val converter = WebLogMessageConverter(objectMapper)
|
||||||
|
val servletRequest: HttpServletRequest = mockk()
|
||||||
|
|
||||||
|
lateinit var method: String
|
||||||
|
lateinit var requestUri: String
|
||||||
|
lateinit var remoteAddr: String
|
||||||
|
lateinit var userAgent: String
|
||||||
|
lateinit var queryString: String
|
||||||
|
|
||||||
|
beforeTest {
|
||||||
|
method = "GET".also { every { servletRequest.method } returns it }
|
||||||
|
requestUri = "/converter/test".also { every { servletRequest.requestURI } returns it }
|
||||||
|
remoteAddr = "localhost".also { every { servletRequest.remoteAddr } returns it }
|
||||||
|
userAgent = "Mozilla/5.0".also { every { servletRequest.getHeader("User-Agent") } returns it }
|
||||||
|
queryString = "key=value".also { every { servletRequest.queryString } returns it }
|
||||||
|
}
|
||||||
|
|
||||||
|
afterSpec {
|
||||||
|
clearMocks(servletRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
context("Http 요청 메시지를 변환한다.") {
|
||||||
|
test("정상 응답") {
|
||||||
|
val result = converter.convertToHttpRequestMessage(servletRequest)
|
||||||
|
|
||||||
|
result shouldBe """
|
||||||
|
{"type":"${LogType.INCOMING_HTTP_REQUEST.name}","endpoint":"$method $requestUri","query_params":"$queryString","client_ip":"$remoteAddr","user_agent":"$userAgent"}
|
||||||
|
""".trimIndent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context("Controller 요청 메시지를 변환한다") {
|
||||||
|
val principalId = 759980174446956066L.also {
|
||||||
|
MdcPrincipalIdUtil.set(it.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
test("정상 응답") {
|
||||||
|
val controllerPayload: Map<String, Any> = mapOf(
|
||||||
|
"controller_method" to "ThemeController.findThemeById(..)",
|
||||||
|
"path_variable" to mapOf("id" to "7599801744469560667")
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = converter.convertToControllerInvokedMessage(servletRequest, controllerPayload)
|
||||||
|
|
||||||
|
result shouldBe """
|
||||||
|
{"type":"${LogType.CONTROLLER_INVOKED.name}","endpoint":"$method $requestUri","principal_id":$principalId,"controller_method":"${controllerPayload["controller_method"]}","path_variable":{"id":"${7599801744469560667}"}}
|
||||||
|
""".trimIndent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context("응답 메시지를 변환한다.") {
|
||||||
|
val principalId = 7599801744469560666
|
||||||
|
|
||||||
|
val body = mapOf(
|
||||||
|
"id" to 7599801744469560667,
|
||||||
|
"name" to "sangdol"
|
||||||
|
)
|
||||||
|
|
||||||
|
val exception = RuntimeException("hello")
|
||||||
|
|
||||||
|
beforeTest {
|
||||||
|
MdcPrincipalIdUtil.set(principalId.toString())
|
||||||
|
MdcStartTimeUtil.setCurrentTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
afterTest {
|
||||||
|
MdcPrincipalIdUtil.clear()
|
||||||
|
MdcStartTimeUtil.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
test("응답 본문을 포함한다.") {
|
||||||
|
val result = converter.convertToResponseMessage(
|
||||||
|
type = LogType.SUCCEED,
|
||||||
|
servletRequest = servletRequest,
|
||||||
|
httpStatusCode = HttpStatus.OK.value(),
|
||||||
|
responseBody = body
|
||||||
|
)
|
||||||
|
|
||||||
|
assertSoftly(objectMapper.readValue(result, LinkedHashMap::class.java)) {
|
||||||
|
this["type"] shouldBe LogType.SUCCEED.name
|
||||||
|
this["endpoint"] shouldBe "$method $requestUri"
|
||||||
|
this["status_code"] shouldBe HttpStatus.OK.value()
|
||||||
|
this["duration_ms"].shouldNotBeNull()
|
||||||
|
this["principal_id"] shouldBe principalId
|
||||||
|
this["response_body"] shouldBe body
|
||||||
|
this["exception"] shouldBe null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("예외를 포함한다.") {
|
||||||
|
val result = converter.convertToResponseMessage(
|
||||||
|
type = LogType.SUCCEED,
|
||||||
|
servletRequest = servletRequest,
|
||||||
|
httpStatusCode = HttpStatus.OK.value(),
|
||||||
|
exception = exception
|
||||||
|
)
|
||||||
|
|
||||||
|
assertSoftly(objectMapper.readValue(result, LinkedHashMap::class.java)) {
|
||||||
|
this["type"] shouldBe LogType.SUCCEED.name
|
||||||
|
this["endpoint"] shouldBe "$method $requestUri"
|
||||||
|
this["status_code"] shouldBe HttpStatus.OK.value()
|
||||||
|
this["duration_ms"].shouldNotBeNull()
|
||||||
|
this["principal_id"] shouldBe principalId
|
||||||
|
this["response_body"] shouldBe null
|
||||||
|
this["exception"] shouldBe mapOf("class" to exception.javaClass.simpleName, "message" to exception.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("예외 + 응답 본문을 모두 포함한다.") {
|
||||||
|
val result = converter.convertToResponseMessage(
|
||||||
|
type = LogType.SUCCEED,
|
||||||
|
servletRequest = servletRequest,
|
||||||
|
httpStatusCode = HttpStatus.OK.value(),
|
||||||
|
responseBody = body,
|
||||||
|
exception = exception
|
||||||
|
)
|
||||||
|
|
||||||
|
assertSoftly(objectMapper.readValue(result, LinkedHashMap::class.java)) {
|
||||||
|
this["type"] shouldBe LogType.SUCCEED.name
|
||||||
|
this["endpoint"] shouldBe "$method $requestUri"
|
||||||
|
this["status_code"] shouldBe HttpStatus.OK.value()
|
||||||
|
this["duration_ms"].shouldNotBeNull()
|
||||||
|
this["principal_id"] shouldBe principalId
|
||||||
|
this["response_body"] shouldBe body
|
||||||
|
this["exception"] shouldBe mapOf("class" to exception.javaClass.simpleName, "message" to exception.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("예외, 응답 본문 모두 제외한다.") {
|
||||||
|
val result = converter.convertToResponseMessage(
|
||||||
|
type = LogType.SUCCEED,
|
||||||
|
servletRequest = servletRequest,
|
||||||
|
httpStatusCode = HttpStatus.OK.value(),
|
||||||
|
)
|
||||||
|
|
||||||
|
assertSoftly(objectMapper.readValue(result, LinkedHashMap::class.java)) {
|
||||||
|
this["type"] shouldBe LogType.SUCCEED.name
|
||||||
|
this["endpoint"] shouldBe "$method $requestUri"
|
||||||
|
this["status_code"] shouldBe HttpStatus.OK.value()
|
||||||
|
this["duration_ms"].shouldNotBeNull()
|
||||||
|
this["principal_id"] shouldBe principalId
|
||||||
|
this["response_body"] shouldBe null
|
||||||
|
this["exception"] shouldBe null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
Loading…
x
Reference in New Issue
Block a user