로그 개발 목적
지금까지 어플리케이션에서 나오는 에러들은 프론트분들이 찾아주셔서 cloudwatch를 들어가서 확인하곤 했다.
하지만 cloudwatch의 로그 가시성이 너무 떨어지고 에러가 났을때 알람과 어디서 에러가났는지 정확히 알기 위해서 개발이 필요했다.
- 멀티 스레드 환경에서 같은 스레드의 작업을 찾을 수 있게 한다.
- 다른 스레드 작업이 있어도 하나의 트레이스 안에 묶일 수 있게 한다.
- 센트리에서 한번에 오류를 볼수있도록 해야한다.(클라우드워치까지 가지않고싶다.)
MDC
- Log4j의 MDC는 스레드 로컬(thread-local) 변수를 이용하여 각 스레드마다 별도의 컨텍스트 정보를 저장한다.
- 이를 통해 멀티스레드 환경에서도 로그 메시지에 일관된 컨텍스트를 포함시킬 수 있다.
TraceId 설정
aop 사용해 MDC tracID 유지(실패)
처음에 aop에 before에서 id를 저장하고 after에서 삭제하는 방법을 사용했지만
내가 설정한 log 출력 전에 after가 실행되서 tarceId가 유지되지 않아 하나의 스레드가 같은 traceid 가지지 않게 되었다.
서블릿 스레드풀 설정 변경(안좋은 방법)
스프링을 시작할때 서블릿에서 생성해주는 스레드 풀을 직접 조정해 각 스레드에 tarceId를 넣어주는 작업을 했다.
하지만 내가 서블릿의 스레드풀을 어떻게 만드는지 서블릿 스레드만의 최적화 방법 등 모르는 부분이 있기때문에 직접 스레드풀을 작업하는건 좋지 않다고생각했다.
@Bean
public TomcatServletWebServerFactory tomcatFactory() {
return new TomcatServletWebServerFactory()
...
return new Thread(() -> {
try {
if (contextMap != null) {
MDC.setContextMap(contextMap);
} else {
String traceId = UUID.randomUUID().toString();
MDC.put("traceId", traceId);
MDC.put("errorFlag", "false");
}
r.run();
} finally {
MDC.clear();
}
}, "bems-thread");
...
}
인터셉터 추가
요청을 받을때 미리 인터셉터에서 필요한 데이트를 세팅하고 끝날때 정리해주는 방식으로 만들었다.
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogMdcInterceptor());
}
}
public class LogMdcInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
MDC.put(필요한 데이터 세팅)
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
Exception ex) {
MDC.clear();
}
}
위 그림처럼 멀티 스레드에서 요청이 와도 traceId로 하나의 요청에 따른 흐름을 제대로 읽을 수 있게 되었다.
스레드가 달라져도 traceId 가져가기
비동기 작업시 따로 설정한 스레드를 사용해 작업이 진행된다 이렇게 되면 가지고 있는 threadlocal특성을 지닌 mdc로는 설정한 속성들을 유지하기 힘들어진다.
public static class MDCCopyTaskDecorator implements TaskDecorator {
@Override
public Runnable decorate(Runnable runnable) {
Map<String, String> contextMap = MDC.getCopyOfContextMap();
return () -> {
try {
if (contextMap != null) {
MDC.setContextMap(contextMap);
}
runnable.run();
} finally {
MDC.clear();
}
};
}
}
스레드를 바꿀때 MDC의 있는 속성을 복사해서 다음 스레드로 넘기는 작업을 하면 스레드가 달라져도 속성들을 지키면서 진행시킬 수 있다.
파라미터 출력
에러가 났을때 어떤 데이터에서 에러가 났는지 확인하기위해 arg를 가져와서 데이터를 만들어 주었다.
private StringBuilder getErrorArgs(JoinPoint joinPoint, String traceId) {
ObjectMapper objectMapper = new ObjectMapper();
Object[] args = joinPoint.getArgs();
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < args.length; i++) {
try {
String json = objectMapper.writeValueAsString(args[i]);
stringBuilder.append(args[i].getClass()).append(":").append(json);
} catch (Exception e) {
log.warn("[traceId]: {},Failed to serialize argument {}: {}", traceId, i, e.getMessage());
}
}
Sentry.setExtra("errorArgs", stringBuilder.toString());
return stringBuilder;
}
sentry 설정
plugins {
id "io.sentry.jvm.gradle" version "4.12.0"
}
sentry {
includeSourceContext = true
org = "org"
projectName = "project-be"
authToken = ""
}
sentry의 가이드를 따라서 gradle을 추가 후 properties를 설정해주어 에러가 났을때 센트리로 전송이 되도록 설정했다.
위 설정을 안하고 dependency와 properties만 추가해도 센트리로 에러 전송은 되지만 센트리에서 제공하는 여러가지 기능들을 사용하려면 위 설정을 추가해줘야한다.
참고
'트러블슈팅 > 회사' 카테고리의 다른 글
Modbus protocol (2) | 2024.12.28 |
---|---|
loki 적용하기 (0) | 2024.11.04 |
modbus 데이터 흐름 구조 (0) | 2024.10.15 |
instancio (0) | 2024.10.15 |
batch 인프라 재구성 (1) | 2024.10.15 |