From df90c84d33825bb77fbdcd67462c7bdea8800e45 Mon Sep 17 00:00:00 2001 From: JiHoon Date: Sun, 28 Sep 2025 17:37:35 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat=20&=20fix=20:=20logging=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=EC=97=90=20ip=20=EB=B0=8F=20user-agent=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20&=20traceId=20=EC=83=9D=EC=84=B1=20=EC=8B=9C?= =?UTF-8?q?=EC=A0=90=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/WorkflowController.java | 7 ++- .../domain/workflow/dto/RequestContext.java | 19 +++++++ .../workflow/manager/ExecutionMdcManager.java | 11 +++- .../domain/workflow/model/WorkflowRun.java | 8 +-- .../scheduler/WorkflowTriggerJob.java | 5 +- .../service/RequestContextService.java | 25 +++++++++ .../service/WorkflowExecutionService.java | 7 +-- .../filter/logging/ClientLoggingFilter.java | 51 +++++++++++++++++++ .../src/main/resources/log4j2-develop.yml | 4 +- .../src/main/resources/log4j2-production.yml | 4 +- .../WorkflowRunApiIntegrationTest.java | 4 +- 11 files changed, 130 insertions(+), 15 deletions(-) create mode 100644 apps/user-service/src/main/java/site/icebang/domain/workflow/dto/RequestContext.java create mode 100644 apps/user-service/src/main/java/site/icebang/domain/workflow/service/RequestContextService.java create mode 100644 apps/user-service/src/main/java/site/icebang/global/filter/logging/ClientLoggingFilter.java diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/controller/WorkflowController.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/controller/WorkflowController.java index c98ece1f..2bc388af 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/controller/WorkflowController.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/controller/WorkflowController.java @@ -14,9 +14,11 @@ import site.icebang.common.dto.PageParams; import site.icebang.common.dto.PageResult; import site.icebang.domain.auth.model.AuthCredential; +import site.icebang.domain.workflow.dto.RequestContext; import site.icebang.domain.workflow.dto.WorkflowCardDto; import site.icebang.domain.workflow.dto.WorkflowCreateDto; import site.icebang.domain.workflow.dto.WorkflowDetailCardDto; +import site.icebang.domain.workflow.service.RequestContextService; import site.icebang.domain.workflow.service.WorkflowExecutionService; import site.icebang.domain.workflow.service.WorkflowService; @@ -26,6 +28,7 @@ public class WorkflowController { private final WorkflowService workflowService; private final WorkflowExecutionService workflowExecutionService; + private final RequestContextService requestContextService; @GetMapping("") public ApiResponse> getWorkflowList( @@ -53,8 +56,10 @@ public ApiResponse createWorkflow( @PostMapping("/{workflowId}/run") public ResponseEntity runWorkflow(@PathVariable Long workflowId) { + + RequestContext context = requestContextService.extractRequestContext(); // HTTP 요청/응답 스레드를 블로킹하지 않도록 비동기 실행 - workflowExecutionService.executeWorkflow(workflowId); + workflowExecutionService.executeWorkflow(workflowId, context); return ResponseEntity.accepted().build(); } diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/RequestContext.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/RequestContext.java new file mode 100644 index 00000000..4881250f --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/RequestContext.java @@ -0,0 +1,19 @@ +package site.icebang.domain.workflow.dto; + +import lombok.Data; + +@Data +public class RequestContext { + + private final String traceId; + private final String clientIp; + private final String userAgent; + + public static RequestContext forScheduler(String traceId) { + return new RequestContext( + traceId, + "scheduler", + "quartz-scheduler" + ); + } +} diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/manager/ExecutionMdcManager.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/manager/ExecutionMdcManager.java index 38c1ae38..4d702b84 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/manager/ExecutionMdcManager.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/manager/ExecutionMdcManager.java @@ -8,11 +8,20 @@ public class ExecutionMdcManager { private static final String SOURCE_ID = "sourceId"; private static final String EXECUTION_TYPE = "executionType"; private static final String TRACE_ID = "traceID"; + private static final String CLIENT_IP = "clientIp"; + private static final String USER_AGENT = "userAgent"; - public void setWorkflowContext(Long workflowId, String traceId) { + public void setWorkflowContext(Long workflowId, String traceId, String clientIp, String userAgent) { MDC.put(SOURCE_ID, workflowId.toString()); MDC.put(EXECUTION_TYPE, "WORKFLOW"); MDC.put(TRACE_ID, traceId); + + if (clientIp != null) { + MDC.put(CLIENT_IP, clientIp); + } + if (userAgent != null) { + MDC.put(USER_AGENT, userAgent); + } } public void setWorkflowContext(Long workflowId) { diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/model/WorkflowRun.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/model/WorkflowRun.java index 1c3a0796..534cfb5b 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/model/WorkflowRun.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/model/WorkflowRun.java @@ -20,18 +20,18 @@ public class WorkflowRun { private Instant finishedAt; private Instant createdAt; - private WorkflowRun(Long workflowId) { + private WorkflowRun(Long workflowId, String traceId) { this.workflowId = workflowId; // MDC에서 현재 요청의 traceId를 가져오거나, 없으면 새로 생성 - this.traceId = MDC.get("traceId") != null ? MDC.get("traceId") : UUID.randomUUID().toString(); + this.traceId = traceId; this.status = "RUNNING"; this.startedAt = Instant.now(); this.createdAt = this.startedAt; } /** 워크플로우 실행 시작을 위한 정적 팩토리 메소드 */ - public static WorkflowRun start(Long workflowId) { - return new WorkflowRun(workflowId); + public static WorkflowRun start(Long workflowId, String traceId) { + return new WorkflowRun(workflowId, traceId); } /** 워크플로우 실행 완료 처리 */ diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/scheduler/WorkflowTriggerJob.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/scheduler/WorkflowTriggerJob.java index a3076d1f..1f258249 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/scheduler/WorkflowTriggerJob.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/scheduler/WorkflowTriggerJob.java @@ -7,6 +7,8 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import site.icebang.domain.workflow.dto.RequestContext; +import site.icebang.domain.workflow.service.RequestContextService; import site.icebang.domain.workflow.service.WorkflowExecutionService; /** @@ -30,6 +32,7 @@ @RequiredArgsConstructor public class WorkflowTriggerJob extends QuartzJobBean { private final WorkflowExecutionService workflowExecutionService; + private final RequestContextService requestContextService; /** * Quartz 스케줄러에 의해 트리거가 발동될 때 호출되는 메인 실행 메소드입니다. @@ -45,6 +48,6 @@ public class WorkflowTriggerJob extends QuartzJobBean { protected void executeInternal(JobExecutionContext context) { Long workflowId = context.getJobDetail().getJobDataMap().getLong("workflowId"); log.info("Quartz가 WorkflowTriggerJob을 실행합니다. WorkflowId={}", workflowId); - workflowExecutionService.executeWorkflow(workflowId); + workflowExecutionService.executeWorkflow(workflowId, requestContextService.quartzContext()); } } diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/service/RequestContextService.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/service/RequestContextService.java new file mode 100644 index 00000000..6da0379e --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/service/RequestContextService.java @@ -0,0 +1,25 @@ +package site.icebang.domain.workflow.service; + +import org.slf4j.MDC; +import org.springframework.stereotype.Service; +import site.icebang.domain.workflow.dto.RequestContext; + +import java.util.UUID; + +@Service +public class RequestContextService { + + public RequestContext extractRequestContext() { + String traceId = MDC.get("traceId") != null ? MDC.get("traceId") : UUID.randomUUID().toString(); + String clientIp = MDC.get("clientIp"); + String userAgent = MDC.get("userAgent"); + + return new RequestContext(traceId, clientIp, userAgent); + } + + public RequestContext quartzContext() { + String traceId = MDC.get("traceId") != null ? MDC.get("traceId") : UUID.randomUUID().toString(); + + return RequestContext.forScheduler(traceId); + } +} diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/service/WorkflowExecutionService.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/service/WorkflowExecutionService.java index 4d781cee..ff177a25 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/service/WorkflowExecutionService.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/service/WorkflowExecutionService.java @@ -20,6 +20,7 @@ import lombok.RequiredArgsConstructor; import site.icebang.domain.workflow.dto.JobDto; +import site.icebang.domain.workflow.dto.RequestContext; import site.icebang.domain.workflow.dto.TaskDto; import site.icebang.domain.workflow.dto.WorkflowDetailCardDto; import site.icebang.domain.workflow.manager.ExecutionMdcManager; @@ -47,11 +48,11 @@ public class WorkflowExecutionService { private final WorkflowMapper workflowMapper; @Async("traceExecutor") - public void executeWorkflow(Long workflowId) { - WorkflowRun workflowRun = WorkflowRun.start(workflowId); + public void executeWorkflow(Long workflowId, RequestContext context) { + WorkflowRun workflowRun = WorkflowRun.start(workflowId, context.getTraceId()); workflowRunMapper.insert(workflowRun); - mdcManager.setWorkflowContext(workflowId, workflowRun.getTraceId()); + mdcManager.setWorkflowContext(workflowId, context.getTraceId(), context.getClientIp(), context.getUserAgent()); try { workflowLogger.info("========== 워크플로우 실행 시작: WorkflowId={} ==========", workflowId); diff --git a/apps/user-service/src/main/java/site/icebang/global/filter/logging/ClientLoggingFilter.java b/apps/user-service/src/main/java/site/icebang/global/filter/logging/ClientLoggingFilter.java new file mode 100644 index 00000000..2c9898ee --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/global/filter/logging/ClientLoggingFilter.java @@ -0,0 +1,51 @@ +package site.icebang.global.filter.logging; + +import jakarta.servlet.*; +import jakarta.servlet.http.HttpServletRequest; +import org.slf4j.MDC; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +@Order(1) +public class ClientLoggingFilter implements Filter { + + @Override + public void doFilter(ServletRequest request, ServletResponse response, + FilterChain chain) throws IOException, ServletException { + + HttpServletRequest httpRequest = (HttpServletRequest) request; + String ip = getClientIp(httpRequest); + String userAgent = httpRequest.getHeader("User-Agent"); + + try { + MDC.put("clientIp", ip); + MDC.put("userAgent", userAgent); + + chain.doFilter(request, response); + } finally { + MDC.remove("clientIp"); + MDC.remove("userAgent"); + } + } + + private String getClientIp(HttpServletRequest request) { + + String[] headers = { + "X-Forwarded-For", "Proxy-Client-IP", "WL-Proxy-Client-IP", + "HTTP_X_FORWARDED_FOR", "HTTP_CLIENT_IP", "REMOTE_ADDR" + }; + + for (String header : headers) { + String ip = request.getHeader(header); + if (ip != null && !ip.isEmpty() && !"unknown".equalsIgnoreCase(ip)) { + return ip.split(",")[0].trim(); + } + } + + return request.getRemoteAddr(); + + } +} diff --git a/apps/user-service/src/main/resources/log4j2-develop.yml b/apps/user-service/src/main/resources/log4j2-develop.yml index 21790eea..f869d73b 100644 --- a/apps/user-service/src/main/resources/log4j2-develop.yml +++ b/apps/user-service/src/main/resources/log4j2-develop.yml @@ -12,10 +12,10 @@ Configuration: value: "UTF-8" # DEBUG 환경용 콘솔 패턴 - 더 간단하고 가독성 좋게 - name: "console-layout-pattern" - value: "%highlight{[%-5level]} [%X{traceId}] [%X{spanId}] %d{HH:mm:ss} [%t] %n %logger{20} - %msg%n%n " + value: "%highlight{[%-5level]} [%X{traceId}] [%X{spanId}] [%X{clientIp}] [%X{userAgent}] %d{HH:mm:ss} [%t] %n %logger{20} - %msg%n%n " # 파일용 패턴 - Promtail이 파싱하기 쉽게 구조화 (UTC 시간 사용) - name: "file-layout-pattern" - value: "[%X{traceId}] [%X{spanId}] %d{yyyy-MM-dd HH:mm:ss.SSS}{UTC} [%t] %-5level %logger{36} - %msg%n" + value: "[%X{traceId}] [%X{spanId}] [%X{clientIp}] [%X{userAgent}] %d{yyyy-MM-dd HH:mm:ss.SSS}{UTC} [%t] %-5level %logger{36} - %msg%n" # 개발 환경용 로그 파일들 - 절대경로나 상대경로 설정 - name: "dev-log" value: ${log-path}/develop/app.log diff --git a/apps/user-service/src/main/resources/log4j2-production.yml b/apps/user-service/src/main/resources/log4j2-production.yml index 79d920fc..efeb7fa1 100644 --- a/apps/user-service/src/main/resources/log4j2-production.yml +++ b/apps/user-service/src/main/resources/log4j2-production.yml @@ -13,10 +13,10 @@ Configuration: value: "UTF-8" # 프로덕션 환경용 콘솔 패턴 - 구조화된 로그 - name: "console-layout-pattern" - value: "%highlight{[%-5level]} [%X{traceId}] [%X{spanId}] %d{HH:mm:ss}{UTC} [%t] %logger{20} - %msg%n" + value: "%highlight{[%-5level]} [%X{traceId}] [%X{spanId}] [%X{clientIp}] [%X{userAgent}] %d{HH:mm:ss}{UTC} [%t] %logger{20} - %msg%n" # 파일용 패턴 - Promtail이 파싱하기 쉽게 구조화 (UTC 시간 사용) - name: "file-layout-pattern" - value: "[%X{traceId}] [%X{spanId}] %d{yyyy-MM-dd HH:mm:ss.SSS}{UTC} [%t] %-5level %logger{36} - %msg%n" + value: "[%X{traceId}] [%X{spanId}] [%X{clientIp}] [%X{userAgent}] %d{yyyy-MM-dd HH:mm:ss.SSS}{UTC} [%t] %-5level %logger{36} - %msg%n" # 프로덕션 환경용 로그 파일들 - name: "prod-log" value: ${log-path}/production/app.log diff --git a/apps/user-service/src/test/java/site/icebang/integration/tests/workflow/WorkflowRunApiIntegrationTest.java b/apps/user-service/src/test/java/site/icebang/integration/tests/workflow/WorkflowRunApiIntegrationTest.java index 23c4eaa4..bc08de7d 100644 --- a/apps/user-service/src/test/java/site/icebang/integration/tests/workflow/WorkflowRunApiIntegrationTest.java +++ b/apps/user-service/src/test/java/site/icebang/integration/tests/workflow/WorkflowRunApiIntegrationTest.java @@ -4,6 +4,7 @@ import static com.epages.restdocs.apispec.ResourceDocumentation.resource; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.verify; +import static org.mockito.ArgumentMatchers.any; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -18,6 +19,7 @@ import com.epages.restdocs.apispec.ResourceSnippetParameters; +import site.icebang.domain.workflow.dto.RequestContext; import site.icebang.domain.workflow.service.WorkflowExecutionService; import site.icebang.integration.setup.support.IntegrationTestSupport; @@ -62,6 +64,6 @@ void runWorkflow_success() throws Exception { .build()))); // 📌 2. 비동기 호출된 executeWorkflow 메소드가 1초 이내에 1번 실행되었는지 검증 - verify(mockWorkflowExecutionService, timeout(1000).times(1)).executeWorkflow(workflowId); + verify(mockWorkflowExecutionService, timeout(1000).times(1)).executeWorkflow(workflowId, any(RequestContext.class)); } } From a1c632d9d2db7aa0ccbe0005e6eba42ae81bfd5d Mon Sep 17 00:00:00 2001 From: JiHoon Date: Sun, 28 Sep 2025 17:49:01 +0900 Subject: [PATCH 2/2] =?UTF-8?q?chore=20&=20style=20:=20spotless=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9=20=EB=B0=8F=20=EC=A3=BC=EC=84=9D=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/workflow/dto/RequestContext.java | 23 ++--- .../workflow/manager/ExecutionMdcManager.java | 3 +- .../domain/workflow/model/WorkflowRun.java | 3 - .../scheduler/WorkflowTriggerJob.java | 1 - .../service/RequestContextService.java | 36 +++++--- .../service/WorkflowExecutionService.java | 3 +- .../filter/logging/ClientLoggingFilter.java | 84 ++++++++++++------- .../WorkflowRunApiIntegrationTest.java | 5 +- 8 files changed, 96 insertions(+), 62 deletions(-) diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/RequestContext.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/RequestContext.java index 4881250f..1812cd32 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/RequestContext.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/RequestContext.java @@ -2,18 +2,21 @@ import lombok.Data; +/** 요청 컨텍스트 정보를 담는 DTO 클래스 분산 추적, 클라이언트 정보 등을 포함하여 워크플로우 실행 시 필요한 컨텍스트를 관리합니다. */ @Data public class RequestContext { - private final String traceId; - private final String clientIp; - private final String userAgent; + private final String traceId; + private final String clientIp; + private final String userAgent; - public static RequestContext forScheduler(String traceId) { - return new RequestContext( - traceId, - "scheduler", - "quartz-scheduler" - ); - } + /** + * 스케줄러 실행용 컨텍스트를 생성하는 정적 팩토리 메서드 HTTP 요청이 아닌 스케줄된 작업에서 사용됩니다. + * + * @param traceId 분산 추적 ID + * @return 스케줄러용 RequestContext 객체 (clientIp와 userAgent는 기본값 설정) + */ + public static RequestContext forScheduler(String traceId) { + return new RequestContext(traceId, "scheduler", "quartz-scheduler"); + } } diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/manager/ExecutionMdcManager.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/manager/ExecutionMdcManager.java index 4d702b84..8dea91f4 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/manager/ExecutionMdcManager.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/manager/ExecutionMdcManager.java @@ -11,7 +11,8 @@ public class ExecutionMdcManager { private static final String CLIENT_IP = "clientIp"; private static final String USER_AGENT = "userAgent"; - public void setWorkflowContext(Long workflowId, String traceId, String clientIp, String userAgent) { + public void setWorkflowContext( + Long workflowId, String traceId, String clientIp, String userAgent) { MDC.put(SOURCE_ID, workflowId.toString()); MDC.put(EXECUTION_TYPE, "WORKFLOW"); MDC.put(TRACE_ID, traceId); diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/model/WorkflowRun.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/model/WorkflowRun.java index 534cfb5b..111b2e89 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/model/WorkflowRun.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/model/WorkflowRun.java @@ -1,9 +1,6 @@ package site.icebang.domain.workflow.model; import java.time.Instant; -import java.util.UUID; - -import org.slf4j.MDC; import lombok.Getter; import lombok.NoArgsConstructor; diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/scheduler/WorkflowTriggerJob.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/scheduler/WorkflowTriggerJob.java index 1f258249..d0bc46f1 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/scheduler/WorkflowTriggerJob.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/scheduler/WorkflowTriggerJob.java @@ -7,7 +7,6 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import site.icebang.domain.workflow.dto.RequestContext; import site.icebang.domain.workflow.service.RequestContextService; import site.icebang.domain.workflow.service.WorkflowExecutionService; diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/service/RequestContextService.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/service/RequestContextService.java index 6da0379e..a4c501af 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/service/RequestContextService.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/service/RequestContextService.java @@ -1,25 +1,37 @@ package site.icebang.domain.workflow.service; +import java.util.UUID; + import org.slf4j.MDC; import org.springframework.stereotype.Service; -import site.icebang.domain.workflow.dto.RequestContext; -import java.util.UUID; +import site.icebang.domain.workflow.dto.RequestContext; +/** 요청 컨텍스트 정보를 추출하고 관리하는 서비스 MDC(Mapped Diagnostic Context)를 사용하여 분산 추적 정보를 처리합니다. */ @Service public class RequestContextService { - public RequestContext extractRequestContext() { - String traceId = MDC.get("traceId") != null ? MDC.get("traceId") : UUID.randomUUID().toString(); - String clientIp = MDC.get("clientIp"); - String userAgent = MDC.get("userAgent"); + /** + * HTTP 요청으로부터 컨텍스트 정보를 추출합니다. + * + * @return 추출된 요청 컨텍스트 + */ + public RequestContext extractRequestContext() { + String traceId = MDC.get("traceId") != null ? MDC.get("traceId") : UUID.randomUUID().toString(); + String clientIp = MDC.get("clientIp"); + String userAgent = MDC.get("userAgent"); - return new RequestContext(traceId, clientIp, userAgent); - } + return new RequestContext(traceId, clientIp, userAgent); + } - public RequestContext quartzContext() { - String traceId = MDC.get("traceId") != null ? MDC.get("traceId") : UUID.randomUUID().toString(); + /** + * Quartz 스케줄러용 컨텍스트를 생성합니다. 스케줄된 작업에서는 HTTP 요청 정보가 없으므로 traceId만 포함됩니다. + * + * @return 스케줄러용 요청 컨텍스트 + */ + public RequestContext quartzContext() { + String traceId = MDC.get("traceId") != null ? MDC.get("traceId") : UUID.randomUUID().toString(); - return RequestContext.forScheduler(traceId); - } + return RequestContext.forScheduler(traceId); + } } diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/service/WorkflowExecutionService.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/service/WorkflowExecutionService.java index ff177a25..d536f4de 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/service/WorkflowExecutionService.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/service/WorkflowExecutionService.java @@ -52,7 +52,8 @@ public void executeWorkflow(Long workflowId, RequestContext context) { WorkflowRun workflowRun = WorkflowRun.start(workflowId, context.getTraceId()); workflowRunMapper.insert(workflowRun); - mdcManager.setWorkflowContext(workflowId, context.getTraceId(), context.getClientIp(), context.getUserAgent()); + mdcManager.setWorkflowContext( + workflowId, context.getTraceId(), context.getClientIp(), context.getUserAgent()); try { workflowLogger.info("========== 워크플로우 실행 시작: WorkflowId={} ==========", workflowId); diff --git a/apps/user-service/src/main/java/site/icebang/global/filter/logging/ClientLoggingFilter.java b/apps/user-service/src/main/java/site/icebang/global/filter/logging/ClientLoggingFilter.java index 2c9898ee..97c87b36 100644 --- a/apps/user-service/src/main/java/site/icebang/global/filter/logging/ClientLoggingFilter.java +++ b/apps/user-service/src/main/java/site/icebang/global/filter/logging/ClientLoggingFilter.java @@ -1,51 +1,71 @@ package site.icebang.global.filter.logging; -import jakarta.servlet.*; -import jakarta.servlet.http.HttpServletRequest; +import java.io.IOException; + import org.slf4j.MDC; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; -import java.io.IOException; +import jakarta.servlet.*; +import jakarta.servlet.http.HttpServletRequest; +/** 클라이언트 요청 정보를 MDC에 설정하는 필터 모든 HTTP 요청에 대해 클라이언트 IP와 User-Agent 정보를 추출하여 로깅 컨텍스트에 저장합니다. */ @Component -@Order(1) +@Order(1) // 필터 체인에서 첫 번째로 실행되도록 우선순위 설정 public class ClientLoggingFilter implements Filter { - @Override - public void doFilter(ServletRequest request, ServletResponse response, - FilterChain chain) throws IOException, ServletException { + /** + * HTTP 요청을 필터링하여 클라이언트 정보를 MDC에 설정합니다. + * + * @param request 서블릿 요청 객체 + * @param response 서블릿 응답 객체 + * @param chain 필터 체인 + * @throws IOException 입출력 예외 + * @throws ServletException 서블릿 예외 + */ + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { - HttpServletRequest httpRequest = (HttpServletRequest) request; - String ip = getClientIp(httpRequest); - String userAgent = httpRequest.getHeader("User-Agent"); + HttpServletRequest httpRequest = (HttpServletRequest) request; + String ip = getClientIp(httpRequest); + String userAgent = httpRequest.getHeader("User-Agent"); - try { - MDC.put("clientIp", ip); - MDC.put("userAgent", userAgent); + try { + MDC.put("clientIp", ip); + MDC.put("userAgent", userAgent); - chain.doFilter(request, response); - } finally { - MDC.remove("clientIp"); - MDC.remove("userAgent"); - } + chain.doFilter(request, response); + } finally { + MDC.remove("clientIp"); + MDC.remove("userAgent"); } + } - private String getClientIp(HttpServletRequest request) { - - String[] headers = { - "X-Forwarded-For", "Proxy-Client-IP", "WL-Proxy-Client-IP", - "HTTP_X_FORWARDED_FOR", "HTTP_CLIENT_IP", "REMOTE_ADDR" - }; + /** + * 프록시 환경을 고려하여 클라이언트의 실제 IP 주소를 추출합니다. 로드 밸런서나 프록시 서버를 통해 들어오는 요청의 원본 IP를 찾습니다. + * + * @param request HTTP 요청 객체 + * @return 클라이언트의 실제 IP 주소 + */ + private String getClientIp(HttpServletRequest request) { - for (String header : headers) { - String ip = request.getHeader(header); - if (ip != null && !ip.isEmpty() && !"unknown".equalsIgnoreCase(ip)) { - return ip.split(",")[0].trim(); - } - } - - return request.getRemoteAddr(); + String[] headers = { + "X-Forwarded-For", // 표준 프록시 헤더 + "Proxy-Client-IP", // Apache 프록시 + "WL-Proxy-Client-IP", // WebLogic 프록시 + "HTTP_X_FORWARDED_FOR", // HTTP 프록시 + "HTTP_CLIENT_IP", // HTTP 클라이언트 IP + "REMOTE_ADDR" // 원격 주소 + }; + for (String header : headers) { + String ip = request.getHeader(header); + if (ip != null && !ip.isEmpty() && !"unknown".equalsIgnoreCase(ip)) { + return ip.split(",")[0].trim(); + } } + + return request.getRemoteAddr(); + } } diff --git a/apps/user-service/src/test/java/site/icebang/integration/tests/workflow/WorkflowRunApiIntegrationTest.java b/apps/user-service/src/test/java/site/icebang/integration/tests/workflow/WorkflowRunApiIntegrationTest.java index bc08de7d..69fd0f4a 100644 --- a/apps/user-service/src/test/java/site/icebang/integration/tests/workflow/WorkflowRunApiIntegrationTest.java +++ b/apps/user-service/src/test/java/site/icebang/integration/tests/workflow/WorkflowRunApiIntegrationTest.java @@ -2,9 +2,9 @@ import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document; import static com.epages.restdocs.apispec.ResourceDocumentation.resource; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.verify; -import static org.mockito.ArgumentMatchers.any; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -64,6 +64,7 @@ void runWorkflow_success() throws Exception { .build()))); // 📌 2. 비동기 호출된 executeWorkflow 메소드가 1초 이내에 1번 실행되었는지 검증 - verify(mockWorkflowExecutionService, timeout(1000).times(1)).executeWorkflow(workflowId, any(RequestContext.class)); + verify(mockWorkflowExecutionService, timeout(1000).times(1)) + .executeWorkflow(workflowId, any(RequestContext.class)); } }