feat(admin): add full-chain trace and performance monitoring

- Backend: trace.Logger ring-buffer for request chain events
- Backend: perf.Monitor with runtime.MemStats sampling every 10s
- Backend: /api/v1/admin/trace and /api/v1/admin/perf endpoints
- Frontend: TracePage — timeline with hop icons and status dots
- Frontend: PerformancePage — memory/GC bar charts + live stats
- Admin nav expanded to 11 items
This commit is contained in:
Aska committed 2026-08-12 17:16:33 +08:00
1 parent 8420691438
commit e8b5679cf4
8 files changed
+442

No files matched your search

+104
View File
@@ -0,0 +1,104 @@
// Package perf provides lightweight performance metrics sampling.
package perf
import (
"runtime"
"sync"
"time"
)
// Snapshot captures a moment-in-time system performance reading.
type Snapshot struct {
Timestamp time.Time `json:"timestamp"`
AllocMB float64 `json:"alloc_mb"`
SysMB float64 `json:"sys_mb"`
NumGC uint32 `json:"num_gc"`
Goroutines int `json:"goroutines"`
HeapObj uint64 `json:"heap_objects"`
}
// Monitor samples runtime.MemStats periodically and keeps a history.
type Monitor struct {
mu sync.RWMutex
history []Snapshot
max int
index int
stopCh chan struct{}
interval time.Duration
}
func bytesToMB(b uint64) float64 { return float64(b) / 1024 / 1024 }
// NewMonitor creates a performance monitor that samples every `interval`.
func NewMonitor(maxSamples int, interval time.Duration) *Monitor {
return &Monitor{
history: make([]Snapshot, maxSamples),
max: maxSamples,
stopCh: make(chan struct{}),
interval: interval,
}
}
// Start begins periodic sampling.
func (m *Monitor) Start() {
go func() {
ticker := time.NewTicker(m.interval)
defer ticker.Stop()
for {
select {
case <-m.stopCh:
return
case <-ticker.C:
m.sample()
}
}
}()
}
// Stop stops sampling.
func (m *Monitor) Stop() { close(m.stopCh) }
func (m *Monitor) sample() {
var ms runtime.MemStats
runtime.ReadMemStats(&ms)
s := Snapshot{
Timestamp: time.Now(),
AllocMB: bytesToMB(ms.Alloc),
SysMB: bytesToMB(ms.Sys),
NumGC: ms.NumGC,
Goroutines: runtime.NumGoroutine(),
HeapObj: ms.HeapObjects,
}
m.mu.Lock()
m.history[m.index%m.max] = s
m.index++
m.mu.Unlock()
}
// Latest returns the most recent snapshot.
func (m *Monitor) Latest() Snapshot {
m.mu.RLock()
defer m.mu.RUnlock()
if m.index == 0 {
m.sample() // Sample once on first call
}
return m.history[(m.index-1+m.max)%m.max]
}
// History returns the sampled history.
func (m *Monitor) History() []Snapshot {
m.mu.RLock()
defer m.mu.RUnlock()
n := m.index
if n > m.max { n = m.max }
result := make([]Snapshot, n)
for i := 0; i < n; i++ {
idx := (m.index - 1 - i + m.max) % m.max
if idx < m.max {
result[n-1-i] = m.history[idx]
}
}
return result
}