mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/1Panel-dev/1Panel.git
synced 2026-09-20 08:03:55 +08:00
feat(core): add runtime diagnostics and pprof capture (#13852)
This commit is contained in:
@@ -9,6 +9,7 @@ type ApiGroup struct {
|
||||
var ApiGroupApp = new(ApiGroup)
|
||||
|
||||
var (
|
||||
runtimeDiagnosticsService = service.NewIRuntimeDiagnosticsService()
|
||||
authService = service.NewIAuthService()
|
||||
backupService = service.NewIBackupService()
|
||||
settingService = service.NewISettingService()
|
||||
|
||||
63
core/app/api/v2/runtime_diagnostics.go
Normal file
63
core/app/api/v2/runtime_diagnostics.go
Normal 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()
|
||||
}
|
||||
30
core/app/dto/runtime_diagnostics.go
Normal file
30
core/app/dto/runtime_diagnostics.go
Normal 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"`
|
||||
}
|
||||
411
core/app/service/runtime_diagnostics.go
Normal file
411
core/app/service/runtime_diagnostics.go
Normal 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
|
||||
}
|
||||
@@ -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": [
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -9,5 +9,6 @@ func commonGroups() []CommonRouter {
|
||||
&CommandRouter{},
|
||||
&GroupRouter{},
|
||||
&ScriptRouter{},
|
||||
&RuntimeDiagnosticsRouter{},
|
||||
}
|
||||
}
|
||||
|
||||
21
core/router/ro_runtime_diagnostics.go
Normal file
21
core/router/ro_runtime_diagnostics.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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, {
|
||||
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;
|
||||
|
||||
@@ -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()]);
|
||||
};
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
Reference in New Issue
Block a user