refactor: remove redis and switch to in-memory, rollback incomplete Spring AI 2.0 upgrade
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 4m31s
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 4m31s
This commit is contained in:
@@ -37,8 +37,8 @@ public class McpGatewayProperties {
|
||||
private Boolean agentClaimsRequired;
|
||||
private Boolean trustedClaimsRequired;
|
||||
private Boolean writeApprovalRequired;
|
||||
private Boolean redisTraceEnabled;
|
||||
private Long redisTraceTtlSeconds;
|
||||
private Boolean traceEnabled;
|
||||
private Long traceRetentionSize;
|
||||
private Long toolTimeoutMillis;
|
||||
private Integer retryMaxAttempts;
|
||||
private Long retryInitialBackoffMillis;
|
||||
@@ -70,8 +70,8 @@ public class McpGatewayProperties {
|
||||
public void setAgentClaimsRequired(Boolean agentClaimsRequired) { this.agentClaimsRequired = agentClaimsRequired; }
|
||||
public void setTrustedClaimsRequired(Boolean trustedClaimsRequired) { this.trustedClaimsRequired = trustedClaimsRequired; }
|
||||
public void setWriteApprovalRequired(Boolean writeApprovalRequired) { this.writeApprovalRequired = writeApprovalRequired; }
|
||||
public void setRedisTraceEnabled(Boolean redisTraceEnabled) { this.redisTraceEnabled = redisTraceEnabled; }
|
||||
public void setRedisTraceTtlSeconds(Long redisTraceTtlSeconds) { this.redisTraceTtlSeconds = redisTraceTtlSeconds; }
|
||||
public void setTraceEnabled(Boolean traceEnabled) { this.traceEnabled = traceEnabled; }
|
||||
public void setTraceRetentionSize(Long traceRetentionSize) { this.traceRetentionSize = traceRetentionSize; }
|
||||
public void setToolTimeoutMillis(Long toolTimeoutMillis) { this.toolTimeoutMillis = toolTimeoutMillis; }
|
||||
public void setRetryMaxAttempts(Integer retryMaxAttempts) { this.retryMaxAttempts = retryMaxAttempts; }
|
||||
public void setRetryInitialBackoffMillis(Long retryInitialBackoffMillis) { this.retryInitialBackoffMillis = retryInitialBackoffMillis; }
|
||||
@@ -103,8 +103,8 @@ public class McpGatewayProperties {
|
||||
public boolean agentClaimsRequired() { return agentClaimsRequired == null || agentClaimsRequired; }
|
||||
public boolean trustedClaimsRequired() { return trustedClaimsRequired == null || trustedClaimsRequired; }
|
||||
public boolean writeApprovalRequired() { return writeApprovalRequired == null || writeApprovalRequired; }
|
||||
public boolean redisTraceEnabled() { return redisTraceEnabled == null || redisTraceEnabled; }
|
||||
public long redisTraceTtlSeconds() { return redisTraceTtlSeconds == null || redisTraceTtlSeconds < 1 ? 3_600 : redisTraceTtlSeconds; }
|
||||
public boolean traceEnabled() { return traceEnabled == null || traceEnabled; }
|
||||
public long traceRetentionSize() { return traceRetentionSize == null || traceRetentionSize < 1 ? 1000 : traceRetentionSize; }
|
||||
public long toolTimeoutMillis() { return toolTimeoutMillis == null || toolTimeoutMillis < 1 ? 5_000 : toolTimeoutMillis; }
|
||||
public int retryMaxAttempts() { return retryMaxAttempts == null || retryMaxAttempts < 1 ? 3 : retryMaxAttempts; }
|
||||
public long retryInitialBackoffMillis() { return retryInitialBackoffMillis == null || retryInitialBackoffMillis < 1 ? 1_000 : retryInitialBackoffMillis; }
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
package io.shinhanlife.dap.mcg.config;
|
||||
|
||||
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
|
||||
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.mcg.config
|
||||
* @className RedisConfig
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Configuration
|
||||
public class RedisConfig {
|
||||
|
||||
@Bean
|
||||
public RedisTemplate<String, ToolMetadata> redisTemplate(RedisConnectionFactory connectionFactory) {
|
||||
RedisTemplate<String, ToolMetadata> template = new RedisTemplate<>();
|
||||
template.setConnectionFactory(connectionFactory);
|
||||
|
||||
// 1. Key는 무조건 String
|
||||
template.setKeySerializer(new StringRedisSerializer());
|
||||
template.setHashKeySerializer(new StringRedisSerializer());
|
||||
|
||||
// 2. Value는 Generic 직렬화기 사용 (생성자 인자 없음)
|
||||
Jackson2JsonRedisSerializer<ToolMetadata> serializer = new Jackson2JsonRedisSerializer<>(ToolMetadata.class);
|
||||
template.setValueSerializer(serializer);
|
||||
template.setHashValueSerializer(serializer);
|
||||
|
||||
template.afterPropertiesSet();
|
||||
return template;
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ package io.shinhanlife.dap.mcg.presentation;
|
||||
*/
|
||||
import io.shinhanlife.dap.lib.adapter.dto.JsonRpcResponse;
|
||||
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
|
||||
import io.shinhanlife.dap.mcg.registry.RedisRegistryService;
|
||||
import io.shinhanlife.dap.mcg.registry.InMemoryRegistryService;
|
||||
import io.shinhanlife.dap.mcg.service.ExecuteService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -41,7 +41,7 @@ public class ChatController {
|
||||
|
||||
private final ExecuteService executeService;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final RedisRegistryService registryService;
|
||||
private final InMemoryRegistryService registryService;
|
||||
private final ChatClient.Builder chatClientBuilder;
|
||||
private final McpRouterController mcpRouterController;
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ import io.shinhanlife.dap.lib.adapter.dto.JsonRpcResponse;
|
||||
import io.shinhanlife.dap.lib.adapter.dto.Params;
|
||||
import io.shinhanlife.dap.mcg.config.GatewayFallbackProperties;
|
||||
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
|
||||
import io.shinhanlife.dap.mcg.registry.RedisRegistryService;
|
||||
import io.shinhanlife.dap.mcg.registry.InMemoryRegistryService;
|
||||
import io.shinhanlife.dap.mcg.service.ExecuteService;
|
||||
import io.shinhanlife.dap.lib.mcp.security.SecurityProperties;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
@@ -49,19 +49,19 @@ import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
||||
@Tag(name = "MCP Router API", description = "AI Agent의 요청을 받아 Adapter 시스템으로 라우팅하는 게이트웨이 API")
|
||||
public class McpRouterController {
|
||||
|
||||
private final RedisRegistryService redisRegistryService;
|
||||
private final InMemoryRegistryService registryService;
|
||||
private final ExecuteService executeService;
|
||||
private final SecurityProperties securityProperties;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final GatewayFallbackProperties gatewayFallbackProperties;
|
||||
private final RestClient restClient;
|
||||
|
||||
public McpRouterController(RedisRegistryService redisRegistryService,
|
||||
public McpRouterController(InMemoryRegistryService registryService,
|
||||
ExecuteService executeService,
|
||||
SecurityProperties securityProperties,
|
||||
ObjectMapper objectMapper,
|
||||
GatewayFallbackProperties gatewayFallbackProperties) {
|
||||
this.redisRegistryService = redisRegistryService;
|
||||
this.registryService = registryService;
|
||||
this.executeService = executeService;
|
||||
this.securityProperties = securityProperties;
|
||||
this.objectMapper = objectMapper;
|
||||
@@ -115,12 +115,12 @@ public class McpRouterController {
|
||||
private List<ToolMetadata> fetchAllActiveTools() {
|
||||
List<ToolMetadata> activeTools = new ArrayList<>();
|
||||
try {
|
||||
activeTools.addAll(redisRegistryService.getAllTools()
|
||||
activeTools.addAll(registryService.getAllTools()
|
||||
.stream()
|
||||
.filter(ToolMetadata::getVisible)
|
||||
.collect(Collectors.toList()));
|
||||
} catch (org.springframework.data.redis.RedisConnectionFailureException exception) {
|
||||
log.warn("Redis is unavailable. Fetching tools from configured fallback Tool Pods instead.");
|
||||
} catch (Exception exception) {
|
||||
log.warn("Registry is unavailable. Fetching tools from configured fallback Tool Pods instead.");
|
||||
}
|
||||
|
||||
Set<String> knownTools = activeTools.stream()
|
||||
@@ -240,19 +240,19 @@ public class McpRouterController {
|
||||
|
||||
@PostMapping("/registry/register")
|
||||
public ResponseEntity<String> registerTool(@RequestBody ToolMetadata meta) {
|
||||
redisRegistryService.saveTool(meta);
|
||||
registryService.saveTool(meta);
|
||||
return ResponseEntity.ok("Registered");
|
||||
}
|
||||
|
||||
@PostMapping("/registry/deregister")
|
||||
public ResponseEntity<String> deregisterTool(@RequestBody String uid) {
|
||||
redisRegistryService.removeTool(uid);
|
||||
registryService.removeTool(uid);
|
||||
return ResponseEntity.ok("Deregistered");
|
||||
}
|
||||
|
||||
@PostMapping("/registry/heartbeat")
|
||||
public ResponseEntity<String> heartbeat(@RequestBody String uid) {
|
||||
boolean success = redisRegistryService.refreshHeartbeat(uid);
|
||||
boolean success = registryService.refreshHeartbeat(uid);
|
||||
if (success) {
|
||||
return ResponseEntity.ok("Heartbeat updated");
|
||||
} else {
|
||||
|
||||
@@ -1,264 +0,0 @@
|
||||
package io.shinhanlife.dap.mcg.redis;
|
||||
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.mcg.redis
|
||||
* @className RedisToolTraceService
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
import io.shinhanlife.dap.mcg.config.McpGatewayProperties;
|
||||
import io.shinhanlife.dap.mcg.security.McpRequestContext;
|
||||
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.HexFormat;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@Service
|
||||
public class RedisToolTraceService {
|
||||
private static final Logger log = LoggerFactory.getLogger(RedisToolTraceService.class);
|
||||
private static final String KEY_PREFIX = "mcp:request:";
|
||||
private static final String RECENT_KEY = "mcp:request:recent";
|
||||
|
||||
private final McpGatewayProperties properties;
|
||||
private final ObjectProvider<StringRedisTemplate> redisProvider;
|
||||
private final ObjectMapper json;
|
||||
private final McpMonitorEventService monitorEvents;
|
||||
private final Map<String, AttemptState> attemptStates = new ConcurrentHashMap<>();
|
||||
|
||||
public RedisToolTraceService(McpGatewayProperties properties,
|
||||
ObjectProvider<StringRedisTemplate> redisProvider,
|
||||
ObjectMapper json,
|
||||
McpMonitorEventService monitorEvents) {
|
||||
this.properties = properties;
|
||||
this.redisProvider = redisProvider;
|
||||
this.json = json;
|
||||
this.monitorEvents = monitorEvents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Agent request has entered the MCP tool gateway.
|
||||
*/
|
||||
public void started(McpRequestContext context, ToolMetadata metadata, ObjectNode arguments) {
|
||||
save(context, metadata, arguments, "STARTED", 0, "", 0, 0, 0,
|
||||
"Agent request entered MCP", "");
|
||||
}
|
||||
|
||||
/**
|
||||
* MCP is attempting to call the target Tool server.
|
||||
*/
|
||||
public void attemptStarted(McpRequestContext context, ToolMetadata metadata, ObjectNode arguments,
|
||||
int attempt, int maxAttempts) {
|
||||
save(context, metadata, arguments, "ATTEMPTING", 0, "", attempt, maxAttempts, 0,
|
||||
"Calling tool server", "");
|
||||
}
|
||||
|
||||
/**
|
||||
* MCP will retry the Tool call after backoff.
|
||||
*/
|
||||
public void retryWaiting(McpRequestContext context, ToolMetadata metadata, ObjectNode arguments,
|
||||
int failedAttempt, int maxAttempts, long backoffMillis, String failureType) {
|
||||
save(context, metadata, arguments, "RETRY_WAITING", 0, failureType, failedAttempt, maxAttempts, backoffMillis,
|
||||
"Retry will be attempted after backoff", "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Tool execution has finished.
|
||||
*/
|
||||
public void finished(McpRequestContext context, ToolMetadata metadata, ObjectNode arguments,
|
||||
long elapsedMillis, boolean success, String failureType, String responseText) {
|
||||
save(context, metadata, arguments, success ? "SUCCESS" : "FAILED", elapsedMillis, failureType, 0, 0, 0,
|
||||
success ? "Tool call completed" : "Tool call failed", responseText);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recent live trace entries. One current entry is kept per request id.
|
||||
*/
|
||||
public List<String> recent() {
|
||||
if (!properties.redisTraceEnabled()) {
|
||||
return List.of("Redis trace is disabled. Set MCP_REDIS_TRACE_ENABLED=true.");
|
||||
}
|
||||
StringRedisTemplate redis = redisProvider.getIfAvailable();
|
||||
if (redis == null) {
|
||||
return List.of("RedisTemplate is not available.");
|
||||
}
|
||||
try {
|
||||
List<String> requestIds = redis.opsForList().range(RECENT_KEY, 0, 49);
|
||||
if (requestIds == null || requestIds.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
return requestIds.stream()
|
||||
.map(requestId -> redis.opsForValue().get(KEY_PREFIX + requestId))
|
||||
.filter(value -> value != null && !value.isBlank())
|
||||
.toList();
|
||||
} catch (Exception error) {
|
||||
log.warn("Redis trace read failed. message={}", error.getMessage());
|
||||
return List.of("Redis trace read failed: " + error.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Historical accumulation is intentionally disabled.
|
||||
*
|
||||
* The MCP monitor is used to observe the current Agent request flow only,
|
||||
* so this method returns the same live entries as recent().
|
||||
*/
|
||||
public List<String> history() {
|
||||
return recent();
|
||||
}
|
||||
|
||||
private void save(McpRequestContext context, ToolMetadata metadata, ObjectNode arguments, String status,
|
||||
long elapsedMillis, String failureType, int attempt, int maxAttempts,
|
||||
long backoffMillis, String message, String responseText) {
|
||||
String stateKey = stateKey(context, metadata);
|
||||
if (attempt > 0 && maxAttempts > 0) {
|
||||
attemptStates.put(stateKey, new AttemptState(attempt, maxAttempts));
|
||||
}
|
||||
String payload;
|
||||
try {
|
||||
payload = tracePayload(context, metadata, arguments, status, elapsedMillis, failureType, attempt,
|
||||
maxAttempts, backoffMillis, message, responseText);
|
||||
monitorEvents.publish(payload);
|
||||
if ("SUCCESS".equals(status) || "FAILED".equals(status)) {
|
||||
attemptStates.remove(stateKey);
|
||||
}
|
||||
} catch (Exception error) {
|
||||
log.warn("MCP monitor event publish failed. requestId={} tool={} message={}",
|
||||
context.requestId(), metadata.getName(), error.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!properties.redisTraceEnabled()) {
|
||||
return;
|
||||
}
|
||||
StringRedisTemplate redis = redisProvider.getIfAvailable();
|
||||
if (redis == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
String requestId = context.requestId();
|
||||
Duration ttl = Duration.ofSeconds(properties.redisTraceTtlSeconds());
|
||||
redis.opsForValue().set(KEY_PREFIX + requestId, payload, ttl);
|
||||
redis.opsForList().remove(RECENT_KEY, 0, requestId);
|
||||
redis.opsForList().leftPush(RECENT_KEY, requestId);
|
||||
redis.opsForList().trim(RECENT_KEY, 0, 49);
|
||||
redis.expire(RECENT_KEY, ttl);
|
||||
} catch (Exception error) {
|
||||
log.warn("Redis trace save failed. requestId={} tool={} message={}",
|
||||
context.requestId(), metadata.getName(), error.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
String tracePayload(McpRequestContext context, ToolMetadata metadata, ObjectNode arguments, String status,
|
||||
long elapsedMillis, String failureType, int attempt, int maxAttempts,
|
||||
long backoffMillis, String message, String responseText) throws Exception {
|
||||
AttemptState state = attemptStates.get(stateKey(context, metadata));
|
||||
int displayAttempt = attempt > 0 ? attempt : state == null ? 0 : state.attempt();
|
||||
int displayMaxAttempts = maxAttempts > 0 ? maxAttempts : state == null ? 0 : state.maxAttempts();
|
||||
int retryCount = Math.max(0, displayAttempt - 1);
|
||||
Map<String, Object> trace = new LinkedHashMap<>();
|
||||
trace.put("requestId", context.requestId());
|
||||
trace.put("traceGroupId", context.traceGroupId());
|
||||
trace.put("agentId", context.agentId());
|
||||
trace.put("userId", context.userId());
|
||||
trace.put("clientAddress", context.clientAddress());
|
||||
trace.put("toolName", metadata.getName());
|
||||
trace.put("operationType", metadata.getOperationType() != null ? metadata.getOperationType().name() : "READ");
|
||||
trace.put("status", status);
|
||||
trace.put("message", message == null ? "" : message);
|
||||
trace.put("failureType", failureType == null ? "" : failureType);
|
||||
trace.put("attempt", displayAttempt);
|
||||
trace.put("maxAttempts", displayMaxAttempts);
|
||||
trace.put("retryCount", retryCount);
|
||||
trace.put("backoffMillis", backoffMillis);
|
||||
trace.put("elapsedMillis", elapsedMillis);
|
||||
trace.put("idempotencyKeyHash", hashText(arguments.path("idempotencyKey").asText("")));
|
||||
|
||||
List<String> argNames = new ArrayList<>();
|
||||
arguments.fieldNames().forEachRemaining(argNames::add);
|
||||
trace.put("argumentNames", argNames);
|
||||
|
||||
trace.put("arguments", arguments.toString());
|
||||
trace.put("responseSummary", responseSummary(responseText));
|
||||
trace.put("timestamp", Instant.now().toString());
|
||||
return json.writeValueAsString(trace);
|
||||
}
|
||||
|
||||
/**
|
||||
* Redis Trace에는 Tool 응답 원문을 저장하지 않고 size/hash/상태/카운트만 저장합니다.
|
||||
*/
|
||||
Map<String, Object> responseSummary(String responseText) {
|
||||
Map<String, Object> summary = new LinkedHashMap<>();
|
||||
if (responseText == null || responseText.isBlank()) {
|
||||
summary.put("bytes", 0);
|
||||
summary.put("sha256", "");
|
||||
return summary;
|
||||
}
|
||||
byte[] bytes = responseText.getBytes(StandardCharsets.UTF_8);
|
||||
summary.put("bytes", bytes.length);
|
||||
summary.put("sha256", sha256(bytes));
|
||||
try {
|
||||
JsonNode root = json.readTree(responseText);
|
||||
summary.put("success", root.path("success").asBoolean(false));
|
||||
summary.put("status", root.path("status").asText(""));
|
||||
summary.put("toolName", root.path("toolName").asText(""));
|
||||
summary.put("pageSize", root.path("pageSize").asInt(0));
|
||||
summary.put("pageCount", root.path("pageCount").asInt(0));
|
||||
summary.put("returnedCount", root.path("returnedCount").asInt(0));
|
||||
summary.put("totalCount", root.path("totalCount").asLong(0));
|
||||
summary.put("hasMore", root.path("hasMore").asBoolean(false));
|
||||
summary.put("nextCursor", root.path("nextCursor").asText(""));
|
||||
summary.put("message", root.path("message").asText(""));
|
||||
summary.put("resultRef", root.path("resultRef").asText(""));
|
||||
} catch (Exception ignored) {
|
||||
summary.put("parseable", false);
|
||||
}
|
||||
return summary;
|
||||
}
|
||||
|
||||
private String sha256(byte[] bytes) {
|
||||
try {
|
||||
return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(bytes));
|
||||
} catch (Exception error) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private String hashText(String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return "";
|
||||
}
|
||||
return sha256(value.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
private String stateKey(McpRequestContext context, ToolMetadata metadata) {
|
||||
return context.requestId() + ":" + metadata.getName();
|
||||
}
|
||||
|
||||
private record AttemptState(int attempt, int maxAttempts) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package io.shinhanlife.dap.mcg.registry;
|
||||
|
||||
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.mcg.registry
|
||||
* @className InMemoryRegistryService
|
||||
* @description AX HUB 인메모리 툴 레지스트리
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class InMemoryRegistryService {
|
||||
|
||||
private final Map<String, ToolMetadata> toolCache = new ConcurrentHashMap<>();
|
||||
private static final long DEFAULT_TTL_MILLIS = 45000; // 45초
|
||||
|
||||
/**
|
||||
* 툴 등록 및 갱신
|
||||
*/
|
||||
public void saveTool(ToolMetadata meta) {
|
||||
meta.setLastHeartbeat(System.currentTimeMillis());
|
||||
toolCache.put(meta.getUid(), meta);
|
||||
log.info(" [InMemoryRegistry] 툴 등록 완료: {}", meta.getUid());
|
||||
}
|
||||
|
||||
/**
|
||||
* 하트비트 갱신 (TTL 초기화)
|
||||
*/
|
||||
public boolean refreshHeartbeat(String uid) {
|
||||
ToolMetadata meta = toolCache.get(uid);
|
||||
if (meta != null) {
|
||||
meta.setLastHeartbeat(System.currentTimeMillis());
|
||||
log.debug(" [InMemoryRegistry] 하트비트 갱신: {}", uid);
|
||||
return true;
|
||||
} else {
|
||||
log.warn(" [InMemoryRegistry] 존재하지 않는 툴에 대한 하트비트 요청: {}", uid);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public List<String> getAvailablePods(String uid) {
|
||||
ToolMetadata tool = getTool(uid);
|
||||
if (tool != null && tool.getPodUrl() != null) {
|
||||
return List.of(tool.getPodUrl());
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
|
||||
/**
|
||||
* 실행 시 툴 정보 조회
|
||||
*/
|
||||
public ToolMetadata getTool(String uid) {
|
||||
return toolCache.get(uid);
|
||||
}
|
||||
|
||||
/**
|
||||
* 툴 이름으로 정보 조회
|
||||
*/
|
||||
public ToolMetadata getToolByName(String name) {
|
||||
for (ToolMetadata tool : toolCache.values()) {
|
||||
if (name.equals(tool.getName())) {
|
||||
return tool;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 등록된 모든 활성 툴 목록 조회
|
||||
*/
|
||||
public List<ToolMetadata> getAllTools() {
|
||||
return List.copyOf(toolCache.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* 툴 명시적 제거 (Deregister)
|
||||
*/
|
||||
public void removeTool(String uid) {
|
||||
toolCache.remove(uid);
|
||||
log.info(" [InMemoryRegistry] 툴 삭제 완료: {}", uid);
|
||||
}
|
||||
|
||||
/**
|
||||
* 만료된 툴을 스케줄러로 삭제합니다. (10초마다 실행)
|
||||
*/
|
||||
@Scheduled(fixedRate = 10000)
|
||||
public void evictExpiredTools() {
|
||||
long now = System.currentTimeMillis();
|
||||
List<String> expiredKeys = toolCache.entrySet().stream()
|
||||
.filter(entry -> (now - entry.getValue().getLastHeartbeat()) > DEFAULT_TTL_MILLIS)
|
||||
.map(Map.Entry::getKey)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
for (String key : expiredKeys) {
|
||||
toolCache.remove(key);
|
||||
log.info(" [InMemoryRegistry] TTL 초과로 툴 자동 삭제: {}", key);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
package io.shinhanlife.dap.mcg.registry;
|
||||
|
||||
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.mcg.registry
|
||||
* @className RedisRegistryService
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class RedisRegistryService {
|
||||
|
||||
private final RedisTemplate<String, ToolMetadata> redisTemplate;
|
||||
private static final String KEY_PREFIX = "mcp:tool:";
|
||||
private static final Duration DEFAULT_TTL = Duration.ofSeconds(45); // 실무 하트비트 주기 반영
|
||||
|
||||
/**
|
||||
* 툴 등록 및 갱신 (TTL 기반으로 60초 뒤 자동 만료)
|
||||
*/
|
||||
public void saveTool(ToolMetadata meta) {
|
||||
String key = KEY_PREFIX + meta.getUid();
|
||||
meta.setLastHeartbeat(System.currentTimeMillis());
|
||||
redisTemplate.opsForValue().set(key, meta, DEFAULT_TTL);
|
||||
log.info(" [RedisRegistry] 툴 등록 완료: {}", meta.getUid());
|
||||
}
|
||||
|
||||
/**
|
||||
* 하트비트 갱신 (TTL 초기화)
|
||||
*/
|
||||
public boolean refreshHeartbeat(String uid) {
|
||||
String key = KEY_PREFIX + uid;
|
||||
Boolean exists = redisTemplate.expire(key, DEFAULT_TTL);
|
||||
if (Boolean.TRUE.equals(exists)) {
|
||||
log.debug(" [RedisRegistry] 하트비트 갱신: {}", uid);
|
||||
return true;
|
||||
} else {
|
||||
log.warn(" [RedisRegistry] 존재하지 않는 툴에 대한 하트비트 요청: {}", uid);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
public List<String> getAvailablePods(String uid) {
|
||||
ToolMetadata tool = getTool(uid);
|
||||
|
||||
if (tool != null && tool.getPodUrl() != null) {
|
||||
return List.of(tool.getPodUrl());
|
||||
}
|
||||
|
||||
return List.of();
|
||||
}
|
||||
/**
|
||||
* 실행 시 툴 정보 조회 (Tool Execution 시 참조)
|
||||
*/
|
||||
public ToolMetadata getTool(String uid) {
|
||||
return redisTemplate.opsForValue().get(KEY_PREFIX + uid);
|
||||
}
|
||||
|
||||
/**
|
||||
* 툴 이름으로 정보 조회
|
||||
*/
|
||||
public ToolMetadata getToolByName(String name) {
|
||||
List<ToolMetadata> allTools = getAllTools();
|
||||
for (ToolMetadata tool : allTools) {
|
||||
if (name.equals(tool.getName())) {
|
||||
return tool;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 등록된 모든 활성 툴 목록 조회
|
||||
*/
|
||||
public List<ToolMetadata> getAllTools() {
|
||||
Set<String> keys = redisTemplate.keys(KEY_PREFIX + "*");
|
||||
if (keys == null || keys.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
List<ToolMetadata> tools = redisTemplate.opsForValue().multiGet(keys);
|
||||
return tools != null ? tools.stream().filter(Objects::nonNull).collect(Collectors.toList()) : List.of();
|
||||
}
|
||||
|
||||
/**
|
||||
* 툴 명시적 제거 (Deregister)
|
||||
*/
|
||||
public void removeTool(String uid) {
|
||||
redisTemplate.delete(KEY_PREFIX + uid);
|
||||
log.info(" [RedisRegistry] 툴 삭제 완료: {}", uid);
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ import io.shinhanlife.dap.mcg.audit.AuditLogService;
|
||||
import io.shinhanlife.dap.mcg.resilience.CircuitBreaker;
|
||||
import io.shinhanlife.dap.mcg.resilience.CircuitBreakerService;
|
||||
import io.shinhanlife.dap.mcg.security.ToolAuthorizationService;
|
||||
import io.shinhanlife.dap.mcg.redis.RedisToolTraceService;
|
||||
import io.shinhanlife.dap.mcg.trace.InMemoryToolTraceService;
|
||||
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import java.util.Map;
|
||||
@@ -62,7 +62,7 @@ public class ExecuteService {
|
||||
private final AuditLogService auditLogService;
|
||||
private final CircuitBreakerService circuitBreakerService;
|
||||
private final ToolAuthorizationService authorizationService;
|
||||
private final RedisToolTraceService redisTrace;
|
||||
private final InMemoryToolTraceService traceService;
|
||||
private final McpGatewayProperties properties;
|
||||
private final LargeToolResponseService largeResponses;
|
||||
private final PaginationRequestValidator paginationValidator;
|
||||
@@ -79,7 +79,7 @@ public class ExecuteService {
|
||||
AuditLogService auditLogService,
|
||||
CircuitBreakerService circuitBreakerService,
|
||||
ToolAuthorizationService authorizationService,
|
||||
RedisToolTraceService redisTrace,
|
||||
InMemoryToolTraceService traceService,
|
||||
McpGatewayProperties properties,
|
||||
LargeToolResponseService largeResponses,
|
||||
PaginationRequestValidator paginationValidator,
|
||||
@@ -94,7 +94,7 @@ public class ExecuteService {
|
||||
this.auditLogService = auditLogService;
|
||||
this.circuitBreakerService = circuitBreakerService;
|
||||
this.authorizationService = authorizationService;
|
||||
this.redisTrace = redisTrace;
|
||||
this.traceService = traceService;
|
||||
this.properties = properties;
|
||||
this.largeResponses = largeResponses;
|
||||
this.paginationValidator = paginationValidator;
|
||||
@@ -133,7 +133,7 @@ public class ExecuteService {
|
||||
|
||||
long startedAt = System.nanoTime();
|
||||
auditLogService.toolStarted(context, toolName, argumentsNode);
|
||||
redisTrace.started(context, metadata, argumentsNode);
|
||||
traceService.started(context, metadata, argumentsNode);
|
||||
|
||||
try {
|
||||
authorizationService.authorize(context, metadata);
|
||||
@@ -174,13 +174,13 @@ public class ExecuteService {
|
||||
finalResult.put("original_size", originalSize);
|
||||
|
||||
auditLogService.toolFinished(context, metadata.getName(), elapsedMillis, true, "");
|
||||
redisTrace.finished(context, metadata, argumentsNode, elapsedMillis, true, "", responseText);
|
||||
traceService.finished(context, metadata, argumentsNode, elapsedMillis, true, "", responseText);
|
||||
|
||||
return finalResult;
|
||||
} catch (ToolExecutionException error) {
|
||||
long elapsedMillis = elapsedMillis(startedAt);
|
||||
auditLogService.toolFinished(context, toolName, elapsedMillis, false, error.failureType().name());
|
||||
redisTrace.finished(context, metadata, argumentsNode, elapsedMillis, false, error.failureType().name(), "");
|
||||
traceService.finished(context, metadata, argumentsNode, elapsedMillis, false, error.failureType().name(), "");
|
||||
|
||||
Map<String, Object> errorResult = new java.util.LinkedHashMap<>();
|
||||
errorResult.put("status", "error");
|
||||
@@ -194,7 +194,7 @@ public class ExecuteService {
|
||||
} catch (Exception error) {
|
||||
long elapsedMillis = elapsedMillis(startedAt);
|
||||
auditLogService.toolFinished(context, toolName, elapsedMillis, false, FailureType.INTERNAL_ERROR.name());
|
||||
redisTrace.finished(context, metadata, argumentsNode, elapsedMillis, false, FailureType.INTERNAL_ERROR.name(), "");
|
||||
traceService.finished(context, metadata, argumentsNode, elapsedMillis, false, FailureType.INTERNAL_ERROR.name(), "");
|
||||
|
||||
Map<String, Object> errorResult = new java.util.LinkedHashMap<>();
|
||||
errorResult.put("status", "error");
|
||||
@@ -225,7 +225,7 @@ public class ExecuteService {
|
||||
ToolExecutionException lastError = null;
|
||||
for (int attempt = 1; attempt <= retryPolicy.maxAttempts(); attempt++) {
|
||||
try {
|
||||
redisTrace.attemptStarted(context, metadata, arguments, attempt, retryPolicy.maxAttempts());
|
||||
traceService.attemptStarted(context, metadata, arguments, attempt, retryPolicy.maxAttempts());
|
||||
circuitBreaker.beforeCall();
|
||||
Object result = executeOnce(context, metadata, arguments, payload);
|
||||
circuitBreaker.recordSuccess();
|
||||
@@ -237,7 +237,7 @@ public class ExecuteService {
|
||||
throw error;
|
||||
}
|
||||
long backoffMillis = retryPolicy.backoffMillis(attempt);
|
||||
redisTrace.retryWaiting(context, metadata, arguments, attempt, retryPolicy.maxAttempts(), backoffMillis,
|
||||
traceService.retryWaiting(context, metadata, arguments, attempt, retryPolicy.maxAttempts(), backoffMillis,
|
||||
error.failureType().name());
|
||||
sleepBeforeRetry(metadata.getName(), attempt, backoffMillis, error.failureType());
|
||||
}
|
||||
|
||||
@@ -2,34 +2,29 @@ package io.shinhanlife.dap.mcg.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.mcg.service
|
||||
* @className KillSwitchService
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @description AX HUB 시스템 킬 스위치 서비스 (인메모리)
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class KillSwitchService {
|
||||
|
||||
// 기본 제공되는 StringRedisTemplate 사용
|
||||
private final StringRedisTemplate stringRedisTemplate;
|
||||
// 인메모리 맵으로 변경
|
||||
private final Map<String, String> killSwitchMap = new ConcurrentHashMap<>();
|
||||
|
||||
public void checkAgent(String tenantId) {
|
||||
String key = "mcp:kill:agent:" + tenantId;
|
||||
if ("true".equalsIgnoreCase(stringRedisTemplate.opsForValue().get(key))) {
|
||||
if ("true".equalsIgnoreCase(killSwitchMap.get(key))) {
|
||||
log.warn(" [KillSwitch] 차단된 에이전트 접근 시도: {}", tenantId);
|
||||
throw new SecurityException(" 비상 차단: 해당 테넌트(" + tenantId + ")의 접근이 관리자에 의해 차단되었습니다.");
|
||||
}
|
||||
@@ -39,7 +34,7 @@ public class KillSwitchService {
|
||||
if (toolName == null || toolName.isBlank()) return;
|
||||
|
||||
String key = "mcp:kill:tool:" + toolName;
|
||||
if ("true".equalsIgnoreCase(stringRedisTemplate.opsForValue().get(key))) {
|
||||
if ("true".equalsIgnoreCase(killSwitchMap.get(key))) {
|
||||
log.warn(" [KillSwitch] 차단된 툴 실행 시도: {}", toolName);
|
||||
throw new SecurityException(" 비상 차단: 해당 툴(" + toolName + ")의 실행이 관리자에 의해 차단되었습니다.");
|
||||
}
|
||||
@@ -49,7 +44,7 @@ public class KillSwitchService {
|
||||
if (integrationType == null || integrationType.isBlank()) return;
|
||||
|
||||
String key = "mcp:kill:route:" + integrationType;
|
||||
if ("true".equalsIgnoreCase(stringRedisTemplate.opsForValue().get(key))) {
|
||||
if ("true".equalsIgnoreCase(killSwitchMap.get(key))) {
|
||||
log.warn(" [KillSwitch] 차단된 라우트 통신 시도: {}", integrationType);
|
||||
throw new SecurityException(" 비상 차단: 해당 레거시 라우트(" + integrationType + ")로의 통신이 관리자에 의해 차단되었습니다.");
|
||||
}
|
||||
@@ -58,14 +53,14 @@ public class KillSwitchService {
|
||||
// 관리자 API용 토글 메서드
|
||||
public void toggleKillSwitch(String type, String target, boolean state) {
|
||||
String key = "mcp:kill:" + type + ":" + target;
|
||||
stringRedisTemplate.opsForValue().set(key, String.valueOf(state));
|
||||
killSwitchMap.put(key, String.valueOf(state));
|
||||
log.info(" [KillSwitch] 상태 변경: {} -> {} (차단 상태: {})", type, target, state);
|
||||
}
|
||||
|
||||
// 관리자 API용 삭제 메서드 (Redis 키 자체를 완전 삭제)
|
||||
// 관리자 API용 삭제 메서드 (맵 항목 삭제)
|
||||
public void removeKillSwitch(String type, String target) {
|
||||
String key = "mcp:kill:" + type + ":" + target;
|
||||
stringRedisTemplate.delete(key);
|
||||
killSwitchMap.remove(key);
|
||||
log.info(" [KillSwitch] 차단 키 완전 삭제: {} -> {}", type, target);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package io.shinhanlife.dap.mcg.service;
|
||||
|
||||
import io.shinhanlife.dap.lib.mcp.security.SecurityProperties;
|
||||
import io.shinhanlife.dap.mcg.registry.RedisRegistryService;
|
||||
import io.shinhanlife.dap.mcg.registry.InMemoryRegistryService;
|
||||
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
|
||||
import io.shinhanlife.dap.mcg.config.GatewayFallbackProperties;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -14,14 +14,14 @@ import java.util.Map;
|
||||
/**
|
||||
* @package io.shinhanlife.dap.mcg.service
|
||||
* @className ToolPlanner
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @description AX HUB ?<3F>스??처리 ?<3F>래??
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- 개정?<3F>력 ----------
|
||||
* ?<3F>정?? ?<3F>정?? ?<3F>정?<3F>용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
* 2026.09.01 0986406 최초?<3F>성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@@ -30,30 +30,30 @@ import java.util.Map;
|
||||
@RequiredArgsConstructor
|
||||
public class ToolPlanner {
|
||||
|
||||
private final RedisRegistryService redisRegistryService;
|
||||
private final InMemoryRegistryService InMemoryRegistryService;
|
||||
private final SecurityProperties securityProperties;
|
||||
private final GatewayFallbackProperties fallbackProperties;
|
||||
|
||||
/**
|
||||
* 요청(payload)을 분석하여 실행해야 할 Tool의 계획을 생성합니다.
|
||||
* ?<3F>청(payload)??분석?<3F>여 ?<3F>행?<3F>야 ??Tool??계획???<3F>성?<3F>니??
|
||||
*/
|
||||
public Object createPlan(Map<String, Object> payload, String tenantId) {
|
||||
log.info(" [Planner] 요청 분석 및 실행 계획 수립 시작");
|
||||
log.info(" [Planner] ?<3F>청 분석 <EFBFBD>??<3F>행 계획 ?<3F>립 ?<3F>작");
|
||||
|
||||
// 1. 요청에서 호출하려는 툴 이름 추출 (JSON-RPC params.name)
|
||||
// 1. ?<3F>청?<3F>서 ?<3F>출?<3F>려?????<3F>름 추출 (JSON-RPC params.name)
|
||||
Map<String, Object> params = (Map<String, Object>) payload.get("params");
|
||||
String toolName = params != null ? (String) params.get("name") : null;
|
||||
|
||||
if (toolName == null || toolName.isEmpty()) {
|
||||
throw new IllegalArgumentException("요청에 toolName이 포함되어 있지 않습니다.");
|
||||
throw new IllegalArgumentException("?<3F>청??toolName???<3F>함?<3F>어 ?<3F><>? ?<3F>습?<3F>다.");
|
||||
}
|
||||
|
||||
// 2. RedisRegistry에서 해당 툴의 메타데이터 조회
|
||||
// (실제로는 이 메타데이터가 실행 계획의 핵심이 됩니다)
|
||||
var toolMetadata = redisRegistryService.getToolByName(toolName);
|
||||
// 2. RedisRegistry?<3F>서 ?<3F>당 ?<3F>의 메<>??<3F>이??조회
|
||||
// (?<3F>제로는 ??메<>??<3F>이?<3F><>? ?<3F>행 계획???<3F>심???<3F>니??
|
||||
var toolMetadata = InMemoryRegistryService.getToolByName(toolName);
|
||||
|
||||
if (toolMetadata == null) {
|
||||
log.warn(" [Planner] 등록되지 않은 툴 요청: {}. Fallback 라우팅 규칙을 확인합니다.", toolName);
|
||||
log.warn(" [Planner] ?<3F>록?<3F><>? ?<3F><>? ???<3F>청: {}. Fallback ?<3F>우??규칙???<3F>인?<3F>니??", toolName);
|
||||
|
||||
String fallbackPodUrl = fallbackProperties.getDefaultUrl();
|
||||
if (fallbackProperties.getRoutes() != null) {
|
||||
@@ -66,11 +66,11 @@ public class ToolPlanner {
|
||||
}
|
||||
|
||||
if (fallbackPodUrl == null || fallbackPodUrl.isEmpty()) {
|
||||
log.error(" [Planner] Fallback 라우팅 대상이 아닙니다. 툴: {}", toolName);
|
||||
throw new RuntimeException("해당 툴(" + toolName + ")이 레지스트리에 존재하지 않습니다.");
|
||||
log.error(" [Planner] Fallback ?<3F>우???<3F>?<3F>이 ?<3F>닙?<3F>다. ?? {}", toolName);
|
||||
throw new RuntimeException("?<3F>당 ??" + toolName + ")???<3F><>??<3F>트리에 존재?<3F><>? ?<3F>습?<3F>다.");
|
||||
}
|
||||
|
||||
log.info(" [Planner] Fallback 라우팅 매칭됨: {} -> {}", toolName, fallbackPodUrl);
|
||||
log.info(" [Planner] Fallback ?<3F>우??매칭?? {} -> {}", toolName, fallbackPodUrl);
|
||||
|
||||
toolMetadata = ToolMetadata.builder()
|
||||
.uid(toolName)
|
||||
@@ -80,26 +80,26 @@ public class ToolPlanner {
|
||||
.build();
|
||||
}
|
||||
|
||||
// 2-1. [신규] 도메인 그룹핑 기반 권한 검증
|
||||
// 2-1. [?<3F>규] ?<3F>메??그룹??기반 권한 검<EFBFBD>?
|
||||
if (tenantId != null && !tenantId.equalsIgnoreCase("system") && toolMetadata.getCategoryKey() != null) {
|
||||
String normalizedTenantId = tenantId.toLowerCase();
|
||||
List<String> allowedDomains = securityProperties.getTenantDomains().get(normalizedTenantId);
|
||||
|
||||
// 만약 대소문자 변환 후에도 없으면 원래 값으로 한 번 더 시도 (하위 호환성)
|
||||
// 만약 ?<3F>?<3F>문??변???<3F>에???<3F>으<EFBFBD>??<3F>래 값으<EAB092>???<3F>????<3F>도 (?<3F>위 ?<3F>환??
|
||||
if (allowedDomains == null) {
|
||||
allowedDomains = securityProperties.getTenantDomains().get(tenantId);
|
||||
}
|
||||
|
||||
if (allowedDomains == null ||
|
||||
(!allowedDomains.contains("ALL") && !allowedDomains.contains(toolMetadata.getCategoryKey()))) {
|
||||
log.warn(" [Planner] 권한 거부 - Tenant: {}, Request Domain: {}", tenantId, toolMetadata.getCategoryKey());
|
||||
throw new SecurityException("해당 도메인(" + toolMetadata.getCategoryKey() + ")의 툴을 실행할 권한이 없습니다.");
|
||||
log.warn(" [Planner] 권한 거<EFBFBD>? - Tenant: {}, Request Domain: {}", tenantId, toolMetadata.getCategoryKey());
|
||||
throw new SecurityException("?<3F>당 ?<3F>메??" + toolMetadata.getCategoryKey() + ")???<3F>을 ?<3F>행??권한???<3F>습?<3F>다.");
|
||||
}
|
||||
}
|
||||
|
||||
log.info(" [Planner] 툴 '{}'에 대한 실행 계획 수립 완료", toolName);
|
||||
log.info(" [Planner] ??'{}'???<3F>???<3F>행 계획 ?<3F>립 ?<3F>료", toolName);
|
||||
|
||||
// 3. 수립된 계획(툴 메타데이터) 반환
|
||||
// 3. ?<3F>립??계획(??메<>??<3F>이?? 반환
|
||||
return toolMetadata;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package io.shinhanlife.dap.mcg.sync;
|
||||
|
||||
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
|
||||
import io.shinhanlife.dap.mcg.registry.RedisRegistryService;
|
||||
import io.shinhanlife.dap.mcg.registry.InMemoryRegistryService;
|
||||
import io.modelcontextprotocol.server.McpSyncServer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -38,18 +38,18 @@ public class RegistryMcpToolSynchronizer {
|
||||
|
||||
private final ObjectProvider<McpSyncServer> mcpServerProvider;
|
||||
private final DynamicMcpServerManager dynamicMcpServerManager;
|
||||
private final RedisRegistryService redisRegistryService;
|
||||
private final InMemoryRegistryService InMemoryRegistryService;
|
||||
private final RegistryMcpToolSpecificationFactory specificationFactory;
|
||||
private final Set<String> managedToolNames = new LinkedHashSet<>();
|
||||
private final ReentrantLock lock = new ReentrantLock();
|
||||
|
||||
public RegistryMcpToolSynchronizer(ObjectProvider<McpSyncServer> mcpServerProvider,
|
||||
DynamicMcpServerManager dynamicMcpServerManager,
|
||||
RedisRegistryService redisRegistryService,
|
||||
InMemoryRegistryService InMemoryRegistryService,
|
||||
RegistryMcpToolSpecificationFactory specificationFactory) {
|
||||
this.mcpServerProvider = mcpServerProvider;
|
||||
this.dynamicMcpServerManager = dynamicMcpServerManager;
|
||||
this.redisRegistryService = redisRegistryService;
|
||||
this.InMemoryRegistryService = InMemoryRegistryService;
|
||||
this.specificationFactory = specificationFactory;
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ public class RegistryMcpToolSynchronizer {
|
||||
public void synchronize() {
|
||||
lock.lock();
|
||||
try {
|
||||
List<ToolMetadata> activeEntries = redisRegistryService.getAllTools()
|
||||
List<ToolMetadata> activeEntries = InMemoryRegistryService.getAllTools()
|
||||
.stream().filter(ToolMetadata::getVisible).collect(Collectors.toList());
|
||||
|
||||
// 1. Dynamic MCP Server 동기화 (카테고리별)
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
package io.shinhanlife.dap.mcg.trace;
|
||||
|
||||
import io.shinhanlife.dap.mcg.config.McpGatewayProperties;
|
||||
import io.shinhanlife.dap.mcg.security.McpRequestContext;
|
||||
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HexFormat;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.mcg.trace
|
||||
* @className InMemoryToolTraceService
|
||||
* @description AX HUB 시스템 인메모리 툴 트레이스 서비스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
*/
|
||||
@Service
|
||||
public class InMemoryToolTraceService {
|
||||
private static final Logger log = LoggerFactory.getLogger(InMemoryToolTraceService.class);
|
||||
|
||||
private final McpGatewayProperties properties;
|
||||
private final ObjectMapper json;
|
||||
private final McpMonitorEventService monitorEvents;
|
||||
private final Map<String, AttemptState> attemptStates = new ConcurrentHashMap<>();
|
||||
|
||||
// LRU Cache for recent traces
|
||||
private final Map<String, String> recentTraces;
|
||||
|
||||
public InMemoryToolTraceService(McpGatewayProperties properties,
|
||||
ObjectMapper json,
|
||||
McpMonitorEventService monitorEvents) {
|
||||
this.properties = properties;
|
||||
this.json = json;
|
||||
this.monitorEvents = monitorEvents;
|
||||
|
||||
final int maxSize = (int) properties.traceRetentionSize();
|
||||
this.recentTraces = new LinkedHashMap<String, String>(maxSize, 0.75f, true) {
|
||||
@Override
|
||||
protected boolean removeEldestEntry(Map.Entry<String, String> eldest) {
|
||||
return size() > maxSize;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public void started(McpRequestContext context, ToolMetadata metadata, ObjectNode arguments) {
|
||||
save(context, metadata, arguments, "STARTED", 0, "", 0, 0, 0,
|
||||
"Agent request entered MCP", "");
|
||||
}
|
||||
|
||||
public void attemptStarted(McpRequestContext context, ToolMetadata metadata, ObjectNode arguments,
|
||||
int attempt, int maxAttempts) {
|
||||
save(context, metadata, arguments, "ATTEMPTING", 0, "", attempt, maxAttempts, 0,
|
||||
"Calling tool server", "");
|
||||
}
|
||||
|
||||
public void retryWaiting(McpRequestContext context, ToolMetadata metadata, ObjectNode arguments,
|
||||
int failedAttempt, int maxAttempts, long backoffMillis, String failureType) {
|
||||
save(context, metadata, arguments, "RETRY_WAITING", 0, failureType, failedAttempt, maxAttempts, backoffMillis,
|
||||
"Retry will be attempted after backoff", "");
|
||||
}
|
||||
|
||||
public void finished(McpRequestContext context, ToolMetadata metadata, ObjectNode arguments,
|
||||
long elapsedMillis, boolean success, String failureType, String responseText) {
|
||||
save(context, metadata, arguments, success ? "SUCCESS" : "FAILED", elapsedMillis, failureType, 0, 0, 0,
|
||||
success ? "Tool call completed" : "Tool call failed", responseText);
|
||||
}
|
||||
|
||||
public List<String> getRecentTraces(int limit) {
|
||||
if (!properties.traceEnabled()) {
|
||||
return List.of("Trace is disabled. Set traceEnabled=true.");
|
||||
}
|
||||
|
||||
List<String> result = new ArrayList<>();
|
||||
synchronized (recentTraces) {
|
||||
List<String> values = new ArrayList<>(recentTraces.values());
|
||||
// reverse to get latest first
|
||||
for (int i = values.size() - 1; i >= 0 && result.size() < limit; i--) {
|
||||
result.add(values.get(i));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public String getTraceDetail(String requestId) {
|
||||
if (!properties.traceEnabled()) {
|
||||
return "Trace is disabled.";
|
||||
}
|
||||
synchronized (recentTraces) {
|
||||
return recentTraces.get(requestId);
|
||||
}
|
||||
}
|
||||
|
||||
private void save(McpRequestContext context, ToolMetadata metadata, ObjectNode arguments,
|
||||
String state, long elapsedMillis, String failureType, int attempt, int maxAttempts, long backoffMillis,
|
||||
String message, String responseText) {
|
||||
|
||||
if (!properties.traceEnabled()) {
|
||||
return; // trace is off
|
||||
}
|
||||
|
||||
try {
|
||||
AttemptState ast = attemptStates.computeIfAbsent(context.requestId(), k -> new AttemptState(0));
|
||||
if (attempt > 0) ast.currentAttempt = attempt;
|
||||
|
||||
ObjectNode root = json.createObjectNode();
|
||||
root.put("requestId", context.requestId());
|
||||
root.put("agentId", context.agentId());
|
||||
root.put("tenantId", context.traceGroupId());
|
||||
root.put("toolName", metadata.getName());
|
||||
root.put("targetPod", metadata.getPodUrl());
|
||||
root.put("state", state);
|
||||
root.put("timestamp", Instant.now().toString());
|
||||
root.put("elapsedMillis", elapsedMillis);
|
||||
root.put("attempt", ast.currentAttempt);
|
||||
root.put("maxAttempts", maxAttempts > 0 ? maxAttempts : properties.retryMaxAttempts());
|
||||
root.put("backoffMillis", backoffMillis);
|
||||
|
||||
if (failureType != null && !failureType.isEmpty()) {
|
||||
root.put("failureType", failureType);
|
||||
}
|
||||
if (message != null && !message.isEmpty()) {
|
||||
root.put("message", message);
|
||||
}
|
||||
|
||||
// Mask arguments if needed, for simplicity we just put them
|
||||
root.set("arguments", arguments);
|
||||
|
||||
if (responseText != null && !responseText.isEmpty()) {
|
||||
if (responseText.length() > 500) {
|
||||
root.put("responsePreview", responseText.substring(0, 500) + "...");
|
||||
} else {
|
||||
root.put("responsePreview", responseText);
|
||||
}
|
||||
}
|
||||
|
||||
String payload = json.writeValueAsString(root);
|
||||
|
||||
synchronized (recentTraces) {
|
||||
recentTraces.put(context.requestId(), payload);
|
||||
}
|
||||
|
||||
monitorEvents.publish(payload);
|
||||
|
||||
if ("SUCCESS".equals(state) || "FAILED".equals(state)) {
|
||||
attemptStates.remove(context.requestId());
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to save trace: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static class AttemptState {
|
||||
int currentAttempt;
|
||||
AttemptState(int currentAttempt) {
|
||||
this.currentAttempt = currentAttempt;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
package io.shinhanlife.dap.mcg.redis;
|
||||
package io.shinhanlife.dap.mcg.trace;
|
||||
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.mcg.redis
|
||||
* @package io.shinhanlife.dap.mcg.trace
|
||||
* @className McpMonitorEventService
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
Reference in New Issue
Block a user