Performance optimization in Spring Boot applications often requires deep visibility into database interactions. While traditional logging provides basic SQL query information, it frequently lacks the crucial context of where these queries originate in your codebase. This is especially important with lazy loading of Hibernate entities, where queries can be triggered unexpectedly throughout the application lifecycle. This article demonstrates how to implement a sophisticated logging solution that captures SQL execution stack traces, enabling developers to quickly identify and resolve performance bottlenecks.
The Challenge: Contextual SQL Performance Monitoring
In complex Spring Boot applications, SQL queries can be triggered from various layers—controllers, services, repositories, or even background processes. When performance issues arise, developers often face the challenge of connecting slow queries back to their source code. Standard Hibernate logging shows what queries are executed but not where they're called from, making optimization efforts time-consuming and error-prone.
Solution Architecture: Custom SQL Logging with Stack Traces
The solution involves creating a custom Logback layout that captures and filters stack traces for SQL operations, providing immediate visibility into the execution path. This approach combines Spring Boot's conditional configuration with Logback's extensible logging framework.
All code examples in this article are written in Kotlin, leveraging its concise syntax, null safety, and seamless Java interoperability. Kotlin's expressive language features make the implementation more readable and maintainable compared to traditional Java approaches, while providing full compatibility with the Spring Boot ecosystem.
Conditional Configuration Setup
The foundation begins with a configuration class that activates only when needed:
@Configuration
@ConditionalOnProperty(
name = ["sql-logging.stack-trace.enabled"],
havingValue = "true",
matchIfMissing = false,
)
class LogConfig(
@Value("\${sql-logging.stack-trace.enabled:false}")
private val enabled: Boolean,
) {
@PostConstruct
fun setMDC() {
MDC.put("springProfile", activeProfiles.ifBlank { "default" })
configureSqlLogging()
}
}
This configuration uses Spring Boot's @ConditionalOnProperty
annotation to ensure the enhanced logging is only active when explicitly enabled via the sql-logging.stack-trace.enabled=true
property. This design prevents performance overhead in production environments where detailed SQL tracing isn't needed.
Dynamic Logger Configuration
The core logging setup programmatically configures Hibernate's SQL loggers:
private fun configureSqlLogging() {
val loggerContext = LoggerFactory.getILoggerFactory() as LoggerContext
val appender = ConsoleAppender<ch.qos.logback.classic.spi.ILoggingEvent>()
appender.context = loggerContext
appender.name = "SQL_STACK_TRACE_APPENDER"
val layout = SqlLogLayout()
layout.context = loggerContext
layout.start()
appender.setLayout(layout)
appender.start()
// Configure SQL query logging
val sqlLogger = loggerContext.getLogger("org.hibernate.SQL") as Logger
sqlLogger.level = ch.qos.logback.classic.Level.DEBUG
sqlLogger.isAdditive = false
sqlLogger.addAppender(appender)
// Configure parameter binding logging
val bindLogger = loggerContext.getLogger("org.hibernate.type.descriptor.sql.BasicBinder") as Logger
bindLogger.level = ch.qos.logback.classic.Level.TRACE
bindLogger.isAdditive = false
bindLogger.addAppender(appender)
}
This configuration creates dedicated appenders for both SQL queries and parameter binding, ensuring complete visibility into database operations while maintaining separation from other application logs.
Custom Stack Trace Layout Implementation
The SqlLogLayout
class provides intelligent stack trace filtering and formatting:
class SqlLogLayout : LayoutBase<ILoggingEvent>() {
private val ignoredPackages = setOf(
"ch.qos.logback",
"org.slf4j",
"java.lang.Thread",
"com.bluesbrackets.config.SqlLogLayout",
)
override fun doLayout(event: ILoggingEvent): String = buildString {
val timestamp = dateFormatter.format(Instant.ofEpochMilli(event.timeStamp))
appendLine("$timestamp [${event.threadName}] ${event.level} ${event.loggerName} - ${event.formattedMessage}")
appendLine("=== SQL EXECUTION STACK TRACE ===")
val stackFrames = Thread.currentThread().stackTrace
.asSequence()
.filter { frame -> ignoredPackages.none { frame.className.startsWith(it) } }
.take(500)
.toList()
var lastWasEllipsis = false
stackFrames.forEach { frame ->
if (frame.className.startsWith("com.bluesbrackets")) {
val location = frame.fileName?.let {
if (frame.lineNumber > 0) "$it:${frame.lineNumber}" else it
} ?: "Unknown Source"
appendLine(" at ${frame.className}.${frame.methodName}($location)")
lastWasEllipsis = false
} else {
if (!lastWasEllipsis) {
appendLine(" ...")
lastWasEllipsis = true
}
}
}
appendLine("=== END STACK TRACE ===")
}
}
Key Features of the Stack Trace Layout
- Intelligent Filtering: Excludes framework and logging infrastructure classes to focus on application code
- Compact Representation: Groups non-application frames into ellipsis markers to reduce noise
- Precise Location Information: Includes file names and line numbers for accurate source mapping
- Performance Conscious: Limits stack depth to 500 frames to prevent excessive memory usage
Benefits and Use Cases
Immediate Performance Bottleneck Identification
With this logging configuration, developers can immediately identify the source of problematic queries:
2024-01-15 14:23:45.123 [http-nio-8080-exec-1] DEBUG org.hibernate.SQL - select user0_.id, user0_.email from users user0_ where user0_.status=?
=== SQL EXECUTION STACK TRACE ===
at com.bluesbrackets.service.UserService.findActiveUsers(UserService.kt:45)
at com.bluesbrackets.controller.UserController.getActiveUsers(UserController.kt:28)
...
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1087)
...
=== END STACK TRACE ===
This output immediately reveals that the query originates from UserService.findActiveUsers()
called by UserController.getActiveUsers()
, enabling rapid identification and optimization.
N+1 Query Detection
The stack trace logging excels at detecting N+1 query patterns by showing repeated calls from the same location:
// Multiple similar stack traces pointing to the same method
// indicate potential N+1 query issues
at com.bluesbrackets.service.OrderService.loadOrderItems(OrderService.kt:67)
Optimization Impact Measurement
By comparing logs before and after optimizations, developers can quantitatively measure the impact of their changes, ensuring modifications actually improve performance.
Configuration and Deployment
Development Environment Setup
Add the following property to your application-dev.properties
:
logging.sql.stack-trace.enabled=true
Production Considerations
In production environments, disable the enhanced logging to avoid performance overhead:
logging.sql.stack-trace.enabled=false
Alternatively, use Spring profiles to automatically configure appropriate logging levels:
spring:
config:
activate:
on-profile: development
logging:
sql:
stack-trace:
enabled: true
Performance Considerations
While this logging approach provides invaluable debugging information, consider these performance implications:
- Stack Trace Capture Overhead: Capturing stack traces for every SQL query introduces CPU overhead
- Memory Usage: Extended stack traces consume additional memory
- I/O Impact: Increased logging volume affects disk I/O performance
Therefore, this configuration should be used judiciously—enabled during development and debugging phases, but disabled in production unless specific performance issues require investigation.
Advanced Usage Patterns
Custom Filtering Logic
Extend the SqlLogLayout
to implement application-specific filtering:
private val criticalPackages = setOf(
"com.yourcompany.service",
"com.yourcompany.repository"
)
// Filter to show only critical application layers
stackFrames.filter { frame ->
criticalPackages.any { frame.className.startsWith(it) }
}
Performance Threshold Logging
Combine this approach with execution time measurement to log stack traces only for slow queries:
if (executionTime > threshold) {
// Log with stack trace
}
Conclusion
Implementing custom SQL logging with stack traces transforms performance debugging from a reactive, time-intensive process into a proactive, efficient workflow. By providing immediate context for database operations, developers can quickly identify bottlenecks, optimize queries, and maintain application performance standards.
The conditional configuration ensures this powerful debugging tool doesn't impact production performance while remaining readily available for development and troubleshooting scenarios. This approach represents a significant advancement in observability for Spring Boot applications, enabling more efficient development cycles and higher-quality software delivery.
The combination of Spring Boot's conditional configuration, Logback's extensibility, and intelligent stack trace filtering creates a robust foundation for database performance monitoring that scales with application complexity while maintaining operational efficiency.
Top comments (0)