feat(core): add runtime diagnostics and pprof capture (#13852)

This commit is contained in:
ssongliu
2026-09-17 15:46:06 +08:00
committed by GitHub
parent 3814525edd
commit 0bad1b471f
13 changed files with 742 additions and 28 deletions

View File

@@ -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()
)

View File

@@ -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()
}

View File

@@ -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"`
}

View File

@@ -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
}

View File

@@ -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": [

View File

@@ -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": [

View File

@@ -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

View File

@@ -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=

View File

@@ -9,5 +9,6 @@ func commonGroups() []CommonRouter {
&CommandRouter{},
&GroupRouter{},
&ScriptRouter{},
&RuntimeDiagnosticsRouter{},
}
}

View File

@@ -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)
}
}

View File

@@ -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<Host.RuntimeDiagnosticsSummary>(
`/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<Host.RuntimeGoroutineSnapshot>(
`/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<Blob>(`/hosts/diagnostics/profiles`, params, {
responseType: 'blob',
timeout: TimeoutEnum.T_60S,
headers: currentNode ? { CurrentNode: currentNode } : undefined,
});
const data = await http.download<Blob>(
`${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;

View File

@@ -2,7 +2,7 @@
<div>
<DrawerPro
v-model="open"
:header="$t('monitor.runtimeDiagnostics')"
:header="`${$t('monitor.runtimeDiagnostics')} - 1panel-${target}`"
size="large"
:confirm-before-close="captureLoading"
@before-close="handleBeforeClose"
@@ -142,6 +142,7 @@ import {
loadRuntimeGoroutines,
RuntimeProfileDownloadError,
} from '@/api/modules/host';
import type { RuntimeDiagnosticsTarget } from '@/api/modules/host';
import { Host } from '@/api/interface/host';
import { useGlobalStore } from '@/composables/useGlobalStore';
import { computeSize } from '@/utils/size';
@@ -149,6 +150,8 @@ import i18n from '@/lang';
import { MsgError, MsgSuccess } from '@/utils/message';
const { currentNode } = useGlobalStore();
const target = ref<RuntimeDiagnosticsTarget>('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()]);
};

View File

@@ -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 }) => {