Files
MyGoNavi/internal/app/methods_jvm_diagnostic.go
Syngnat 6f14e827ab feat(jvm): 完成资源治理与诊断增强
- 新增 JMX/Endpoint/Agent 三种 JVM 连接模式与配置归一化链路
- 支持资源浏览、变更预览、写入应用、审计记录与只读约束
- 接入 AI 结构化写入计划与诊断计划回填能力
- 新增 Agent Bridge、Arthas Tunnel、JMX Helper 诊断传输实现
- 增加诊断控制台、命令模板、输出历史与自动补全交互
- 补齐前后端契约、运行夹具与 JVM 相关回归测试
2026-04-24 16:45:34 +08:00

295 lines
8.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package app
import (
"errors"
"fmt"
"path/filepath"
"strings"
"time"
"GoNavi-Wails/internal/connection"
"GoNavi-Wails/internal/jvm"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
var newJVMDiagnosticTransport = jvm.NewDiagnosticTransport
const diagnosticChunkEvent = "jvm:diagnostic:chunk"
type diagnosticChunkEventPayload struct {
TabID string `json:"tabId"`
Chunk jvm.DiagnosticEventChunk `json:"chunk"`
}
func swapJVMDiagnosticTransportFactory(factory func(mode string) (jvm.DiagnosticTransport, error)) func() {
prev := newJVMDiagnosticTransport
newJVMDiagnosticTransport = factory
return func() { newJVMDiagnosticTransport = prev }
}
func resolveJVMDiagnosticTransport(cfg connection.ConnectionConfig) (connection.ConnectionConfig, jvm.DiagnosticTransport, error) {
normalized, err := jvm.NormalizeConnectionConfig(cfg)
if err != nil {
return connection.ConnectionConfig{}, nil, err
}
diagCfg, err := jvm.NormalizeDiagnosticConfig(normalized)
if err != nil {
return connection.ConnectionConfig{}, nil, err
}
if !diagCfg.Enabled {
return connection.ConnectionConfig{}, nil, errors.New("当前连接未启用 JVM 诊断增强模式")
}
normalized.JVM.Diagnostic = diagCfg
transport, err := newJVMDiagnosticTransport(diagCfg.Transport)
if err != nil {
return connection.ConnectionConfig{}, nil, err
}
return normalized, transport, nil
}
func (a *App) JVMProbeDiagnosticCapabilities(cfg connection.ConnectionConfig) connection.QueryResult {
normalized, transport, err := resolveJVMDiagnosticTransport(cfg)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
items, err := transport.ProbeCapabilities(a.ctx, normalized)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
return connection.QueryResult{Success: true, Data: items}
}
func (a *App) JVMStartDiagnosticSession(cfg connection.ConnectionConfig, req jvm.DiagnosticSessionRequest) connection.QueryResult {
normalized, transport, err := resolveJVMDiagnosticTransport(cfg)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
handle, err := transport.StartSession(a.ctx, normalized, req)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
return connection.QueryResult{Success: true, Data: handle}
}
func (a *App) JVMExecuteDiagnosticCommand(cfg connection.ConnectionConfig, tabID string, req jvm.DiagnosticCommandRequest) connection.QueryResult {
normalized, transport, err := resolveJVMDiagnosticTransport(cfg)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
req.SessionID = strings.TrimSpace(req.SessionID)
req.CommandID = strings.TrimSpace(req.CommandID)
req.Command = strings.TrimSpace(req.Command)
req.Source = strings.TrimSpace(req.Source)
req.Reason = strings.TrimSpace(req.Reason)
if req.SessionID == "" {
return connection.QueryResult{Success: false, Message: "诊断会话 ID 不能为空,请先创建会话"}
}
if req.Command == "" {
return connection.QueryResult{Success: false, Message: "诊断命令不能为空"}
}
if req.CommandID == "" {
req.CommandID = fmt.Sprintf("diag-%d", time.Now().UnixNano())
}
if req.Source == "" {
req.Source = "manual"
}
commandType, err := jvm.ValidateDiagnosticCommandPolicy(normalized.JVM.Diagnostic, req.Command)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
riskLevel := diagnosticRiskLevel(commandType)
auditStore := jvm.NewDiagnosticAuditStore(filepath.Join(a.auditRootDir(), "jvm_diag_audit.jsonl"))
var auditWarnings []string
if err := auditStore.Append(jvm.DiagnosticAuditRecord{
ConnectionID: normalized.ID,
SessionID: req.SessionID,
CommandID: req.CommandID,
Transport: normalized.JVM.Diagnostic.Transport,
Command: req.Command,
CommandType: commandType,
Source: req.Source,
Reason: req.Reason,
RiskLevel: riskLevel,
Status: "running",
}); err != nil {
auditWarnings = append(auditWarnings, "审计记录写入失败: "+err.Error())
}
terminalSeen := false
appendTerminalAudit := func(status string) {
if terminalSeen {
return
}
terminalSeen = true
if err := auditStore.Append(jvm.DiagnosticAuditRecord{
ConnectionID: normalized.ID,
SessionID: req.SessionID,
CommandID: req.CommandID,
Transport: normalized.JVM.Diagnostic.Transport,
Command: req.Command,
CommandType: commandType,
Source: req.Source,
Reason: req.Reason,
RiskLevel: riskLevel,
Status: status,
}); err != nil {
auditWarnings = append(auditWarnings, "审计记录写入失败: "+err.Error())
}
}
if binder, ok := transport.(interface{ SetEventSink(jvm.DiagnosticEventSink) }); ok {
binder.SetEventSink(func(chunk jvm.DiagnosticEventChunk) {
if chunk.Timestamp == 0 {
chunk.Timestamp = time.Now().UnixMilli()
}
if strings.TrimSpace(chunk.SessionID) == "" {
chunk.SessionID = req.SessionID
}
if strings.TrimSpace(chunk.CommandID) == "" {
chunk.CommandID = req.CommandID
}
a.emitDiagnosticChunk(tabID, chunk)
if isDiagnosticTerminalPhase(chunk.Phase) {
appendTerminalAudit(chunk.Phase)
}
})
}
if err := transport.ExecuteCommand(a.ctx, normalized, req); err != nil {
phase := "failed"
if strings.Contains(strings.ToLower(err.Error()), "canceled") {
phase = "canceled"
}
if !terminalSeen {
chunk := jvm.DiagnosticEventChunk{
SessionID: req.SessionID,
CommandID: req.CommandID,
Event: "diagnostic",
Phase: phase,
Content: err.Error(),
Timestamp: time.Now().UnixMilli(),
}
a.emitDiagnosticChunk(tabID, chunk)
appendTerminalAudit(phase)
}
return connection.QueryResult{Success: false, Message: joinDiagnosticMessages(err.Error(), auditWarnings)}
}
if !terminalSeen {
chunk := jvm.DiagnosticEventChunk{
SessionID: req.SessionID,
CommandID: req.CommandID,
Event: "diagnostic",
Phase: "completed",
Content: "诊断命令执行完成",
Timestamp: time.Now().UnixMilli(),
}
a.emitDiagnosticChunk(tabID, chunk)
appendTerminalAudit("completed")
}
return connection.QueryResult{
Success: true,
Message: joinDiagnosticMessages("", auditWarnings),
Data: map[string]any{
"sessionId": req.SessionID,
"commandId": req.CommandID,
"status": "accepted",
},
}
}
func (a *App) JVMCancelDiagnosticCommand(cfg connection.ConnectionConfig, tabID string, sessionID string, commandID string) connection.QueryResult {
normalized, transport, err := resolveJVMDiagnosticTransport(cfg)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
sessionID = strings.TrimSpace(sessionID)
commandID = strings.TrimSpace(commandID)
if sessionID == "" || commandID == "" {
return connection.QueryResult{Success: false, Message: "取消命令缺少 sessionId 或 commandId"}
}
if err := transport.CancelCommand(a.ctx, normalized, sessionID, commandID); err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
a.emitDiagnosticChunk(tabID, jvm.DiagnosticEventChunk{
SessionID: sessionID,
CommandID: commandID,
Event: "diagnostic",
Phase: "canceling",
Content: "已发送取消请求,等待诊断桥接端结束命令",
Timestamp: time.Now().UnixMilli(),
})
return connection.QueryResult{
Success: true,
Data: map[string]any{
"sessionId": sessionID,
"commandId": commandID,
"status": "cancel-requested",
},
}
}
func (a *App) JVMListDiagnosticAuditRecords(connectionID string, limit int) connection.QueryResult {
records, err := jvm.NewDiagnosticAuditStore(filepath.Join(a.auditRootDir(), "jvm_diag_audit.jsonl")).List(connectionID, limit)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
return connection.QueryResult{Success: true, Data: records}
}
func (a *App) emitDiagnosticChunk(tabID string, chunk jvm.DiagnosticEventChunk) {
if a.ctx == nil {
return
}
runtime.EventsEmit(a.ctx, diagnosticChunkEvent, diagnosticChunkEventPayload{
TabID: strings.TrimSpace(tabID),
Chunk: chunk,
})
}
func diagnosticRiskLevel(commandType string) string {
switch strings.TrimSpace(commandType) {
case jvm.DiagnosticCommandCategoryObserve:
return "low"
case jvm.DiagnosticCommandCategoryTrace:
return "medium"
default:
return "high"
}
}
func isDiagnosticTerminalPhase(phase string) bool {
switch strings.ToLower(strings.TrimSpace(phase)) {
case "completed", "failed", "canceled":
return true
default:
return false
}
}
func joinDiagnosticMessages(primary string, warnings []string) string {
items := make([]string, 0, 1+len(warnings))
if strings.TrimSpace(primary) != "" {
items = append(items, strings.TrimSpace(primary))
}
for _, warning := range warnings {
if strings.TrimSpace(warning) == "" {
continue
}
items = append(items, strings.TrimSpace(warning))
}
return strings.Join(items, "")
}