티스토리 뷰

목차

이제 X-Ray에 기본적인 데이터가 잘 쌓이고는 있지만 추가로 보고 싶은 내용들이 있어

SQL문과 Segment 정보를 일부 커스터마이즈 하였습니다.

SQL에 query 문 추가와 Subsegment 이름을 "클래스.메소드"으로 생성하도록 해보겠습니다.

Segment에 Query문 기록하는 방법

X-Ray의 트레이스에서 SQL 쿼리를 기록하고 있는 subsegment 정보를 보면,

데이터베이스의 각종 정보는 나오나 정작 호출한 쿼리에 대해서는 수집을 하지 않고 있습니다.

X-Ray > Trace

문서에 보면, 보안 때문에 SQL query문은 기록을 하지 않는다고 합니다.

그런데 또 문서의 segment documents 에는 SQL 쿼리 subsegment를 만들때 사용하는 쿼리로 sanitized_query가 있습니다. 
초기에는 query를 기록하도록 되어 있다가 추후에는 query 문을 기록하지 않도록 구현을 한 것 같습니다.

protected class TracingStatementProxy implements InvocationHandler {
        protected boolean closed = false;
        protected Object delegate;
        protected final String query;
        protected final String hostname;
        protected Map<String, Object> additionalParams;

        public TracingStatementProxy(Object parent, String query, String hostname, Map<String, Object> additionalParams) {
            this.delegate = parent;
            this.query = query;
            this.hostname = hostname;
            this.additionalParams = additionalParams;
        }
        ...
}

aws sdk에서 제공해주는 aws-xray-recorder-sdk-sql-mysql 에서 코드를 보면 query 값을 인자로 받기는 하지만 query 변수에 값을 저장만 할 뿐 정작 사용을 하지는 않습니다.

그래서 sdk에서 제공해주는 JdbcInterceptor 대신에 이를 상속받아 쿼리 정보를 생성하는 부분을 아래와 같이 오버라이드하여 쿼리를 전달하도록 구현하였습니다.

public class MyTracingInterceptor extends TracingInterceptor {
    @Override
    public Object createStatement(Object proxy, Method method, Object[] args, Object statementObject) {
        try {
            String name = method.getName();
            String sql = null;
            Constructor<?> constructor = null;
            Map<String, Object> additionalParams = new HashMap();
            if (this.compare("createStatement", name)) {
                constructor = this.getConstructor(0, Statement.class);
            } else if (this.compare("prepareStatement", name)) {
                additionalParams.put("preparation", "statement");
                sql = (String)args[0];
                additionalParams.put("sanitized_query", substringSQL(sql));
                constructor = this.getConstructor(1, PreparedStatement.class);
            } else {
                if (!this.compare("prepareCall", name)) {
                    return statementObject;
                }

                additionalParams.put("preparation", "call");
                sql = (String)args[0];
                additionalParams.put("sanitized_query", substringSQL(sql));
                constructor = this.getConstructor(2, CallableStatement.class);
            }

            Statement statement = (Statement)statementObject;
            Connection connection = statement.getConnection();
            DatabaseMetaData metadata = connection.getMetaData();
            additionalParams.put("url", metadata.getURL());
            additionalParams.put("user", metadata.getUserName());
            additionalParams.put("driver_version", metadata.getDriverVersion());
            additionalParams.put("database_type", metadata.getDatabaseProductName());
            additionalParams.put("database_version", metadata.getDatabaseProductVersion());
            String hostname = "database";

            try {
                URI normalizedUri = new URI((new URI(metadata.getURL())).getSchemeSpecificPart());
                hostname = connection.getCatalog() + "@" + normalizedUri.getHost();
            } catch (URISyntaxException var14) {
                log.warn("Unable to parse database URI. Falling back to default 'database' for subsegment name.", var14);
            }

            log.debug("Instantiating new statement proxy.");
            return constructor.newInstance(new TracingInterceptor.TracingStatementProxy(statement, sql, hostname, additionalParams));
        } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException | SQLException var15) {
            log.warn("Unable to create statement proxy for tracing.", var15);
            return statementObject;
        }
    }

    private String substringSQL(String sql){
        if(sql.length() > 250){
            return sql.substring(0, 250);
        }
        else{
            return sql;
        }
    }
}

참고로 여기서 쿼리문을 250자 까지만 보내고 있는데, 다른 필드들은 몇 자까지 제한이 되는지 명시된 필드가 많은데 SQL 문에는 필드별로 별도 길이 제한이 명시되어 있지 않습니다.

테스트를 해보니 별도 제한이 없긴 한것 같은데, Segment 전체가 최대 64KB까지 저장할 수 있기에 서비스에 따라 적절한 길이로 제한을 두는 곳이 좋습니다.

X-Ray > Trace

X-Ray에 다시 subsegment를 확인하면 query문이 표시되는 것을 확인할 수 있습니다.

Segment에 class 이름 표시하기

aws sdk에서 제공해주는 XrayInterceptor에서는 subsegment를 생성할 때, 메소드 이름으로 생성을 하고

이름을 변경할 수 있는 메소드를 제공하지 않습니다.

그래서 sdk에서 제공해주는 interceptor를 사용하지 않고 아래 코드와 같이 subsegment를 직접 생성하였습니다.

public class XRayInspector{

    @Pointcut("within(app.test..*) || execution(* org.springframework.data.jpa.repository.JpaRepository+.*(..))")
    public void xrayEnabledClasses() {}

    @Around(value = "xrayEnabledClasses()")
    public Object processXRayTrace(ProceedingJoinPoint pjp) throws Throwable {
        Object process;
        try {
            Subsegment subsegment = AWSXRay.beginSubsegment(generateSubsegmentName(pjp));
            if (subsegment != null) {
                subsegment.setMetadata(this.generateMetadata(pjp, subsegment));
            }

            process = pjp.getArgs().length == 0 ? pjp.proceed() : pjp.proceed(pjp.getArgs());
        } catch (Exception var7) {
            AWSXRay.getCurrentSegmentOptional().ifPresent((x) -> {
                x.addException(var7);
            });
            throw var7;
        } finally {
            log.trace("Ending Subsegment");
            AWSXRay.endSubsegment();
        }

        return process;
    }

    private Map<String, Map<String, Object>> generateMetadata(ProceedingJoinPoint pjp, Subsegment subsegment) {
        Map<String, Map<String, Object>> metadata = new HashMap();
        Map<String, Object> classInfo = new HashMap();
        classInfo.put("Class", pjp.getTarget().getClass().getSimpleName());
        metadata.put("ClassInfo", classInfo);
        return metadata;
    }

    private String generateSubsegmentName(ProceedingJoinPoint pjp){
        String[] declaringTypeName = pjp.getSignature().getDeclaringTypeName().split("\\.");
        if(declaringTypeName.length > 0) {
            return declaringTypeName[declaringTypeName.length - 1] + "." + pjp.getSignature().getName();
        }
        else{
            return pjp.getSignature().getName();
        }
    }
}

 

적용 전
적용 후

위의 코드 적용 전에는 메소드 이름으로 subsegment명이 표시되었지만 적용 후에는 클래스 이름도 같이 표시되는 걸 확인할 수 있습니다.

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/06   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
글 보관함