Cloud Platform/AWS

EC2에 AWS X-Ray 적용하기 (3) - Customize SQL, Segment

구티맨 2022. 3. 24. 16:20

목차

    이제 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명이 표시되었지만 적용 후에는 클래스 이름도 같이 표시되는 걸 확인할 수 있습니다.