이전부터 키오스쿨에 로깅 시스템은 필요하다고 생각하긴 했다.
아무래도 아무런 정보없이 이런 저런 문제를 해결하기는 쉽지 않으니깐…
그래서 이번에 로깅 시스템을 추가해보려고 하는데… 사실 이 분야에 대해서는 아는 지식이 전무해서… 이번에 새로 나온 gemini-cli를 활용해서 시스템을 구축해보았다!
일단 기본적으로 프롬프트도 잘 못짜기도 하고!! 이전에 gemini한테 로그와 모니터링 시스템으로 상담을 한 적이 있어서 그걸 토대로 프롬프트를 부탁했다.
저는 Spring Boot 프로젝트를 위한 **자동화된 로그 시스템** 구축 방안을 찾고 있습니다. 제가 원하는 시스템은 다음과 같은 핵심 기능을 갖춰야 합니다:
1. **자동 로깅 (Auto-Logging):**
* 개발자가 Logger 객체를 직접 주입받아 로그를 남기지 않더라도, **주요 애플리케이션 이벤트 (예: 컨트롤러 엔드포인트 호출, 서비스 메서드 실행, 데이터베이스 접근 등)가 자동으로 기록**되길 원합니다.
* **새로운 엔드포인트나 메서드가 추가될 때 별도의 코드 수정 없이** 해당 호출 정보(메서드명, 인자, 반환 값, 실행 시간 등)가 자동으로 로깅되어야 합니다.
2. **민감 정보 자동 마스킹 (Automatic Data Masking):**
* 개발자가 로그 레벨에서 개별 필드를 수동으로 마스킹하는 대신, 특정 필드에 **간단한 어노테이션 (`@Masked`, `@Sensitive` 등)을 추가**하는 것만으로 해당 필드의 값이 로그에 노출되지 않도록 (예: `****` 또는 `[MASKED]`) 처리되길 원합니다.
* 이 마스킹 기능은 **로그 출력 시점에 동적으로 적용**되어야 합니다.
이러한 요구사항을 만족하는 로그 시스템 아키텍처와 구체적인 Spring Boot 구현 방안, 그리고 필요한 라이브러리 및 설정에 대해 상세히 설명해 주세요. **각 기능별 코드 예시도 함께 제공**해 주시면 좋겠습니다.
백엔드에는 **Loki**를 로그 저장소로, **Promtail**을 로그 수집 에이전트로, **Grafana**를 시각화 도구로 활용하는 스택을 고려하고 있습니다. Spring Boot 애플리케이션의 로그가 이 스택으로 효과적으로 연동될 수 있는 방안도 함께 제안해 주세요. 로그는 **JSON 형식**으로 출력되어 Loki에서 분석하기 용이하도록 구성되어야 합니다.
이전에 내가 원했던 구상으로는
이런 방식이었다.
그렇게 gemini-cli에게 일을 시켜본 결과… 아주 만족스럽게 잘하더라… 내 일자리에 위협을 아주 강하게 주는….
우리 프로젝트에서는 아래와 같이 구성되었다.
LoggingAspect
package com.kioschool.kioschoolapi.global.common.annotation
import com.fasterxml.jackson.databind.ObjectMapper
import jakarta.servlet.ServletRequest
import jakarta.servlet.ServletResponse
import org.aspectj.lang.ProceedingJoinPoint
import org.aspectj.lang.annotation.Around
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.Pointcut
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Component
import org.springframework.util.StopWatch
import org.springframework.web.multipart.MultipartFile
@Aspect
@Component
class LoggingAspect(private val objectMapper: ObjectMapper) {
private val log = LoggerFactory.getLogger(this.javaClass)
// Pointcut: com.kioschool.kioschoolapi 하위의 모든 컨트롤러와 서비스에 적용
@Pointcut(
"within(@org.springframework.web.bind.annotation.RestController *)"
)
fun isApiService() {
}
@Around("isApiService()")
fun logExecutionTime(joinPoint: ProceedingJoinPoint): Any? {
val stopWatch = StopWatch()
stopWatch.start()
val className = joinPoint.signature.declaringTypeName
val methodName = joinPoint.signature.name
val args = joinPoint.args
// Filter out web-related objects from arguments
val filteredArgs = filterWebObjects(args)
// 메서드 실행 전 로그
log.info(
"--> {}#{}() called with args: {}",
className,
methodName,
objectMapper.writeValueAsString(filteredArgs)
)
var result: Any? = null
try {
result = joinPoint.proceed()
return result
} catch (e: Exception) {
log.error("<-- {}#{}() threw exception: {}", className, methodName, e.message)
throw e // 예외를 다시 던져서 트랜잭션 등에 영향을 주지 않도록 함
} finally {
stopWatch.stop()
val executionTime = stopWatch.totalTimeMillis
// Filter out web-related objects from result
val filteredResult = filterWebObjects(result)
// 메서드 실행 후 로그
log.info(
"<-- {}#{}() returned: {} (execution time: {}ms)",
className,
methodName,
objectMapper.writeValueAsString(filteredResult),
executionTime
)
}
}
private fun filterWebObjects(obj: Any?): Any? {
return when (obj) {
is Array<*> -> obj.map { filterWebObjects(it) }.toTypedArray()
is Collection<*> -> obj.map { filterWebObjects(it) }
is ServletRequest, is ServletResponse, is MultipartFile -> "[WEB_OBJECT]"
else -> obj
}
}
}
MaskingSerializer
package com.kioschool.kioschoolapi.global.common.util
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.databind.JsonSerializer
import com.fasterxml.jackson.databind.SerializerProvider
class MaskingSerializer : JsonSerializer<Any>() {
override fun serialize(value: Any?, gen: JsonGenerator, serializers: SerializerProvider) {
if (value != null) {
gen.writeString("****")
} else {
gen.writeNull()
}
}
}