mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-09-22 08:00:53 +00:00
feat: add runtime diagnostics (#13295)
This commit is contained in:
@@ -37,14 +37,15 @@ var (
|
||||
|
||||
cronjobService = service.NewICronjobService()
|
||||
|
||||
fileService = service.NewIFileService()
|
||||
fileHistoryService = service.NewIFileHistoryService()
|
||||
fileShareService = service.NewIFileShareService()
|
||||
sshService = service.NewISSHService()
|
||||
firewallService = service.NewIFirewallService()
|
||||
iptablesService = service.NewIIptablesService()
|
||||
monitorService = service.NewIMonitorService()
|
||||
systemService = service.NewISystemService()
|
||||
fileService = service.NewIFileService()
|
||||
fileHistoryService = service.NewIFileHistoryService()
|
||||
fileShareService = service.NewIFileShareService()
|
||||
sshService = service.NewISSHService()
|
||||
firewallService = service.NewIFirewallService()
|
||||
iptablesService = service.NewIIptablesService()
|
||||
monitorService = service.NewIMonitorService()
|
||||
systemService = service.NewISystemService()
|
||||
runtimeDiagnosticsService = service.NewIRuntimeDiagnosticsService()
|
||||
|
||||
deviceService = service.NewIDeviceService()
|
||||
fail2banService = service.NewIFail2BanService()
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// @Tags RuntimeDiagnostics
|
||||
// @Summary Load runtime diagnostics summary
|
||||
// @Success 200 {object} dto.RuntimeDiagnosticsSummary
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /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 grouped goroutine snapshot
|
||||
// @Success 200 {object} dto.RuntimeGoroutineSnapshot
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/diagnostics/goroutines [get]
|
||||
func (b *BaseApi) LoadRuntimeGoroutines(c *gin.Context) {
|
||||
data, err := runtimeDiagnosticsService.Goroutines()
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithDataGzipped(c, data)
|
||||
}
|
||||
|
||||
// @Tags RuntimeDiagnostics
|
||||
// @Summary Capture runtime profile
|
||||
// @Param request body dto.RuntimeProfileCreate true "request"
|
||||
// @Success 200 {file} file
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /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()
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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/agent/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("%s-%s-%ds.pb.gz", req.Type, newRuntimeEventID(), duration)
|
||||
file, err := os.CreateTemp("", "1panel-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
|
||||
}
|
||||
+1
-1
@@ -20,6 +20,7 @@ require (
|
||||
github.com/go-redis/redis v6.15.9+incompatible
|
||||
github.com/go-resty/resty/v2 v2.17.2
|
||||
github.com/go-sql-driver/mysql v1.10.0
|
||||
github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674
|
||||
github.com/jackc/pgx/v5 v5.10.0
|
||||
@@ -130,7 +131,6 @@ require (
|
||||
github.com/gofrs/flock v0.13.0 // indirect
|
||||
github.com/golang/snappy v1.0.0 // indirect
|
||||
github.com/google/go-querystring v1.2.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.198 // indirect
|
||||
|
||||
@@ -24,5 +24,6 @@ func commonGroups() []CommonRouter {
|
||||
&AIToolsRouter{},
|
||||
&GroupRouter{},
|
||||
&AlertRouter{},
|
||||
&RuntimeDiagnosticsRouter{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
v2 "github.com/1Panel-dev/1Panel/agent/app/api/v2"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type RuntimeDiagnosticsRouter struct{}
|
||||
|
||||
func (s *RuntimeDiagnosticsRouter) InitRouter(Router *gin.RouterGroup) {
|
||||
diagnosticsRouter := Router.Group("hosts/diagnostics")
|
||||
baseApi := v2.ApiGroupApp.BaseApi
|
||||
{
|
||||
diagnosticsRouter.GET("/summary", baseApi.LoadRuntimeDiagnosticsSummary)
|
||||
diagnosticsRouter.GET("/goroutines", baseApi.LoadRuntimeGoroutines)
|
||||
diagnosticsRouter.POST("/profiles", baseApi.CreateRuntimeProfile)
|
||||
}
|
||||
}
|
||||
@@ -160,6 +160,29 @@ export namespace Host {
|
||||
endTime: Date;
|
||||
}
|
||||
|
||||
export interface RuntimeDiagnosticsSummary {
|
||||
rss: number;
|
||||
heapAlloc: number;
|
||||
heapObjects: number;
|
||||
goroutines: number;
|
||||
}
|
||||
export interface RuntimeGoroutineGroup {
|
||||
state: string;
|
||||
top: string;
|
||||
count: number;
|
||||
stack: string[];
|
||||
}
|
||||
export interface RuntimeGoroutineSnapshot {
|
||||
total: number;
|
||||
groupCount: number;
|
||||
truncated: boolean;
|
||||
capturedAt: string;
|
||||
goroutines: RuntimeGoroutineGroup[];
|
||||
}
|
||||
export interface RuntimeProfileCreate {
|
||||
type: 'cpu' | 'heap' | 'goroutine' | 'mutex' | 'block';
|
||||
duration: number;
|
||||
}
|
||||
export interface SSHInfo {
|
||||
autoStart: boolean;
|
||||
isActive: boolean;
|
||||
|
||||
@@ -97,7 +97,58 @@ 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) => {
|
||||
return http.get<Host.RuntimeDiagnosticsSummary>(
|
||||
`/hosts/diagnostics/summary`,
|
||||
{},
|
||||
currentNode ? { headers: { CurrentNode: currentNode } } : {},
|
||||
);
|
||||
};
|
||||
export const loadRuntimeGoroutines = (currentNode?: string) => {
|
||||
return http.get<Host.RuntimeGoroutineSnapshot>(
|
||||
`/hosts/diagnostics/goroutines`,
|
||||
{},
|
||||
currentNode ? { headers: { CurrentNode: currentNode } } : {},
|
||||
);
|
||||
};
|
||||
export class RuntimeProfileDownloadError extends Error {
|
||||
constructor(message = '') {
|
||||
super(message);
|
||||
this.name = 'RuntimeProfileDownloadError';
|
||||
}
|
||||
}
|
||||
const parseRuntimeProfileError = async (data: unknown) => {
|
||||
if (!(data instanceof Blob) || !data.type.includes('application/json')) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = JSON.parse(await data.text()) as { message?: string };
|
||||
return new RuntimeProfileDownloadError(response.message);
|
||||
} catch {
|
||||
return new RuntimeProfileDownloadError();
|
||||
}
|
||||
};
|
||||
export const createRuntimeProfile = async (params: Host.RuntimeProfileCreate, currentNode?: string) => {
|
||||
try {
|
||||
const data = await http.download<Blob>(`/hosts/diagnostics/profiles`, params, {
|
||||
responseType: 'blob',
|
||||
timeout: TimeoutEnum.T_60S,
|
||||
headers: currentNode ? { CurrentNode: currentNode } : undefined,
|
||||
});
|
||||
const profileError = await parseRuntimeProfileError(data);
|
||||
if (profileError) {
|
||||
throw profileError;
|
||||
}
|
||||
return data;
|
||||
} catch (error) {
|
||||
if (error instanceof RuntimeProfileDownloadError) {
|
||||
throw error;
|
||||
}
|
||||
const responseData = (error as { response?: { data?: unknown } })?.response?.data;
|
||||
const profileError = await parseRuntimeProfileError(responseData);
|
||||
throw profileError || error;
|
||||
}
|
||||
};
|
||||
// ssh
|
||||
export const getSSHInfo = (currentNode?: string) => {
|
||||
return http.post<Host.SSHInfo>(
|
||||
|
||||
@@ -1905,6 +1905,41 @@ const message = {
|
||||
down: 'Down',
|
||||
interval: 'Collection Interval',
|
||||
intervalHelper: 'Enter an appropriate monitoring collection interval (10 seconds - 12 hours)',
|
||||
runtimeDiagnostics: 'Runtime Diagnostics',
|
||||
runtimeRSS: 'Process Memory (RSS)',
|
||||
runtimeHeapAlloc: 'Heap Memory',
|
||||
runtimeHeapObjects: 'Heap Objects',
|
||||
runtimeGoroutines: 'Goroutines',
|
||||
goroutineSnapshot: 'Goroutine Snapshot',
|
||||
goroutineSummary: '{0} goroutines grouped into {1} call stacks',
|
||||
goroutineTruncated: 'The snapshot is too large. Only part of the call stacks are shown.',
|
||||
captureSnapshot: 'Refresh Snapshot',
|
||||
count: 'Count',
|
||||
topFunction: 'Top Function',
|
||||
manualCapture: 'Manual Capture',
|
||||
startCapture: 'Start Capture',
|
||||
capturing: 'Capturing',
|
||||
captureSuccess: 'Diagnostic file generated; download started',
|
||||
profileCPU: 'CPU',
|
||||
profileHeap: 'Heap',
|
||||
profileGoroutine: 'Goroutine',
|
||||
profileMutex: 'Mutex',
|
||||
profileBlock: 'Block',
|
||||
runtimeRSSDesc:
|
||||
'Physical memory currently occupied by the process, including the Go heap, thread stacks, and native memory.',
|
||||
runtimeHeapAllocDesc:
|
||||
'Memory allocated on the Go heap that has not been released. Sustained growth may indicate retained objects.',
|
||||
runtimeHeapObjectsDesc:
|
||||
'Number of live objects on the Go heap. Sustained growth may indicate retained objects.',
|
||||
runtimeGoroutineDesc:
|
||||
'Current lightweight concurrent tasks. Sustained growth usually warrants checking for goroutine leaks.',
|
||||
profileCPUDesc:
|
||||
'Samples CPU call stacks for the selected duration to locate functions that actually consume CPU.',
|
||||
profileHeapDesc:
|
||||
'Records Go heap objects still in use and their allocation stacks to investigate sustained memory growth.',
|
||||
profileGoroutineDesc: 'Saves the state and call stack of every goroutine and groups identical call paths.',
|
||||
profileMutexDesc: 'Measures wait time caused by mutex contention to locate lock contention hotspots.',
|
||||
profileBlockDesc: 'Measures blocking on channels, select statements, and synchronization primitives.',
|
||||
},
|
||||
terminal: {
|
||||
local: 'Local',
|
||||
|
||||
@@ -1946,6 +1946,42 @@ const message = {
|
||||
down: 'Bajada',
|
||||
interval: 'Intervalo de Recolección',
|
||||
intervalHelper: 'Ingrese un intervalo de recolección de monitoreo apropiado (10 segundos - 12 horas)',
|
||||
runtimeDiagnostics: 'Diagnóstico en tiempo de ejecución',
|
||||
runtimeRSS: 'Memoria del proceso (RSS)',
|
||||
runtimeHeapAlloc: 'Memoria del heap',
|
||||
runtimeHeapObjects: 'Objetos del heap',
|
||||
runtimeGoroutines: 'Goroutines',
|
||||
goroutineSnapshot: 'Instantánea de Goroutines',
|
||||
goroutineSummary: '{0} goroutines agrupadas en {1} pilas de llamadas',
|
||||
goroutineTruncated: 'La instantánea es demasiado grande. Solo se muestran algunas pilas de llamadas.',
|
||||
captureSnapshot: 'Actualizar instantánea',
|
||||
count: 'Cantidad',
|
||||
topFunction: 'Función superior',
|
||||
manualCapture: 'Captura manual',
|
||||
startCapture: 'Iniciar captura',
|
||||
capturing: 'Capturando',
|
||||
captureSuccess: 'Archivo de diagnóstico generado; descarga iniciada',
|
||||
profileCPU: 'CPU',
|
||||
profileHeap: 'Heap',
|
||||
profileGoroutine: 'Goroutine',
|
||||
profileMutex: 'Mutex',
|
||||
profileBlock: 'Bloqueo',
|
||||
runtimeRSSDesc:
|
||||
'Memoria física ocupada por el proceso, incluido el heap de Go, las pilas de hilos y la memoria nativa.',
|
||||
runtimeHeapAllocDesc:
|
||||
'Memoria asignada en el heap de Go que aún no se ha liberado. Un crecimiento continuo puede indicar objetos retenidos.',
|
||||
runtimeHeapObjectsDesc:
|
||||
'Número de objetos vivos en el heap de Go. Un crecimiento continuo puede indicar objetos retenidos.',
|
||||
runtimeGoroutineDesc:
|
||||
'Tareas concurrentes ligeras actuales. Un crecimiento continuo puede indicar una fuga de goroutines.',
|
||||
profileCPUDesc:
|
||||
'Muestrea las pilas de CPU durante el tiempo seleccionado para localizar las funciones que consumen CPU.',
|
||||
profileHeapDesc:
|
||||
'Registra los objetos del heap aún en uso y sus pilas de asignación para investigar el crecimiento de memoria.',
|
||||
profileGoroutineDesc: 'Guarda el estado y la pila de cada goroutine y agrupa las rutas de llamada idénticas.',
|
||||
profileMutexDesc:
|
||||
'Mide el tiempo de espera causado por la contención de mutex para localizar bloqueos disputados.',
|
||||
profileBlockDesc: 'Mide los bloqueos en canales, select y primitivas de sincronización.',
|
||||
},
|
||||
terminal: {
|
||||
local: 'Local',
|
||||
|
||||
@@ -1895,6 +1895,36 @@ const message = {
|
||||
down: 'پایین',
|
||||
interval: 'فاصله جمعآوری',
|
||||
intervalHelper: 'فاصله جمعآوری نظارت مناسب را وارد کنید (۱۰ ثانیه - ۱۲ ساعت)',
|
||||
runtimeDiagnostics: 'تشخیص زمان اجرا',
|
||||
runtimeRSS: 'حافظه پردازش (RSS)',
|
||||
runtimeHeapAlloc: 'حافظه Heap',
|
||||
runtimeHeapObjects: 'اشیای Heap',
|
||||
runtimeGoroutines: 'Goroutineها',
|
||||
goroutineSnapshot: 'تصویر لحظهای Goroutine',
|
||||
goroutineSummary: '{0} goroutine در {1} پشته فراخوانی گروهبندی شدهاند',
|
||||
goroutineTruncated: 'تصویر لحظهای بسیار بزرگ است. فقط بخشی از پشتههای فراخوانی نمایش داده میشود.',
|
||||
captureSnapshot: 'تازهسازی تصویر',
|
||||
count: 'تعداد',
|
||||
topFunction: 'تابع بالایی',
|
||||
manualCapture: 'ثبت دستی',
|
||||
startCapture: 'شروع ثبت',
|
||||
capturing: 'در حال ثبت',
|
||||
captureSuccess: 'فایل تشخیصی ایجاد شد؛ دانلود آغاز شد',
|
||||
profileCPU: 'CPU',
|
||||
profileHeap: 'Heap',
|
||||
profileGoroutine: 'Goroutine',
|
||||
profileMutex: 'Mutex',
|
||||
profileBlock: 'انسداد',
|
||||
runtimeRSSDesc: 'حافظه فیزیکی اشغالشده توسط پردازش، شامل heap زبان Go، پشته رشتهها و حافظه بومی.',
|
||||
runtimeHeapAllocDesc:
|
||||
'حافظه تخصیصیافته در heap زبان Go که هنوز آزاد نشده است. رشد مداوم میتواند نشانه باقیماندن اشیا باشد.',
|
||||
runtimeHeapObjectsDesc: 'تعداد اشیای زنده در heap زبان Go. رشد مداوم میتواند نشانه باقیماندن اشیا باشد.',
|
||||
runtimeGoroutineDesc: 'تعداد وظایف همزمان سبک فعلی. رشد مداوم میتواند نشانه نشت Goroutine باشد.',
|
||||
profileCPUDesc: 'پشتههای فراخوانی CPU را در مدت انتخابشده نمونهبرداری میکند تا توابع پرمصرف مشخص شوند.',
|
||||
profileHeapDesc: 'اشیای heap در حال استفاده و پشته تخصیص آنها را برای بررسی رشد حافظه ثبت میکند.',
|
||||
profileGoroutineDesc: 'وضعیت و پشته فراخوانی هر Goroutine را ذخیره و مسیرهای یکسان را گروهبندی میکند.',
|
||||
profileMutexDesc: 'زمان انتظار ناشی از رقابت mutex را برای یافتن نقاط رقابت قفل اندازهگیری میکند.',
|
||||
profileBlockDesc: 'زمان انسداد روی Channel، Select و ابزارهای همگامسازی را اندازهگیری میکند.',
|
||||
},
|
||||
terminal: {
|
||||
local: 'محلی',
|
||||
|
||||
@@ -1911,6 +1911,37 @@ const message = {
|
||||
down: '下',
|
||||
interval: '収集間隔',
|
||||
intervalHelper: '適切な監視収集間隔を入力してください(10秒 - 12時間)',
|
||||
runtimeDiagnostics: 'ランタイム診断',
|
||||
runtimeRSS: 'プロセスメモリ (RSS)',
|
||||
runtimeHeapAlloc: 'ヒープメモリ',
|
||||
runtimeHeapObjects: 'ヒープオブジェクト',
|
||||
runtimeGoroutines: 'Goroutine 数',
|
||||
goroutineSnapshot: 'Goroutine スナップショット',
|
||||
goroutineSummary: '{0} 個の Goroutine を {1} 個の呼び出しスタックに集約',
|
||||
goroutineTruncated: 'スナップショットが大きすぎるため、一部の呼び出しスタックのみ表示しています。',
|
||||
captureSnapshot: 'スナップショットを更新',
|
||||
count: '数',
|
||||
topFunction: 'トップ関数',
|
||||
manualCapture: '手動取得',
|
||||
startCapture: '取得開始',
|
||||
capturing: '取得中',
|
||||
captureSuccess: '診断ファイルを生成し、ダウンロードを開始しました',
|
||||
profileCPU: 'CPU',
|
||||
profileHeap: 'ヒープ',
|
||||
profileGoroutine: 'Goroutine',
|
||||
profileMutex: 'ミューテックス',
|
||||
profileBlock: 'ブロック',
|
||||
runtimeRSSDesc: 'Go ヒープ、スレッドスタック、ネイティブメモリを含む、プロセスが使用中の物理メモリです。',
|
||||
runtimeHeapAllocDesc:
|
||||
'Go ヒープに割り当てられ、まだ解放されていないメモリです。継続的な増加はオブジェクトの滞留を示す場合があります。',
|
||||
runtimeHeapObjectsDesc:
|
||||
'Go ヒープ上の生存オブジェクト数です。継続的な増加はオブジェクトの滞留を示す場合があります。',
|
||||
runtimeGoroutineDesc: '現在の軽量な並行タスク数です。継続的な増加は Goroutine リークの確認が必要です。',
|
||||
profileCPUDesc: '選択した期間の CPU 呼び出しスタックを取得し、CPU を消費する関数を特定します。',
|
||||
profileHeapDesc: '使用中の Go ヒープオブジェクトと割り当てスタックを記録し、メモリ増加を調査します。',
|
||||
profileGoroutineDesc: 'すべての Goroutine の状態と呼び出しスタックを保存し、同じ呼び出し経路を集約します。',
|
||||
profileMutexDesc: 'Mutex 競合による待機時間を測定し、ロック競合のホットスポットを特定します。',
|
||||
profileBlockDesc: 'Channel、Select、同期プリミティブでのブロック待機時間を測定します。',
|
||||
},
|
||||
terminal: {
|
||||
local: 'ローカル',
|
||||
|
||||
@@ -1879,6 +1879,37 @@ const message = {
|
||||
down: '다운',
|
||||
interval: '수집 간격',
|
||||
intervalHelper: '적절한 모니터링 수집 간격을 입력하세요 (10초 - 12시간)',
|
||||
runtimeDiagnostics: '런타임 진단',
|
||||
runtimeRSS: '프로세스 메모리 (RSS)',
|
||||
runtimeHeapAlloc: '힙 메모리',
|
||||
runtimeHeapObjects: '힙 객체',
|
||||
runtimeGoroutines: 'Goroutine 수',
|
||||
goroutineSnapshot: 'Goroutine 스냅샷',
|
||||
goroutineSummary: 'Goroutine {0}개를 호출 스택 {1}개로 그룹화',
|
||||
goroutineTruncated: '스냅샷이 너무 커서 일부 호출 스택만 표시됩니다.',
|
||||
captureSnapshot: '스냅샷 새로고침',
|
||||
count: '수량',
|
||||
topFunction: '최상위 함수',
|
||||
manualCapture: '수동 캡처',
|
||||
startCapture: '캡처 시작',
|
||||
capturing: '캡처 중',
|
||||
captureSuccess: '진단 파일이 생성되어 다운로드를 시작했습니다',
|
||||
profileCPU: 'CPU',
|
||||
profileHeap: '힙',
|
||||
profileGoroutine: 'Goroutine',
|
||||
profileMutex: '뮤텍스',
|
||||
profileBlock: '블록',
|
||||
runtimeRSSDesc: 'Go 힙, 스레드 스택 및 네이티브 메모리를 포함해 프로세스가 사용하는 실제 메모리입니다.',
|
||||
runtimeHeapAllocDesc:
|
||||
'Go 힙에 할당되어 아직 해제되지 않은 메모리입니다. 지속적인 증가는 객체가 유지되고 있음을 나타낼 수 있습니다.',
|
||||
runtimeHeapObjectsDesc:
|
||||
'Go 힙의 현재 활성 객체 수입니다. 지속적인 증가는 객체가 유지되고 있음을 나타낼 수 있습니다.',
|
||||
runtimeGoroutineDesc: '현재 경량 동시 작업 수입니다. 지속적으로 증가하면 Goroutine 누수를 확인해야 합니다.',
|
||||
profileCPUDesc: '선택한 시간 동안 CPU 호출 스택을 샘플링하여 CPU를 소비하는 함수를 찾습니다.',
|
||||
profileHeapDesc: '사용 중인 Go 힙 객체와 할당 스택을 기록하여 지속적인 메모리 증가를 조사합니다.',
|
||||
profileGoroutineDesc: '모든 Goroutine의 상태와 호출 스택을 저장하고 동일한 호출 경로를 그룹화합니다.',
|
||||
profileMutexDesc: 'Mutex 경합으로 인한 대기 시간을 측정하여 잠금 경합 지점을 찾습니다.',
|
||||
profileBlockDesc: 'Channel, Select 및 동기화 프리미티브의 차단 대기 시간을 측정합니다.',
|
||||
},
|
||||
terminal: {
|
||||
local: '로컬',
|
||||
|
||||
@@ -1929,6 +1929,41 @@ const message = {
|
||||
down: 'Turun',
|
||||
interval: 'Selang Kumpulan',
|
||||
intervalHelper: 'Sila masukkan selang kumpulan pemantauan yang sesuai (10 saat - 12 jam)',
|
||||
runtimeDiagnostics: 'Diagnostik Masa Jalan',
|
||||
runtimeRSS: 'Memori Proses (RSS)',
|
||||
runtimeHeapAlloc: 'Memori Heap',
|
||||
runtimeHeapObjects: 'Objek Heap',
|
||||
runtimeGoroutines: 'Goroutine',
|
||||
goroutineSnapshot: 'Petikan Goroutine',
|
||||
goroutineSummary: '{0} goroutine dikumpulkan kepada {1} tindanan panggilan',
|
||||
goroutineTruncated: 'Petikan terlalu besar. Hanya sebahagian tindanan panggilan dipaparkan.',
|
||||
captureSnapshot: 'Segarkan Petikan',
|
||||
count: 'Bilangan',
|
||||
topFunction: 'Fungsi Teratas',
|
||||
manualCapture: 'Tangkapan Manual',
|
||||
startCapture: 'Mula Tangkap',
|
||||
capturing: 'Sedang Menangkap',
|
||||
captureSuccess: 'Fail diagnostik dijana; muat turun dimulakan',
|
||||
profileCPU: 'CPU',
|
||||
profileHeap: 'Heap',
|
||||
profileGoroutine: 'Goroutine',
|
||||
profileMutex: 'Mutex',
|
||||
profileBlock: 'Sekatan',
|
||||
runtimeRSSDesc: 'Memori fizikal yang digunakan proses, termasuk heap Go, tindanan thread dan memori natif.',
|
||||
runtimeHeapAllocDesc:
|
||||
'Memori yang diperuntukkan pada heap Go dan belum dilepaskan. Pertumbuhan berterusan mungkin menunjukkan objek tertahan.',
|
||||
runtimeHeapObjectsDesc:
|
||||
'Bilangan objek hidup dalam heap Go. Pertumbuhan berterusan mungkin menunjukkan objek tertahan.',
|
||||
runtimeGoroutineDesc:
|
||||
'Bilangan tugas serentak ringan semasa. Pertumbuhan berterusan mungkin menunjukkan kebocoran Goroutine.',
|
||||
profileCPUDesc:
|
||||
'Mengambil sampel tindanan panggilan CPU untuk tempoh dipilih bagi mencari fungsi yang menggunakan CPU.',
|
||||
profileHeapDesc:
|
||||
'Merekod objek heap Go yang masih digunakan dan tindanan peruntukannya untuk menyiasat pertumbuhan memori.',
|
||||
profileGoroutineDesc:
|
||||
'Menyimpan keadaan dan tindanan panggilan setiap Goroutine serta mengumpulkan laluan yang sama.',
|
||||
profileMutexDesc: 'Mengukur masa menunggu akibat persaingan mutex untuk mencari titik persaingan kunci.',
|
||||
profileBlockDesc: 'Mengukur sekatan pada Channel, Select dan primitif penyegerakan.',
|
||||
},
|
||||
terminal: {
|
||||
local: 'Tempatan',
|
||||
|
||||
@@ -1937,6 +1937,41 @@ const message = {
|
||||
down: 'Para baixo',
|
||||
interval: 'Intervalo de Coleta',
|
||||
intervalHelper: 'Insira um intervalo de coleta de monitoramento apropriado (10 segundos - 12 horas)',
|
||||
runtimeDiagnostics: 'Diagnóstico de execução',
|
||||
runtimeRSS: 'Memória do processo (RSS)',
|
||||
runtimeHeapAlloc: 'Memória do heap',
|
||||
runtimeHeapObjects: 'Objetos do heap',
|
||||
runtimeGoroutines: 'Goroutines',
|
||||
goroutineSnapshot: 'Instantâneo de Goroutines',
|
||||
goroutineSummary: '{0} goroutines agrupadas em {1} pilhas de chamadas',
|
||||
goroutineTruncated: 'O instantâneo é muito grande. Apenas parte das pilhas de chamadas é exibida.',
|
||||
captureSnapshot: 'Atualizar instantâneo',
|
||||
count: 'Quantidade',
|
||||
topFunction: 'Função principal',
|
||||
manualCapture: 'Captura manual',
|
||||
startCapture: 'Iniciar captura',
|
||||
capturing: 'Capturando',
|
||||
captureSuccess: 'Arquivo de diagnóstico gerado; download iniciado',
|
||||
profileCPU: 'CPU',
|
||||
profileHeap: 'Heap',
|
||||
profileGoroutine: 'Goroutine',
|
||||
profileMutex: 'Mutex',
|
||||
profileBlock: 'Bloqueio',
|
||||
runtimeRSSDesc:
|
||||
'Memória física ocupada pelo processo, incluindo heap do Go, pilhas de threads e memória nativa.',
|
||||
runtimeHeapAllocDesc:
|
||||
'Memória alocada no heap do Go que ainda não foi liberada. Crescimento contínuo pode indicar objetos retidos.',
|
||||
runtimeHeapObjectsDesc:
|
||||
'Número de objetos vivos no heap do Go. Crescimento contínuo pode indicar objetos retidos.',
|
||||
runtimeGoroutineDesc:
|
||||
'Tarefas concorrentes leves atuais. Crescimento contínuo geralmente indica possível vazamento de goroutines.',
|
||||
profileCPUDesc:
|
||||
'Amostra pilhas de CPU pelo período selecionado para localizar funções que realmente consomem CPU.',
|
||||
profileHeapDesc:
|
||||
'Registra objetos do heap ainda em uso e suas pilhas de alocação para investigar crescimento de memória.',
|
||||
profileGoroutineDesc: 'Salva o estado e a pilha de cada goroutine e agrupa caminhos de chamada idênticos.',
|
||||
profileMutexDesc: 'Mede o tempo de espera causado por contenção de mutex para localizar pontos de disputa.',
|
||||
profileBlockDesc: 'Mede bloqueios em canais, select e primitivas de sincronização.',
|
||||
},
|
||||
terminal: {
|
||||
local: 'Local',
|
||||
|
||||
@@ -1920,6 +1920,38 @@ const message = {
|
||||
down: 'Входящий',
|
||||
interval: 'Интервал Сбора',
|
||||
intervalHelper: 'Пожалуйста, введите подходящий интервал сбора мониторинга (10 секунд - 12 часов)',
|
||||
runtimeDiagnostics: 'Диагностика среды выполнения',
|
||||
runtimeRSS: 'Память процесса (RSS)',
|
||||
runtimeHeapAlloc: 'Память кучи',
|
||||
runtimeHeapObjects: 'Объекты кучи',
|
||||
runtimeGoroutines: 'Goroutine',
|
||||
goroutineSnapshot: 'Снимок Goroutine',
|
||||
goroutineSummary: '{0} goroutine сгруппированы в {1} стеков вызовов',
|
||||
goroutineTruncated: 'Снимок слишком большой. Показана только часть стеков вызовов.',
|
||||
captureSnapshot: 'Обновить снимок',
|
||||
count: 'Количество',
|
||||
topFunction: 'Верхняя функция',
|
||||
manualCapture: 'Ручной сбор',
|
||||
startCapture: 'Начать сбор',
|
||||
capturing: 'Идёт сбор',
|
||||
captureSuccess: 'Диагностический файл создан; скачивание началось',
|
||||
profileCPU: 'CPU',
|
||||
profileHeap: 'Куча',
|
||||
profileGoroutine: 'Goroutine',
|
||||
profileMutex: 'Mutex',
|
||||
profileBlock: 'Блокировки',
|
||||
runtimeRSSDesc: 'Физическая память процесса, включая кучу Go, стеки потоков и нативную память.',
|
||||
runtimeHeapAllocDesc:
|
||||
'Выделенная, но ещё не освобождённая память кучи Go. Постоянный рост может указывать на удерживаемые объекты.',
|
||||
runtimeHeapObjectsDesc:
|
||||
'Количество живых объектов в куче Go. Постоянный рост может указывать на удерживаемые объекты.',
|
||||
runtimeGoroutineDesc:
|
||||
'Текущее число лёгких параллельных задач. Постоянный рост может указывать на утечку goroutine.',
|
||||
profileCPUDesc: 'Собирает стеки CPU за выбранное время для поиска функций, реально потребляющих процессор.',
|
||||
profileHeapDesc: 'Записывает используемые объекты кучи и стеки их выделения для анализа роста памяти.',
|
||||
profileGoroutineDesc: 'Сохраняет состояние и стек каждой goroutine и группирует одинаковые пути вызовов.',
|
||||
profileMutexDesc: 'Измеряет ожидание из-за конкуренции mutex для поиска горячих блокировок.',
|
||||
profileBlockDesc: 'Измеряет блокировки на каналах, select и примитивах синхронизации.',
|
||||
},
|
||||
terminal: {
|
||||
local: 'Локальный',
|
||||
|
||||
@@ -1927,6 +1927,36 @@ const message = {
|
||||
down: 'Aşağı',
|
||||
interval: 'Toplama Aralığı',
|
||||
intervalHelper: 'Lütfen uygun bir izleme toplama aralığı girin (10 saniye - 12 saat)',
|
||||
runtimeDiagnostics: 'Çalışma Zamanı Tanılama',
|
||||
runtimeRSS: 'İşlem Belleği (RSS)',
|
||||
runtimeHeapAlloc: 'Heap Belleği',
|
||||
runtimeHeapObjects: 'Heap Nesneleri',
|
||||
runtimeGoroutines: 'Goroutine',
|
||||
goroutineSnapshot: 'Goroutine Anlık Görüntüsü',
|
||||
goroutineSummary: '{0} goroutine, {1} çağrı yığını altında gruplandı',
|
||||
goroutineTruncated: 'Anlık görüntü çok büyük. Çağrı yığınlarının yalnızca bir kısmı gösteriliyor.',
|
||||
captureSnapshot: 'Anlık Görüntüyü Yenile',
|
||||
count: 'Sayı',
|
||||
topFunction: 'Üst İşlev',
|
||||
manualCapture: 'Manuel Yakalama',
|
||||
startCapture: 'Yakalamayı Başlat',
|
||||
capturing: 'Yakalanıyor',
|
||||
captureSuccess: 'Tanılama dosyası oluşturuldu; indirme başladı',
|
||||
profileCPU: 'CPU',
|
||||
profileHeap: 'Heap',
|
||||
profileGoroutine: 'Goroutine',
|
||||
profileMutex: 'Mutex',
|
||||
profileBlock: 'Engelleme',
|
||||
runtimeRSSDesc: 'Go heap, iş parçacığı yığınları ve yerel bellek dahil işlemin kullandığı fiziksel bellek.',
|
||||
runtimeHeapAllocDesc:
|
||||
'Go heap üzerinde ayrılmış ve henüz serbest bırakılmamış bellek. Sürekli artış tutulan nesneleri gösterebilir.',
|
||||
runtimeHeapObjectsDesc: 'Go heap üzerindeki canlı nesne sayısı. Sürekli artış tutulan nesneleri gösterebilir.',
|
||||
runtimeGoroutineDesc: 'Geçerli hafif eşzamanlı görevler. Sürekli artış goroutine sızıntısını gösterebilir.',
|
||||
profileCPUDesc: 'CPU tüketen işlevleri bulmak için seçilen süre boyunca CPU çağrı yığınlarını örnekler.',
|
||||
profileHeapDesc: 'Bellek artışını incelemek için kullanılan heap nesnelerini ve ayırma yığınlarını kaydeder.',
|
||||
profileGoroutineDesc: 'Her goroutine durumunu ve çağrı yığınını kaydeder, aynı çağrı yollarını gruplar.',
|
||||
profileMutexDesc: 'Kilit çekişmesi noktalarını bulmak için mutex bekleme süresini ölçer.',
|
||||
profileBlockDesc: 'Channel, select ve eşzamanlama araçlarındaki engelleme süresini ölçer.',
|
||||
},
|
||||
terminal: {
|
||||
local: 'Yerel',
|
||||
|
||||
@@ -1793,6 +1793,35 @@ const message = {
|
||||
down: '下行',
|
||||
interval: '採集間隔',
|
||||
intervalHelper: '請輸入合適的監控採集時間間隔(10秒 - 12小時)',
|
||||
runtimeDiagnostics: '執行階段診斷',
|
||||
runtimeRSS: '程序記憶體 (RSS)',
|
||||
runtimeHeapAlloc: '堆積記憶體',
|
||||
runtimeHeapObjects: '堆積物件',
|
||||
runtimeGoroutines: 'Goroutine 數量',
|
||||
goroutineSnapshot: 'Goroutine 快照',
|
||||
goroutineSummary: '共 {0} 個 Goroutine,彙總為 {1} 組呼叫堆疊',
|
||||
goroutineTruncated: '快照內容過大,目前僅顯示部分呼叫堆疊。',
|
||||
captureSnapshot: '重新整理快照',
|
||||
count: '數量',
|
||||
topFunction: '頂層函式',
|
||||
manualCapture: '手動擷取',
|
||||
startCapture: '開始擷取',
|
||||
capturing: '正在擷取',
|
||||
captureSuccess: '診斷檔案已產生,已開始下載',
|
||||
profileCPU: 'CPU',
|
||||
profileHeap: '堆積記憶體',
|
||||
profileGoroutine: 'Goroutine',
|
||||
profileMutex: '互斥鎖',
|
||||
profileBlock: '阻塞',
|
||||
runtimeRSSDesc: '程序目前佔用的實體記憶體,包括 Go 堆積、執行緒堆疊及原生記憶體。',
|
||||
runtimeHeapAllocDesc: 'Go 堆積中已配置且尚未釋放的記憶體,持續增長可能表示物件未釋放。',
|
||||
runtimeHeapObjectsDesc: 'Go 堆積中目前存活的物件數量,持續增長可能表示物件未釋放。',
|
||||
runtimeGoroutineDesc: '目前輕量級並行工作數量,持續增長通常需要檢查 Goroutine 洩漏。',
|
||||
profileCPUDesc: '在指定時間內取樣 CPU 呼叫堆疊,用於定位實際消耗 CPU 的熱點函式。',
|
||||
profileHeapDesc: '記錄目前仍在使用的 Go 堆積物件及其配置呼叫堆疊,用於排查記憶體持續增長。',
|
||||
profileGoroutineDesc: '儲存所有 Goroutine 的狀態和呼叫堆疊,並依相同呼叫鏈彙總。',
|
||||
profileMutexDesc: '統計互斥鎖競爭造成的等待時間,用於定位鎖競爭熱點。',
|
||||
profileBlockDesc: '統計 Channel、Select 和同步原語的阻塞等待,用於定位長時間阻塞。',
|
||||
},
|
||||
terminal: {
|
||||
local: '本機',
|
||||
|
||||
@@ -1803,6 +1803,35 @@ const message = {
|
||||
down: '下行',
|
||||
interval: '采集间隔',
|
||||
intervalHelper: '请输入合适的监控采集时间间隔(10秒 - 12小时)',
|
||||
runtimeDiagnostics: '运行时诊断',
|
||||
runtimeRSS: '进程内存 (RSS)',
|
||||
runtimeHeapAlloc: '堆内存',
|
||||
runtimeHeapObjects: '堆对象',
|
||||
runtimeGoroutines: 'Goroutine 数量',
|
||||
goroutineSnapshot: 'Goroutine 快照',
|
||||
goroutineSummary: '共 {0} 个 Goroutine,聚合为 {1} 组调用栈',
|
||||
goroutineTruncated: '快照内容过大,当前仅展示部分调用栈。',
|
||||
captureSnapshot: '刷新快照',
|
||||
count: '数量',
|
||||
topFunction: '顶层函数',
|
||||
manualCapture: '手动抓取',
|
||||
startCapture: '开始抓取',
|
||||
capturing: '正在抓取',
|
||||
captureSuccess: '诊断文件已生成,已开始下载',
|
||||
profileCPU: 'CPU',
|
||||
profileHeap: '堆内存',
|
||||
profileGoroutine: 'Goroutine',
|
||||
profileMutex: '互斥锁',
|
||||
profileBlock: '阻塞',
|
||||
runtimeRSSDesc: '进程当前占用的物理内存,包括 Go 堆、线程栈及原生内存。',
|
||||
runtimeHeapAllocDesc: 'Go 堆中已分配且尚未释放的内存,持续增长可能表示对象未释放。',
|
||||
runtimeHeapObjectsDesc: 'Go 堆中当前存活的对象数量,持续增长可能表示对象未释放。',
|
||||
runtimeGoroutineDesc: '当前轻量级并发任务数量,持续增长通常需要检查 Goroutine 泄漏。',
|
||||
profileCPUDesc: '在指定时间内采样 CPU 调用栈,用于定位实际消耗 CPU 的热点函数。',
|
||||
profileHeapDesc: '记录当前仍在使用的 Go 堆对象及其分配调用栈,用于排查内存持续增长。',
|
||||
profileGoroutineDesc: '保存所有 Goroutine 的状态和调用栈,并按相同调用链聚合。',
|
||||
profileMutexDesc: '统计互斥锁竞争造成的等待时间,用于定位锁竞争热点。',
|
||||
profileBlockDesc: '统计 Channel、Select 和同步原语的阻塞等待,用于定位长时间阻塞。',
|
||||
},
|
||||
terminal: {
|
||||
local: '本机',
|
||||
|
||||
@@ -0,0 +1,379 @@
|
||||
<template>
|
||||
<div>
|
||||
<DrawerPro
|
||||
v-model="open"
|
||||
:header="$t('monitor.runtimeDiagnostics')"
|
||||
size="large"
|
||||
:confirm-before-close="captureLoading"
|
||||
@before-close="handleBeforeClose"
|
||||
>
|
||||
<div
|
||||
v-loading="captureLoading"
|
||||
element-loading-lock
|
||||
:element-loading-text="$t('monitor.capturing')"
|
||||
class="diagnostics-content"
|
||||
>
|
||||
<section>
|
||||
<el-row :gutter="12">
|
||||
<el-col v-for="item in summaryCards" :key="item.label" :xs="12" :sm="8" :md="6">
|
||||
<div class="summary-item">
|
||||
<el-card class="summary-title">
|
||||
<span>{{ item.label }}</span>
|
||||
<el-tooltip :content="item.description" placement="top" :show-after="200">
|
||||
<el-icon class="summary-help"><QuestionFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
<div class="summary-value">{{ item.value }}</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</section>
|
||||
|
||||
<el-divider />
|
||||
|
||||
<section>
|
||||
<div class="section-title">{{ $t('monitor.manualCapture') }}</div>
|
||||
<div class="profile-toolbar">
|
||||
<el-select
|
||||
v-model="captureForm.type"
|
||||
class="profile-type"
|
||||
popper-class="runtime-profile-select-popper"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in profileTypes"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
>
|
||||
<div class="runtime-profile-option">
|
||||
<span class="runtime-profile-option-title">{{ item.label }}</span>
|
||||
<span class="runtime-profile-option-description">{{ item.description }}</span>
|
||||
</div>
|
||||
</el-option>
|
||||
</el-select>
|
||||
<el-input-number
|
||||
v-if="showDuration"
|
||||
v-model="captureForm.duration"
|
||||
:min="5"
|
||||
:max="30"
|
||||
controls-position="right"
|
||||
/>
|
||||
<span v-if="showDuration" class="text-sm">{{ $t('commons.units.secondUnit') }}</span>
|
||||
<el-button type="primary" @click="captureProfile">
|
||||
{{ $t('monitor.startCapture') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<el-divider />
|
||||
|
||||
<section>
|
||||
<div class="section-header">
|
||||
<div>
|
||||
<div class="section-title">{{ $t('monitor.goroutineSnapshot') }}</div>
|
||||
<span class="goroutine-summary">
|
||||
{{
|
||||
$t('monitor.goroutineSummary', [
|
||||
goroutineSnapshot.total,
|
||||
goroutineSnapshot.groupCount,
|
||||
])
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<el-tooltip :content="$t('monitor.captureSnapshot')" placement="top">
|
||||
<el-button
|
||||
icon="Refresh"
|
||||
text
|
||||
circle
|
||||
:loading="goroutineLoading"
|
||||
:aria-label="$t('monitor.captureSnapshot')"
|
||||
@click="loadGoroutines"
|
||||
/>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
|
||||
<el-alert
|
||||
v-if="goroutineSnapshot.truncated"
|
||||
class="snapshot-warning"
|
||||
type="warning"
|
||||
:title="$t('monitor.goroutineTruncated')"
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
|
||||
<ComplexTable :data="goroutineSnapshot.goroutines" v-loading="goroutineLoading" max-height="420">
|
||||
<el-table-column prop="count" :label="$t('monitor.count')" width="90" sortable />
|
||||
<el-table-column prop="state" :label="$t('commons.table.status')" width="180" />
|
||||
<el-table-column
|
||||
prop="top"
|
||||
:label="$t('monitor.topFunction')"
|
||||
min-width="360"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column :label="$t('commons.table.operate')" fixed="right" width="90">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" @click="showGoroutineStack(scope.row)">
|
||||
{{ $t('commons.button.view') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</ComplexTable>
|
||||
</section>
|
||||
</div>
|
||||
</DrawerPro>
|
||||
|
||||
<el-dialog
|
||||
v-model="stackDialogVisible"
|
||||
:title="$t('monitor.goroutineSnapshot')"
|
||||
width="70%"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
>
|
||||
<pre class="stack-view">{{ selectedStack.join('\n') }}</pre>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
import {
|
||||
createRuntimeProfile,
|
||||
loadRuntimeDiagnosticsSummary,
|
||||
loadRuntimeGoroutines,
|
||||
RuntimeProfileDownloadError,
|
||||
} from '@/api/modules/host';
|
||||
import { Host } from '@/api/interface/host';
|
||||
import { useGlobalStore } from '@/composables/useGlobalStore';
|
||||
import { computeSize } from '@/utils/size';
|
||||
import i18n from '@/lang';
|
||||
import { MsgError, MsgSuccess } from '@/utils/message';
|
||||
|
||||
const { currentNode } = useGlobalStore();
|
||||
const open = ref(false);
|
||||
const captureLoading = ref(false);
|
||||
const goroutineLoading = ref(false);
|
||||
const stackDialogVisible = ref(false);
|
||||
const selectedStack = ref<string[]>([]);
|
||||
const summary = reactive<Host.RuntimeDiagnosticsSummary>({
|
||||
rss: 0,
|
||||
heapAlloc: 0,
|
||||
heapObjects: 0,
|
||||
goroutines: 0,
|
||||
});
|
||||
const goroutineSnapshot = reactive<Host.RuntimeGoroutineSnapshot>({
|
||||
total: 0,
|
||||
groupCount: 0,
|
||||
truncated: false,
|
||||
capturedAt: '',
|
||||
goroutines: [],
|
||||
});
|
||||
const captureForm = reactive<Host.RuntimeProfileCreate>({ type: 'cpu', duration: 15 });
|
||||
|
||||
const profileTypes = computed(() => [
|
||||
{
|
||||
label: i18n.global.t('monitor.profileCPU'),
|
||||
description: i18n.global.t('monitor.profileCPUDesc'),
|
||||
value: 'cpu',
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('monitor.profileHeap'),
|
||||
description: i18n.global.t('monitor.profileHeapDesc'),
|
||||
value: 'heap',
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('monitor.profileGoroutine'),
|
||||
description: i18n.global.t('monitor.profileGoroutineDesc'),
|
||||
value: 'goroutine',
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('monitor.profileMutex'),
|
||||
description: i18n.global.t('monitor.profileMutexDesc'),
|
||||
value: 'mutex',
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('monitor.profileBlock'),
|
||||
description: i18n.global.t('monitor.profileBlockDesc'),
|
||||
value: 'block',
|
||||
},
|
||||
]);
|
||||
|
||||
const showDuration = computed(() => captureForm.type !== 'heap' && captureForm.type !== 'goroutine');
|
||||
|
||||
const summaryCards = computed(() => [
|
||||
{
|
||||
label: i18n.global.t('monitor.runtimeRSS'),
|
||||
value: computeSize(summary.rss),
|
||||
description: i18n.global.t('monitor.runtimeRSSDesc'),
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('monitor.runtimeHeapAlloc'),
|
||||
value: computeSize(summary.heapAlloc),
|
||||
description: i18n.global.t('monitor.runtimeHeapAllocDesc'),
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('monitor.runtimeHeapObjects'),
|
||||
value: summary.heapObjects.toLocaleString(),
|
||||
description: i18n.global.t('monitor.runtimeHeapObjectsDesc'),
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('monitor.runtimeGoroutines'),
|
||||
value: summary.goroutines.toLocaleString(),
|
||||
description: i18n.global.t('monitor.runtimeGoroutineDesc'),
|
||||
},
|
||||
]);
|
||||
|
||||
const loadSummary = async () => {
|
||||
const res = await loadRuntimeDiagnosticsSummary(currentNode.value);
|
||||
Object.assign(summary, res.data);
|
||||
};
|
||||
|
||||
const loadGoroutines = async () => {
|
||||
goroutineLoading.value = true;
|
||||
try {
|
||||
const res = await loadRuntimeGoroutines(currentNode.value);
|
||||
Object.assign(goroutineSnapshot, res.data);
|
||||
} finally {
|
||||
goroutineLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const showGoroutineStack = (row: Host.RuntimeGoroutineGroup) => {
|
||||
selectedStack.value = row.stack || [];
|
||||
stackDialogVisible.value = true;
|
||||
};
|
||||
|
||||
const captureProfile = async () => {
|
||||
captureLoading.value = true;
|
||||
try {
|
||||
const data = await createRuntimeProfile(captureForm, currentNode.value);
|
||||
const url = window.URL.createObjectURL(data);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `${captureForm.type}-${Date.now()}.pb.gz`;
|
||||
link.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
MsgSuccess(i18n.global.t('monitor.captureSuccess'));
|
||||
await loadSummary();
|
||||
} catch (error) {
|
||||
if (error instanceof RuntimeProfileDownloadError) {
|
||||
MsgError(error.message || i18n.global.t('commons.res.commonError'));
|
||||
}
|
||||
} finally {
|
||||
captureLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const acceptParams = () => {
|
||||
open.value = true;
|
||||
Promise.all([loadSummary(), loadGoroutines()]);
|
||||
};
|
||||
|
||||
const handleBeforeClose = (done: () => void) => {
|
||||
if (!captureLoading.value) {
|
||||
done();
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({ acceptParams });
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.summary-item {
|
||||
padding: 4px 12px 4px 0;
|
||||
}
|
||||
.summary-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
.summary-help {
|
||||
flex-shrink: 0;
|
||||
color: var(--el-text-color-placeholder);
|
||||
cursor: help;
|
||||
}
|
||||
.summary-value {
|
||||
margin-top: 5px;
|
||||
color: var(--el-text-color-primary);
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.diagnostics-content {
|
||||
min-height: 520px;
|
||||
}
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-height: 32px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.section-title {
|
||||
margin-bottom: 10px;
|
||||
color: var(--el-text-color-primary);
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.section-header .section-title {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.profile-toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.profile-type {
|
||||
width: 160px;
|
||||
}
|
||||
.goroutine-summary {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
.snapshot-warning {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.stack-view {
|
||||
max-height: 65vh;
|
||||
margin: 0;
|
||||
padding: 14px;
|
||||
overflow: auto;
|
||||
border-radius: 4px;
|
||||
background: var(--el-fill-color-light);
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.runtime-profile-select-popper {
|
||||
min-width: min(560px, calc(100vw - 64px));
|
||||
}
|
||||
.runtime-profile-select-popper .el-select-dropdown__item {
|
||||
height: auto;
|
||||
min-height: 54px;
|
||||
padding-top: 6px;
|
||||
padding-bottom: 6px;
|
||||
line-height: normal;
|
||||
}
|
||||
.runtime-profile-option {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
white-space: normal;
|
||||
}
|
||||
.runtime-profile-option-title {
|
||||
color: var(--el-text-color-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
.runtime-profile-option-description {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
line-height: 17px;
|
||||
}
|
||||
</style>
|
||||
@@ -59,6 +59,7 @@
|
||||
|
||||
<OpDialog ref="opRef" @search="search" />
|
||||
<ProcessDetail ref="detailRef" />
|
||||
<RuntimeDiagnostics ref="runtimeDiagnosticsRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -71,6 +72,7 @@ import { stopProcess } from '@/api/modules/process';
|
||||
import { ProcessStore } from '@/store';
|
||||
import { SortBy, TableV2SortOrder, ElButton } from 'element-plus';
|
||||
import { useGlobalStore } from '@/composables/useGlobalStore';
|
||||
import RuntimeDiagnostics from './diagnostics/index.vue';
|
||||
|
||||
const { currentNode } = useGlobalStore();
|
||||
const processStore = ProcessStore();
|
||||
@@ -94,6 +96,7 @@ const sortState = ref<SortBy>({
|
||||
|
||||
const data = ref<any[]>([]);
|
||||
const detailRef = ref();
|
||||
const runtimeDiagnosticsRef = ref<InstanceType<typeof RuntimeDiagnostics>>();
|
||||
const filters = ref<string[]>([]);
|
||||
|
||||
const sortByNum = (a: any, b: any, prop: string): number => {
|
||||
@@ -186,7 +189,7 @@ const columns = ref([
|
||||
key: 'actions',
|
||||
title: i18n.global.t('commons.table.operate'),
|
||||
dataKey: 'actions',
|
||||
width: 300,
|
||||
width: 420,
|
||||
cellRenderer: ({ rowData }) => {
|
||||
const stopButton = h(
|
||||
ElButton,
|
||||
@@ -197,7 +200,7 @@ const columns = ref([
|
||||
() => i18n.global.t('process.stopProcess'),
|
||||
);
|
||||
|
||||
return h('div', { class: 'action-buttons' }, [
|
||||
const buttons = [
|
||||
h(
|
||||
ElButton,
|
||||
{
|
||||
@@ -206,8 +209,23 @@ const columns = ref([
|
||||
},
|
||||
() => i18n.global.t('process.viewDetails'),
|
||||
),
|
||||
permissionDirective ? withDirectives(stopButton, [[permissionDirective]]) : stopButton,
|
||||
]);
|
||||
];
|
||||
|
||||
if (rowData.name === '1panel-agent') {
|
||||
buttons.push(
|
||||
h(
|
||||
ElButton,
|
||||
{
|
||||
type: 'text',
|
||||
onClick: openRuntimeDiagnostics,
|
||||
},
|
||||
() => i18n.global.t('monitor.runtimeDiagnostics'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
buttons.push(permissionDirective ? withDirectives(stopButton, [[permissionDirective]]) : stopButton);
|
||||
return h('div', { class: 'action-buttons' }, buttons);
|
||||
},
|
||||
},
|
||||
]);
|
||||
@@ -250,6 +268,10 @@ const openDetail = (row: any) => {
|
||||
detailRef.value.acceptParams(row.PID);
|
||||
};
|
||||
|
||||
const openRuntimeDiagnostics = () => {
|
||||
runtimeDiagnosticsRef.value?.acceptParams();
|
||||
};
|
||||
|
||||
const changeSort = ({ key, order }) => {
|
||||
if (!order) order = TableV2SortOrder.ASC;
|
||||
sortState.value = { key, order };
|
||||
|
||||
Reference in New Issue
Block a user