Files
chat_room/internal/monitoring/store.go
T
mobiniqow 1dc96e86fc Add admin user moderation: block/unblock, reports, matches list, photo review
- User blocking (with reason) now closes the auth gap immediately — a
  blocked user's still-valid access token stops working on the next
  request, not just on their next OTP/refresh.
- Admin-created reports (moderation notes) per user, viewable in a history
  list independent of block status.
- GET /admin/users: full paginated, searchable user list with report counts.
- GET /admin/matches: all matches across the app ("friends" who mutually
  liked each other), for admin visibility.
- GET/DELETE /admin/photos: paginated feed of every uploaded photo with
  uploader info, so admins can review and remove content without going
  user-by-user.
- Per-user activity: monitoring requests can now be filtered by user_id, so
  a user's detail page can show their recent API activity.
- Batched the profile-selections query in fullProfilePayload (was 6 queries
  per view, now 1) for consistency with the earlier N+1 fixes.
2026-09-20 05:30:14 +03:30

158 lines
4.4 KiB
Go

// 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(),
}
}