Spring Framework/Filter

Spring Filter Logging- Spring Filter Hands On 4

Junuuu 2023. 10. 15. 00:01

개요

Spring Filter를 활용해서 Response, Request에 대한 로깅을 수행해보고자 합니다.

 

시작 전 주의사항

HttpServletRequest, HttpServletResponse의 경우에 내부적으로 Stream을 사용하기 때문에 ContentCachingRequestWrapper와 ContentCachingResponseWrapper를 사용해주어야 합니다.

 

Stream은 한번 소비하면 재사용할 수 없습니다.

 

Response Request Logging

class ResponseRequestLoggingFilter : OncePerRequestFilter() {

    override fun doFilterInternal(
        request: HttpServletRequest,
        response: HttpServletResponse,
        filterChain: FilterChain
    ) {
        val requestWrapper = ContentCachingRequestWrapper(request)
        val responseWrapper = ContentCachingResponseWrapper(response)
        val start = System.currentTimeMillis()
        filterChain.doFilter(requestWrapper, responseWrapper)
        val end = System.currentTimeMillis()

        com.example.study.log.logger.info{
            """
                "[REQUEST] ${request.method} - ${request.requestURI} ${responseWrapper.status} - ${(end - start) / 1000.0}
                "Headers : ${getHeaders(request)}
                "Request : ${getRequestBody(requestWrapper)}
                "Response : ${getResponseBody(responseWrapper)}
            """.trimIndent()
        }
    }

    private fun getHeaders(request: HttpServletRequest): Map<Any, Any> {
        val headerMap: MutableMap<Any, Any> = HashMap<Any, Any>()
        val headerArray: Enumeration<*> = request.headerNames
        while (headerArray.hasMoreElements()) {
            val headerName = headerArray.nextElement() as String
            headerMap[headerName] = request.getHeader(headerName)
        }
        return headerMap
    }

    private fun getRequestBody(request: ContentCachingRequestWrapper): String {
        val wrapper = WebUtils.getNativeRequest(
            request,
            ContentCachingRequestWrapper::class.java
        )
        if (wrapper != null) {
            val buf = wrapper.contentAsByteArray
            if (buf.size > 0) {
                try {
                    return String(buf, 0, buf.size, charset(wrapper.characterEncoding))
                } catch (e: UnsupportedEncodingException) {
                    return " - "
                }
            }
        }
        return " - "
    }

    @Throws(IOException::class)
    private fun getResponseBody(response: HttpServletResponse): String {
        var payload: String? = null
        val wrapper = WebUtils.getNativeResponse(
            response,
            ContentCachingResponseWrapper::class.java
        )
        if (wrapper != null) {
            val buf = wrapper.contentAsByteArray
            if (buf.size > 0) {
                payload = String(buf, 0, buf.size, charset(wrapper.characterEncoding))
                wrapper.copyBodyToResponse()
            }
        }
        return payload ?: " - "
    }
}

 

여러 가지 Controller를 구현하여 테스트

@GetMapping("/path-and-header-logging/{user-id}")
fun pathVariableAndHeaderLogging(@RequestHeader role: String, @PathVariable("user-id") id: String): ResponseEntity<TestDTO> {
    return ResponseEntity.ok(
        TestDTO(
            role = role,
            id = id,
        )
    )
}

### 로깅 테스트
GET localhost:8080/path-and-header-logging/1234
role: admin

로그 결과

@PostMapping("/request-body")
fun responseEntity(@RequestBody testDTO: TestDTO): String{
    println(testDTO)
    return "ok"
}

### 로깅 테스트
POST localhost:8080/request-body
content-type: application/json
role: admin

{
  "id": "1234",
  "role": "user"
}

로그 결과

@GetMapping("/exception-log")
fun illegalArgumentExceptionLogging() {
    throw IllegalArgumentException("컨트롤러로 들어와서 예외발생")
}

### 로깅 예외 테스트
GET localhost:8080/exception-log
role: admin

로그 결과

마무리

@RestControllerAdvice
class GlobalExceptionHandler {
    @ExceptionHandler(Exception::class)
    fun globalException(e: Exception): ResponseEntity<String> {
        return ResponseEntity
            .status(HttpStatus.BAD_REQUEST)
            .body(e.message)
    }
}

위의 방법으로 구현하면 FilterChain 과정에서 예외가 발생하는 경우에는 Filter Logging이 남지 않습니다.

이런 경우에는 try-catch나 runCatching으로 잡아서 해결해주어야 합니다.

또한 컨트롤러에 진입하여 예외가 발생하는 경우 ControllerAdvice에서 예외 메시지를 response Body로 전달해 주어야 합니다.

 

response에 예외 메시지가 출력됩니다.