Skip to content

Commit

Permalink
Handle logging: log request + response+ MDC
Browse files Browse the repository at this point in the history
  • Loading branch information
haiks-2382 committed Oct 31, 2023
1 parent 79eff5c commit 7881823
Show file tree
Hide file tree
Showing 7 changed files with 289 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.vndevteam.kotlinwebspringboot3.application.logging

import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Value
import org.springframework.core.MethodParameter
import org.springframework.http.MediaType
import org.springframework.http.converter.HttpMessageConverter
import org.springframework.http.server.ServerHttpRequest
import org.springframework.http.server.ServerHttpResponse
import org.springframework.http.server.ServletServerHttpRequest
import org.springframework.http.server.ServletServerHttpResponse
import org.springframework.web.bind.annotation.ControllerAdvice
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice


@ControllerAdvice
class CustomResponseBody : ResponseBodyAdvice<Any> {
companion object {
val log: Logger = LoggerFactory.getLogger(CustomResponseBody::class.java)
}

@Value("\${app.logging.enable-log-response}")
val enableLogResponse: Boolean = false

override fun supports(returnType: MethodParameter, converterType: Class<out HttpMessageConverter<*>>): Boolean {
return true
}

override fun beforeBodyWrite(
body: Any?,
returnType: MethodParameter,
selectedContentType: MediaType,
selectedConverterType: Class<out HttpMessageConverter<*>>,
request: ServerHttpRequest,
response: ServerHttpResponse
): Any? {
if (enableLogResponse && request is ServletServerHttpRequest && response is ServletServerHttpResponse) {
loggingResponse(request.servletRequest, response.servletResponse, body)
}

return body
}

fun loggingResponse(request: HttpServletRequest, response: HttpServletResponse, body: Any?) {
val respMessage = StringBuilder()
val headers = getHeaders(response)
respMessage.append("RESPONSE ")
respMessage.append(" method = [").append(request.method).append("]")
if (headers.isNotEmpty()) {
respMessage.append(" ResponseHeaders = [").append(headers).append("]")
}
respMessage.append(" responseBody = [").append(body).append("]")
log.info("Log response: {}", respMessage)
}

private fun getHeaders(response: HttpServletResponse): Map<String, String> {
val headers: MutableMap<String, String> = HashMap()
val headerMap = response.headerNames
for (str in headerMap) {
headers[str] = response.getHeader(str)
}
return headers
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package com.vndevteam.kotlinwebspringboot3.application.logging

import jakarta.servlet.FilterChain
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.jboss.logging.MDC
import org.springframework.web.filter.OncePerRequestFilter
import java.util.*

class MDCLoggingFilter : OncePerRequestFilter {
companion object {
private const val X_FORWARDED_FOR = "X-Forwarded-For"
}

private var responseHeader: String? = null
private var mdcTokenKey: String? = null
private var mdcClientIpKey: String? = null
private var requestHeader: String? = null

constructor() {
this.responseHeader = Slf4jMDCFilterConfig.DEFAULT_RESPONSE_TOKEN_HEADER
this.mdcTokenKey = Slf4jMDCFilterConfig.DEFAULT_MDC_UUID_TOKEN_KEY
this.mdcClientIpKey = Slf4jMDCFilterConfig.DEFAULT_MDC_CLIENT_IP_KEY
this.requestHeader = null
}

constructor(responseHeader: String, mdcTokenKey: String, mdcClientIpKey: String, requestHeader: String?) {
this.responseHeader = responseHeader
this.mdcTokenKey = mdcTokenKey
this.mdcClientIpKey = mdcClientIpKey
this.requestHeader = requestHeader
}

override fun doFilterInternal(
request: HttpServletRequest,
response: HttpServletResponse,
filterChain: FilterChain
) {
try {
val clientId: String = getClientId(request)
val clientIp: String = getClientIpAddress(request)
MDC.put(mdcClientIpKey, clientIp)
MDC.put(mdcTokenKey, clientId)

if (!responseHeader.isNullOrBlank()) {
response.addHeader(responseHeader, clientId)
}

filterChain.doFilter(request, response)
} finally {
MDC.remove(mdcTokenKey)
MDC.remove(mdcClientIpKey)
}
}

override fun isAsyncDispatch(request: HttpServletRequest): Boolean {
return false
}

override fun shouldNotFilterErrorDispatch(): Boolean {
return false
}

private fun getClientIpAddress(httpHeaders: HttpServletRequest): String {
val xForwardedHeader = httpHeaders.getHeader(X_FORWARDED_FOR)
if (xForwardedHeader.isNullOrBlank()) {
return httpHeaders.remoteAddr
}
return xForwardedHeader.split(',')[0].trim()
}

private fun getClientId(request: HttpServletRequest): String {
val clientId: String =
if (!requestHeader.isNullOrBlank() && !request.getHeader(requestHeader).isNullOrBlank()) {
request.getHeader(requestHeader)
} else {
UUID.randomUUID().toString()
}
return clientId
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.vndevteam.kotlinwebspringboot3.application.logging

import jakarta.servlet.http.HttpServletRequest
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Configuration
import org.springframework.web.filter.CommonsRequestLoggingFilter

@Configuration
class RequestLoggingFilter : CommonsRequestLoggingFilter() {
companion object {
private const val HEADER_NAME_API_KEY = "api-key"
}

@Value("\${app.logging.enable-log-request}")
val enableLogRequest: Boolean = false

init {
isIncludeClientInfo = true
isIncludeQueryString = true
isIncludePayload = true
maxPayloadLength = 10000
isIncludeHeaders = true
super.setBeforeMessagePrefix("REQUEST DATA: ")
// Hide sensitive header if need
super.setHeaderPredicate { header -> header != HEADER_NAME_API_KEY }
}

override fun beforeRequest(request: HttpServletRequest, message: String) {
if (enableLogRequest) {
logger.info(message)
}
}

override fun afterRequest(request: HttpServletRequest, message: String) {

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.vndevteam.kotlinwebspringboot3.application.logging

import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.web.servlet.FilterRegistrationBean
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration


@Configuration
@ConfigurationProperties(prefix = "app.slf4jfilter")
class Slf4jMDCFilterConfig {
companion object {
const val DEFAULT_RESPONSE_TOKEN_HEADER = "x-client-id"
const val DEFAULT_MDC_UUID_TOKEN_KEY = "RequestFilter.UUID"
const val DEFAULT_MDC_CLIENT_IP_KEY = "RequestFilter.ClientIP"
}

var responseHeader: String = DEFAULT_RESPONSE_TOKEN_HEADER
var mdcTokenKey : String = DEFAULT_MDC_UUID_TOKEN_KEY
var mdcClientIpKey: String = DEFAULT_MDC_CLIENT_IP_KEY
var requestHeader: String? = null

@Bean
fun servletRegistrationBean(): FilterRegistrationBean<MDCLoggingFilter>? {
val registrationBean: FilterRegistrationBean<MDCLoggingFilter> = FilterRegistrationBean<MDCLoggingFilter>()
val log4jMDCFilterFilter = MDCLoggingFilter(responseHeader, mdcTokenKey, mdcClientIpKey, requestHeader)
registrationBean.filter = log4jMDCFilterFilter
registrationBean.order = 2
return registrationBean
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.vndevteam.kotlinwebspringboot3.util

import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.SerializationFeature
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule

class MapperUtils {
companion object {
fun objectMapperForKotlin(): ObjectMapper {
val objectMapper = ObjectMapper()
objectMapper.registerModule(JavaTimeModule())
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
return objectMapper
}
}
}
10 changes: 10 additions & 0 deletions kotlin-web-spring-boot-3/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,13 @@ app:
locale:
default: ja # ja, vi, en,...

slf4jfilter:
response-header: 'x-client-id'
mdc-token-key: 'RequestFilter.UUID'
mdc-client-ip-key: 'RequestFilter.ClientIP'
request-header:

logging:
enable-log-request: true
enable-log-response: true
enable-log-response-time: true
47 changes: 47 additions & 0 deletions kotlin-web-spring-boot-3/src/main/resources/logback-spring.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>

<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>
%black(%d{ISO8601}) [%X{RequestFilter.UUID}] %highlight(%-5level) [%blue(%t)] %yellow(%C{2}:%L): %msg%n%throwable
</pattern>
</encoder>
</appender>

<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>

<logger name="org.springframework.web" level="INFO" additivity="false">
<appender-ref ref="STDOUT"/>
</logger>

<logger name="org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver"
level="WARN" additivity="false">
<appender-ref ref="STDOUT"/>
</logger>

<logger name="org.hibernate" level="ERROR" additivity="false">
<appender-ref ref="STDOUT"/>
</logger>

<!-- service -->

<logger name="com.vndevteam.kotlinwebspringboot3.application" level="DEBUG" additivity="false">
<appender-ref ref="STDOUT"/>
</logger>

<logger name="com.vndevteam.kotlinwebspringboot3.domain" level="DEBUG" additivity="false">
<appender-ref ref="STDOUT"/>
</logger>

<logger name="com.vndevteam.kotlinwebspringboot3.infrastructure" level="DEBUG" additivity="false">
<appender-ref ref="STDOUT"/>
</logger>

<logger name="com.vndevteam.kotlinwebspringboot3.util" level="DEBUG" additivity="false">
<appender-ref ref="STDOUT"/>
</logger>

</configuration>

0 comments on commit 7881823

Please sign in to comment.