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:
1 parent
8420691438
commit
e8b5679cf4
8 files changed
+442
No files matched your search
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user