chore: merge backend repo history into monorepo at backend/
git-subtree-dir: backend git-subtree-mainline:dc11190527git-subtree-split:acae976622
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
// Package monitoring یک ذخیرهساز سبک و درونحافظهای برای دیدبانی درخواستهاست:
|
||||
// آخرین درخواستها (برای دیباگ لحظهای) + آمار تجمیعی (بار، زمان پاسخ، نرخ خطا).
|
||||
// عمداً بدون وابستگی بیرونی (بدون Prometheus/Redis) چون در این مقیاس یک حلقهی
|
||||
// درونحافظهای thread-safe کافیه؛ اگه بعداً مقیاس بزرگتر شد، همین Store رو میشه
|
||||
// پشت یک exporter واقعی گذاشت.
|
||||
package monitoring
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const bufferSize = 1000
|
||||
|
||||
type RequestRecord struct {
|
||||
Time time.Time `json:"time"`
|
||||
Method string `json:"method"`
|
||||
Path string `json:"path"`
|
||||
StatusCode int `json:"status_code"`
|
||||
DurationMs float64 `json:"duration_ms"`
|
||||
ClientIP string `json:"client_ip"`
|
||||
UserID *uint `json:"user_id,omitempty"`
|
||||
}
|
||||
|
||||
type Summary struct {
|
||||
TotalRequests int64 `json:"total_requests"`
|
||||
TotalErrors int64 `json:"total_errors"`
|
||||
ErrorRatePct float64 `json:"error_rate_pct"`
|
||||
AvgLatencyMs float64 `json:"avg_latency_ms"`
|
||||
P95LatencyMs float64 `json:"p95_latency_ms"`
|
||||
RequestsLastMin int `json:"requests_last_min"`
|
||||
StartedAt time.Time `json:"started_at"`
|
||||
UptimeSeconds float64 `json:"uptime_seconds"`
|
||||
}
|
||||
|
||||
// Store یک حلقهی ثابتاندازه (ring buffer) از آخرین درخواستها بههمراه
|
||||
// شمارندههای تجمیعیه. نوشتن/خواندن با RWMutex محافظت میشه؛ هزینهی هر
|
||||
// درخواست فقط یک قفل کوتاه و بدون I/O روی دیسکه (سریع، مناسب مسیر داغ).
|
||||
type Store struct {
|
||||
mu sync.RWMutex
|
||||
buf []RequestRecord
|
||||
next int
|
||||
count int
|
||||
total int64
|
||||
totalErr int64
|
||||
sumLatency float64
|
||||
startedAt time.Time
|
||||
}
|
||||
|
||||
func NewStore() *Store {
|
||||
return &Store{
|
||||
buf: make([]RequestRecord, bufferSize),
|
||||
startedAt: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Store) Record(rec RequestRecord) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
s.buf[s.next] = rec
|
||||
s.next = (s.next + 1) % bufferSize
|
||||
if s.count < bufferSize {
|
||||
s.count++
|
||||
}
|
||||
s.total++
|
||||
s.sumLatency += rec.DurationMs
|
||||
if rec.StatusCode >= 400 {
|
||||
s.totalErr++
|
||||
}
|
||||
}
|
||||
|
||||
// Recent آخرین n درخواست رو برمیگردونه (جدیدترین اول).
|
||||
func (s *Store) Recent(n int) []RequestRecord {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
if n > s.count {
|
||||
n = s.count
|
||||
}
|
||||
out := make([]RequestRecord, 0, n)
|
||||
idx := s.next - 1
|
||||
for i := 0; i < n; i++ {
|
||||
if idx < 0 {
|
||||
idx = bufferSize - 1
|
||||
}
|
||||
out = append(out, s.buf[idx])
|
||||
idx--
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// RecentByUser مثل Recent ولی فقط درخواستهای یک کاربر مشخص رو برمیگردونه
|
||||
// (برای دیدن فعالیت اخیر یک کاربر خاص از صفحهی جزئیاتش توی پنل ادمین).
|
||||
// کل بافر رو میگرده (نه فقط آخرین n)، چون فیلتر شده و ممکنه درخواستهای آن
|
||||
// کاربر پراکنده باشن.
|
||||
func (s *Store) RecentByUser(userID uint, n int) []RequestRecord {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
out := make([]RequestRecord, 0, n)
|
||||
idx := s.next - 1
|
||||
for i := 0; i < s.count && len(out) < n; i++ {
|
||||
if idx < 0 {
|
||||
idx = bufferSize - 1
|
||||
}
|
||||
rec := s.buf[idx]
|
||||
if rec.UserID != nil && *rec.UserID == userID {
|
||||
out = append(out, rec)
|
||||
}
|
||||
idx--
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (s *Store) Summary() Summary {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
var errRate, avgLatency float64
|
||||
if s.total > 0 {
|
||||
errRate = float64(s.totalErr) / float64(s.total) * 100
|
||||
avgLatency = s.sumLatency / float64(s.total)
|
||||
}
|
||||
|
||||
cutoff := time.Now().Add(-1 * time.Minute)
|
||||
lastMin := 0
|
||||
latencies := make([]float64, 0, s.count)
|
||||
for i := 0; i < s.count; i++ {
|
||||
r := s.buf[i]
|
||||
if r.Time.After(cutoff) {
|
||||
lastMin++
|
||||
}
|
||||
latencies = append(latencies, r.DurationMs)
|
||||
}
|
||||
sort.Float64s(latencies)
|
||||
p95 := 0.0
|
||||
if len(latencies) > 0 {
|
||||
idx := int(float64(len(latencies)) * 0.95)
|
||||
if idx >= len(latencies) {
|
||||
idx = len(latencies) - 1
|
||||
}
|
||||
p95 = latencies[idx]
|
||||
}
|
||||
|
||||
return Summary{
|
||||
TotalRequests: s.total,
|
||||
TotalErrors: s.totalErr,
|
||||
ErrorRatePct: errRate,
|
||||
AvgLatencyMs: avgLatency,
|
||||
P95LatencyMs: p95,
|
||||
RequestsLastMin: lastMin,
|
||||
StartedAt: s.startedAt,
|
||||
UptimeSeconds: time.Since(s.startedAt).Seconds(),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package monitoring
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestStore_RecordAndSummary(t *testing.T) {
|
||||
s := NewStore()
|
||||
|
||||
s.Record(RequestRecord{Time: time.Now(), Method: "GET", Path: "/x", StatusCode: 200, DurationMs: 10})
|
||||
s.Record(RequestRecord{Time: time.Now(), Method: "GET", Path: "/x", StatusCode: 500, DurationMs: 20})
|
||||
|
||||
sum := s.Summary()
|
||||
if sum.TotalRequests != 2 {
|
||||
t.Fatalf("expected 2 total requests, got %d", sum.TotalRequests)
|
||||
}
|
||||
if sum.TotalErrors != 1 {
|
||||
t.Fatalf("expected 1 error (status>=400), got %d", sum.TotalErrors)
|
||||
}
|
||||
if sum.ErrorRatePct != 50 {
|
||||
t.Fatalf("expected 50%% error rate, got %v", sum.ErrorRatePct)
|
||||
}
|
||||
if sum.AvgLatencyMs != 15 {
|
||||
t.Fatalf("expected avg latency 15ms, got %v", sum.AvgLatencyMs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStore_RecentOrder(t *testing.T) {
|
||||
s := NewStore()
|
||||
s.Record(RequestRecord{Time: time.Now(), Path: "/1", StatusCode: 200})
|
||||
s.Record(RequestRecord{Time: time.Now(), Path: "/2", StatusCode: 200})
|
||||
s.Record(RequestRecord{Time: time.Now(), Path: "/3", StatusCode: 200})
|
||||
|
||||
recent := s.Recent(10)
|
||||
if len(recent) != 3 {
|
||||
t.Fatalf("expected 3 records, got %d", len(recent))
|
||||
}
|
||||
// جدیدترین باید اول باشه
|
||||
if recent[0].Path != "/3" || recent[1].Path != "/2" || recent[2].Path != "/1" {
|
||||
t.Fatalf("expected newest-first order, got %+v", recent)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStore_RecentLimit(t *testing.T) {
|
||||
s := NewStore()
|
||||
for i := 0; i < 5; i++ {
|
||||
s.Record(RequestRecord{Time: time.Now(), Path: "/x", StatusCode: 200})
|
||||
}
|
||||
if got := len(s.Recent(2)); got != 2 {
|
||||
t.Fatalf("expected limit of 2, got %d", got)
|
||||
}
|
||||
if got := len(s.Recent(100)); got != 5 {
|
||||
t.Fatalf("expected all 5 when limit exceeds count, got %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStore_RingBufferWraparound(t *testing.T) {
|
||||
s := NewStore()
|
||||
// بیشتر از ظرفیت بافر ثبت میکنیم تا رفتار wraparound رو تست کنیم
|
||||
for i := 0; i < bufferSize+50; i++ {
|
||||
s.Record(RequestRecord{Time: time.Now(), Path: "/x", StatusCode: 200, DurationMs: 1})
|
||||
}
|
||||
|
||||
sum := s.Summary()
|
||||
if sum.TotalRequests != int64(bufferSize+50) {
|
||||
t.Fatalf("expected total requests to keep counting past buffer size, got %d", sum.TotalRequests)
|
||||
}
|
||||
|
||||
recent := s.Recent(bufferSize + 100)
|
||||
if len(recent) != bufferSize {
|
||||
t.Fatalf("expected Recent to be capped at bufferSize (%d), got %d", bufferSize, len(recent))
|
||||
}
|
||||
}
|
||||
|
||||
func TestStore_RecentByUser(t *testing.T) {
|
||||
s := NewStore()
|
||||
uidA := uint(1)
|
||||
uidB := uint(2)
|
||||
|
||||
s.Record(RequestRecord{Time: time.Now(), Path: "/a1", UserID: &uidA})
|
||||
s.Record(RequestRecord{Time: time.Now(), Path: "/b1", UserID: &uidB})
|
||||
s.Record(RequestRecord{Time: time.Now(), Path: "/a2", UserID: &uidA})
|
||||
s.Record(RequestRecord{Time: time.Now(), Path: "/none"}) // بدون کاربر (مثل OTP)
|
||||
|
||||
got := s.RecentByUser(uidA, 10)
|
||||
if len(got) != 2 {
|
||||
t.Fatalf("expected 2 records for user A, got %d", len(got))
|
||||
}
|
||||
if got[0].Path != "/a2" || got[1].Path != "/a1" {
|
||||
t.Fatalf("expected newest-first order for user A, got %+v", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user