// Package telescope یک بازرسِ سبکِ درخواست‌ها است (شبیهِ Laravel Telescope). // آخرین N درخواست را در یک رینگ‌بافرِ حافظه نگه می‌دارد: متد، مسیر، وضعیت، // مدت‌زمان، IP و در صورتِ خطا (>=۴۰۰) بدنه‌ی درخواست. بدونِ هیچ وابستگیِ خارجی // و با سرباری ناچیز؛ برای پایشِ سریع روی سرورِ کم‌منابع مناسب است. package telescope import ( "bufio" "bytes" "encoding/json" "io" "net" "net/http" "strings" "sync" "time" ) // Entry یک درخواستِ ثبت‌شده را نگه می‌دارد. type Entry struct { ID int64 `json:"id"` Time time.Time `json:"time"` Method string `json:"method"` Path string `json:"path"` Status int `json:"status"` Duration int64 `json:"duration_ms"` IP string `json:"ip"` Bytes int `json:"bytes"` Body string `json:"body,omitempty"` // فقط برای خطاها (>=۴۰۰) و POST/PUT } // Telescope رینگ‌بافرِ هم‌زمان‌امنِ درخواست‌ها. type Telescope struct { mu sync.Mutex buf []Entry size int next int64 // شناسه‌ی افزایشی full bool head int // اندیسِ نوشتنِ بعدی } // New یک بازرس با ظرفیتِ size می‌سازد (حداقل ۱). func New(size int) *Telescope { if size < 1 { size = 200 } return &Telescope{buf: make([]Entry, size), size: size} } func (t *Telescope) add(e Entry) { t.mu.Lock() t.next++ e.ID = t.next t.buf[t.head] = e t.head = (t.head + 1) % t.size if t.head == 0 { t.full = true } t.mu.Unlock() } // Entries آخرین درخواست‌ها را به‌ترتیبِ جدید-به-قدیم برمی‌گرداند. func (t *Telescope) Entries() []Entry { t.mu.Lock() defer t.mu.Unlock() n := t.head if t.full { n = t.size } out := make([]Entry, 0, n) // از جدیدترین (درست قبلِ head) عقب می‌رویم. for i := 0; i < n; i++ { idx := (t.head - 1 - i + t.size*2) % t.size out = append(out, t.buf[idx]) } return out } // statusRecorder کدِ وضعیت و حجمِ پاسخ را می‌گیرد. type statusRecorder struct { http.ResponseWriter status int bytes int } func (r *statusRecorder) WriteHeader(code int) { r.status = code r.ResponseWriter.WriteHeader(code) } func (r *statusRecorder) Write(b []byte) (int, error) { if r.status == 0 { r.status = http.StatusOK } n, err := r.ResponseWriter.Write(b) r.bytes += n return n, err } // Hijack برای WebSocket لازم است تا میدل‌ور مانعِ ارتقاء نشود. func (r *statusRecorder) Hijack() (net.Conn, *bufio.ReadWriter, error) { if h, ok := r.ResponseWriter.(http.Hijacker); ok { return h.Hijack() } return nil, nil, http.ErrNotSupported } // Middleware هر درخواستِ /api و /admin را ثبت می‌کند (به‌جز خودِ صفحه‌ی تلسکوپ). func (t *Telescope) Middleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { p := req.URL.Path if p == "/ws" || strings.HasPrefix(p, "/admin/telescope") || strings.HasPrefix(p, "/cards/") || strings.HasPrefix(p, "/carpets/") { next.ServeHTTP(w, req) return } var body string if (req.Method == http.MethodPost || req.Method == http.MethodPut) && req.Body != nil { b, _ := io.ReadAll(io.LimitReader(req.Body, 4096)) req.Body = io.NopCloser(bytes.NewReader(b)) body = maskSecrets(string(b)) } rec := &statusRecorder{ResponseWriter: w} start := time.Now() next.ServeHTTP(rec, req) dur := time.Since(start) e := Entry{ Time: start, Method: req.Method, Path: p, Status: rec.status, Duration: dur.Milliseconds(), IP: req.RemoteAddr, Bytes: rec.bytes, } // بدنه را فقط برای خطاها نگه می‌داریم تا حافظه/حریمِ خصوصی حفظ شود. if rec.status >= 400 { e.Body = body } t.add(e) }) } // maskSecrets مقادیرِ حساس (توکن، کد، پسورد) را در بدنه‌ی JSON پنهان می‌کند. func maskSecrets(s string) string { for _, k := range []string{"password", "token", "code", "otp", "signature"} { s = maskField(s, k) } return s } func maskField(s, key string) string { needle := `"` + key + `"` i := strings.Index(s, needle) if i < 0 { return s } // دنبالِ ": " و سپس مقدار می‌گردیم. j := strings.Index(s[i:], ":") if j < 0 { return s } rest := s[i+j+1:] rest = strings.TrimLeft(rest, " ") if strings.HasPrefix(rest, `"`) { end := strings.Index(rest[1:], `"`) if end >= 0 { masked := s[:i+j+1] + `"***"` + rest[end+2:] return masked } } return s } // Data آخرین درخواست‌ها را به‌صورتِ JSON برمی‌گرداند. func (t *Telescope) Data(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8") _ = json.NewEncoder(w).Encode(t.Entries()) } // Page یک صفحه‌ی HTML سبک برای مرورِ درخواست‌ها می‌دهد. func (t *Telescope) Page(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") _, _ = io.WriteString(w, pageHTML) }