generated from pricelees/issue-pr-template
refactor: 기존 log 관련 클래스 모듈 분리 및 일부 클래스는 재사용성 고려 리팩터링
This commit is contained in:
parent
7c52460ac6
commit
51a0dab2b4
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.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.context.annotation.Bean
|
||||
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 jakarta.servlet.http.HttpServletRequest
|
||||
import com.sangdol.common.log.config.LogType
|
||||
import com.sangdol.common.utils.MdcPrincipalIdUtil
|
||||
|
||||
enum class LogType {
|
||||
INCOMING_HTTP_REQUEST,
|
||||
CONTROLLER_INVOKED,
|
||||
CONTROLLER_SUCCESS,
|
||||
AUTHENTICATION_FAILURE,
|
||||
APPLICATION_FAILURE,
|
||||
UNHANDLED_EXCEPTION
|
||||
}
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
|
||||
class ApiLogMessageConverter(
|
||||
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.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.KotlinLogging
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
@ -24,7 +28,7 @@ class ControllerLoggingAspect(
|
||||
private val messageConverter: ApiLogMessageConverter,
|
||||
) {
|
||||
|
||||
@Pointcut("execution(* com.sangdol.roomescape..web..*Controller*.*(..))")
|
||||
@Pointcut("execution(* com.sangdol..web..*Controller*.*(..))")
|
||||
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.KotlinLogging
|
||||
import jakarta.servlet.FilterChain
|
||||
@ -9,7 +11,6 @@ import org.slf4j.MDC
|
||||
import org.springframework.web.filter.OncePerRequestFilter
|
||||
import org.springframework.web.util.ContentCachingRequestWrapper
|
||||
import org.springframework.web.util.ContentCachingResponseWrapper
|
||||
import com.sangdol.common.utils.MdcPrincipalIdUtil
|
||||
|
||||
private val log: KLogger = KotlinLogging.logger {}
|
||||
|
||||
@ -57,6 +57,7 @@ dependencies {
|
||||
implementation(project(":common:persistence"))
|
||||
implementation(project(":common:utils"))
|
||||
implementation(project(":common:types"))
|
||||
implementation(project(":common:log"))
|
||||
}
|
||||
|
||||
tasks.jar {
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
package com.sangdol.roomescape.common.log
|
||||
|
||||
import com.sangdol.common.log.sql.SlowQueryDataSourceFactory
|
||||
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.boot.context.properties.ConfigurationProperties
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties
|
||||
@ -23,15 +22,12 @@ class ProxyDataSourceConfig {
|
||||
fun dataSource(
|
||||
@Qualifier("actualDataSource") actualDataSource: DataSource,
|
||||
properties: SlowQueryProperties
|
||||
): DataSource = ProxyDataSourceBuilder.create(actualDataSource)
|
||||
.name(properties.loggerName)
|
||||
.listener(
|
||||
MDCAwareSlowQueryListenerWithoutParams(
|
||||
logLevel = SLF4JLogLevel.nullSafeValueOf(properties.logLevel.uppercase()),
|
||||
): DataSource = SlowQueryDataSourceFactory.create(
|
||||
dataSource = actualDataSource,
|
||||
loggerName = properties.loggerName,
|
||||
logLevel = properties.logLevel,
|
||||
thresholdMs = properties.thresholdMs
|
||||
)
|
||||
)
|
||||
.buildProxy()
|
||||
|
||||
@Bean
|
||||
@ConfigurationProperties(prefix = "spring.datasource.hikari")
|
||||
|
||||
@ -1,81 +1,9 @@
|
||||
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.log.message.AbstractLogMaskingConverter
|
||||
|
||||
private const val MASK: String = "****"
|
||||
private val SENSITIVE_KEYS = setOf("password", "accessToken", "phone")
|
||||
private val 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()}"
|
||||
}
|
||||
}
|
||||
}
|
||||
class RoomescapeLogMaskingConverter: AbstractLogMaskingConverter(
|
||||
sensitiveKeys = setOf("password", "accessToken", "phone"),
|
||||
objectMapper = JacksonConfig().objectMapper()
|
||||
)
|
||||
|
||||
@ -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