이 글에서는 널리 쓰이는 모니터링 솔루션 중 하나인 datadog을 적용하면서 알게 된 정보를 기록해봅니다.
공식문서에 있는 내용은 언급만 하고 넘어가며, 완결되지 않은 이슈들도 있기에 맛보기로 생각하고 읽어주세요!
AS-IS
- 자동화된 모니터링 룰이 없으며, cloudwatch로 사후 모니터링만 가능.
- datadog를 도입하기로 했으나, graphql이 제대로 지원되지 않음.
datadog 은 공식 문서가 잘 나와있는 편이기에 설치나 일반적인 세팅에는 어려움이 없었습니다. 특히 resource를 기반으로 제공되는 기능들이 강력해서 REST API를 쓰는 환경이라면 크게 어려움 없이 세팅이 가능했습니다. 하지만 당사에서 사용하는 graphql 은 resource 가 모두 POST /graphql로 통일되어 버리는 문제가 있어 커스텀 설정이 필요했습니다.
커스텀 설정으로 넘어가기 전 환경 변수로서 제공하는 기능들을 먼저 소개합니다.
환경 변수로 사용 가능한 기능
1. 요청 고유 값
- -Ddd.logs.injection=true
- default=false
- Enable automatic MDC key injection for Datadog trace and span IDs.
2. Query Param
- -Ddd.http.server.tag.query-string=true
- default=false
3. Header
- -Ddd.trace.header.tags={header_name}:{datagod_display_name}
- -Ddd.trace.header.tags=Content-Type:Content-Type-Name
- default=null
4. Body
- 기본 기능 없음
커스텀 설정으로 사용 가능한 기능 (CustomTag)
1. Custom Tag 개발을 위한 기본 이해
childSpan(Span) ↔︎ rootSpan(MutableSpan)
- datadog의 모니터링 단위는 span으로 이루어져 있습니다.
- span을 사용하는 곳에 @Trace 어노테이션 사용이 필수입니다.
- custom tag는 항상 rootSpan 에도 기록하는 것이 모니터링 편의상 좋습니다.
- Span과 MutableSpan 은 캐스팅이 필요하며, 서로 다른 라이브러리를 참조하고 있기에 미묘하게 사용법이 다릅니다.
final Span childSpan = GlobalTracer.get().activeSpan();
final MutableSpan rootSpan = ((MutableSpan) GlobalTracer.get().activeSpan()).getLocalRootSpan();
// error 처리를 위한 사용법이 서로 다름
childSpan.setTag(Tags.ERROR,true);
rootSpan.setError(true);
customTag ↔︎ hostTag
- hostTag - 대시보드, 메트릭 등에서 감지되는 tag로 사전 정의되었기에 환경 변수를 통해 사용 가능합니다.
- customTag - 임의로 주입할 수 있지만, 대시보드, 메트릭 등에서 감지되는 않는 tag입니다.
2. Custom Tag 사용을 위한 Component 만들기
public class DatadogSender {
private static final String GRAPHQL_RESOURCE_PREFIX = "/graphql";
private static final String ERROR_CODE_TAG_KEY = "error.error_code";
private static final String USERNAME_TAG_KEY = "user.username";
@Trace
public static void setTag(final String key, final Object value) {
final Span childSpan = GlobalTracer.get().activeSpan();
final MutableSpan rootSpan = ((MutableSpan) GlobalTracer.get().activeSpan()).getLocalRootSpan();
childSpan.setTag(key, String.valueOf(value));
rootSpan.setTag(key, String.valueOf(value));
}
@Trace
public static void setErrorTag(final GraphQLError error) {
final Span childSpan = GlobalTracer.get().activeSpan();
final MutableSpan rootSpan = ((MutableSpan) GlobalTracer.get().activeSpan()).getLocalRootSpan();
childSpan.setTag(Tags.ERROR, true);
rootSpan.setError(true);
// errorMsg 와 errorCode 는 항상 기록됩니다.
setTag(DDTags.ERROR_MSG, error.getMessage());
setTag(ERROR_CODE_TAG_KEY, GraphQLExceptionUtils.errorCode(error));
// stackTrace, ResourceName, Principal 은 가져올 수 있는 경우만 기록됩니다.
setTag(DDTags.ERROR_STACK, GraphQLExceptionUtils.stackTrace(error));
setResourceNameTag(GraphQLExceptionUtils.resourceName(error));
setPrincipalTag();
}
public static void setResourceNameTag(final String value) {
final String resourceName = GRAPHQL_RESOURCE_PREFIX + (value.startsWith("/") ? value : "/" + value);
setTag(DDTags.RESOURCE_NAME, resourceName);
}
public static void setPrincipalTag() {
Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication().getPrincipal())
.filter(UserPrincipal.class::isInstance)
.map(UserPrincipal.class::cast)
.ifPresent(principal -> setTag(USERNAME_TAG_KEY, principal.getUsername()));
}
}
모니터링 편의상 rootSpan에도 항상 기록해주는 것이 좋기 때문에 헬프성 Component를 구현했습니다.
실무에서 사용하는 Component에서 일부 기능만을 가져와 각색한 것이기에 흐름만 이해해주시길 바랍니다.
요점은 childSpan과 rootSpan을 항상 같이 사용해주고, error 처리는 handler에서 GraphQLError를 통해 한다는 것입니다.
3. GraphQL에서의 사용
이제 제일 고생했던 GraphQL에 적용입니다.
글의 처음에 쓴 것처럼 datadog 은 아직 GraphQL을 지원하지 않고 있습니다.
AOP를 쓰면 되겠지라는 생각으로 접근해서 requestJson의 기록은 성공했지만, resourceName 은 쉽지 않았습니다.
GraphQL의 특성상 requestBody를 파싱해야 resourceName 이 나오는데 AOP를 설정이 힘들었습니다.
처음에는 프론트의 도움을 받아 resourceName을 별도의 key로 빼려고 생각했지만 프론트의 작업량이 너무 많아 취소했고, 어쩔 수 없이 우회 안을 생각해야만 했습니다.
그러다 로그인 요청을 제외한 모든 요청이 인가가 필요하다는 것에 착안해서 PreAuthorize에 AOP를 거는 생각을 했고 성공했습니다.
@Aspect
@Component
@RequiredArgsConstructor
public class GraphQLDatadogAspect {
@Pointcut("@annotation(org.springframework.security.access.prepost.PreAuthorize)")
public void preAuthorizePointCut() {
}
@Before("preAuthorizePointCut()")
public void beforePreAuthorize(final JoinPoint joinPoint) {
DatadogSender.setResourceNameTag(joinPoint.getSignature().getName());
}
@Before(value = "execution(* graphql.kickstart.execution.GraphQLObjectMapper.readGraphQLRequest(..))")
public void graphqlRequest(final JoinPoint joinPoint) {
...
}
}
하지만 실패 건의 경우 status_code는 200이고 좌측에 빨간 불이 들어온 것을 볼 수 있습니다. 이 이유는 예외 처리에서 설명하겠습니다.
4. 예외 처리
Error Span 처리 흐름
제가 다니는 회사를 포함하여 많은 회사들은 RESTful API를 사용하지 못하고 있습니다.
대표적인 것 중 하나가 status_code의 별도 관리 입니다.
보통 요청 결과가 에러이더라도 status_code는 200으로 받고 respone body에 별도의 code를 사용합니다.
시퀀스 다이어그램을 보면 내부 에러 코드가 9999 인 경우에만 error로 간주하여 setErrorTag()를 거치게 됩니다.
그리고 이 과정에서 status_code는 200으로 유지되지만 span 은 error로 처리되어 모니터링 대상이 됩니다.
모니터링 편의성 & 자동화
위에서 작업한 errorSpan 은 datadog에서 지원하는 Monitors 기능을 통해 slack 알림이 가능합니다.
마치며
저는 백엔드 개발자로서 개발보다 중요한 게 운영이고, 그러기 위해 모니터링 자동화가 필수적이라고 생각합니다.
비록 한글 자료가 적어 설정하는 데에는 정말 고생했지만, 목표한 바를 이뤄 조금 만족했습니다.
하지만 datadog 은 정말 많은 integration을 제공해주고 있고 이것들을 조금씩 설정해나가야 하기에 아직 할 것은 많아 보입니다..
이외에도 작업하거나 시행착오 겪은 것들이 많지만 모두 기록하기는 힘들어 키워드만 남기면서 글을 마무리합니다.
- cloudwatch와 datadog logs의 연동
- datadog agent의 한계상 response body의 기록은 5000byte
- 유저의 액션을 모두 추적 - RUM(프런트)과 APM(백엔드)의 연계
- Session Replay 기능
'Series > 실전!' 카테고리의 다른 글
레거시 코드 개선하기 with delegate pattern (0) | 2022.06.05 |
---|---|
Custom LocalDateUtils 리팩토링 (0) | 2022.01.04 |
느슨한 결합도의 설계를 위해! (2) (0) | 2021.12.04 |
느슨한 결합도의 설계를 위해! (1) (0) | 2021.12.03 |
업무에 적절한 pagination 선택하기 (0) | 2021.11.07 |