Files
developerandClaude Sonnet 5 f77b89a779 Improve agent-facing history endpoints (Phase 1)
- Fix /api/history/{type} to search the full persistent store instead of
  only the bounded in-memory ring buffer, so filtered history isn't
  silently truncated.
- Add structured type/since/until filters to /api/history/search and
  /api/history/wait, alongside the existing free-text query.
- Add GET /api/traces/{traceId} to fetch all entries for a trace in one
  call instead of relying on substring search.
- Rate-limit /api/history/wait per client IP.
- Add docs/PLAN.md tracking the phased improvement plan; update README
  and agents_example.md for the new/changed endpoints.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 09:30:18 +00:00

82 lines
1.8 KiB
Go

package collector
import (
"net"
"net/http"
"strings"
"sync"
"time"
"golang.org/x/time/rate"
)
// ipRateLimiter tracks a token-bucket rate limiter per client IP, so a single
// caller can't monopolize a blocking endpoint like /api/history/wait while
// leaving other clients unaffected. Entries are evicted after being idle.
type ipRateLimiter struct {
rps rate.Limit
burst int
mu sync.Mutex
limiters map[string]*rate.Limiter
lastSeen map[string]time.Time
}
func newIPRateLimiter(rps rate.Limit, burst int) *ipRateLimiter {
l := &ipRateLimiter{
rps: rps,
burst: burst,
limiters: make(map[string]*rate.Limiter),
lastSeen: make(map[string]time.Time),
}
go l.evictLoop()
return l
}
// Allow reports whether a request from ip may proceed.
func (l *ipRateLimiter) Allow(ip string) bool {
l.mu.Lock()
limiter, ok := l.limiters[ip]
if !ok {
limiter = rate.NewLimiter(l.rps, l.burst)
l.limiters[ip] = limiter
}
l.lastSeen[ip] = time.Now()
l.mu.Unlock()
return limiter.Allow()
}
func (l *ipRateLimiter) evictLoop() {
ticker := time.NewTicker(10 * time.Minute)
defer ticker.Stop()
for range ticker.C {
cutoff := time.Now().Add(-10 * time.Minute)
l.mu.Lock()
for ip, seen := range l.lastSeen {
if seen.Before(cutoff) {
delete(l.limiters, ip)
delete(l.lastSeen, ip)
}
}
l.mu.Unlock()
}
}
// clientIP extracts the caller's IP for rate limiting purposes, preferring
// X-Forwarded-For (set by the ingress/proxy in front of this service) and
// falling back to the raw connection address.
func clientIP(r *http.Request) string {
if fwd := r.Header.Get("X-Forwarded-For"); fwd != "" {
if idx := strings.IndexByte(fwd, ','); idx != -1 {
return fwd[:idx]
}
return fwd
}
host, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
return r.RemoteAddr
}
return host
}