From fb9a647f3248bffc4bf9c8375d5ad40da43575ab Mon Sep 17 00:00:00 2001 From: pricelees Date: Sun, 27 Jul 2025 22:34:21 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=A0=84=EC=97=AD=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=93=B0=EC=9D=B4=EB=8A=94=20Masking=20Converter=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20=EB=B0=8F=20logback=20default=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../log/RoomescapeLogMaskingConverter.kt | 77 +++++++++++++++++++ src/main/resources/logback-local.xml | 5 +- src/main/resources/logback-spring.xml | 4 + 3 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/roomescape/common/log/RoomescapeLogMaskingConverter.kt diff --git a/src/main/kotlin/roomescape/common/log/RoomescapeLogMaskingConverter.kt b/src/main/kotlin/roomescape/common/log/RoomescapeLogMaskingConverter.kt new file mode 100644 index 00000000..cb12be26 --- /dev/null +++ b/src/main/kotlin/roomescape/common/log/RoomescapeLogMaskingConverter.kt @@ -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) } + } + } + } + } +} diff --git a/src/main/resources/logback-local.xml b/src/main/resources/logback-local.xml index 2465fb75..1e7a67ba 100644 --- a/src/main/resources/logback-local.xml +++ b/src/main/resources/logback-local.xml @@ -1,7 +1,10 @@ + + + value="%d{yyyy-MM-dd HH:mm:ss.SSS} %highlight(%-5level) %green(${PID:- }) --- [%15.15thread] %cyan(%-40logger{36}) : %maskedMessage%n%throwable"/> diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml index 69dd0b75..d60c1c92 100644 --- a/src/main/resources/logback-spring.xml +++ b/src/main/resources/logback-spring.xml @@ -3,4 +3,8 @@ + + + + \ No newline at end of file