Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -26,6 +28,7 @@
public class WorkflowController {
private final WorkflowService workflowService;
private final WorkflowExecutionService workflowExecutionService;
private final RequestContextService requestContextService;

@GetMapping("")
public ApiResponse<PageResult<WorkflowCardDto>> getWorkflowList(
Expand Down Expand Up @@ -53,8 +56,10 @@ public ApiResponse<Void> createWorkflow(

@PostMapping("/{workflowId}/run")
public ResponseEntity<Void> runWorkflow(@PathVariable Long workflowId) {

RequestContext context = requestContextService.extractRequestContext();
// HTTP 요청/응답 스레드를 블로킹하지 않도록 비동기 실행
workflowExecutionService.executeWorkflow(workflowId);
workflowExecutionService.executeWorkflow(workflowId, context);
return ResponseEntity.accepted().build();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package site.icebang.domain.workflow.dto;

import lombok.Data;

/** 요청 컨텍스트 정보를 담는 DTO 클래스 분산 추적, 클라이언트 정보 등을 포함하여 워크플로우 실행 시 필요한 컨텍스트를 관리합니다. */
@Data
public class RequestContext {

private final String traceId;
private final String clientIp;
private final String userAgent;

/**
* 스케줄러 실행용 컨텍스트를 생성하는 정적 팩토리 메서드 HTTP 요청이 아닌 스케줄된 작업에서 사용됩니다.
*
* @param traceId 분산 추적 ID
* @return 스케줄러용 RequestContext 객체 (clientIp와 userAgent는 기본값 설정)
*/
public static RequestContext forScheduler(String traceId) {
return new RequestContext(traceId, "scheduler", "quartz-scheduler");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,21 @@ 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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -20,18 +17,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);
}

/** 워크플로우 실행 완료 처리 */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import site.icebang.domain.workflow.service.RequestContextService;
import site.icebang.domain.workflow.service.WorkflowExecutionService;

/**
Expand All @@ -30,6 +31,7 @@
@RequiredArgsConstructor
public class WorkflowTriggerJob extends QuartzJobBean {
private final WorkflowExecutionService workflowExecutionService;
private final RequestContextService requestContextService;

/**
* Quartz 스케줄러에 의해 트리거가 발동될 때 호출되는 메인 실행 메소드입니다.
Expand All @@ -45,6 +47,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());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +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;

/** 요청 컨텍스트 정보를 추출하고 관리하는 서비스 MDC(Mapped Diagnostic Context)를 사용하여 분산 추적 정보를 처리합니다. */
@Service
public class RequestContextService {

/**
* 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);
}

/**
* Quartz 스케줄러용 컨텍스트를 생성합니다. 스케줄된 작업에서는 HTTP 요청 정보가 없으므로 traceId만 포함됩니다.
*
* @return 스케줄러용 요청 컨텍스트
*/
public RequestContext quartzContext() {
String traceId = MDC.get("traceId") != null ? MDC.get("traceId") : UUID.randomUUID().toString();

return RequestContext.forScheduler(traceId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -47,11 +48,12 @@ 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);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package site.icebang.global.filter.logging;

import java.io.IOException;

import org.slf4j.MDC;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;

/** 클라이언트 요청 정보를 MDC에 설정하는 필터 모든 HTTP 요청에 대해 클라이언트 IP와 User-Agent 정보를 추출하여 로깅 컨텍스트에 저장합니다. */
@Component
@Order(1) // 필터 체인에서 첫 번째로 실행되도록 우선순위 설정
public class ClientLoggingFilter implements Filter {

/**
* 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");

try {
MDC.put("clientIp", ip);
MDC.put("userAgent", userAgent);

chain.doFilter(request, response);
} finally {
MDC.remove("clientIp");
MDC.remove("userAgent");
}
}

/**
* 프록시 환경을 고려하여 클라이언트의 실제 IP 주소를 추출합니다. 로드 밸런서나 프록시 서버를 통해 들어오는 요청의 원본 IP를 찾습니다.
*
* @param request HTTP 요청 객체
* @return 클라이언트의 실제 IP 주소
*/
private String getClientIp(HttpServletRequest request) {

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();
}
}
4 changes: 2 additions & 2 deletions apps/user-service/src/main/resources/log4j2-develop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions apps/user-service/src/main/resources/log4j2-production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

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.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post;
Expand All @@ -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;

Expand Down Expand Up @@ -62,6 +64,7 @@ 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));
}
}
Loading