diff --git a/apis/build.gradle.kts b/apis/build.gradle.kts index 96e2b325..bce7bff6 100644 --- a/apis/build.gradle.kts +++ b/apis/build.gradle.kts @@ -22,7 +22,7 @@ dependencies { implementation(Dependencies.BouncyCastle.BC_PROV) implementation(Dependencies.BouncyCastle.BC_PKIX) - annotationProcessor(Dependencies.Spring.CONFIGURATION_PROCESSOR) + kapt(Dependencies.Spring.CONFIGURATION_PROCESSOR) testImplementation(Dependencies.Spring.BOOT_STARTER_TEST) testImplementation(Dependencies.TestContainers.MYSQL) diff --git a/apis/src/main/kotlin/org/yapp/apis/book/service/AladinBookQueryService.kt b/apis/src/main/kotlin/org/yapp/apis/book/service/AladinBookQueryService.kt index 760ac181..7a1fb417 100644 --- a/apis/src/main/kotlin/org/yapp/apis/book/service/AladinBookQueryService.kt +++ b/apis/src/main/kotlin/org/yapp/apis/book/service/AladinBookQueryService.kt @@ -25,7 +25,7 @@ class AladinBookQueryService( private val MAX_ALADIN_RESULTS = 200 // Added constant override fun searchBooks(request: BookSearchRequest): BookSearchResponse { - log.info { "Service - Converting BookSearchRequest to AladinBookSearchRequest and calling Aladin API for book search." } +// log.info { "Service - Converting BookSearchRequest to AladinBookSearchRequest and calling Aladin API for book search." } val aladinSearchRequest = AladinBookSearchRequest.of( request.validQuery(), request.queryType, @@ -44,7 +44,7 @@ class AladinBookQueryService( throw BookException(BookErrorCode.ALADIN_API_SEARCH_FAILED, exception.message) } - log.info { "Before filtering - Full Response: $response" } +// log.info { "Before filtering - Full Response: $response" } val transformedItems = response.item.mapNotNull { item -> val validIsbn13 = getValidAndFilteredIsbn13(item) @@ -75,13 +75,13 @@ class AladinBookQueryService( searchCategoryName = response.searchCategoryName, item = transformedItems ) - log.info { "After filtering - Full Response: $filteredResponse" } +// log.info { "After filtering - Full Response: $filteredResponse" } return BookSearchResponse.of(filteredResponse, isLastPage) // Passed isLastPage } override fun getBookDetail(request: BookDetailRequest): BookDetailResponse { - log.info("Service - Converting BookDetailRequest to AladinBookLookupRequest and calling Aladin API for book detail lookup.") +// log.info("Service - Converting BookDetailRequest to AladinBookLookupRequest and calling Aladin API for book detail lookup.") val aladinLookupRequest = AladinBookLookupRequest.from(request.validIsbn13()) val response: AladinBookDetailResponse = aladinApi.lookupBook(aladinLookupRequest) .onSuccess { response -> diff --git a/apis/src/main/kotlin/org/yapp/apis/config/InfraConfig.kt b/apis/src/main/kotlin/org/yapp/apis/config/InfraConfig.kt index 0c03e16a..feba8433 100644 --- a/apis/src/main/kotlin/org/yapp/apis/config/InfraConfig.kt +++ b/apis/src/main/kotlin/org/yapp/apis/config/InfraConfig.kt @@ -13,6 +13,7 @@ import org.yapp.infra.InfraBaseConfigGroup InfraBaseConfigGroup.REST_CLIENT, InfraBaseConfigGroup.QUERY_DSL, InfraBaseConfigGroup.PAGE, + InfraBaseConfigGroup.AOP ] ) class InfraConfig diff --git a/build.gradle.kts b/build.gradle.kts index 8084f136..d6f4ce65 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -48,6 +48,7 @@ subprojects { apply(plugin = Plugins.Kotlin.JPA) apply(plugin = Plugins.Kotlin.JVM) apply(plugin = Plugins.JACOCO) + apply(plugin = Plugins.Kotlin.KAPT) java { toolchain { @@ -55,6 +56,14 @@ subprojects { } } + configurations.all { + exclude(group = "org.springframework.boot", module = "spring-boot-starter-logging") + } + + dependencies { + implementation(Dependencies.Spring.STARTER_LOG4J2) + } + plugins.withId(Plugins.Kotlin.ALLOPEN) { extensions.configure { annotation("jakarta.persistence.Entity") @@ -71,6 +80,7 @@ subprojects { "-Xconsistent-data-class-copy-visibility" ) jvmTarget = Versions.JAVA_VERSION + javaParameters = true } } } diff --git a/buildSrc/src/main/kotlin/Dependencies.kt b/buildSrc/src/main/kotlin/Dependencies.kt index b4d5270f..949b9e38 100644 --- a/buildSrc/src/main/kotlin/Dependencies.kt +++ b/buildSrc/src/main/kotlin/Dependencies.kt @@ -12,6 +12,9 @@ object Dependencies { const val BOOT_STARTER_OAUTH2_CLIENT = "org.springframework.boot:spring-boot-starter-oauth2-client" const val KOTLIN_REFLECT = "org.jetbrains.kotlin:kotlin-reflect" const val CONFIGURATION_PROCESSOR = "org.springframework.boot:spring-boot-configuration-processor" + const val SPRING_DATA_COMMONS = "org.springframework.data:spring-data-commons" + const val SPRING_WEB = "org.springframework:spring-web" + const val STARTER_LOG4J2 = "org.springframework.boot:spring-boot-starter-log4j2" } object Database { diff --git a/buildSrc/src/main/kotlin/Plugins.kt b/buildSrc/src/main/kotlin/Plugins.kt index 3651cb0e..569910d8 100644 --- a/buildSrc/src/main/kotlin/Plugins.kt +++ b/buildSrc/src/main/kotlin/Plugins.kt @@ -11,6 +11,7 @@ object Plugins { const val SPRING = "org.jetbrains.kotlin.plugin.spring" const val JPA = "org.jetbrains.kotlin.plugin.jpa" const val JVM = "org.jetbrains.kotlin.jvm" + const val KAPT = "org.jetbrains.kotlin.kapt" object Short { const val KAPT = "kapt" diff --git a/domain/build.gradle.kts b/domain/build.gradle.kts index a2bd4d90..a5632868 100644 --- a/domain/build.gradle.kts +++ b/domain/build.gradle.kts @@ -2,9 +2,9 @@ import org.springframework.boot.gradle.tasks.bundling.BootJar dependencies { implementation(project(Dependencies.Projects.GLOBAL_UTILS)) - implementation(Dependencies.Spring.BOOT_STARTER_WEB) - implementation(Dependencies.Spring.BOOT_STARTER_DATA_JPA) testImplementation(Dependencies.Spring.BOOT_STARTER_TEST) + implementation(Dependencies.Spring.SPRING_DATA_COMMONS) // Pageable, Page용 + implementation(Dependencies.Spring.SPRING_WEB) // HttpsStatus용으로 추가한 것이기 때문에 추후 Global-utils 모듈에 httpStatus를 정의하고 이를 의존해서 사용하도록 바꾸면 해당 의존성 삭제 가능 } tasks { diff --git a/domain/src/main/kotlin/org/yapp/domain/config/CoreDomainConfig.kt b/domain/src/main/kotlin/org/yapp/domain/config/CoreDomainConfig.kt deleted file mode 100644 index 3451fc5b..00000000 --- a/domain/src/main/kotlin/org/yapp/domain/config/CoreDomainConfig.kt +++ /dev/null @@ -1,8 +0,0 @@ -package org.yapp.domain.config - -import org.springframework.boot.autoconfigure.domain.EntityScan -import org.springframework.data.jpa.repository.config.EnableJpaRepositories - -@EntityScan("org.yapp") -@EnableJpaRepositories("org.yapp") -class CoreDomainConfig diff --git a/gateway/src/main/kotlin/org/yapp/gateway/filter/MdcLoggingFilter.kt b/gateway/src/main/kotlin/org/yapp/gateway/filter/MdcLoggingFilter.kt new file mode 100644 index 00000000..b2277191 --- /dev/null +++ b/gateway/src/main/kotlin/org/yapp/gateway/filter/MdcLoggingFilter.kt @@ -0,0 +1,79 @@ +package org.yapp.gateway.filter + +import jakarta.servlet.FilterChain +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.slf4j.MDC +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.security.oauth2.jwt.Jwt +import org.springframework.stereotype.Component +import org.springframework.web.filter.OncePerRequestFilter +import java.util.* + +@Component +class MdcLoggingFilter : OncePerRequestFilter() { + companion object { + private const val TRACE_ID_HEADER = "X-Request-ID" + private const val XFF_HEADER = "X-Forwarded-For" + private const val X_REAL_IP_HEADER = "X-Real-IP" + private const val TRACE_ID_KEY = "traceId" + private const val USER_ID_KEY = "userId" + private const val CLIENT_IP_KEY = "clientIp" + private const val REQUEST_INFO_KEY = "requestInfo" + private const val DEFAULT_GUEST_USER = "GUEST" + } + + override fun doFilterInternal( + request: HttpServletRequest, + response: HttpServletResponse, + filterChain: FilterChain + ) { + val traceId = resolveTraceId(request) + populateMdc(request, traceId) + + try { + filterChain.doFilter(request, response) + } finally { + MDC.clear() + } + } + + private fun resolveTraceId(request: HttpServletRequest): String { + val incomingTraceId = request.getHeader(TRACE_ID_HEADER) + return incomingTraceId?.takeIf { it.isNotBlank() } + ?: UUID.randomUUID().toString().replace("-", "") + } + + private fun populateMdc(request: HttpServletRequest, traceId: String) { + MDC.put(TRACE_ID_KEY, traceId) + MDC.put(CLIENT_IP_KEY, extractClientIp(request)) + MDC.put(REQUEST_INFO_KEY, "${request.method} ${request.requestURI}") + + val userId = resolveUserId() + MDC.put(USER_ID_KEY, userId ?: DEFAULT_GUEST_USER) + } + + private fun extractClientIp(request: HttpServletRequest): String { + val xffHeader = request.getHeader(XFF_HEADER) + if (!xffHeader.isNullOrBlank()) { + return xffHeader.split(",").first().trim() + } + + val xRealIp = request.getHeader(X_REAL_IP_HEADER) + if (!xRealIp.isNullOrBlank()) { + return xRealIp.trim() + } + + return request.remoteAddr + } + + private fun resolveUserId(): String? { + val authentication = SecurityContextHolder.getContext().authentication ?: return null + + return when (val principal = authentication.principal) { + is Jwt -> principal.subject + else -> principal?.toString() + } + } +} + diff --git a/gateway/src/main/kotlin/org/yapp/gateway/security/SecurityConfig.kt b/gateway/src/main/kotlin/org/yapp/gateway/security/SecurityConfig.kt index e89b5b94..1e59e331 100644 --- a/gateway/src/main/kotlin/org/yapp/gateway/security/SecurityConfig.kt +++ b/gateway/src/main/kotlin/org/yapp/gateway/security/SecurityConfig.kt @@ -2,21 +2,23 @@ package org.yapp.gateway.security import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration +import org.springframework.core.convert.converter.Converter +import org.springframework.security.authentication.AbstractAuthenticationToken import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity import org.springframework.security.config.http.SessionCreationPolicy -import org.springframework.core.convert.converter.Converter -import org.springframework.http.HttpMethod -import org.springframework.security.authentication.AbstractAuthenticationToken import org.springframework.security.oauth2.jwt.Jwt +import org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationFilter import org.springframework.security.web.SecurityFilterChain +import org.yapp.gateway.filter.MdcLoggingFilter @Configuration @EnableWebSecurity class SecurityConfig( private val jwtAuthenticationConverter: Converter, private val customAuthenticationEntryPoint: CustomAuthenticationEntryPoint, - private val customAccessDeniedHandler: CustomAccessDeniedHandler + private val customAccessDeniedHandler: CustomAccessDeniedHandler, + private val mdcLoggingFilter: MdcLoggingFilter ) { companion object { private val WHITELIST_URLS = arrayOf( @@ -53,6 +55,7 @@ class SecurityConfig( it.requestMatchers("/api/v1/admin/**").hasRole("ADMIN") it.anyRequest().authenticated() } + .addFilterAfter(mdcLoggingFilter, BearerTokenAuthenticationFilter::class.java) return http.build() } diff --git a/global-utils/src/main/kotlin/org/yapp/globalutils/annotation/NoLogging.kt b/global-utils/src/main/kotlin/org/yapp/globalutils/annotation/NoLogging.kt new file mode 100644 index 00000000..cc11ec0f --- /dev/null +++ b/global-utils/src/main/kotlin/org/yapp/globalutils/annotation/NoLogging.kt @@ -0,0 +1,5 @@ +package org.yapp.globalutils.annotation + +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +annotation class NoLogging diff --git a/global-utils/src/main/kotlin/org/yapp/globalutils/exception/GlobalExceptionHandler.kt b/global-utils/src/main/kotlin/org/yapp/globalutils/exception/GlobalExceptionHandler.kt index 95ffb25e..74ded8ca 100644 --- a/global-utils/src/main/kotlin/org/yapp/globalutils/exception/GlobalExceptionHandler.kt +++ b/global-utils/src/main/kotlin/org/yapp/globalutils/exception/GlobalExceptionHandler.kt @@ -9,6 +9,7 @@ import org.springframework.web.HttpRequestMethodNotSupportedException import org.springframework.web.bind.MethodArgumentNotValidException import org.springframework.web.bind.annotation.ExceptionHandler import org.springframework.web.bind.annotation.RestControllerAdvice +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException /** * Global exception handler for the application. @@ -159,6 +160,27 @@ class GlobalExceptionHandler { return ResponseEntity(error, commonErrorCode.getHttpStatus()) } + /** + * 메서드 파라미터 타입 변환 실패 처리 + * + * 주로 @RequestParam, @PathVariable 등에서 클라이언트가 잘못된 타입의 값을 전달했을 때 발생합니다. + * 예: 문자열을 int 타입으로 변환 시도 + */ + @ExceptionHandler(MethodArgumentTypeMismatchException::class) + protected fun handleMethodArgumentTypeMismatch(ex: MethodArgumentTypeMismatchException): ResponseEntity { + val commonErrorCode = CommonErrorCode.INVALID_REQUEST + + log.warn { "Method argument type mismatch: ${ex.name}, value: ${ex.value}, requiredType: ${ex.requiredType}" } + + val error = ErrorResponse.builder() + .status(commonErrorCode.getHttpStatus().value()) + .message("Invalid value '${ex.value}' for parameter '${ex.name}'. Expected type: ${ex.requiredType?.simpleName}") + .code(commonErrorCode.getCode()) + .build() + + return ResponseEntity(error, commonErrorCode.getHttpStatus()) + } + /** * 그 외 모든 예외 처리 * diff --git a/infra/build.gradle.kts b/infra/build.gradle.kts index 56f6a6f4..9e63dd68 100644 --- a/infra/build.gradle.kts +++ b/infra/build.gradle.kts @@ -1,30 +1,25 @@ import org.springframework.boot.gradle.tasks.bundling.BootJar -plugins { - kotlin(Plugins.Kotlin.Short.KAPT) version Versions.KOTLIN -} - dependencies { implementation(project(Dependencies.Projects.GLOBAL_UTILS)) implementation(project(Dependencies.Projects.DOMAIN)) implementation(Dependencies.Spring.BOOT_STARTER_WEB) implementation(Dependencies.Spring.BOOT_STARTER_DATA_JPA) implementation(Dependencies.Spring.BOOT_STARTER_DATA_REDIS) + implementation(Dependencies.Spring.KOTLIN_REFLECT) + implementation(Dependencies.RestClient.HTTP_CLIENT5) implementation(Dependencies.RestClient.HTTP_CORE5) - testImplementation(Dependencies.Spring.BOOT_STARTER_TEST) - - implementation(Dependencies.Spring.KOTLIN_REFLECT) implementation(Dependencies.Database.MYSQL_CONNECTOR) - implementation(Dependencies.Flyway.MYSQL) implementation(Dependencies.QueryDsl.JPA) - kapt(Dependencies.QueryDsl.APT) + kapt(Dependencies.QueryDsl.APT) + testImplementation(Dependencies.Spring.BOOT_STARTER_TEST) testImplementation(Dependencies.TestContainers.MYSQL) testImplementation(Dependencies.TestContainers.JUNIT_JUPITER) testImplementation(Dependencies.TestContainers.REDIS) diff --git a/infra/src/main/kotlin/org/yapp/infra/InfraBaseConfigGroup.kt b/infra/src/main/kotlin/org/yapp/infra/InfraBaseConfigGroup.kt index c1b1612b..b1550582 100644 --- a/infra/src/main/kotlin/org/yapp/infra/InfraBaseConfigGroup.kt +++ b/infra/src/main/kotlin/org/yapp/infra/InfraBaseConfigGroup.kt @@ -2,6 +2,7 @@ package org.yapp.infra import org.yapp.infra.config.external.api.RestClientConfig import org.yapp.infra.config.external.redis.RedisConfig +import org.yapp.infra.config.internal.aop.AopConfig import org.yapp.infra.config.internal.async.AsyncConfig import org.yapp.infra.config.internal.jpa.JpaConfig import org.yapp.infra.config.internal.page.PageConfig @@ -15,5 +16,6 @@ enum class InfraBaseConfigGroup( PAGE(PageConfig::class.java), REDIS(RedisConfig::class.java), REST_CLIENT(RestClientConfig::class.java), - QUERY_DSL(QuerydslConfig::class.java) + QUERY_DSL(QuerydslConfig::class.java), + AOP(AopConfig::class.java) } diff --git a/infra/src/main/kotlin/org/yapp/infra/aop/aspect/ControllerLoggingAspect.kt b/infra/src/main/kotlin/org/yapp/infra/aop/aspect/ControllerLoggingAspect.kt new file mode 100644 index 00000000..b9a465e3 --- /dev/null +++ b/infra/src/main/kotlin/org/yapp/infra/aop/aspect/ControllerLoggingAspect.kt @@ -0,0 +1,62 @@ +package org.yapp.infra.aop.aspect + +import jakarta.servlet.http.HttpServletRequest +import org.aspectj.lang.ProceedingJoinPoint +import org.aspectj.lang.annotation.Around +import org.aspectj.lang.annotation.Aspect +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Component +import org.springframework.web.context.request.RequestContextHolder +import org.springframework.web.context.request.ServletRequestAttributes +import org.yapp.infra.aop.properties.LoggingAopProperties +import java.time.Duration +import java.time.Instant + +@Aspect +@Component +class ControllerLoggingAspect( + private val properties: LoggingAopProperties +) { + private val log = LoggerFactory.getLogger(ControllerLoggingAspect::class.java) + + @Around("org.yapp.infra.aop.pointcut.CommonPointcuts.controller() && !org.yapp.infra.aop.pointcut.CommonPointcuts.noLogging()") + fun logController(joinPoint: ProceedingJoinPoint): Any? { + if (!properties.controller.enabled) { + return joinPoint.proceed() + } + + val startTime = Instant.now() + logRequestStart(joinPoint) + + try { + val result = joinPoint.proceed() + logRequestSuccess(startTime) + return result + } catch (e: Throwable) { + throw e + } + } + + private fun logRequestStart(joinPoint: ProceedingJoinPoint) { + val signature = joinPoint.signature + val className = signature.declaringType.simpleName + val methodName = signature.name + + val request = getCurrentRequest() + val httpMethod = request?.method ?: "UNKNOWN" + val uri = request?.requestURI ?: "UNKNOWN" + + log.info("[API-REQ] {} {} | Controller: {}.{}", httpMethod, uri, className, methodName) + } + + private fun logRequestSuccess(startTime: Instant) { + val executionTimeMs = getExecutionTimeMs(startTime) + log.info("[API-RES] Logic Duration: {}ms", executionTimeMs) + } + + private fun getCurrentRequest(): HttpServletRequest? = + (RequestContextHolder.getRequestAttributes() as? ServletRequestAttributes)?.request + + private fun getExecutionTimeMs(startTime: Instant): Long = + Duration.between(startTime, Instant.now()).toMillis() +} diff --git a/infra/src/main/kotlin/org/yapp/infra/aop/aspect/ServiceLoggingAspect.kt b/infra/src/main/kotlin/org/yapp/infra/aop/aspect/ServiceLoggingAspect.kt new file mode 100644 index 00000000..76416c3c --- /dev/null +++ b/infra/src/main/kotlin/org/yapp/infra/aop/aspect/ServiceLoggingAspect.kt @@ -0,0 +1,130 @@ +package org.yapp.infra.aop.aspect + +import com.fasterxml.jackson.databind.ObjectMapper +import org.aspectj.lang.ProceedingJoinPoint +import org.aspectj.lang.annotation.Around +import org.aspectj.lang.annotation.Aspect +import org.aspectj.lang.reflect.MethodSignature +import org.slf4j.LoggerFactory +import org.springframework.core.env.Environment +import org.springframework.stereotype.Component +import org.yapp.infra.aop.properties.LoggingAopProperties +import java.time.Duration +import java.time.Instant +import java.util.* + +@Aspect +@Component +class ServiceLoggingAspect( + private val properties: LoggingAopProperties, + private val objectMapper: ObjectMapper, + private val environment: Environment +) { + companion object { + private val log = LoggerFactory.getLogger(ServiceLoggingAspect::class.java) + private const val MASKING_TEXT = "****" + private const val PROD_PROFILE = "prod" + } + + private val isProdEnvironment: Boolean by lazy { + environment.activeProfiles.contains(PROD_PROFILE) + } + + @Around("org.yapp.infra.aop.pointcut.CommonPointcuts.serviceLayer() && !org.yapp.infra.aop.pointcut.CommonPointcuts.noLogging()") + fun logService(joinPoint: ProceedingJoinPoint): Any? { + if (!properties.service.enabled) { + return joinPoint.proceed() + } + + val signature = joinPoint.signature as MethodSignature + val startTime = Instant.now() + + logServiceStart(signature, joinPoint.args) + + try { + val result = joinPoint.proceed() + logServiceSuccess(signature, startTime, result) + return result + } catch (e: Throwable) { + throw e + } + } + + private fun logServiceStart(signature: MethodSignature, args: Array) { + val className = signature.declaringType.simpleName + val methodName = signature.name + val params = getArgumentsAsString(signature, args) + + log.info("[SVC-START] {}.{} | Params: {}", className, methodName, truncateIfNeeded(params)) + } + + private fun logServiceSuccess(signature: MethodSignature, startTime: Instant, result: Any?) { + val className = signature.declaringType.simpleName + val methodName = signature.name + val duration = Duration.between(startTime, Instant.now()).toMillis() + val returnValue = formatValue(result) + + log.info( + "[SVC-SUCCESS] {}.{} | Result: {} | Duration: {}ms", + className, + methodName, + truncateIfNeeded(returnValue), + duration + ) + } + + private fun getArgumentsAsString(signature: MethodSignature, args: Array): String { + return signature.parameterNames.mapIndexed { index, paramName -> + val value = if (shouldMaskParameter(paramName)) { + MASKING_TEXT + } else { + formatValue(args.getOrNull(index)) + } + "$paramName=$value" + }.joinToString(", ") + } + + private fun shouldMaskParameter(paramName: String): Boolean { + return isProdEnvironment && properties.service.sensitiveFields.any { sensitiveField -> + paramName.lowercase().contains(sensitiveField.lowercase()) + } + } + + private fun formatValue(obj: Any?): String { + return when (obj) { + null -> "null" + is Unit -> "void" + is String -> "\"$obj\"" + is Number, is Boolean -> obj.toString() + is UUID -> obj.toString() + is Enum<*> -> obj.name + is Collection<*> -> "[${obj.size} items]" + is Array<*> -> "${obj.javaClass.simpleName}[${obj.size}]" + else -> maskMapLikeObject(obj) + } + } + + private fun maskMapLikeObject(obj: Any): String { + return try { + val map: Map<*, *> = objectMapper.convertValue(obj, Map::class.java) + val maskedMap = map.mapValues { (key, value) -> + val keyStr = key.toString() + if (shouldMaskParameter(keyStr)) { + return@mapValues MASKING_TEXT + } + value + } + objectMapper.writeValueAsString(maskedMap) + } catch (e: Exception) { + "${obj.javaClass.simpleName}@${Integer.toHexString(obj.hashCode())}" + } + } + + private fun truncateIfNeeded(text: String): String { + return if (text.length > properties.service.maxLogLength) { + "${text.substring(0, properties.service.maxLogLength)}...[truncated]" + } else { + text + } + } +} diff --git a/infra/src/main/kotlin/org/yapp/infra/aop/pointcut/CommonPointcuts.kt b/infra/src/main/kotlin/org/yapp/infra/aop/pointcut/CommonPointcuts.kt new file mode 100644 index 00000000..5b5b34a7 --- /dev/null +++ b/infra/src/main/kotlin/org/yapp/infra/aop/pointcut/CommonPointcuts.kt @@ -0,0 +1,29 @@ +package org.yapp.infra.aop.pointcut + +import org.aspectj.lang.annotation.Pointcut + +object CommonPointcuts { + @Pointcut( + """ + within(@org.springframework.web.bind.annotation.RestController *) || + within(@org.springframework.stereotype.Controller *) + """ + ) + fun controller() { + } + + @Pointcut( + """ + within(@org.yapp.globalutils.annotation.UseCase *) || + within(@org.yapp.globalutils.annotation.ApplicationService *) || + within(@org.yapp.globalutils.annotation.DomainService *) || + within(@org.springframework.stereotype.Service *) + """ + ) + fun serviceLayer() { + } + + @Pointcut("@annotation(org.yapp.globalutils.annotation.NoLogging) || @within(org.yapp.globalutils.annotation.NoLogging)") + fun noLogging() { + } +} diff --git a/infra/src/main/kotlin/org/yapp/infra/aop/properties/LoggingAopProperties.kt b/infra/src/main/kotlin/org/yapp/infra/aop/properties/LoggingAopProperties.kt new file mode 100644 index 00000000..3c229a2e --- /dev/null +++ b/infra/src/main/kotlin/org/yapp/infra/aop/properties/LoggingAopProperties.kt @@ -0,0 +1,25 @@ +package org.yapp.infra.aop.properties + +import org.springframework.boot.context.properties.ConfigurationProperties + +@ConfigurationProperties(prefix = "logging.aop") +data class LoggingAopProperties( + val service: ServiceLoggingProperties = ServiceLoggingProperties(), + val controller: ControllerLoggingProperties = ControllerLoggingProperties() +) { + data class ServiceLoggingProperties( + val enabled: Boolean = true, + val maxLogLength: Int = 1000, + val sensitiveFields: Set = setOf( + "refreshToken", + "oauthToken", + "authorizationCode", + "providerId", + "accessToken" + ) + ) + + data class ControllerLoggingProperties( + val enabled: Boolean = true + ) +} diff --git a/infra/src/main/kotlin/org/yapp/infra/config/internal/aop/AopConfig.kt b/infra/src/main/kotlin/org/yapp/infra/config/internal/aop/AopConfig.kt new file mode 100644 index 00000000..63fe78fe --- /dev/null +++ b/infra/src/main/kotlin/org/yapp/infra/config/internal/aop/AopConfig.kt @@ -0,0 +1,10 @@ +package org.yapp.infra.config.internal.aop + +import org.springframework.boot.context.properties.EnableConfigurationProperties +import org.springframework.context.annotation.Configuration +import org.yapp.infra.InfraBaseConfig +import org.yapp.infra.aop.properties.LoggingAopProperties + +@Configuration +@EnableConfigurationProperties(LoggingAopProperties::class) +class AopConfig : InfraBaseConfig diff --git a/infra/src/main/kotlin/org/yapp/infra/config/internal/async/AsyncConfig.kt b/infra/src/main/kotlin/org/yapp/infra/config/internal/async/AsyncConfig.kt index c011c5fb..7589eb39 100644 --- a/infra/src/main/kotlin/org/yapp/infra/config/internal/async/AsyncConfig.kt +++ b/infra/src/main/kotlin/org/yapp/infra/config/internal/async/AsyncConfig.kt @@ -1,6 +1,7 @@ package org.yapp.infra.config.internal.async import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration import org.springframework.context.event.ApplicationEventMulticaster import org.springframework.context.event.SimpleApplicationEventMulticaster import org.springframework.context.support.AbstractApplicationContext.APPLICATION_EVENT_MULTICASTER_BEAN_NAME @@ -10,6 +11,7 @@ import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor import java.util.concurrent.Executor import org.yapp.infra.InfraBaseConfig +@Configuration @EnableAsync class AsyncConfig : InfraBaseConfig, AsyncConfigurer { @@ -45,6 +47,7 @@ class AsyncConfig : InfraBaseConfig, AsyncConfigurer { executor.setThreadNamePrefix("async-") executor.setWaitForTasksToCompleteOnShutdown(true) executor.setAwaitTerminationSeconds(30) + executor.setTaskDecorator(MdcTaskDecorator()) executor.initialize() return executor } diff --git a/infra/src/main/kotlin/org/yapp/infra/config/internal/async/MdcTaskDecorator.kt b/infra/src/main/kotlin/org/yapp/infra/config/internal/async/MdcTaskDecorator.kt new file mode 100644 index 00000000..1858fa7f --- /dev/null +++ b/infra/src/main/kotlin/org/yapp/infra/config/internal/async/MdcTaskDecorator.kt @@ -0,0 +1,22 @@ +package org.yapp.infra.config.internal.async + +import org.slf4j.MDC +import org.springframework.core.task.TaskDecorator + +class MdcTaskDecorator : TaskDecorator { + + override fun decorate(runnable: Runnable): Runnable { + val contextMap = MDC.getCopyOfContextMap() + + return Runnable { + val previous = MDC.getCopyOfContextMap() + + try { + contextMap?.let(MDC::setContextMap) ?: MDC.clear() + runnable.run() + } finally { + previous?.let(MDC::setContextMap) ?: MDC.clear() + } + } + } +} diff --git a/infra/src/main/kotlin/org/yapp/infra/userbook/repository/JpaUserBookRepository.kt b/infra/src/main/kotlin/org/yapp/infra/userbook/repository/JpaUserBookRepository.kt index 6db7d51b..05c8fec9 100644 --- a/infra/src/main/kotlin/org/yapp/infra/userbook/repository/JpaUserBookRepository.kt +++ b/infra/src/main/kotlin/org/yapp/infra/userbook/repository/JpaUserBookRepository.kt @@ -9,5 +9,5 @@ interface JpaUserBookRepository : JpaRepository, JpaUserBo fun findByBookIdAndUserId(bookId: UUID, userId: UUID): UserBookEntity? fun existsByIdAndUserId(id: UUID, userId: UUID): Boolean fun findAllByUserId(userId: UUID): List - fun findAllByUserIdAndBookIsbn13In(userId: UUID, bookIsbn13List: List): List + fun findAllByUserIdAndBookIsbn13In(userId: UUID, bookIsbn13s: List): List } diff --git a/infra/src/main/kotlin/org/yapp/infra/userbook/repository/impl/UserBookRepositoryImpl.kt b/infra/src/main/kotlin/org/yapp/infra/userbook/repository/impl/UserBookRepositoryImpl.kt index 08bbffb4..4453b3f3 100644 --- a/infra/src/main/kotlin/org/yapp/infra/userbook/repository/impl/UserBookRepositoryImpl.kt +++ b/infra/src/main/kotlin/org/yapp/infra/userbook/repository/impl/UserBookRepositoryImpl.kt @@ -44,9 +44,9 @@ class UserBookRepositoryImpl( override fun findAllByUserIdAndBookIsbn13In( userId: UUID, - bookIsbns: List + bookIsbn13s: List ): List { - return jpaUserBookRepository.findAllByUserIdAndBookIsbn13In(userId, bookIsbns) + return jpaUserBookRepository.findAllByUserIdAndBookIsbn13In(userId, bookIsbn13s) .map { it.toDomain() } } diff --git a/infra/src/main/resources/application-persistence.yml b/infra/src/main/resources/application-persistence.yml index 762a2bfe..59935a7f 100644 --- a/infra/src/main/resources/application-persistence.yml +++ b/infra/src/main/resources/application-persistence.yml @@ -24,6 +24,17 @@ spring: - classpath:db/migration - classpath:db/seed + cache: + type: simple + +logging: + aop: + service: + enabled: true + max-log-length: 1000 + controller: + enabled: true + --- spring: config: diff --git a/infra/src/main/resources/log4j2-spring.xml b/infra/src/main/resources/log4j2-spring.xml new file mode 100644 index 00000000..b17c0030 --- /dev/null +++ b/infra/src/main/resources/log4j2-spring.xml @@ -0,0 +1,46 @@ + + + + + + + %d{yyyy-MM-dd HH:mm:ss.SSSSSS} %-5level [%thread] [%X{traceId}] [%X{userId}] [%X{clientIp}] [%X{requestInfo}] %logger{36} - %msg%n + + + + %d{yyyy-MM-dd HH:mm:ss.SSSSSS} %highlight{%-5level}{FATAL=red blink, ERROR=red, WARN=yellow bold, INFO=green, DEBUG=green bold, TRACE=blue} %style{[%thread]}{cyan} %style{[%X{traceId}]}{green} %style{[%X{userId}]}{magenta} %style{[%X{clientIp}]}{blue} %style{[%X{requestInfo}]}{yellow} %style{%logger{36}}{bright,white} - %msg%n + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +