Adds GET /api/v1/admin/ops/ws/requests — a fan-out WebSocket that pushes
per-request metadata (method, path, model, account_id, status, latency_ms)
to all connected admin clients the moment each gateway dispatch completes.
- service/request_event_bus.go: lock-free pub/sub with non-blocking drop
when per-subscriber buffer (64 slots) is full; nil-safe Publish
- service/request_event_bus_test.go: 6 tests (basic, fanout, drop, nil, close)
- GatewayHandler: records reqStartTime at entry; defer emits RequestEvent on
every return; sets status success/error/rate_limited in both Gemini and
Anthropic dispatch paths
- OpsHandler: accepts *RequestEventBus; wires it to RequestStreamWSHandler
- ops_ws_requests_handler.go: subscribes to bus, pushes JSON per event,
reuses existing upgrader/conn-limit/ping-pong infrastructure
- Route: ws.GET("/requests", ...) alongside existing /ws/qps
- wire_gen.go: requestEventBus shared between OpsHandler and GatewayHandler
76 lines
1.8 KiB
Go
76 lines
1.8 KiB
Go
package service
|
|
|
|
import (
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
)
|
|
|
|
const requestEventBufSize = 64
|
|
|
|
// RequestEvent is published for every gateway dispatch completion.
|
|
type RequestEvent struct {
|
|
Timestamp time.Time `json:"timestamp"`
|
|
Method string `json:"method"`
|
|
Path string `json:"path"`
|
|
Model string `json:"model"`
|
|
AccountID int64 `json:"account_id"`
|
|
// Status is "success", "error", or "rate_limited".
|
|
Status string `json:"status"`
|
|
LatencyMS int64 `json:"latency_ms"`
|
|
}
|
|
|
|
// RequestEventBus is a fan-out hub for real-time request events.
|
|
// Publishers call Publish; subscribers call Subscribe/Unsubscribe.
|
|
// Each subscriber gets its own buffered channel. If the buffer is full
|
|
// the event is dropped for that subscriber (non-blocking publish).
|
|
type RequestEventBus struct {
|
|
mu sync.RWMutex
|
|
subscribers map[uint64]chan RequestEvent
|
|
nextID atomic.Uint64
|
|
}
|
|
|
|
func NewRequestEventBus() *RequestEventBus {
|
|
return &RequestEventBus{
|
|
subscribers: make(map[uint64]chan RequestEvent),
|
|
}
|
|
}
|
|
|
|
// Subscribe registers a new subscriber and returns its ID and a receive-only channel.
|
|
func (b *RequestEventBus) Subscribe() (uint64, <-chan RequestEvent) {
|
|
id := b.nextID.Add(1)
|
|
ch := make(chan RequestEvent, requestEventBufSize)
|
|
b.mu.Lock()
|
|
b.subscribers[id] = ch
|
|
b.mu.Unlock()
|
|
return id, ch
|
|
}
|
|
|
|
// Unsubscribe removes a subscriber and closes its channel.
|
|
func (b *RequestEventBus) Unsubscribe(id uint64) {
|
|
b.mu.Lock()
|
|
ch, ok := b.subscribers[id]
|
|
if ok {
|
|
delete(b.subscribers, id)
|
|
}
|
|
b.mu.Unlock()
|
|
if ok {
|
|
close(ch)
|
|
}
|
|
}
|
|
|
|
// Publish sends an event to all current subscribers without blocking.
|
|
func (b *RequestEventBus) Publish(e RequestEvent) {
|
|
if b == nil {
|
|
return
|
|
}
|
|
b.mu.RLock()
|
|
defer b.mu.RUnlock()
|
|
for _, ch := range b.subscribers {
|
|
select {
|
|
case ch <- e:
|
|
default:
|
|
}
|
|
}
|
|
}
|