EC2에 AWS X-Ray 적용하기 (3) - Customize SQL, Segment
목차
이제 X-Ray에 기본적인 데이터가 잘 쌓이고는 있지만 추가로 보고 싶은 내용들이 있어
SQL문과 Segment 정보를 일부 커스터마이즈 하였습니다.
SQL에 query 문 추가와 Subsegment 이름을 "클래스.메소드"으로 생성하도록 해보겠습니다.
Segment에 Query문 기록하는 방법
X-Ray의 트레이스에서 SQL 쿼리를 기록하고 있는 subsegment 정보를 보면,
데이터베이스의 각종 정보는 나오나 정작 호출한 쿼리에 대해서는 수집을 하지 않고 있습니다.
문서에 보면, 보안 때문에 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에 다시 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명이 표시되었지만 적용 후에는 클래스 이름도 같이 표시되는 걸 확인할 수 있습니다.