AWS X-Ray 적용하기(using Spring AOP)
AWS X-Ray 적용하기(using Spring AOP)
Spring Boot Application에 AWS X-Ray를 적용해 보았다. 아쉽지만 sql 트레이싱의 경우에는 아직 HikariCP 로는 적용할 수 있는 방법이 없어 라이브러리만 추가해두었다. X-Ray 버전이 업그레이드 되면 boot 2.x의 기본 DataSource인 Hikari도 지원되면 좋겠다.
현재 적용한 내역은 아래와 같다.
- Front / Backend API가 분리되어 있으므로 /api/* 로 시작되는 영역만을 트레이싱한다.
- 필요없는 특정 API는 수집하지 않는다.
- X-Ray User 개념을 도입하여 사용자 ID를 기록한다. (로그인 한 유저의 호출 API 추적)
- 추후 문제 추적을 위해 X-Ray Trace ID를 어플리케이션 로그 파일에 함께 넣어준다.
- 참고 자료
- 개념 : https://docs.aws.amazon.com/ko_kr/xray/latest/devguide/aws-xray.html
- X-Ray SDK For Java 구성 : https://docs.aws.amazon.com/ko_kr/xray/latest/devguide/xray-sdk-java-configuration.html
- AWS X-Ray 샘플 애플리케이션 : https://github.com/aws-samples/eb-java-scorekeep
- 어느(?) 개발자 님(sykim) 블로그 (Spring Boot 프로젝트에 AWS X-Ray 적용하기 - 1 ~ 5)
- https://ccii83.blogspot.com/2019/04/spring-boot-aws-x-ray-1.html
- Enviroment
- Springboot 2.2.4(with Spring Security)
- Elastic Beanstalk(환경 > 구성 > 소프트웨어 > X-Ray데몬 활성화 체크하여야 함)
- S3(AWS SDK For Java v2)
- 요약
- XRay 관련 라이브러리 추가
- AWSXRayServletFilter 추가
- 샘플링 규칙 추가(xray-custom-sampling-rules.json)
- Spring Security SuccessHandler에 User 정보 추가(로그인 성공 시)
- X-Ray 활성화(AbstractXRayInterceptor 상속) - AOP Pointcut 지정
- S3 연동(AWS SDK For Java v2)
- Application 로그(Logback)에 트레이스 ID 주입
1. 라이브러리 추가(maven : pom.xml)
...
<dependencyManagement>
<dependencies>
<!-- AWS SDK for Java 2.0 -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>bom</artifactId>
<version>2.10.57</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- AWS XRAY SDK for Java -->
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-xray-recorder-sdk-bom</artifactId>
<version>2.4.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
...
<!-- dependencies 추가 -->
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-xray-recorder-sdk-aws-sdk-v2-instrumentor</artifactId>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-xray-recorder-sdk-spring</artifactId>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-xray-recorder-sdk-slf4j</artifactId>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-xray-recorder-sdk-sql-postgres</artifactId>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-xray-recorder-sdk-sql</artifactId>
</dependency>
...
2. AWSXRayServletFilter 추가(AWSXRayTracingFilterConfig)
@Profile("prd")
@Configuration
public class AWSXRayTracingFilterConfig {
@Bean
public FilterRegistrationBean<AWSXRayServletFilter> TracingFilter() {
String from = null;
try {
from = InetAddress.getLocalHost().getHostName();
} catch (UnknownHostException e) {
from = "serverName";
}
FilterRegistrationBean<AWSXRayServletFilter> registration = new FilterRegistrationBean<>();
registration.setOrder(Ordered.HIGHEST_PRECEDENCE);
registration.setFilter(
new AWSXRayServletFilter(
new DynamicSegmentNamingStrategy(from, "*.도메인명")));
registration.addUrlPatterns("/api/*", CustomWebSecurityConfig.LOGIN_API_ENTRY_POINT);
return registration;
}
static {
AWSXRayRecorderBuilder builder = AWSXRayRecorderBuilder.standard()
.withPlugin(new EC2Plugin())
.withPlugin(new ElasticBeanstalkPlugin())
.withSegmentListener(new SLF4JSegmentListener());
URL ruleFile;
try {
ruleFile = ResourceUtils.getURL("classpath:xray-custom-sampling-rules.json");
builder.withSamplingStrategy(new LocalizedSamplingStrategy(ruleFile));
} catch (FileNotFoundException e) {}
AWSXRay.setGlobalRecorder(builder.build());
}
}
3. 샘플링 규칙 항목 추가(src/main/resources/xray-custom-sampling-rules.json)
{
"version": 1,
"default": {
"fixed_target": 1,
"rate": 1
},
"rules": [
{
"description": "Notification",
"host ": "*",
"service_name": "*",
"http_method": "GET",
"url_path": "/api/notifications",
"fixed_target": 0,
"rate": 0.0
},
{
"description": "Notification All",
"host ": "*",
"service_name": "*",
"http_method": "GET",
"url_path": "/api/notifications/all",
"fixed_target": 0,
"rate": 0.0
},
{
"description": "User Auth Status",
"host ": "*",
"service_name": "*",
"http_method": "GET",
"url_path": "/api/auth/status",
"fixed_target": 1,
"rate": 0.01
},
{
"description": "Menu Item",
"host ": "*",
"service_name": "*",
"http_method": "GET",
"url_path": "/api/menu-items",
"fixed_target": 1,
"rate": 0.01
},
{
"description": "Code Controller",
"host ": "*",
"service_name": "*",
"http_method": "GET",
"url_path": "/api/user-codes",
"fixed_target": 1,
"rate": 0.01
}
]
}
4. Spring Security SuccessHandler에 User 정보 추가
@Component
public class AjaxAwareAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private static Logger logger = LoggerFactory.getLogger(AjaxAwareAuthenticationSuccessHandler.class);
private final ObjectMapper mapper;
@Autowired
public AjaxAwareAuthenticationSuccessHandler(final ObjectMapper mapper) {
this.mapper = mapper;
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
CustomUser customUser = (CustomUser) authentication.getPrincipal();
response.setStatus(HttpStatus.OK.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
Map<String, Object> resultMap = new HashMap<String, Object>();
resultMap.put("isLogin", true);
resultMap.put("userMessage", "환영합니다.");
resultMap.put("userRole", customUser.getUserRole());
// Xray User 추가
AWSXRay.getCurrentSegmentOptional()
.ifPresent(e -> e.setUser(customUser.getUserId()));
this.mapper.writeValue(response.getWriter(), resultMap);
}
}
5. X-Ray 활성화(AbstractXRayInterceptor 상속) - AOP Pointcut 지정
@Profile("prd")
@Aspect
@Component
public class XRayInspector extends AbstractXRayInterceptor {
@Override
protected Map<String, Map<String, Object>> generateMetadata(ProceedingJoinPoint proceedingJoinPoint, Subsegment subsegment) {
// User를 체크하여 존재하면 해당 User를 기준으로 Tracing 한다.
Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication())
.filter(Authentication::isAuthenticated)
.map(Authentication::getPrincipal)
.filter(CustomUser.class::isInstance)
.map(CustomUser.class::cast)
.ifPresent(user -> {
subsegment.getParentSegment().setUser(user.getUserId());
});
return super.generateMetadata(proceedingJoinPoint, subsegment);
}
@Override
@Pointcut("execution(* 패키지명..*(..)) && @within(org.springframework.web.bind.annotation.RestController)")
public void xrayEnabledClasses() {
}
}
6. S3 연동(AWS SDK For Java v2)
@Slf4j
public class S3FileService implements FileService {
....
public S3FileService(String bucketName) {
this.bucketName = bucketName;
// S3 Client에 TracingInterceptor 추가
// ClientOverrideConfiguration.builder().addExecutionInterceptor(new TracingInterceptor()).build()
Region region = Region.AP_NORTHEAST_2;
AwsCredentialsProvider provider = EnvironmentVariableCredentialsProvider.create();
this.s3 = S3Client.builder()
.credentialsProvider(provider)
.region(region)
.overrideConfiguration(ClientOverrideConfiguration.builder().addExecutionInterceptor(new TracingInterceptor()).build())
.build();
}
....
}
7. Application 로그(Logback)에 AWS-TRACE-ID 주입 - 운영계 properties 파일
logging.file.path=/var/log/appLog
logging.file.name=${logging.file.path}/${spring.application.name}.log
logging.pattern.file=%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}} ${LOG_LEVEL_PATTERN:-%5p} ${PID:- } --- [%t] %-40.40logger{39} %X{AWS-XRAY-TRACE-ID} : %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}
Comments