Add production hardening: monitoring, backups, caching, and high-traffic resilience
- Recovery middleware (no more crash-on-panic), request metrics/monitoring store + admin API, in-memory TTL caching for read-heavy option/question/ category lookups, gzip + rate-limiting middleware, sanitized chat message encoding, and pg_dump-based database backups with an admin API. - Fix N+1 queries in the discover feed, candidate detail, and match list endpoints by batching selection/photo/user/last-message lookups into single queries instead of one-per-item loops. - Tune DB connection pooling and add HTTP server timeouts + graceful shutdown so the API degrades gracefully instead of falling over under load. - Fix a null (vs empty array) JSON response for candidates/matches without photos or interests, which crashed the web app's swipe card.
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
// 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
|
||||
}
|
||||
|
||||
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(),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user