mirror of
https://github.com/MengMengCode/CLICD.git
synced 2026-08-08 06:24:44 +08:00
Support Debian 13.
#29 and fix useage chart data display problem, storage on database instead of localstorage.
This commit is contained in:
@@ -0,0 +1,242 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"clicd/internal/config"
|
||||
)
|
||||
|
||||
type ContainerMetricPoint struct {
|
||||
TS int64 `json:"ts"`
|
||||
CPU float64 `json:"cpu"`
|
||||
Memory float64 `json:"memory"`
|
||||
Network float64 `json:"network"`
|
||||
NetworkRx float64 `json:"network_rx"`
|
||||
NetworkTx float64 `json:"network_tx"`
|
||||
DiskIO float64 `json:"disk_io"`
|
||||
DiskRead float64 `json:"disk_read"`
|
||||
DiskWrite float64 `json:"disk_write"`
|
||||
}
|
||||
|
||||
var containerMetricSamplerOnce sync.Once
|
||||
var containerMetricMu sync.RWMutex
|
||||
var containerMetricHistory = map[string][]ContainerMetricPoint{}
|
||||
var containerMetricInFlight sync.Map
|
||||
|
||||
const (
|
||||
containerMetricSampleInterval = 30 * time.Second
|
||||
containerMetricSampleTimeout = 20 * time.Second
|
||||
containerMetricConcurrency = 4
|
||||
)
|
||||
|
||||
func StartContainerMetricSampler() {
|
||||
containerMetricSamplerOnce.Do(func() {
|
||||
go func() {
|
||||
sampleAllContainerMetrics()
|
||||
ticker := time.NewTicker(containerMetricSampleInterval)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
sampleAllContainerMetrics()
|
||||
}
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
func sampleAllContainerMetrics() {
|
||||
containers, _ := listByRuntime()
|
||||
sem := make(chan struct{}, containerMetricConcurrency)
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for _, c := range containers {
|
||||
c := c
|
||||
if c.Status != "running" {
|
||||
continue
|
||||
}
|
||||
sem <- struct{}{}
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
defer func() { <-sem }()
|
||||
sampleContainerMetricWithTimeout(c)
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
pruneContainerMetricHistory()
|
||||
}
|
||||
|
||||
func sampleContainerMetricWithTimeout(c config.Container) {
|
||||
key := containerMetricKey(c)
|
||||
if key == "" {
|
||||
return
|
||||
}
|
||||
if _, loaded := containerMetricInFlight.LoadOrStore(key, struct{}{}); loaded {
|
||||
return
|
||||
}
|
||||
done := make(chan struct{}, 1)
|
||||
go func() {
|
||||
defer containerMetricInFlight.Delete(key)
|
||||
if usage, err := usageByRuntime(c.ID); err == nil {
|
||||
appendContainerMetricPoint(c, usage)
|
||||
}
|
||||
done <- struct{}{}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(containerMetricSampleTimeout):
|
||||
}
|
||||
}
|
||||
|
||||
func appendContainerMetricPoint(c config.Container, usage map[string]interface{}) {
|
||||
key := containerMetricKey(c)
|
||||
if key == "" {
|
||||
return
|
||||
}
|
||||
memoryTotal := numberFromUsage(usage, "memory_total_bytes")
|
||||
if memoryTotal <= 0 {
|
||||
memoryTotal = float64(c.RAMMB) * 1024 * 1024
|
||||
}
|
||||
memoryPct := 0.0
|
||||
if memoryTotal > 0 {
|
||||
memoryPct = clampPercent(numberFromUsage(usage, "memory_usage_bytes") / memoryTotal * 100)
|
||||
}
|
||||
vcpu := c.VCPU
|
||||
if vcpu <= 0 {
|
||||
vcpu = 1
|
||||
}
|
||||
cpuPct := clampPercent(numberFromUsage(usage, "cpu_usage_pct") / vcpu)
|
||||
networkRx := positiveNumberFromUsage(usage, "network_rx_bps")
|
||||
networkTx := positiveNumberFromUsage(usage, "network_tx_bps")
|
||||
diskRead := positiveNumberFromUsage(usage, "disk_read_bps")
|
||||
diskWrite := positiveNumberFromUsage(usage, "disk_write_bps")
|
||||
point := ContainerMetricPoint{
|
||||
TS: time.Now().UnixMilli(),
|
||||
CPU: cpuPct,
|
||||
Memory: memoryPct,
|
||||
NetworkRx: networkRx,
|
||||
NetworkTx: networkTx,
|
||||
Network: networkRx + networkTx,
|
||||
DiskRead: diskRead,
|
||||
DiskWrite: diskWrite,
|
||||
DiskIO: diskRead + diskWrite,
|
||||
}
|
||||
cutoff := time.Now().Add(-hostMetricRetention).UnixMilli()
|
||||
|
||||
containerMetricMu.Lock()
|
||||
defer containerMetricMu.Unlock()
|
||||
|
||||
history := containerMetricHistory[key]
|
||||
keepFrom := 0
|
||||
for keepFrom < len(history) && history[keepFrom].TS < cutoff {
|
||||
keepFrom++
|
||||
}
|
||||
if keepFrom > 0 {
|
||||
copy(history, history[keepFrom:])
|
||||
history = history[:len(history)-keepFrom]
|
||||
}
|
||||
containerMetricHistory[key] = append(history, point)
|
||||
}
|
||||
|
||||
func getContainerMetricHistory(c *config.Container) []ContainerMetricPoint {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
key := containerMetricKey(*c)
|
||||
containerMetricMu.RLock()
|
||||
defer containerMetricMu.RUnlock()
|
||||
|
||||
history := containerMetricHistory[key]
|
||||
result := make([]ContainerMetricPoint, len(history))
|
||||
copy(result, history)
|
||||
return result
|
||||
}
|
||||
|
||||
func pruneContainerMetricHistory() {
|
||||
cutoff := time.Now().Add(-hostMetricRetention).UnixMilli()
|
||||
valid := map[string]bool{}
|
||||
if config.AppConfig != nil {
|
||||
for _, c := range config.AppConfig.Containers {
|
||||
valid[containerMetricKey(c)] = true
|
||||
}
|
||||
}
|
||||
|
||||
containerMetricMu.Lock()
|
||||
defer containerMetricMu.Unlock()
|
||||
|
||||
for key, history := range containerMetricHistory {
|
||||
if !valid[key] {
|
||||
delete(containerMetricHistory, key)
|
||||
continue
|
||||
}
|
||||
keepFrom := 0
|
||||
for keepFrom < len(history) && history[keepFrom].TS < cutoff {
|
||||
keepFrom++
|
||||
}
|
||||
if keepFrom > 0 {
|
||||
copy(history, history[keepFrom:])
|
||||
containerMetricHistory[key] = history[:len(history)-keepFrom]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func containerMetricKey(c config.Container) string {
|
||||
if c.UUID != "" {
|
||||
return "uuid:" + c.UUID
|
||||
}
|
||||
if c.ID > 0 {
|
||||
return fmt.Sprintf("id:%d", c.ID)
|
||||
}
|
||||
if c.Name != "" {
|
||||
return "name:" + c.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func numberFromUsage(usage map[string]interface{}, key string) float64 {
|
||||
value, ok := usage[key]
|
||||
if !ok || value == nil {
|
||||
return 0
|
||||
}
|
||||
switch v := value.(type) {
|
||||
case float64:
|
||||
if math.IsNaN(v) || math.IsInf(v, 0) {
|
||||
return 0
|
||||
}
|
||||
return v
|
||||
case float32:
|
||||
return float64(v)
|
||||
case int:
|
||||
return float64(v)
|
||||
case int64:
|
||||
return float64(v)
|
||||
case int32:
|
||||
return float64(v)
|
||||
case uint:
|
||||
return float64(v)
|
||||
case uint64:
|
||||
return float64(v)
|
||||
case uint32:
|
||||
return float64(v)
|
||||
case json.Number:
|
||||
n, _ := v.Float64()
|
||||
return n
|
||||
case string:
|
||||
n, _ := strconv.ParseFloat(v, 64)
|
||||
return n
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func positiveNumberFromUsage(usage map[string]interface{}, key string) float64 {
|
||||
value := numberFromUsage(usage, key)
|
||||
if value < 0 {
|
||||
return 0
|
||||
}
|
||||
return value
|
||||
}
|
||||
Reference in New Issue
Block a user