diff --git a/core/app/api/v2/entry.go b/core/app/api/v2/entry.go index bc295a333..617460a30 100644 --- a/core/app/api/v2/entry.go +++ b/core/app/api/v2/entry.go @@ -9,12 +9,13 @@ type ApiGroup struct { var ApiGroupApp = new(ApiGroup) var ( - authService = service.NewIAuthService() - backupService = service.NewIBackupService() - settingService = service.NewISettingService() - logService = service.NewILogService() - upgradeService = service.NewIUpgradeService() - groupService = service.NewIGroupService() - commandService = service.NewICommandService() - scriptService = service.NewIScriptService() + runtimeDiagnosticsService = service.NewIRuntimeDiagnosticsService() + authService = service.NewIAuthService() + backupService = service.NewIBackupService() + settingService = service.NewISettingService() + logService = service.NewILogService() + upgradeService = service.NewIUpgradeService() + groupService = service.NewIGroupService() + commandService = service.NewICommandService() + scriptService = service.NewIScriptService() ) diff --git a/core/app/api/v2/runtime_diagnostics.go b/core/app/api/v2/runtime_diagnostics.go new file mode 100644 index 000000000..66bb11974 --- /dev/null +++ b/core/app/api/v2/runtime_diagnostics.go @@ -0,0 +1,63 @@ +package v2 + +import ( + "os" + + "github.com/1Panel-dev/1Panel/core/app/api/v2/helper" + "github.com/1Panel-dev/1Panel/core/app/dto" + "github.com/gin-gonic/gin" +) + +// @Tags RuntimeDiagnostics +// @Summary Load Core runtime diagnostics summary +// @Success 200 {object} dto.RuntimeDiagnosticsSummary +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /core/hosts/diagnostics/summary [get] +func (b *BaseApi) LoadRuntimeDiagnosticsSummary(c *gin.Context) { + data, err := runtimeDiagnosticsService.Summary() + if err != nil { + helper.InternalServer(c, err) + return + } + helper.SuccessWithData(c, data) +} + +// @Tags RuntimeDiagnostics +// @Summary Load Core grouped goroutine snapshot +// @Success 200 {object} dto.RuntimeGoroutineSnapshot +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /core/hosts/diagnostics/goroutines [get] +func (b *BaseApi) LoadRuntimeGoroutines(c *gin.Context) { + data, err := runtimeDiagnosticsService.Goroutines() + if err != nil { + helper.InternalServer(c, err) + return + } + helper.SuccessWithData(c, data) +} + +// @Tags RuntimeDiagnostics +// @Summary Capture Core runtime profile +// @Param request body dto.RuntimeProfileCreate true "request" +// @Success 200 {file} file +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /core/hosts/diagnostics/profiles [post] +func (b *BaseApi) CreateRuntimeProfile(c *gin.Context) { + var req dto.RuntimeProfileCreate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + profile, err := runtimeDiagnosticsService.CreateProfile(req) + if err != nil { + helper.BadRequest(c, err) + return + } + defer os.Remove(profile.Path) + c.Header("Content-Disposition", `attachment; filename="`+profile.Name+`"`) + c.Header("Content-Type", "application/octet-stream") + c.File(profile.Path) + c.Abort() +} diff --git a/core/app/dto/runtime_diagnostics.go b/core/app/dto/runtime_diagnostics.go new file mode 100644 index 000000000..187f68910 --- /dev/null +++ b/core/app/dto/runtime_diagnostics.go @@ -0,0 +1,30 @@ +package dto + +import "time" + +type RuntimeDiagnosticsSummary struct { + RSS uint64 `json:"rss"` + HeapAlloc uint64 `json:"heapAlloc"` + HeapObjects uint64 `json:"heapObjects"` + Goroutines int `json:"goroutines"` +} + +type RuntimeGoroutineGroup struct { + State string `json:"state"` + Top string `json:"top"` + Count int `json:"count"` + Stack []string `json:"stack"` +} + +type RuntimeGoroutineSnapshot struct { + Total int `json:"total"` + GroupCount int `json:"groupCount"` + Truncated bool `json:"truncated"` + CapturedAt time.Time `json:"capturedAt"` + Goroutines []RuntimeGoroutineGroup `json:"goroutines"` +} + +type RuntimeProfileCreate struct { + Type string `json:"type" validate:"required,oneof=cpu heap goroutine mutex block"` + Duration int `json:"duration" validate:"omitempty,min=5,max=30"` +} diff --git a/core/app/service/runtime_diagnostics.go b/core/app/service/runtime_diagnostics.go new file mode 100644 index 000000000..96f18f4a9 --- /dev/null +++ b/core/app/service/runtime_diagnostics.go @@ -0,0 +1,411 @@ +package service + +import ( + "bufio" + "bytes" + "errors" + "fmt" + "io" + "os" + "regexp" + "runtime" + stdpprof "runtime/pprof" + "sort" + "strings" + "sync" + "time" + + "github.com/1Panel-dev/1Panel/core/app/dto" + profile "github.com/google/pprof/profile" + "github.com/shirou/gopsutil/v4/process" +) + +const ( + diagnosticsDefaultDuration = 15 + diagnosticsMaxProfileSize = 64 * 1024 * 1024 + diagnosticsMaxSnapshotSize = 16 * 1024 * 1024 + diagnosticsDetailedGoroutines = 10_000 + diagnosticsBlockProfileRate = 1_000_000 + diagnosticsMaxGroups = 200 +) + +var ( + runtimeDiagnosticsInstance = &RuntimeDiagnosticsService{} + goroutineHeaderPattern = regexp.MustCompile(`^goroutine \d+ \[([^]]+)\]:$`) + goroutineArgPattern = regexp.MustCompile(`0x[0-9a-fA-F]+`) + errProfileSizeLimit = errors.New("runtime profile exceeds the 64 MiB size limit") +) + +type cappedWriter struct { + writer io.Writer + remaining int64 + exceeded bool +} + +func (w *cappedWriter) Write(data []byte) (int, error) { + if int64(len(data)) <= w.remaining { + n, err := w.writer.Write(data) + w.remaining -= int64(n) + return n, err + } + w.exceeded = true + if w.remaining <= 0 { + return 0, errProfileSizeLimit + } + allowed := int(w.remaining) + n, err := w.writer.Write(data[:allowed]) + w.remaining -= int64(n) + if err != nil { + return n, err + } + return n, errProfileSizeLimit +} + +type IRuntimeDiagnosticsService interface { + Summary() (dto.RuntimeDiagnosticsSummary, error) + Goroutines() (dto.RuntimeGoroutineSnapshot, error) + CreateProfile(req dto.RuntimeProfileCreate) (RuntimeProfileResult, error) +} + +type RuntimeProfileResult struct { + Path string + Name string +} + +type RuntimeDiagnosticsService struct { + captureMu sync.Mutex + processMu sync.Mutex + process *process.Process +} + +func NewIRuntimeDiagnosticsService() IRuntimeDiagnosticsService { + return runtimeDiagnosticsInstance +} + +func (s *RuntimeDiagnosticsService) Summary() (dto.RuntimeDiagnosticsSummary, error) { + rss, err := s.processRSS() + if err != nil { + return dto.RuntimeDiagnosticsSummary{}, err + } + var stats runtime.MemStats + runtime.ReadMemStats(&stats) + return dto.RuntimeDiagnosticsSummary{ + RSS: rss, + HeapAlloc: stats.HeapAlloc, + HeapObjects: stats.HeapObjects, + Goroutines: runtime.NumGoroutine(), + }, nil +} + +func (s *RuntimeDiagnosticsService) Goroutines() (dto.RuntimeGoroutineSnapshot, error) { + total := runtime.NumGoroutine() + if total > diagnosticsDetailedGoroutines { + groups, truncated := compactGoroutineSnapshot(total) + return dto.RuntimeGoroutineSnapshot{ + Total: total, GroupCount: len(groups), Truncated: truncated, CapturedAt: time.Now(), Goroutines: groups, + }, nil + } + var data bytes.Buffer + writer := &cappedWriter{writer: &data, remaining: diagnosticsMaxSnapshotSize} + if err := stdpprof.Lookup("goroutine").WriteTo(writer, 2); err != nil { + groups, truncated := compactGoroutineSnapshot(total) + return dto.RuntimeGoroutineSnapshot{ + Total: total, GroupCount: len(groups), Truncated: truncated, CapturedAt: time.Now(), Goroutines: groups, + }, nil + } + groups, truncated := parseGoroutineDump(&data, diagnosticsMaxGroups) + result := dto.RuntimeGoroutineSnapshot{ + Total: total, + GroupCount: len(groups), + Truncated: truncated, + CapturedAt: time.Now(), + } + result.Goroutines = groups + return result, nil +} + +func (s *RuntimeDiagnosticsService) CreateProfile(req dto.RuntimeProfileCreate) (RuntimeProfileResult, error) { + if !s.captureMu.TryLock() { + return RuntimeProfileResult{}, errors.New("another runtime profile is being captured") + } + defer s.captureMu.Unlock() + return captureRuntimeProfile(req) +} + +func captureRuntimeProfile(req dto.RuntimeProfileCreate) (result RuntimeProfileResult, err error) { + duration := req.Duration + if duration == 0 { + duration = diagnosticsDefaultDuration + } + if duration < 5 || duration > 30 { + return result, errors.New("profile duration must be between 5 and 30 seconds") + } + if req.Type == "heap" || req.Type == "goroutine" { + duration = 0 + } + + name := fmt.Sprintf("core-%s-%s-%ds.pb.gz", req.Type, newRuntimeEventID(), duration) + file, err := os.CreateTemp("", "1panel-core-runtime-profile-*.tmp") + if err != nil { + return result, err + } + writer := &cappedWriter{writer: file, remaining: diagnosticsMaxProfileSize} + removeOnError := true + defer func() { + _ = file.Close() + if removeOnError { + _ = os.Remove(file.Name()) + } + }() + + switch req.Type { + case "cpu": + if err = stdpprof.StartCPUProfile(writer); err != nil { + return result, err + } + time.Sleep(time.Duration(duration) * time.Second) + stdpprof.StopCPUProfile() + case "heap": + err = stdpprof.Lookup("heap").WriteTo(writer, 0) + case "goroutine": + err = stdpprof.Lookup("goroutine").WriteTo(writer, 0) + case "mutex": + err = captureWindowedRuntimeProfile("mutex", duration, writer, func() func() { + previous := runtime.SetMutexProfileFraction(5) + return func() { runtime.SetMutexProfileFraction(previous) } + }) + case "block": + err = captureWindowedRuntimeProfile("block", duration, writer, func() func() { + runtime.SetBlockProfileRate(diagnosticsBlockProfileRate) + return func() { runtime.SetBlockProfileRate(0) } + }) + default: + return result, errors.New("unsupported runtime profile type") + } + if err != nil { + return result, err + } + if writer.exceeded { + return result, errProfileSizeLimit + } + if err = file.Close(); err != nil { + return result, err + } + removeOnError = false + return RuntimeProfileResult{Path: file.Name(), Name: name}, nil +} + +func captureWindowedRuntimeProfile(name string, duration int, writer io.Writer, enable func() func()) error { + restore := enable() + sampling := true + defer func() { + if sampling { + restore() + } + }() + + before, err := readRuntimeProfile(name) + if err != nil { + return err + } + startedAt := time.Now() + time.Sleep(time.Duration(duration) * time.Second) + restore() + sampling = false + after, err := readRuntimeProfile(name) + if err != nil { + return err + } + delta, err := diffRuntimeProfiles(before, after, startedAt, time.Duration(duration)*time.Second) + if err != nil { + return err + } + return delta.Write(writer) +} + +func readRuntimeProfile(name string) (*profile.Profile, error) { + var data bytes.Buffer + writer := &cappedWriter{writer: &data, remaining: diagnosticsMaxProfileSize} + if err := stdpprof.Lookup(name).WriteTo(writer, 0); err != nil { + return nil, err + } + if writer.exceeded { + return nil, errProfileSizeLimit + } + return profile.Parse(&data) +} + +func diffRuntimeProfiles(before, after *profile.Profile, startedAt time.Time, duration time.Duration) (*profile.Profile, error) { + baseline := before.Copy() + baseline.Scale(-1) + delta, err := profile.Merge([]*profile.Profile{after, baseline}) + if err != nil { + return nil, err + } + delta.TimeNanos = startedAt.UnixNano() + delta.DurationNanos = duration.Nanoseconds() + return delta, nil +} + +func (s *RuntimeDiagnosticsService) processRSS() (uint64, error) { + s.processMu.Lock() + defer s.processMu.Unlock() + if s.process == nil { + proc, err := process.NewProcess(int32(os.Getpid())) + if err != nil { + return 0, err + } + s.process = proc + } + memoryInfo, err := s.process.MemoryInfo() + if err != nil { + return 0, err + } + if memoryInfo == nil { + return 0, errors.New("process memory information is unavailable") + } + return memoryInfo.RSS, nil +} + +func newRuntimeEventID() string { + return time.Now().Format("20060102-150405.000000000") +} + +func parseGoroutineDump(reader io.Reader, maxGroups int) ([]dto.RuntimeGoroutineGroup, bool) { + type groupValue struct { + state string + top string + stack []string + count int + } + groups := make(map[string]*groupValue) + truncated := false + scanner := bufio.NewScanner(reader) + scanner.Buffer(make([]byte, 64*1024), 4*1024*1024) + var block []string + flush := func() { + if len(block) == 0 { + return + } + matches := goroutineHeaderPattern.FindStringSubmatch(block[0]) + state := "unknown" + if len(matches) == 2 { + state = matches[1] + } + stack := append([]string(nil), block[1:]...) + if len(stack) > 40 { + stack = stack[:40] + } + top := "runtime" + functionLines := make([]string, 0, len(stack)/2) + for _, line := range stack { + trimmed := strings.TrimSpace(line) + if trimmed == "" || strings.HasPrefix(trimmed, "/") || strings.HasPrefix(trimmed, "created by ") { + continue + } + normalized := goroutineArgPattern.ReplaceAllString(trimmed, "0x…") + functionLines = append(functionLines, normalized) + if top == "runtime" { + top = strings.Split(normalized, "(")[0] + } + } + signature := state + "\n" + strings.Join(functionLines, "\n") + if existing, ok := groups[signature]; ok { + existing.count++ + return + } + if len(groups) >= maxGroups { + truncated = true + return + } + groups[signature] = &groupValue{state: state, top: top, stack: stack, count: 1} + } + for scanner.Scan() { + line := scanner.Text() + if strings.HasPrefix(line, "goroutine ") && strings.HasSuffix(line, "]:") { + flush() + block = []string{line} + continue + } + if len(block) > 0 { + block = append(block, line) + } + } + if scanner.Err() != nil { + truncated = true + } + flush() + + result := make([]dto.RuntimeGoroutineGroup, 0, len(groups)) + for _, group := range groups { + result = append(result, dto.RuntimeGoroutineGroup{State: group.state, Top: group.top, Count: group.count, Stack: group.stack}) + } + sort.Slice(result, func(i, j int) bool { + if result[i].Count == result[j].Count { + return result[i].Top < result[j].Top + } + return result[i].Count > result[j].Count + }) + return result, truncated +} + +func compactGoroutineSnapshot(initialSize int) ([]dto.RuntimeGoroutineGroup, bool) { + records := make([]runtime.StackRecord, initialSize+32) + count, ok := runtime.GoroutineProfile(records) + if !ok { + records = make([]runtime.StackRecord, count+32) + count, ok = runtime.GoroutineProfile(records) + } + truncated := !ok + if count > len(records) { + count = len(records) + truncated = true + } + records = records[:count] + type compactGroup struct { + top string + stack []string + count int + } + groups := make(map[string]*compactGroup) + for _, record := range records { + frames := runtime.CallersFrames(record.Stack()) + stack := make([]string, 0, 16) + functions := make([]string, 0, 8) + top := "runtime" + for { + frame, more := frames.Next() + if frame.Function != "" { + if top == "runtime" { + top = frame.Function + } + functions = append(functions, frame.Function) + stack = append(stack, frame.Function, fmt.Sprintf("\t%s:%d", frame.File, frame.Line)) + } + if !more || len(functions) >= 20 { + break + } + } + signature := strings.Join(functions, "\n") + if existing, exists := groups[signature]; exists { + existing.count++ + continue + } + if len(groups) >= diagnosticsMaxGroups { + truncated = true + continue + } + groups[signature] = &compactGroup{top: top, stack: stack, count: 1} + } + result := make([]dto.RuntimeGoroutineGroup, 0, len(groups)) + for _, group := range groups { + result = append(result, dto.RuntimeGoroutineGroup{State: "profiled", Top: group.top, Count: group.count, Stack: group.stack}) + } + sort.Slice(result, func(i, j int) bool { + if result[i].Count == result[j].Count { + return result[i].Top < result[j].Top + } + return result[i].Count > result[j].Count + }) + return result, truncated +} diff --git a/core/cmd/server/docs/docs.go b/core/cmd/server/docs/docs.go index e9a1084e1..3dad4da3b 100644 --- a/core/cmd/server/docs/docs.go +++ b/core/cmd/server/docs/docs.go @@ -10701,6 +10701,89 @@ const docTemplate = `{ } } }, + "/core/hosts/diagnostics/goroutines": { + "get": { + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.RuntimeGoroutineSnapshot" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Load Core grouped goroutine snapshot", + "tags": [ + "RuntimeDiagnostics" + ] + } + }, + "/core/hosts/diagnostics/profiles": { + "post": { + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.RuntimeProfileCreate" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "file" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Capture Core runtime profile", + "tags": [ + "RuntimeDiagnostics" + ] + } + }, + "/core/hosts/diagnostics/summary": { + "get": { + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.RuntimeDiagnosticsSummary" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Load Core runtime diagnostics summary", + "tags": [ + "RuntimeDiagnostics" + ] + } + }, "/core/logs/clean": { "post": { "consumes": [ diff --git a/core/cmd/server/docs/swagger.json b/core/cmd/server/docs/swagger.json index bb52fdf73..5ebf2ce42 100644 --- a/core/cmd/server/docs/swagger.json +++ b/core/cmd/server/docs/swagger.json @@ -10697,6 +10697,89 @@ } } }, + "/core/hosts/diagnostics/goroutines": { + "get": { + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.RuntimeGoroutineSnapshot" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Load Core grouped goroutine snapshot", + "tags": [ + "RuntimeDiagnostics" + ] + } + }, + "/core/hosts/diagnostics/profiles": { + "post": { + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.RuntimeProfileCreate" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "file" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Capture Core runtime profile", + "tags": [ + "RuntimeDiagnostics" + ] + } + }, + "/core/hosts/diagnostics/summary": { + "get": { + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.RuntimeDiagnosticsSummary" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Load Core runtime diagnostics summary", + "tags": [ + "RuntimeDiagnostics" + ] + } + }, "/core/logs/clean": { "post": { "consumes": [ diff --git a/core/go.mod b/core/go.mod index 5f163ca80..4afda1d06 100644 --- a/core/go.mod +++ b/core/go.mod @@ -17,6 +17,7 @@ require ( github.com/go-playground/validator/v10 v10.30.3 github.com/go-resty/resty/v2 v2.17.2 github.com/go-webauthn/webauthn v0.17.4 + github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.3 github.com/jinzhu/copier v0.4.0 diff --git a/core/go.sum b/core/go.sum index dd11a2e1d..55fc093b3 100644 --- a/core/go.sum +++ b/core/go.sum @@ -126,6 +126,8 @@ github.com/google/go-tpm-tools v0.3.13-0.20230620182252-4639ecce2aba/go.mod h1:E github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 h1:EEHtgt9IwisQ2AZ4pIsMjahcegHh6rmhqxzIRQIyepY= +github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= diff --git a/core/router/common.go b/core/router/common.go index f4180a55f..626ef2148 100644 --- a/core/router/common.go +++ b/core/router/common.go @@ -9,5 +9,6 @@ func commonGroups() []CommonRouter { &CommandRouter{}, &GroupRouter{}, &ScriptRouter{}, + &RuntimeDiagnosticsRouter{}, } } diff --git a/core/router/ro_runtime_diagnostics.go b/core/router/ro_runtime_diagnostics.go new file mode 100644 index 000000000..5d1ff5fb0 --- /dev/null +++ b/core/router/ro_runtime_diagnostics.go @@ -0,0 +1,21 @@ +package router + +import ( + v2 "github.com/1Panel-dev/1Panel/core/app/api/v2" + "github.com/1Panel-dev/1Panel/core/middleware" + "github.com/gin-gonic/gin" +) + +type RuntimeDiagnosticsRouter struct{} + +func (s *RuntimeDiagnosticsRouter) InitRouter(Router *gin.RouterGroup) { + diagnosticsRouter := Router.Group("hosts/diagnostics"). + Use(middleware.SessionAuth()). + Use(middleware.PasswordExpired()) + baseApi := v2.ApiGroupApp.BaseApi + { + diagnosticsRouter.GET("/summary", baseApi.LoadRuntimeDiagnosticsSummary) + diagnosticsRouter.GET("/goroutines", baseApi.LoadRuntimeGoroutines) + diagnosticsRouter.POST("/profiles", baseApi.CreateRuntimeProfile) + } +} diff --git a/frontend/src/api/modules/host.ts b/frontend/src/api/modules/host.ts index 4e232c8e7..c75434b3f 100644 --- a/frontend/src/api/modules/host.ts +++ b/frontend/src/api/modules/host.ts @@ -41,16 +41,18 @@ export const loadMonitorSetting = (currentNode?: string) => { export const updateMonitorSetting = (key: string, value: string) => { return http.post(`/hosts/monitor/setting/update`, { key: key, value: value }); }; -export const loadRuntimeDiagnosticsSummary = (currentNode?: string) => { +export type RuntimeDiagnosticsTarget = 'agent' | 'core'; + +export const loadRuntimeDiagnosticsSummary = (currentNode?: string, target: RuntimeDiagnosticsTarget = 'agent') => { return http.get( - `/hosts/diagnostics/summary`, + `${target === 'core' ? '/core' : ''}/hosts/diagnostics/summary`, {}, currentNode ? { headers: { CurrentNode: currentNode } } : {}, ); }; -export const loadRuntimeGoroutines = (currentNode?: string) => { +export const loadRuntimeGoroutines = (currentNode?: string, target: RuntimeDiagnosticsTarget = 'agent') => { return http.get( - `/hosts/diagnostics/goroutines`, + `${target === 'core' ? '/core' : ''}/hosts/diagnostics/goroutines`, {}, currentNode ? { headers: { CurrentNode: currentNode } } : {}, ); @@ -72,13 +74,21 @@ const parseRuntimeProfileError = async (data: unknown) => { return new RuntimeProfileDownloadError(); } }; -export const createRuntimeProfile = async (params: Host.RuntimeProfileCreate, currentNode?: string) => { +export const createRuntimeProfile = async ( + params: Host.RuntimeProfileCreate, + currentNode?: string, + target: RuntimeDiagnosticsTarget = 'agent', +) => { try { - const data = await http.download(`/hosts/diagnostics/profiles`, params, { - responseType: 'blob', - timeout: TimeoutEnum.T_60S, - headers: currentNode ? { CurrentNode: currentNode } : undefined, - }); + const data = await http.download( + `${target === 'core' ? '/core' : ''}/hosts/diagnostics/profiles`, + params, + { + responseType: 'blob', + timeout: TimeoutEnum.T_60S, + headers: currentNode ? { CurrentNode: currentNode } : undefined, + }, + ); const profileError = await parseRuntimeProfileError(data); if (profileError) { throw profileError; diff --git a/frontend/src/views/host/process/process/diagnostics/index.vue b/frontend/src/views/host/process/process/diagnostics/index.vue index 3da5eff3f..abfa3c905 100644 --- a/frontend/src/views/host/process/process/diagnostics/index.vue +++ b/frontend/src/views/host/process/process/diagnostics/index.vue @@ -2,7 +2,7 @@
('agent'); +const diagnosticsNode = ref('local'); const open = ref(false); const captureLoading = ref(false); const goroutineLoading = ref(false); @@ -223,14 +226,14 @@ const summaryCards = computed(() => [ ]); const loadSummary = async () => { - const res = await loadRuntimeDiagnosticsSummary(currentNode.value); + const res = await loadRuntimeDiagnosticsSummary(diagnosticsNode.value, target.value); Object.assign(summary, res.data); }; const loadGoroutines = async () => { goroutineLoading.value = true; try { - const res = await loadRuntimeGoroutines(currentNode.value); + const res = await loadRuntimeGoroutines(diagnosticsNode.value, target.value); Object.assign(goroutineSnapshot, res.data); } finally { goroutineLoading.value = false; @@ -245,11 +248,11 @@ const showGoroutineStack = (row: Host.RuntimeGoroutineGroup) => { const captureProfile = async () => { captureLoading.value = true; try { - const data = await createRuntimeProfile(captureForm, currentNode.value); + const data = await createRuntimeProfile(captureForm, diagnosticsNode.value, target.value); const url = window.URL.createObjectURL(data); const link = document.createElement('a'); link.href = url; - link.download = `${captureForm.type}-${Date.now()}.pb.gz`; + link.download = `${target.value}-${captureForm.type}-${Date.now()}.pb.gz`; link.click(); window.URL.revokeObjectURL(url); MsgSuccess(i18n.global.t('monitor.captureSuccess')); @@ -263,7 +266,9 @@ const captureProfile = async () => { } }; -const acceptParams = () => { +const acceptParams = (process: RuntimeDiagnosticsTarget = 'agent') => { + target.value = process; + diagnosticsNode.value = process === 'core' ? 'local' : currentNode.value || 'local'; open.value = true; Promise.all([loadSummary(), loadGoroutines()]); }; diff --git a/frontend/src/views/host/process/process/index.vue b/frontend/src/views/host/process/process/index.vue index f90a4ed86..b62ff865b 100644 --- a/frontend/src/views/host/process/process/index.vue +++ b/frontend/src/views/host/process/process/index.vue @@ -218,13 +218,16 @@ const columns = ref([ ), ]; - if (rowData.name === '1panel-agent') { + if ( + rowData.name === '1panel-agent' || + (rowData.name === '1panel-core' && (!currentNode.value || currentNode.value === 'local')) + ) { buttons.push( h( ElButton, { type: 'text', - onClick: openRuntimeDiagnostics, + onClick: () => openRuntimeDiagnostics(rowData.name), }, () => i18n.global.t('monitor.runtimeDiagnostics'), ), @@ -275,8 +278,8 @@ const openDetail = (row: any) => { detailRef.value.acceptParams(row.PID); }; -const openRuntimeDiagnostics = () => { - runtimeDiagnosticsRef.value?.acceptParams(); +const openRuntimeDiagnostics = (name: string) => { + runtimeDiagnosticsRef.value?.acceptParams(name === '1panel-core' ? 'core' : 'agent'); }; const changeSort = ({ key, order }) => {