feat: 전역으로 쓰이는 Masking Converter 구현 및 logback default 설정 추가

This commit is contained in:
이상진 2025-07-27 22:34:21 +09:00
parent 38d82e06b2
commit fb9a647f32
3 changed files with 85 additions and 1 deletions

View File

@ -0,0 +1,77 @@
package 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 io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging
import roomescape.common.config.JacksonConfig
private val SENSITIVE_KEYS = setOf("password", "accessToken")
private val log: KLogger = KotlinLogging.logger {}
class RoomescapeLogMaskingConverter(
private val objectMapper: ObjectMapper = JacksonConfig().objectMapper()
) : MessageConverter() {
override fun convert(event: ILoggingEvent): String {
val message: String = event.formattedMessage
log.warn { "[RoomescapeLogMaskingConverter.convert] formattedMessage: $message" }
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]
"${key}${delimiter}****"
}
}
private fun maskRecursive(node: JsonNode?) {
node?.forEachEntry { key, childNode ->
when {
childNode.isValueNode -> {
if (key in SENSITIVE_KEYS) (node as ObjectNode).put(key, "****")
}
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) }
}
}
}
}
}

View File

@ -1,7 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<included> <included>
<conversionRule conversionWord="maskedMessage"
class="roomescape.common.log.RoomescapeLogMaskingConverter" />
<property name="CONSOLE_LOG_PATTERN" <property name="CONSOLE_LOG_PATTERN"
value="%d{yyyy-MM-dd HH:mm:ss.SSS} %highlight(%-5level) %green(${PID:- }) --- [%15.15thread] %cyan(%-40logger{36}) : %msg%n%throwable"/> value="%d{yyyy-MM-dd HH:mm:ss.SSS} %highlight(%-5level) %green(${PID:- }) --- [%15.15thread] %cyan(%-40logger{36}) : %maskedMessage%n%throwable"/>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder> <encoder>

View File

@ -3,4 +3,8 @@
<springProfile name="local"> <springProfile name="local">
<include resource="logback-local.xml"/> <include resource="logback-local.xml"/>
</springProfile> </springProfile>
<springProfile name="default">
<include resource="logback-local.xml"/>
</springProfile>
</configuration> </configuration>