Back-end/Spring Boot, JPA

p6spy로 로그 포맷팅

philo0407 2023. 6. 16. 15:25

 

https://unsplash.com/ko/%EC%82%AC%EC%A7%84/fPkvU7RDmCo

 

JPA 로그를 보다보면

 

select ~ from 테이블 where col1 = ? and col2 = ?

 

로 포맷팅 되지 않은 형태로 sql 로그가 찍히는 걸 볼 수 있다

 

1. 개행 없음

2. 바인딩 되는 파라미터 정보 부재

 

spring data jpa 기능으로만 위 1, 2 문제는 해결이 가능하다,

 

그러나 

 

select ~ from 테이블 where col1 = ? and col2 = ?
~ binding parameter ['1']
~ binding parameter ['philz']

 

로 파라미터와 sql로그가 따로 찍혀서 불편하게 해석해야 한다

 

따라서 p6spy를 이용하면 저 물음표 자리에 파라미터 정보를 넣어서 편하게 볼 수 있다

 

select ...
from 테이블 
where col1 = '1' and col2 = 'philz'

 

설정법

p6spy 의존성 추가

// p6spy
implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.8.1'

 

아래 파일 2개 추가

 

이때 META-INF.spring 폴더에서 "."은 이름의 한 요소가 아니라, 폴더 구분자다!

 

즉 실제로는 아래와 같음

 

 

그리고 아래와 같은 이름의 파일들을 만들자

 

resources/ META-INF/spring/

spy.properties

appender=com.p6spy.engine.spy.appender.Slf4JLogger

 

 

resources/ META-INF/spring/

org.springframework.boot.autoconfigure.AutoConfiguration.imports

com.github.gavlyukovskiy.boot.jdbc.decorator.DataSourceDecoratorAutoConfiguration

 

 

다시 정리하면 최종적으로 아래와 같이 나오면 된다

 

 

포맷팅 Bean 등록

 

Java ver

@Profile({"default", "local"})
@Configuration
public class P6SpySqlFormatter implements MessageFormattingStrategy {

    @PostConstruct
    public void setLogMessageFormat() {
        P6SpyOptions.getActiveInstance().setLogMessageFormat(this.getClass().getName());
    }

    @Override
    public String formatMessage(int connectionId, String now, long elapsed, String category, String prepared, String sql, String url) {
        String formattedSql = formatSql(category, sql);
        return formatLog(elapsed, category, formattedSql);
    }

    private String formatSql(String category, String sql) {
        if (hasText(sql) && isStatement(category)) {
            String trimmedSQL = trim(sql);
            if (isDdl(trimmedSQL)) {
                return FormatStyle.DDL.getFormatter().format(sql);
            }
            return FormatStyle.BASIC.getFormatter().format(sql); // maybe DML
        }
        return sql;
    }

    private static boolean isDdl(String trimmedSQL) {
        return trimmedSQL.startsWith("create") || trimmedSQL.startsWith("alter") || trimmedSQL.startsWith("comment");
    }

    private static String trim(String sql) {
        return sql.trim().toLowerCase(Locale.ROOT);
    }

    private static boolean isStatement(String category) {
        return Category.STATEMENT.getName().equals(category);
    }

    private String formatLog(long elapsed, String category, String formattedSql) {
        return String.format("[%s] | %d ms | %s", category, elapsed, formatSql(category, formattedSql));
    }
}

 

Kotlin Ver

import com.p6spy.engine.logging.Category
import com.p6spy.engine.spy.P6SpyOptions
import com.p6spy.engine.spy.appender.MessageFormattingStrategy
import jakarta.annotation.PostConstruct
import org.hibernate.engine.jdbc.internal.FormatStyle
import org.springframework.context.annotation.Configuration
import org.springframework.util.StringUtils.hasText

@Configuration
class P6SpySqlFormatter : MessageFormattingStrategy {
    @PostConstruct
    fun setLogMessageFormat() {
        P6SpyOptions.getActiveInstance().logMessageFormat = this.javaClass.getName()
    }

    override fun formatMessage(
        connectionId: Int,
        now: String,
        elapsed: Long,
        category: String,
        prepared: String,
        sql: String,
        url: String,
    ): String {
        val formattedSql = formatSql(category, sql)
        return formatLog(elapsed, category, formattedSql)
    }

    private fun formatSql(category: String, sql: String): String {
        if (hasText(sql) && isStatement(category)) {
            val trimmedSQL = trim(sql)
            return if (isDdl(trimmedSQL)) {
                FormatStyle.DDL.formatter.format(sql)
            } else FormatStyle.BASIC.formatter.format(sql) // maybe this line is DML
        }
        return sql
    }

    private fun formatLog(elapsed: Long, category: String, formattedSql: String): String {
        return String.format("[%s] | %d ms | %s", category, elapsed, formatSql(category, formattedSql))
    }

    companion object {
        private fun isDdl(trimmedSQL: String): Boolean {
            return trimmedSQL.startsWith("create") || trimmedSQL.startsWith("alter") || trimmedSQL.startsWith("comment")
        }

        private fun trim(sql: String): String {
            return sql.trim { it <= ' ' }.lowercase()
        }

        private fun isStatement(category: String): Boolean {
            return Category.STATEMENT.name == category
        }
    }
}

 

P6Sp6 설정을 하면 기존에 설정하던 아래의 yaml 설정 들은 필요 없어진다

 

spring:
  jpa:
    show-sql: true
    properties:
	  format_sql: true
	  use_sql_comments: true
logging.level:
    org.hibernate:
        orm.jdbc.bind: trace
        type: trace
        stat: debug

 

참고

https://jong-bae.tistory.com/23

https://shanepark.tistory.com/415

 

같이 보면 좋은 자료

https://curiousjinan.tistory.com/entry/spring-boot-3-p6spy-jpa-logging

https://www.inflearn.com/questions/1032603/p6spy-%ED%8F%AC%EB%A7%B7-%EC%84%A4%EC%A0%95-%EA%B3%B5%EC%9C%A0%ED%95%A9%EB%8B%88%EB%8B%A4