Phase 5 Batch 2: 4 RM Tool + rm 客户端 + 日志降级链
- internal/rm/client.go: YARN RM 客户端 - ListApps / GetApp / KillApp / GetLogs - GetLogs 降级链数据驱动 (amContainerLogs → aggregated-logs → logs) - 跨主机 redirect 保留 Authorization (URLAllowlist 兜底) - 4 Tool: list_applications / get_application_status / get_application_logs / kill_application - mcp.WithEnum(state) 约束 YARN app 状态 - tool handler 永远 (result, nil), 业务错误用 NewToolResultError - deps.go: +HTTPClient + MaxResponseBytes - main.go: 注入 httpclient.Client - 端到端实测: mock RM 验证 4 Tool + 降级链 + 错误路径 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
|
||||
"spark-mcp-go/internal/rm"
|
||||
)
|
||||
|
||||
const GetApplicationLogsName = "get_application_logs"
|
||||
|
||||
const defaultTailBytes = 1 << 20
|
||||
|
||||
// NewGetApplicationLogsTool returns the schema for the get_application_logs MCP Tool.
|
||||
func NewGetApplicationLogsTool() mcp.Tool {
|
||||
return mcp.NewTool(GetApplicationLogsName,
|
||||
mcp.WithDescription("Fetch YARN application logs. Tries amContainerLogs first, then aggregated-logs, then the legacy /logs endpoint. Returns {source, text}."),
|
||||
mcp.WithString("cluster_id",
|
||||
mcp.Required(),
|
||||
mcp.Description("ID of the configured cluster (from list_clusters)"),
|
||||
),
|
||||
mcp.WithString("app_id",
|
||||
mcp.Required(),
|
||||
mcp.Description("YARN application ID, e.g. application_1234567890_0001"),
|
||||
),
|
||||
mcp.WithString("container",
|
||||
mcp.Description("Container ID used for the aggregated-logs fallback, e.g. container_1234567890_0001_01_000001."),
|
||||
),
|
||||
mcp.WithNumber("tail_bytes",
|
||||
mcp.Description("Maximum bytes to return. 0 means use the server-wide response byte limit."),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// GetApplicationLogsHandler retrieves logs using the RM fallback chain.
|
||||
func (d *Deps) GetApplicationLogsHandler(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
clusterID, err := req.RequireString("cluster_id")
|
||||
if err != nil {
|
||||
return errResult("get_application_logs: " + err.Error()), nil
|
||||
}
|
||||
appID, err := req.RequireString("app_id")
|
||||
if err != nil {
|
||||
return errResult("get_application_logs: " + err.Error()), nil
|
||||
}
|
||||
|
||||
args := req.GetArguments()
|
||||
container := ""
|
||||
if v, ok := args["container"].(string); ok {
|
||||
container = v
|
||||
}
|
||||
|
||||
tailBytes := defaultTailBytes
|
||||
if v, ok := args["tail_bytes"].(float64); ok {
|
||||
tailBytes = int(v)
|
||||
}
|
||||
if tailBytes == 0 {
|
||||
tailBytes = int(d.MaxResponseBytes)
|
||||
}
|
||||
|
||||
callLog := startToolCall(ctx, d.Logger, GetApplicationLogsName, map[string]any{
|
||||
"cluster_id": clusterID,
|
||||
"app_id": appID,
|
||||
"container": container,
|
||||
"tail_bytes": tailBytes,
|
||||
})
|
||||
defer callLog.End()
|
||||
|
||||
cl, err := d.ClusterRepo.Get(ctx, clusterID)
|
||||
if err != nil {
|
||||
callLog.WithError(err)
|
||||
return errResult("get_application_logs: cluster " + clusterID + ": " + err.Error()), nil
|
||||
}
|
||||
|
||||
rmc := rm.New(d.HTTPClient, cl)
|
||||
body, source, err := rmc.GetLogs(ctx, appID, container)
|
||||
if err != nil {
|
||||
callLog.WithError(err)
|
||||
return errResult("get_application_logs: " + err.Error()), nil
|
||||
}
|
||||
|
||||
text := string(body)
|
||||
if len(text) > tailBytes {
|
||||
text = truncateMiddle(text, tailBytes)
|
||||
}
|
||||
|
||||
result := map[string]any{
|
||||
"source": source,
|
||||
"text": text,
|
||||
}
|
||||
callLog.WithResult(map[string]any{"source": source, "bytes": len(body), "returned_bytes": len(text)})
|
||||
return textResult(encodeJSON(result)), nil
|
||||
}
|
||||
Reference in New Issue
Block a user