Guide to Real-time Object Logging in Kotlin + Spring Boot using Graylog
Overview
Graylog
is an open-source centralized log management platform that enables the collection and management of logs from dispersed microservices centrally. It supports the unique Key-Value based GELF log format and, from version v4.0.0, supportsElasticsearch
7.10 or higher as a log store. This article aims to explain how to transmit execution logs, such as all HTTP requests, in real-time to GELF UDP from a Spring Boot-based project.
The Need for Structured Logs
- With the advent of AI, application logs have transformed from information for human eyes to data for machine analysis. Traditional line-based text logs are insufficient for machine analysis. Well-designed structured logs can be analyzed by machines to be processed into meaningful data. The structured GELF log format was born for this purpose. [Related Link]
GELF UDP Transmission
- The original GELF log comprises all Key-Value pairs loaded by MDC.put() in the application, collected into a JSON string. This string is compressed by the appender before transmission using GZIP, and in the case of UDP, if the compressed total size exceeds 8,192 bytes, it is transmitted in parts. A problem arises if the message input is being load-balanced at the running Graylog server; even the fragmented packets can be load-balanced, leading to message loss. Therefore, it is advisable to design the compressed log message size to not exceed 8,192 bytes. Most logs generated by the application can be accommodated within this size.
Objectives
Transfer all HTTP request logs.
Transfer all @Controller execution logs.
Transfer all @Service execution logs.
Transfer all @Repository execution logs.
The transferred logs can be filtered and aggregated by object field units in near real-time at the remote Graylog.
build.gradle.kts
- Add the following content to the build.gradle.kts at the project root.
dependencies {
implementation("org.springframework.boot:spring-boot-starter-aop:3.3.3")
implementation("io.github.microutils:kotlin-logging-jvm:3.0.5")
implementation("de.siegmar:logback-gelf:6.0.2")
implementation("com.github.ksuid:ksuid:1.1.2")
implementation("commons-io:commons-io:2.16.1")
implementation("org.apache.commons:commons-lang3:3.17.0")
implementation("commons-validator:commons-validator:1.9.0")
implementation("com.maxmind.geoip2:geoip2:4.2.0")
}
Enabling GELF UDP Logging
- Write the /src/main/resources/logback-spring.xml file at the project root as follows.
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration>
<configuration scan="true">
<springProperty scope="context" name="graylogHost" source="graylog.host" defaultValue="127.0.0.1"/>
<springProperty scope="context" name="graylogPort" source="graylog.port" defaultValue="12201"/>
<appender name="TEXT_CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<charset>UTF-8</charset>
<Pattern>%d %-4relative [%thread] %-5level %logger{35} - %msg%n</Pattern>
</encoder>
</appender>
<appender name="GELF_UDP" class="de.siegmar.logbackgelf.GelfUdpAppender">
<graylogHost>${graylogHost}</graylogHost>
<graylogPort>${graylogPort}</graylogPort>
<maxChunkSize>8192</maxChunkSize>
<useCompression>true</useCompression>
<messageIdSupplier class="de.siegmar.logbackgelf.MessageIdSupplier"/>
<encoder class="de.siegmar.logbackgelf.GelfEncoder">
<includeRawMessage>false</includeRawMessage>
<includeMarker>true</includeMarker>
<includeMdcData>true</includeMdcData>
<numbersAsString>false</numbersAsString>
<staticField>appender=GELF_UDP</staticField>
</encoder>
</appender>
<logger name="GELF_UDP" level="DEBUG" additivity="false">
<appender-ref ref="GELF_UDP"/>
</logger>
<root>
<level value="INFO"/>
<appender-ref ref="TEXT_CONSOLE"/>
</root>
</configuration>
Through the
GRAYLOG_HOST
,GRAYLOG_PORT
environment variables, you can inject the input information of the remote Graylog server responsible for log storage.Logs named GELF_UDP are all sent through the GELF_UDP appender.
By specifying
additivity="false"
(default is true) in the logger, the log is transmitted only through the GELF_UDP appender and does not propagate to upper appenders. This prevents unnecessary duplication or output of logs.
Application Startup
- For logging via UDP within the Amazon ecosystem, as of 2022-11-01, AWS NLB does not support UDP + IPv6 packet transmission. To prevent log loss, it's necessary to enforce the transmission of UDP packets via IPv4 only at the application level by adding the
-
Djava.net
.preferIPv4Stack=true
option as follows.
$ java ${JAVA_OPTS} -XX:+AlwaysPreTouch -Djava.security.egd=file:/dev/./urandom -Djava.net.preferIPv4Stack=true -jar /app.jar
Creating a Logging Utility
- This example involves writing a common utility for logging.
import org.aspectj.lang.ProceedingJoinPoint
import org.aspectj.lang.reflect.MethodSignature
import org.apache.commons.lang3.exception.ExceptionUtils
import org.slf4j.MDC
import java.net.URLDecoder
import java.nio.charset.StandardCharsets
import java.time.Instant
import java.util.concurrent.atomic.AtomicInteger
object LoggingUtil {
private const val MAX_FIELD_VALUE_LENGTH = 30000
@JvmStatic
fun getCurrentFullProfile(): String {
val fullProfile: String? =
System.getProperty("spring.profiles.active") ?: System.getenv("SPRING_PROFILES_ACTIVE")
return if (fullProfile.isNullOrBlank()) {
"local"
} else {
fullProfile
}
}
@JvmStatic
fun getCurrentMainProfile(): String {
val profile: String = getCurrentFullProfile()
return profile.split(",").firstOrNull() ?: "local"
}
@JvmStatic
fun isLocal(): Boolean {
return when (getCurrentMainProfile()) {
"local" -> true
else -> false
}
}
@JvmStatic
fun generateUUID(): String {
return KsuidGenerator.generate()
}
@JvmStatic
fun String?.convertUrlEncodedToUrlDecoded(): String {
this ?: return ""
return URLDecoder.decode(this, StandardCharsets.UTF_8)
}
@JvmStatic
fun getClassNameMethodNamePair(joinPoint: ProceedingJoinPoint): Pair<String, String> {
val className = joinPoint.signature.declaringTypeName.split(".").last()
val methodName = (joinPoint.signature as MethodSignature).method.name
return Pair(className, methodName)
}
@JvmStatic
fun mdcPut(key: String?, value: Int?) {
key ?: return
value ?: return
MDC.remove(key)
MDC.put(key, value.toString())
}
@JvmStatic
fun mdcPut(key: String?, value: Long?) {
key ?: return
value ?: return
MDC.remove(key)
MDC.put(key, value.toString())
}
@JvmStatic
fun mdcPut(key: String?, value: Boolean?) {
key ?: return
value ?: return
MDC.remove(key)
MDC.put(key, value.toString())
}
@JvmStatic
fun mdcPut(key: String?, value: String?) {
key ?: return
value ?: return
MDC.remove(key)
MDC.put(key, value)
}
@JvmStatic
fun mdcPut(key: String?, value: Instant?) {
key ?: return
value ?: return
MDC.remove(key)
MDC.put(key, value.toString())
}
@JvmStatic
fun mdcPutTrimmed(key: String?, value: String?) {
key ?: return
value ?: return
if (value.length <= MAX_FIELD_VALUE_LENGTH) {
mdcPut(key, value)
return
}
mdcPut(key, value.substring(0, MAX_FIELD_VALUE_LENGTH - 1))
}
@JvmStatic
fun mdcPutRawMessage(rawMessage: String?) {
rawMessage ?: return
mdcPutCumulative("raw_message", rawMessage, "\n")
}
@JvmStatic
fun mdcPutCumulative(fieldName: String, fieldValue: String, separator: String = "\n") {
val existsFieldValue = MDC.get(fieldName)
if (existsFieldValue.isNullOrEmpty()) {
MDC.put(fieldName, fieldValue)
return
}
when (existsFieldValue.toByteArray().size > LoggingUtil.MAX_FIELD_VALUE_LENGTH) {
true -> {
val currentIndexNo = fieldName.substringAfterLast('_')
val newIndexNo = AtomicInteger(1)
val hasIndexNo = currentIndexNo.matches("[0-9]+".toRegex())
val currentFieldNameWithoutIndexNo = when (hasIndexNo) {
true -> {
newIndexNo.addAndGet(Integer.valueOf(currentIndexNo))
fieldName.substringBeforeLast('_')
}
else -> fieldName
}
val newFieldName = "${currentFieldNameWithoutIndexNo}_$newIndexNo"
MDC.put(newFieldName, fieldValue)
}
else -> MDC.put(fieldName, existsFieldValue + separator + fieldValue)
}
}
@JvmStatic
fun mdcPutRequestBody(requestBody: String?) {
requestBody ?: return
mdcPut("request_body", requestBody)
}
@JvmStatic
fun mdcPutException(ex: Throwable?, prefix: String? = null) {
ex ?: return
val prefixString: String = if (prefix.isNullOrBlank()) {
""
} else {
"${prefix}_"
}
mdcPut("${prefixString}error_exception", ex.javaClass.name)
mdcPutTrimmed("${prefixString}error_message", ex.message)
ExceptionUtils.getRootCause(ex)?.let {
mdcPut("${prefixString}error_root_exception", it.javaClass.name)
mdcPutTrimmed("${prefixString}error_root_message", ExceptionUtils.getMessage(it))
}
mdcPutTrimmed("${prefixString}error_trace", ExceptionUtils.getStackTrace(ex))
}
@JvmStatic
fun mdcClearException() {
MDC.remove("error_exception")
MDC.remove("error_message")
MDC.remove("error_root_exception")
MDC.remove("error_root_message")
MDC.remove("error_trace")
}
@JvmStatic
fun mergeMdcContextMap(mdcContextMap: Map<String, String>?) {
if (mdcContextMap.isNullOrEmpty()) return
MDC.setContextMap(MDC.getCopyOfContextMap() + mdcContextMap)
}
@JvmStatic
fun clearAndPutMdcContextMap(mdcContextMap: Map<String, String>?) {
if (mdcContextMap.isNullOrEmpty()) return
MDC.clear()
MDC.setContextMap(mdcContextMap)
}
}
Storing Request ID in ThreadLocal
- The Request ID plays a crucial role in identifying individual HTTP request logs. The following code allows acquiring the Request ID value for the current request as RequestContext.REQUEST_ID.
class RequestContext {
companion object {
private val requestId = ThreadLocal<String?>()
var REQUEST_ID: String?
get() = requestId.get()
set(requestId) {
Companion.requestId.set(requestId)
}
fun remove() {
requestId.remove()
}
}
}
Capturing HTTP Request Body
- Through the settings below, it's possible to capture the body of every HTTP request. (If the request body is not captured at this stage, it will be impossible to obtain the request body during the loading of HTTP request logs as described below.)
import jakarta.servlet.*
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletRequestWrapper
import org.apache.commons.io.IOUtils
import org.springframework.stereotype.Component
import java.io.*
@Component
class MultiReadableRequestBodyFilter : Filter {
@Throws(IOException::class, ServletException::class)
override fun doFilter(servletRequest: ServletRequest, servletResponse: ServletResponse, filterChain: FilterChain) {
if (servletRequest.contentType == null) {
filterChain.doFilter(servletRequest, servletResponse)
return
}
if (servletRequest.contentType.startsWith("multipart/form-data")) {
filterChain.doFilter(servletRequest, servletResponse)
} else {
val wrappedRequest = MultiReadableRequestBodyHttpServletRequest(servletRequest as HttpServletRequest)
filterChain.doFilter(wrappedRequest, servletResponse)
}
}
}
class MultiReadableRequestBodyHttpServletRequest(request: HttpServletRequest?) : HttpServletRequestWrapper(request) {
private var cachedBytes: ByteArrayOutputStream? = null
@Throws(IOException::class)
override fun getInputStream(): ServletInputStream {
if (cachedBytes == null) cacheInputStream()
return CachedServletInputStream(cachedBytes!!.toByteArray())
}
@Throws(IOException::class)
override fun getReader(): BufferedReader {
return BufferedReader(InputStreamReader(inputStream))
}
@Throws(IOException::class)
private fun cacheInputStream() {
cachedBytes = ByteArrayOutputStream()
IOUtils.copy(super.getInputStream(), cachedBytes)
}
class CachedServletInputStream(contents: ByteArray?) : ServletInputStream() {
private val buffer: ByteArrayInputStream = ByteArrayInputStream(contents)
override fun read(): Int {
return buffer.read()
}
override fun isFinished(): Boolean {
return buffer.available() == 0
}
override fun isReady(): Boolean {
return true
}
override fun setReadListener(listener: ReadListener) {
throw RuntimeException("Not implemented")
}
}
}
HTTP Request Logging
- The following configuration enables the loading of all HTTP request logs. (For details on obtaining Geolocation information from a specific IP address, refer to this article.)
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import mu.KLogging
import mu.NamedKLogging
import org.apache.commons.io.IOUtils
import org.slf4j.MDC
import org.springframework.stereotype.Component
import org.springframework.web.servlet.HandlerInterceptor
import org.springframework.web.servlet.ModelAndView
import java.net.InetAddress
import java.nio.charset.StandardCharsets
import java.util.concurrent.TimeUnit
private val consoleLogger = KLogging().logger
@Component
class LoggingRequestInterceptor(
// [Optional] Geolocation logging
private val geoLocationService: GeoLocationService
) : HandlerInterceptor {
companion object : NamedKLogging("GELF_UDP")
override fun preHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any): Boolean {
if (request.method == "OPTIONS") {
return true
}
if (request.contextPath + request.servletPath in arrayOf("/health-check", "/swagger-ui/index.html", "/error") {
return true
}
MDC.clear()
mdcPut("execution_start_time", System.nanoTime().toString())
var requestId: String? = request.getHeader("X-Request-Id")
if (requestId.isNullOrEmpty()) {
requestId = LoggingUtil.generateUUID()
}
RequestContext.REQUEST_ID = requestId
mdcPut("request_id", requestId)
mdcPut("transaction_id", requestId)
mdcPut("application", "{your-app-name}")
mdcPut("main_profile", CommonUtil.getCurrentMainProfile())
mdcPut("full_profile", CommonUtil.getCurrentFullProfile())
mdcPut("node_ip_address", InetAddress.getLocalHost().hostAddress)
mdcPut("log_type", "REQUEST")
mdcPut("request_referer", request.getHeader("Referer"))
mdcPut("request_user_agent", request.getHeader("User-Agent"))
mdcPut("request_platform", request.getHeader("sec-ch-ua-platform")?.replace("\"", "")?.trim())
val requestIpAddress = request.getHeader("X-Forwarded-For")?.split(",")?.first() ?: request.remoteAddr
mdcPut("request_ip_address", requestIpAddress)
// [Optional] Geolocation logging
geoLocationService.getGeoLocation(requestIpAddress)?.logging("request")
mdcPut("request_method", request.method)
val requestPath = request.contextPath + request.servletPath
mdcPut("request_path", requestPath)
request.queryString?.let { mdcPut("request_query_string", it.convertUrlEncodedToUrlDecoded()) }
request.contentType?.let { contentType ->
if (!(contentType.startsWith("multipart/form-data") || contentType.startsWith("application/x-www-form-urlencoded"))) {
val requestBody = IOUtils.toString(request.inputStream, StandardCharsets.UTF_8)
if (!requestBody.contains("password")) {
mdcPutRequestBody(requestBody)
}
}
}
return true
}
override fun postHandle(
request: HttpServletRequest, response: HttpServletResponse, handler: Any, modelAndView: ModelAndView?
) {
if (request.method == "OPTIONS") {
return
}
if (request.contextPath + request.servletPath in arrayOf("/health-check", "/swagger-ui/index.html", "/error") {
return
}
MDC.getCopyOfContextMap() ?: return
MDC.get("execution_start_time")?.let {
LoggingUtil.putMdcField(
"execution_time",
TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - it.toLong()).toString()
)
MDC.remove("execution_start_time")
}
LoggingUtil.putMdcField("response_status_code", response.status.toString())
when (response.status) {
in (200..300) -> {
if (MDC.get("result")?.toString() == "false") {
mdcPut("log_level", "INFO")
val errorMessage = "API_REQUEST_FAILED"
mdcPut("message", errorMessage)
logger.error(errorMessage)
} else {
mdcPut("log_level", "INFO")
val message = "API_REQUEST_FINISHED"
MDC.remove("error_code")
MDC.remove("error_message")
mdcPut("message", message)
logger.info(message)
}
}
in (400..499) -> {
mdcPut("log_level", "WARN")
val message = "API_REQUEST_REJECTED"
mdcPut("message", message)
MDC.remove("class")
MDC.remove("class_method")
MDC.remove("method")
MDC.remove("method_args")
if (response.status == 404 && MDC.get("error_code") == null) {
mdcPut("error_code", "API_NOT_FOUND")
mdcPut("error_message", "Requested API is not found.")
}
logger.warn(message)
consoleLogger.warn(MDC.getCopyOfContextMap().toString())
}
in (500..599) -> {
mdcPut("log_level", "ERROR")
val message = "API_REQUEST_FAILED"
mdcPut("message", message)
logger.error(message)
consoleLogger.error(MDC.getCopyOfContextMap().toString())
}
}
RequestContext.remove()
MDC.clear()
}
}
Adding Request ID to HTTP Response Header
- Through the configuration below, it is possible to pass the Request ID obtained or generated during the log loading process back to the requesting client. (The client can use this value to identify and query logs for a specific request.)
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.lang.Nullable
import org.springframework.stereotype.Component
import org.springframework.web.bind.annotation.ControllerAdvice
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice
@ControllerAdvice
@Component
class LoggingResponseBodyAdvice : ResponseBodyAdvice<Any?> {
override fun supports(returnType: MethodParameter, converterType: Class<out HttpMessageConverter<*>>): Boolean {
return true
}
override fun beforeBodyWrite(
@Nullable body: Any?,
returnType: MethodParameter,
selectedContentType: MediaType,
selectedConverterType: Class<out HttpMessageConverter<*>>,
request: ServerHttpRequest,
response: ServerHttpResponse
): Any? {
response.headers.add("X-Request-Id", RequestContext.REQUEST_ID)
return body
}
}
Enabling Logging Interceptor
- The settings below allow for the addition of a log loading interceptor and specifying paths to exclude.
import org.springframework.context.annotation.Configuration
import org.springframework.web.servlet.config.annotation.InterceptorRegistry
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
@Configuration
class InterceptorConfig(
private val loggingRequestInterceptor: LoggingRequestInterceptor
) : WebMvcConfigurer {
override fun addInterceptors(registry: InterceptorRegistry) {
registry
.addInterceptor(loggingRequestInterceptor)
.addPathPatterns("/**")
.excludePathPatterns(
"/health-check",
"/management/health",
"/swagger-ui/**"
)
}
}
Exception Logging
- The following configuration enables the logging of all exception handling results.
import jakarta.servlet.http.HttpServletRequest
import mu.KLogging
import org.apache.commons.lang3.exception.ExceptionUtils
import org.slf4j.MDC
import org.springframework.core.Ordered
import org.springframework.core.annotation.Order
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.ControllerAdvice
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.ResponseBody
private val consoleLogger = KLogging().logger
@ControllerAdvice
@Order(Ordered.HIGHEST_PRECEDENCE)
class FooControllerAdvice {
@ExceptionHandler(FooException::class)
@ResponseBody
fun handleFooException(
request: HttpServletRequest,
ex: FooException
): ResponseEntity<*> {
mdcPut("message", "API_REQUEST_REJECTED")
mdcPut("error_code", ex.errorCode.name)
mdcPut("error_message", ex.errorMessage)
return ResponseEntity(
FooErrorResponse(
ex.errorCode.name,
ex.errorMessage,
ex.errorDetail
),
ex.httpStatus
)
}
@ExceptionHandler(Exception::class)
@ResponseBody
fun handleException(
request: HttpServletRequest,
ex: Exception
): ResponseEntity<*> {
mdcPut("message", "API_REQUEST_FAILED")
mdcPut("error_code", FooErrorCode.INTERNAL_SERVER_ERROR.name)
mdcPut("error_exception", ex.javaClass.name)
mdcPut("error_message", ex.message)
ExceptionUtils.getRootCause(ex)?.let {
mdcPut("error_root_exception", ExceptionUtils.getRootCause(ex).javaClass.name)
mdcPut("error_root_message", ExceptionUtils.getRootCauseMessage(ex))
}
mdcPut("error_trace", ExceptionUtils.getStackTrace(ex))
if (LoggingUtil.isLocal()) {
consoleLogger.warn(MDC.getCopyOfContextMap().toString())
}
return ResponseEntity(
FooErrorResponse
(
FooErrorCode.INTERNAL_SERVER_ERROR.name,
FooErrorCode.INTERNAL_SERVER_ERROR.message
),
HttpStatus.INTERNAL_SERVER_ERROR
)
}
}
@Controller Logging
- The settings below allow for the logging of arguments and execution time of methods executed in all @Controller beans.
import mu.NamedKLogging
import org.aspectj.lang.ProceedingJoinPoint
import org.aspectj.lang.annotation.Around
import org.aspectj.lang.annotation.Aspect
import org.slf4j.MDC
import org.springframework.core.annotation.Order
import org.springframework.stereotype.Component
import java.util.concurrent.TimeUnit
@Component
@Aspect
@Order(65535)
class LoggingControllerAspect {
companion object : NamedKLogging("GELF_UDP")
@Around("@annotation(org.springframework.web.bind.annotation.RequestMapping)" + " || @annotation(org.springframework.web.bind.annotation.PostMapping)" + " || @annotation(org.springframework.web.bind.annotation.GetMapping)" + " || @annotation(org.springframework.web.bind.annotation.PutMapping)" + " || @annotation(org.springframework.web.bind.annotation.PatchMapping)" + " || @annotation(org.springframework.web.bind.annotation.DeleteMapping)")
fun aroundControllerExecution(joinPoint: ProceedingJoinPoint): Any? {
val classNameMethodNamePair = CommonUtil.getClassNameMethodNamePair(joinPoint)
LoggingUtil.mdcPut("class", classNameMethodNamePair.first)
LoggingUtil.mdcPut("method", classNameMethodNamePair.second)
val classMethod = "${classNameMethodNamePair.first}#${classNameMethodNamePair.second}"
LoggingUtil.mdcPut("class_method", classMethod)
LoggingUtil.mdcPut("controller_class_method", classMethod)
joinPoint.args?.let { args ->
val argsString = ToStringBuilder.reflectionToString(args, ToStringStyle.JSON_STYLE)
LoggingUtil.mdcPutTrimmed(
"method_args",
argsString
)
}
return try {
joinPoint.proceed()
} catch (ex: Exception) {
doRequestErrorLogging(ex)
throw ex
}
}
private fun doRequestErrorLogging(ex: Exception) {
mdcPutException(ex)
MDC.get("execution_start_time")?.let {
LoggingUtil.putMdcField(
"execution_time",
TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - MDC.get("execution_start_time").toLong()).toString()
)
MDC.remove("execution_start_time")
}
RequestContext.remove()
MDC.clear()
}
}
@Service Logging
- The following configuration allows for the logging of arguments and execution time for methods executed in all @Service beans.
import mu.NamedKLogging
import org.apache.commons.lang3.builder.ToStringBuilder
import org.apache.commons.lang3.builder.ToStringStyle
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.MDC
import org.springframework.stereotype.Component
import java.time.Instant
import java.util.concurrent.TimeUnit
@Component
@Aspect
class LoggingServiceAspect {
companion object : NamedKLogging("GELF_UDP")
@Pointcut("within(@org.springframework.stereotype.Service *)")
fun serviceClassMethods() {
}
@Around("serviceClassMethods()")
fun aroundServiceExecution(joinPoint: ProceedingJoinPoint): Any? {
val classNameMethodNamePair = getClassNameMethodNamePair(joinPoint)
val originalMdcMap = MDC.getCopyOfContextMap()
mdcClearException()
mdcPut("log_level", "DEBUG")
mdcPut("log_type", "SERVICE")
mdcPut("class", classNameMethodNamePair.first)
mdcPut("method", classNameMethodNamePair.second)
val classMethod = "${classNameMethodNamePair.first}#${classNameMethodNamePair.second}"
mdcPut("class_method", classMethod)
mdcPut("service_class_method", classMethod)
joinPoint.args?.let { args ->
val argsString = ToStringBuilder.reflectionToString(args, ToStringStyle.JSON_STYLE)
if (!argsString.contains("password")) {
putMdcFieldTrimmed(
"method_args",
argsString
)
}
}
var message = "SERVICE_EXECUTION_FINISHED"
val executionStartTime = System.nanoTime()
val result = try {
joinPoint.proceed()
} catch (ex: Exception) {
message = "SERVICE_EXECUTION_FAILED"
mdcPutException(ex)
mdcPut(
"execution_time",
TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - executionStartTime).toString()
)
mdcPut("message", message)
if (ex is YourAppException) {
logger.debug(message)
} else {
mdcPut("log_level", "ERROR")
logger.error(message)
}
mergeMdcContextMap(originalMdcMap)
throw ex
}
mdcPut(
"execution_time",
TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - executionStartTime).toString()
)
mdcPut("message", message)
if (classMethod == "ModelSpecificationFactory#create") {
return result
}
logger.debug(message)
LoggingUtil.mergeMdcFields(originalMdcMap)
return result
}
}
@Repository Query Logging
- The settings below enable the logging of arguments and execution time for all queries executed through Spring Data JPA or Querydsl. (Logging at the code level offers incomparable advantages in readability and traceability over database-level logging.)
import mu.NamedKLogging
import org.apache.commons.lang3.builder.ToStringBuilder
import org.apache.commons.lang3.builder.ToStringStyle
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.MDC
import org.springframework.stereotype.Component
import java.util.concurrent.TimeUnit
@Component
@Aspect
class LoggingRepositoryAspect {
companion object : NamedKLogging("GELF_UDP")
@Pointcut("within(@org.springframework.stereotype.Repository *)")
fun repositoryClassMethods() {
}
@Around("repositoryClassMethods() || within(org.springframework.data.jpa.repository.JpaRepository+) || within(org.springframework.data.querydsl.QuerydslPredicateExecutor+)")
fun aroundQueryExecution(joinPoint: ProceedingJoinPoint): Any? {
val classNameMethodNamePair = getClassNameMethodNamePair(joinPoint)
val originalMdcMap = MDC.getCopyOfContextMap()
mdcClearException()
mdcPut("log_level", "DEBUG")
mdcPut("log_type", "QUERY")
mdcPut("class", classNameMethodNamePair.first)
mdcPut("method", classNameMethodNamePair.second)
val classMethod = "${classNameMethodNamePair.first}#${classNameMethodNamePair.second}"
mdcPut("class_method", classMethod)
mdcPut("repository_class_method", classMethod)
val args = joinPoint.args
when (classMethod) {
"CrudRepository#save" -> {
args?.let {
if (it.isNotEmpty()) {
val entity = args[0]
val entityName = entity::class.simpleName
entityName?.let {
mdcPut("class", classNameMethodNamePair.first.replace("Crud", entityName))
mdcPut("class_method", classMethod.replace("Crud", entityName))
}
}
}
}
else -> {}
}
args?.let {
LoggingUtil.mdcPutTrimmed(
"method_args",
ToStringBuilder.reflectionToString(it, ToStringStyle.JSON_STYLE)
)
}
var message = "QUERY_EXECUTION_FINISHED"
val executionStartTime = System.nanoTime()
val result = try {
joinPoint.proceed()
} catch (ex: Exception) {
message = "QUERY_EXECUTION_FAILED"
mdcPutException(ex)
mdcPut(
"execution_time",
TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - executionStartTime).toString()
)
mdcPut("message", message)
mdcPut("log_level", "ERROR")
logger.error(message)
if (isLocal()) {
consoleLogger.debug(MDC.getCopyOfContextMap().toString())
}
clearAndPutMdcContextMap(originalMdcMap)
throw ex
}
mdcPut(
"execution_time",
TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - executionStartTime).toString()
)
mdcPut("message", message)
logger.debug(message)
clearAndPutMdcContextMap(originalMdcMap)
return result
}
}
@Scheduled Logging
- Batch schedulers annotated with
@Scheduled
at the method level can also log as follows:
import mu.NamedKLogging
import org.aspectj.lang.ProceedingJoinPoint
import org.aspectj.lang.annotation.Around
import org.aspectj.lang.annotation.Aspect
import org.slf4j.MDC
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Component
import java.net.InetAddress
import java.util.concurrent.TimeUnit
@Component
@Aspect
class LoggingScheduleAspect {
companion object : NamedKLogging("GELF_UDP")
@Around("@annotation(scheduled)")
fun aroundScheduled(joinPoint: ProceedingJoinPoint, scheduled: Scheduled): Any? {
MDC.clear()
mdcPut("application", "{your-app-name}")
mdcPut("main_profile", CommonUtil.getCurrentMainProfile())
mdcPut("full_profile", CommonUtil.getCurrentFullProfile())
mdcPut("node_ip_address", InetAddress.getLocalHost().hostAddress)
mdcPut("log_type", "SCHEDULE")
val transactionId = generateUUID()
mdcPut("schedule_id", transactionId)
mdcPut("transaction_id", transactionId)
if (scheduled.cron.isNotEmpty()) {
mdcPut("cron_expression", scheduled.cron)
}
val classNameMethodNamePair = CommonUtil.getClassNameMethodNamePair(joinPoint)
val classMethod = "${classNameMethodNamePair.first}#${classNameMethodNamePair.second}"
mdcPut("class", classNameMethodNamePair.first)
mdcPut("method", classNameMethodNamePair.second)
mdcPut("class_method", classMethod)
mdcPut("schedule_class_method", classMethod)
mdcPut("log_level", "INFO")
var message = "API_SCHEDULE_STARTED"
mdcPut("message", message)
if (classMethod != "LoggingHeartbeatComponent#heartbeat") {
logger.info(message)
}
val processStartTime = System.nanoTime()
val result = try {
joinPoint.proceed()
} catch (ex: Exception) {
mdcPut("execution_time", TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - processStartTime).toString())
mdcPutException(ex)
mdcPut("log_level", "ERROR")
logger.error("API_SCHEDULE_FAILED")
return null
}
mdcPut("execution_time", TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - processStartTime).toString())
message = "API_SCHEDULE_FINISHED"
mdcPut("message", message)
if (classMethod != "LoggingHeartbeatComponent#heartbeat") {
logger.info(message)
}
return result
}
}
Asynchronous Thread Logging
- Through the following configuration, methods executed asynchronously with @Async(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME) can also be logged. For more detailed information, refer to this article.
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asCoroutineDispatcher
import org.slf4j.MDC
import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.core.task.AsyncTaskExecutor
import org.springframework.core.task.TaskDecorator
import org.springframework.core.task.support.TaskExecutorAdapter
import org.springframework.scheduling.annotation.EnableAsync
import java.util.concurrent.Executors
@Configuration
@EnableAsync
class AsyncConfig {
@Bean(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME)
fun asyncTaskExecutor(): AsyncTaskExecutor {
val taskExecutor = TaskExecutorAdapter(Executors.newVirtualThreadPerTaskExecutor())
taskExecutor.setTaskDecorator(LoggingTaskDecorator())
return taskExecutor
}
}
class LoggingTaskDecorator : TaskDecorator {
override fun decorate(task: Runnable): Runnable {
val callerThreadContext = MDC.getCopyOfContextMap()
return Runnable {
callerThreadContext?.let {
MDC.setContextMap(it)
}
task.run()
}
}
}
val Dispatchers.LOOM: CoroutineDispatcher
get() = Executors.newVirtualThreadPerTaskExecutor().asCoroutineDispatcher()
Application Status Logging
- Logging the CPU usage, RAM usage, and other metrics of running instances every minute as shown below can greatly aid in performance monitoring.
import mu.NamedKLogging
import org.slf4j.MDC
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Component
import java.lang.management.ManagementFactory
import java.net.InetAddress
import java.util.concurrent.TimeUnit
@Component
class LoggingHeartbeatComponent {
companion object : NamedKLogging("GELF_UDP")
@Scheduled(fixedRate = 60000)
fun heartbeat() {
MDC.clear()
LoggingUtil.putMdcField("execution_start_time", System.nanoTime().toString())
val runtime = Runtime.getRuntime()
val memoryBean = ManagementFactory.getMemoryMXBean()
val threadBean = ManagementFactory.getThreadMXBean()
val cpuLoad = ManagementFactory.getOperatingSystemMXBean().systemLoadAverage
val cpuCoreMax = ManagementFactory.getOperatingSystemMXBean().availableProcessors
val cpuUsagePercentage = if (cpuLoad >= 0) (cpuLoad / cpuCoreMax * 100) else 0.0
val maxMemory = runtime.maxMemory()
val memoryHeapUsage = memoryBean.heapMemoryUsage.used
val memoryNonHeapUsage = memoryBean.nonHeapMemoryUsage.used
val threadUsageCount = threadBean.threadCount
mdcPut("application", "{your-app-name}")
mdcPut("main_profile", getCurrentMainProfile())
mdcPut("full_profile", getCurrentFullProfile())
mdcPut("node_ip_address", InetAddress.getLocalHost().hostAddress)
mdcPut("log_type", "HEARTBEAT")
mdcPut("cpu_core_max_count", cpuCoreMax.toString())
mdcPut("cpu_load", cpuLoad.toString())
mdcPut("cpu_usage_percentage", cpuUsagePercentage.toString())
mdcPut("thread_usage_count", threadUsageCount.toString())
mdcPut("memory_max_mb", convertBytesToMegabytes(maxMemory).toString())
mdcPut("memory_heap_usage_mb", convertBytesToMegabytes(memoryHeapUsage).toString())
mdcPut("memory_non_heap_usage_mb", convertBytesToMegabytes(memoryNonHeapUsage).toString())
mdcPut("log_level", "DEBUG")
val message = "API_HEARTBEAT_FINISHED"
mdcPut("message", message)
MDC.get("execution_start_time")?.let {
mdcPut(
"execution_time",
TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - it.toLong()).toString()
)
MDC.remove("execution_start_time")
}
logger.debug(message)
MDC.clear()
}
private fun convertValueToPercent(value: Double): Double {
if (value <= 0.0) {
return 0.0
}
if (value >= 100.0) {
return 100.0
}
return value
}
private fun convertBytesToMegabytes(bytes: Long): Long = bytes / 1024 / 1024
}
Application Deployment Logging
- By logging at the point when an application is newly deployed and ready to receive HTTP requests, and at the point when it terminates, you can get an overview of the deployment status of 'n' instances.
import mu.NamedKLogging
import org.slf4j.MDC
import org.springframework.boot.ApplicationArguments
import org.springframework.boot.ApplicationRunner
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.context.event.ContextClosedEvent
import org.springframework.context.event.EventListener
import java.net.InetAddress
@SpringBootApplication
class FooApplication : ApplicationRunner {
companion object : NamedKLogging("GELF_UDP")
override fun run(args: ApplicationArguments?) {
MDC.clear()
mdcPutApplication()
val message = "API_APPLICATION_STARTED"
mdcPut("message", message)
logger.info(message)
MDC.clear()
}
@EventListener
fun onApplicationContextClosedEvent(event: ContextClosedEvent) {
MDC.clear()
mdcPutApplication()
val message = "API_APPLICATION_TERMINATED"
mdcPut("message", message)
logger.info(message)
MDC.clear()
}
private fun mdcPutApplication() {
mdcPut("application", "{your-app-name}")
mdcPut("main_profile", getCurrentMainProfile())
mdcPut("full_profile", getCurrentFullProfile())
mdcPut("node_ip_address", InetAddress.getLocalHost().hostAddress)
mdcPut("log_type", "DEPLOYMENT")
mdcPut("log_level", "DEBUG")
}
}
fun main(args: Array<String>) {
runApplication<FooApplication>(*args)
}
Subscribe to my newsletter
Read articles from Taehyeong Lee directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by