DEV Community

Cover image for Leveraging Stack Traces for SQL Performance Analysis in Spring Boot Applications
Tomasz Szewczyk
Tomasz Szewczyk

Posted on

Leveraging Stack Traces for SQL Performance Analysis in Spring Boot Applications

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()
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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 ===")
    }
}
Enter fullscreen mode Exit fullscreen mode

Key Features of the Stack Trace Layout

  1. Intelligent Filtering: Excludes framework and logging infrastructure classes to focus on application code
  2. Compact Representation: Groups non-application frames into ellipsis markers to reduce noise
  3. Precise Location Information: Includes file names and line numbers for accurate source mapping
  4. 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 ===
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Production Considerations

In production environments, disable the enhanced logging to avoid performance overhead:

logging.sql.stack-trace.enabled=false
Enter fullscreen mode Exit fullscreen mode

Alternatively, use Spring profiles to automatically configure appropriate logging levels:

spring:
  config:
    activate:
      on-profile: development
  logging:
    sql:
      stack-trace: 
        enabled: true
Enter fullscreen mode Exit fullscreen mode

Performance Considerations

While this logging approach provides invaluable debugging information, consider these performance implications:

  1. Stack Trace Capture Overhead: Capturing stack traces for every SQL query introduces CPU overhead
  2. Memory Usage: Extended stack traces consume additional memory
  3. 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) }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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)