Files
sub2api/backend/internal/securityaudit/prompt_metrics.go
T

146 lines
3.4 KiB
Go
Raw Normal View History

package securityaudit
import (
"sort"
"sync"
"sync/atomic"
"time"
)
const latencySampleCapacity = 2048
type AtomicMetrics struct {
total atomic.Int64
allowed atomic.Int64
flagged atomic.Int64
blocked atomic.Int64
unavailable atomic.Int64
invalid atomic.Int64
timeouts atomic.Int64
failovers atomic.Int64
bulkheadFull atomic.Int64
recordFailed atomic.Int64
latencyTotal atomic.Int64
latencyMax atomic.Int64
enqueued atomic.Int64
dropped atomic.Int64
latencyMu sync.RWMutex
latencies []int64
latencyNext int
}
func NewAtomicMetrics() *AtomicMetrics { return &AtomicMetrics{} }
func (m *AtomicMetrics) Snapshot() GuardMetricsSnapshot {
if m == nil {
return GuardMetricsSnapshot{}
}
snapshot := GuardMetricsSnapshot{
Total: m.total.Load(), Allowed: m.allowed.Load(), Flagged: m.flagged.Load(),
Blocked: m.blocked.Load(), Unavailable: m.unavailable.Load(), Invalid: m.invalid.Load(),
Timeouts: m.timeouts.Load(), Failovers: m.failovers.Load(), BulkheadFull: m.bulkheadFull.Load(),
RecordFailed: m.recordFailed.Load(), LatencyCount: m.total.Load(), LatencyMaxMS: m.latencyMax.Load(),
}
if snapshot.LatencyCount > 0 {
snapshot.LatencyAvgMS = m.latencyTotal.Load() / snapshot.LatencyCount
}
m.latencyMu.RLock()
samples := append([]int64(nil), m.latencies...)
m.latencyMu.RUnlock()
if len(samples) > 0 {
sort.Slice(samples, func(i, j int) bool { return samples[i] < samples[j] })
snapshot.LatencyP50MS = percentile(samples, 0.50)
snapshot.LatencyP95MS = percentile(samples, 0.95)
snapshot.LatencyP99MS = percentile(samples, 0.99)
}
return snapshot
}
func (m *AtomicMetrics) AuditSnapshot() AuditMetricsSnapshot {
if m == nil {
return AuditMetricsSnapshot{}
}
return AuditMetricsSnapshot{Enqueued: m.enqueued.Load(), Dropped: m.dropped.Load()}
}
func (m *AtomicMetrics) Observe(kind DecisionKind, latency time.Duration) {
if m == nil {
return
}
m.total.Add(1)
latencyMS := latency.Milliseconds()
if latencyMS < 0 {
latencyMS = 0
}
m.latencyTotal.Add(latencyMS)
for current := m.latencyMax.Load(); latencyMS > current && !m.latencyMax.CompareAndSwap(current, latencyMS); current = m.latencyMax.Load() {
}
m.latencyMu.Lock()
if len(m.latencies) < latencySampleCapacity {
m.latencies = append(m.latencies, latencyMS)
} else {
m.latencies[m.latencyNext] = latencyMS
m.latencyNext = (m.latencyNext + 1) % latencySampleCapacity
}
m.latencyMu.Unlock()
switch kind {
case DecisionFlag:
m.flagged.Add(1)
case DecisionBlock:
m.blocked.Add(1)
case DecisionUnavailable:
m.unavailable.Add(1)
case DecisionInvalid:
m.invalid.Add(1)
default:
m.allowed.Add(1)
}
}
func percentile(sorted []int64, quantile float64) int64 {
if len(sorted) == 0 {
return 0
}
index := int(float64(len(sorted)-1) * quantile)
if index < 0 {
index = 0
}
if index >= len(sorted) {
index = len(sorted) - 1
}
return sorted[index]
}
func (m *AtomicMetrics) IncEnqueued() {
if m != nil {
m.enqueued.Add(1)
}
}
func (m *AtomicMetrics) IncDropped() {
if m != nil {
m.dropped.Add(1)
}
}
func (m *AtomicMetrics) IncTimeout() {
if m != nil {
m.timeouts.Add(1)
}
}
func (m *AtomicMetrics) IncFailover() {
if m != nil {
m.failovers.Add(1)
}
}
func (m *AtomicMetrics) IncBulkheadFull() {
if m != nil {
m.bulkheadFull.Add(1)
}
}
func (m *AtomicMetrics) IncRecordFailed() {
if m != nil {
m.recordFailed.Add(1)
}
}