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