generated from pricelees/issue-pr-template
[#48] Tosspay mocking 서버 구현을 위한 멀티모듈 전환 #49
15
common/log/build.gradle.kts
Normal file
15
common/log/build.gradle.kts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
plugins {
|
||||||
|
id("org.springframework.boot")
|
||||||
|
kotlin("plugin.spring")
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation("org.springframework.boot:spring-boot-starter-web")
|
||||||
|
implementation("org.springframework.boot:spring-boot-starter-aop")
|
||||||
|
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
|
||||||
|
implementation("net.ttddyy.observation:datasource-micrometer-spring-boot:1.1.1")
|
||||||
|
|
||||||
|
testImplementation("io.mockk:mockk:1.14.4")
|
||||||
|
|
||||||
|
implementation(project(":common:utils"))
|
||||||
|
}
|
||||||
@ -1,6 +1,9 @@
|
|||||||
package com.sangdol.roomescape.common.log
|
package com.sangdol.common.log.config
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import com.sangdol.common.log.message.ApiLogMessageConverter
|
||||||
|
import com.sangdol.common.log.web.ControllerLoggingAspect
|
||||||
|
import com.sangdol.common.log.web.HttpRequestLoggingFilter
|
||||||
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
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
package com.sangdol.common.log.config
|
||||||
|
|
||||||
|
enum class LogType {
|
||||||
|
INCOMING_HTTP_REQUEST,
|
||||||
|
CONTROLLER_INVOKED,
|
||||||
|
CONTROLLER_SUCCESS,
|
||||||
|
AUTHENTICATION_FAILURE,
|
||||||
|
APPLICATION_FAILURE,
|
||||||
|
UNHANDLED_EXCEPTION
|
||||||
|
}
|
||||||
@ -0,0 +1,78 @@
|
|||||||
|
package com.sangdol.common.log.message
|
||||||
|
|
||||||
|
import ch.qos.logback.classic.pattern.MessageConverter
|
||||||
|
import ch.qos.logback.classic.spi.ILoggingEvent
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import com.fasterxml.jackson.databind.node.ArrayNode
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode
|
||||||
|
import com.fasterxml.jackson.databind.node.TextNode
|
||||||
|
|
||||||
|
abstract class AbstractLogMaskingConverter(
|
||||||
|
val sensitiveKeys: Set<String>,
|
||||||
|
val objectMapper: ObjectMapper
|
||||||
|
) : MessageConverter() {
|
||||||
|
|
||||||
|
val mask: String = "****"
|
||||||
|
|
||||||
|
override fun convert(event: ILoggingEvent): String {
|
||||||
|
val message: String = event.formattedMessage
|
||||||
|
|
||||||
|
return if (isJsonString(message)) {
|
||||||
|
maskedJsonString(message)
|
||||||
|
} else {
|
||||||
|
maskedPlainMessage(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isJsonString(message: String): Boolean {
|
||||||
|
val trimmed = message.trim()
|
||||||
|
|
||||||
|
return trimmed.startsWith("{") && trimmed.endsWith("}")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun maskedJsonString(body: String): String = objectMapper.readValue(body, JsonNode::class.java)
|
||||||
|
.apply { maskRecursive(this) }
|
||||||
|
.toString()
|
||||||
|
|
||||||
|
private fun maskedPlainMessage(message: String): String {
|
||||||
|
val keys: String = sensitiveKeys.joinToString("|")
|
||||||
|
val regex = Regex("(?i)($keys)(\\s*[:=]\\s*)([^(,|\"|?)]+)")
|
||||||
|
|
||||||
|
return regex.replace(message) { matchResult ->
|
||||||
|
val key = matchResult.groupValues[1]
|
||||||
|
val delimiter = matchResult.groupValues[2]
|
||||||
|
val maskedValue = maskValue(matchResult.groupValues[3].trim())
|
||||||
|
|
||||||
|
"${key}${delimiter}${maskedValue}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun maskRecursive(node: JsonNode?) {
|
||||||
|
node?.forEachEntry { key, childNode ->
|
||||||
|
when {
|
||||||
|
childNode.isValueNode -> {
|
||||||
|
if (key in sensitiveKeys) (node as ObjectNode).put(key, maskValue(childNode.asText()))
|
||||||
|
}
|
||||||
|
|
||||||
|
childNode.isObject -> maskRecursive(childNode)
|
||||||
|
childNode.isArray -> {
|
||||||
|
val arrayNode = childNode as ArrayNode
|
||||||
|
val originSize = arrayNode.size()
|
||||||
|
if (originSize > 1) {
|
||||||
|
val first = arrayNode.first()
|
||||||
|
arrayNode.removeAll()
|
||||||
|
arrayNode.add(first)
|
||||||
|
arrayNode.add(TextNode("(...logged only first of $originSize elements)"))
|
||||||
|
}
|
||||||
|
|
||||||
|
arrayNode.forEach { maskRecursive(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun maskValue(value: String): String {
|
||||||
|
return "${value.first()}$mask${value.last()}"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,17 +1,9 @@
|
|||||||
package com.sangdol.roomescape.common.log
|
package com.sangdol.common.log.message
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import jakarta.servlet.http.HttpServletRequest
|
import com.sangdol.common.log.config.LogType
|
||||||
import com.sangdol.common.utils.MdcPrincipalIdUtil
|
import com.sangdol.common.utils.MdcPrincipalIdUtil
|
||||||
|
import jakarta.servlet.http.HttpServletRequest
|
||||||
enum class LogType {
|
|
||||||
INCOMING_HTTP_REQUEST,
|
|
||||||
CONTROLLER_INVOKED,
|
|
||||||
CONTROLLER_SUCCESS,
|
|
||||||
AUTHENTICATION_FAILURE,
|
|
||||||
APPLICATION_FAILURE,
|
|
||||||
UNHANDLED_EXCEPTION
|
|
||||||
}
|
|
||||||
|
|
||||||
class ApiLogMessageConverter(
|
class ApiLogMessageConverter(
|
||||||
private val objectMapper: ObjectMapper
|
private val objectMapper: ObjectMapper
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package com.sangdol.roomescape.common.log
|
package com.sangdol.common.log.sql
|
||||||
|
|
||||||
import net.ttddyy.dsproxy.ExecutionInfo
|
import net.ttddyy.dsproxy.ExecutionInfo
|
||||||
import net.ttddyy.dsproxy.QueryInfo
|
import net.ttddyy.dsproxy.QueryInfo
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
package com.sangdol.common.log.sql
|
||||||
|
|
||||||
|
import net.ttddyy.dsproxy.listener.logging.SLF4JLogLevel
|
||||||
|
import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder
|
||||||
|
import javax.sql.DataSource
|
||||||
|
|
||||||
|
object SlowQueryDataSourceFactory {
|
||||||
|
|
||||||
|
fun create(dataSource: DataSource, loggerName: String, logLevel: String, thresholdMs: Long): DataSource {
|
||||||
|
val mdcAwareListener = MDCAwareSlowQueryListenerWithoutParams(
|
||||||
|
logLevel = SLF4JLogLevel.nullSafeValueOf(logLevel.uppercase()),
|
||||||
|
thresholdMs = thresholdMs
|
||||||
|
)
|
||||||
|
|
||||||
|
return ProxyDataSourceBuilder.create(dataSource)
|
||||||
|
.name(loggerName)
|
||||||
|
.listener(mdcAwareListener)
|
||||||
|
.buildProxy()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,9 @@
|
|||||||
package com.sangdol.roomescape.common.log
|
package com.sangdol.common.log.web
|
||||||
|
|
||||||
|
import com.sangdol.common.log.config.LogType
|
||||||
|
import com.sangdol.common.log.message.ApiLogMessageConverter
|
||||||
|
import com.sangdol.common.log.message.ConvertResponseMessageRequest
|
||||||
|
import com.sangdol.common.log.message.getEndpoint
|
||||||
import io.github.oshai.kotlinlogging.KLogger
|
import io.github.oshai.kotlinlogging.KLogger
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import jakarta.servlet.http.HttpServletRequest
|
import jakarta.servlet.http.HttpServletRequest
|
||||||
@ -24,7 +28,7 @@ class ControllerLoggingAspect(
|
|||||||
private val messageConverter: ApiLogMessageConverter,
|
private val messageConverter: ApiLogMessageConverter,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@Pointcut("execution(* com.sangdol.roomescape..web..*Controller*.*(..))")
|
@Pointcut("execution(* com.sangdol..web..*Controller*.*(..))")
|
||||||
fun allController() {
|
fun allController() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1,5 +1,7 @@
|
|||||||
package com.sangdol.roomescape.common.log
|
package com.sangdol.common.log.web
|
||||||
|
|
||||||
|
import com.sangdol.common.log.message.ApiLogMessageConverter
|
||||||
|
import com.sangdol.common.utils.MdcPrincipalIdUtil
|
||||||
import io.github.oshai.kotlinlogging.KLogger
|
import io.github.oshai.kotlinlogging.KLogger
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import jakarta.servlet.FilterChain
|
import jakarta.servlet.FilterChain
|
||||||
@ -9,7 +11,6 @@ import org.slf4j.MDC
|
|||||||
import org.springframework.web.filter.OncePerRequestFilter
|
import org.springframework.web.filter.OncePerRequestFilter
|
||||||
import org.springframework.web.util.ContentCachingRequestWrapper
|
import org.springframework.web.util.ContentCachingRequestWrapper
|
||||||
import org.springframework.web.util.ContentCachingResponseWrapper
|
import org.springframework.web.util.ContentCachingResponseWrapper
|
||||||
import com.sangdol.common.utils.MdcPrincipalIdUtil
|
|
||||||
|
|
||||||
private val log: KLogger = KotlinLogging.logger {}
|
private val log: KLogger = KotlinLogging.logger {}
|
||||||
|
|
||||||
@ -57,6 +57,7 @@ dependencies {
|
|||||||
implementation(project(":common:persistence"))
|
implementation(project(":common:persistence"))
|
||||||
implementation(project(":common:utils"))
|
implementation(project(":common:utils"))
|
||||||
implementation(project(":common:types"))
|
implementation(project(":common:types"))
|
||||||
|
implementation(project(":common:log"))
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.jar {
|
tasks.jar {
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
package com.sangdol.roomescape.common.log
|
package com.sangdol.roomescape.common.log
|
||||||
|
|
||||||
|
import com.sangdol.common.log.sql.SlowQueryDataSourceFactory
|
||||||
import com.zaxxer.hikari.HikariDataSource
|
import com.zaxxer.hikari.HikariDataSource
|
||||||
import net.ttddyy.dsproxy.listener.logging.SLF4JLogLevel
|
|
||||||
import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder
|
|
||||||
import org.springframework.beans.factory.annotation.Qualifier
|
import org.springframework.beans.factory.annotation.Qualifier
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||||
import org.springframework.boot.context.properties.EnableConfigurationProperties
|
import org.springframework.boot.context.properties.EnableConfigurationProperties
|
||||||
@ -23,15 +22,12 @@ class ProxyDataSourceConfig {
|
|||||||
fun dataSource(
|
fun dataSource(
|
||||||
@Qualifier("actualDataSource") actualDataSource: DataSource,
|
@Qualifier("actualDataSource") actualDataSource: DataSource,
|
||||||
properties: SlowQueryProperties
|
properties: SlowQueryProperties
|
||||||
): DataSource = ProxyDataSourceBuilder.create(actualDataSource)
|
): DataSource = SlowQueryDataSourceFactory.create(
|
||||||
.name(properties.loggerName)
|
dataSource = actualDataSource,
|
||||||
.listener(
|
loggerName = properties.loggerName,
|
||||||
MDCAwareSlowQueryListenerWithoutParams(
|
logLevel = properties.logLevel,
|
||||||
logLevel = SLF4JLogLevel.nullSafeValueOf(properties.logLevel.uppercase()),
|
|
||||||
thresholdMs = properties.thresholdMs
|
thresholdMs = properties.thresholdMs
|
||||||
)
|
)
|
||||||
)
|
|
||||||
.buildProxy()
|
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@ConfigurationProperties(prefix = "spring.datasource.hikari")
|
@ConfigurationProperties(prefix = "spring.datasource.hikari")
|
||||||
|
|||||||
@ -1,81 +1,9 @@
|
|||||||
package com.sangdol.roomescape.common.log
|
package com.sangdol.roomescape.common.log
|
||||||
|
|
||||||
import ch.qos.logback.classic.pattern.MessageConverter
|
|
||||||
import ch.qos.logback.classic.spi.ILoggingEvent
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
|
||||||
import com.fasterxml.jackson.databind.node.ArrayNode
|
|
||||||
import com.fasterxml.jackson.databind.node.ObjectNode
|
|
||||||
import com.fasterxml.jackson.databind.node.TextNode
|
|
||||||
import com.sangdol.common.config.JacksonConfig
|
import com.sangdol.common.config.JacksonConfig
|
||||||
|
import com.sangdol.common.log.message.AbstractLogMaskingConverter
|
||||||
|
|
||||||
private const val MASK: String = "****"
|
class RoomescapeLogMaskingConverter: AbstractLogMaskingConverter(
|
||||||
private val SENSITIVE_KEYS = setOf("password", "accessToken", "phone")
|
sensitiveKeys = setOf("password", "accessToken", "phone"),
|
||||||
private val objectMapper: ObjectMapper = JacksonConfig().objectMapper()
|
objectMapper = JacksonConfig().objectMapper()
|
||||||
|
)
|
||||||
class RoomescapeLogMaskingConverter : MessageConverter() {
|
|
||||||
override fun convert(event: ILoggingEvent): String {
|
|
||||||
val message: String = event.formattedMessage
|
|
||||||
|
|
||||||
return if (isJsonString(message)) {
|
|
||||||
maskedJsonString(message)
|
|
||||||
} else {
|
|
||||||
maskedPlainMessage(message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isJsonString(message: String): Boolean {
|
|
||||||
val trimmed = message.trim()
|
|
||||||
|
|
||||||
return trimmed.startsWith("{") && trimmed.endsWith("}")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun maskedJsonString(body: String): String = objectMapper.readValue(body, JsonNode::class.java)
|
|
||||||
.apply { maskRecursive(this) }
|
|
||||||
.toString()
|
|
||||||
|
|
||||||
private fun maskedPlainMessage(message: String): String {
|
|
||||||
val keys: String = SENSITIVE_KEYS.joinToString("|")
|
|
||||||
val regex = Regex("(?i)($keys)(\\s*=\\s*)([^(,|\"|?)\\s]+)")
|
|
||||||
|
|
||||||
return regex.replace(message) { matchResult ->
|
|
||||||
val key = matchResult.groupValues[1]
|
|
||||||
val delimiter = matchResult.groupValues[2]
|
|
||||||
val maskedValue = maskValue(matchResult.groupValues[3])
|
|
||||||
|
|
||||||
"${key}${delimiter}${maskedValue}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun maskRecursive(node: JsonNode?) {
|
|
||||||
node?.forEachEntry { key, childNode ->
|
|
||||||
when {
|
|
||||||
childNode.isValueNode -> {
|
|
||||||
if (key in SENSITIVE_KEYS) (node as ObjectNode).put(key, maskValue(childNode.asText()))
|
|
||||||
}
|
|
||||||
|
|
||||||
childNode.isObject -> maskRecursive(childNode)
|
|
||||||
childNode.isArray -> {
|
|
||||||
val arrayNode = childNode as ArrayNode
|
|
||||||
val originSize = arrayNode.size()
|
|
||||||
if (originSize > 1) {
|
|
||||||
val first = arrayNode.first()
|
|
||||||
arrayNode.removeAll()
|
|
||||||
arrayNode.add(first)
|
|
||||||
arrayNode.add(TextNode("(...logged only first of $originSize elements)"))
|
|
||||||
}
|
|
||||||
|
|
||||||
arrayNode.forEach { maskRecursive(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun maskValue(value: String): String {
|
|
||||||
return if (value.length <= 2) {
|
|
||||||
MASK
|
|
||||||
} else {
|
|
||||||
"${value.first()}$MASK${value.last()}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,26 +0,0 @@
|
|||||||
package com.sangdol.roomescape.common.log
|
|
||||||
|
|
||||||
import io.kotest.assertions.assertSoftly
|
|
||||||
import io.kotest.core.spec.style.StringSpec
|
|
||||||
import io.kotest.matchers.shouldBe
|
|
||||||
|
|
||||||
class MDCAwareSlowQueryListenerWithoutParamsTest : StringSpec({
|
|
||||||
"SQL 메시지에서 Params 항목은 가린다." {
|
|
||||||
val message = """Query:["select * from members m where m.email=?"], Params:[(a@a.a)]"""
|
|
||||||
val expected = """Query:["select * from members m where m.email=?"]"""
|
|
||||||
val result = SqlLogFormatter().maskParams(message)
|
|
||||||
|
|
||||||
result shouldBe expected
|
|
||||||
}
|
|
||||||
|
|
||||||
"입력된 thresholdMs 보다 소요시간이 긴 쿼리를 기록한다." {
|
|
||||||
val slowQueryThreshold = 10L
|
|
||||||
val slowQueryPredicate = SlowQueryPredicate(thresholdMs = slowQueryThreshold)
|
|
||||||
|
|
||||||
assertSoftly(slowQueryPredicate) {
|
|
||||||
it.test(slowQueryThreshold) shouldBe true
|
|
||||||
it.test(slowQueryThreshold + 1) shouldBe true
|
|
||||||
it.test(slowQueryThreshold - 1) shouldBe false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
Loading…
x
Reference in New Issue
Block a user