ai-servers/llm-gateway/internal/webhook/webhook.go

123 lines
2.6 KiB
Go

package webhook
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"log"
"net/http"
"time"
"llm-gateway/internal/config"
)
// Event types.
const (
EventCircuitBreakerOpen = "circuit_breaker.open"
EventCircuitBreakerClosed = "circuit_breaker.closed"
EventBudgetThreshold = "budget.threshold"
)
// Event represents a webhook notification payload.
type Event struct {
Type string `json:"type"`
Timestamp time.Time `json:"timestamp"`
Data map[string]any `json:"data"`
}
// Notifier sends webhook notifications.
type Notifier struct {
webhooks []config.WebhookConfig
ch chan Event
done chan struct{}
client *http.Client
}
// NewNotifier creates a webhook notifier from config.
func NewNotifier(webhooks []config.WebhookConfig) *Notifier {
n := &Notifier{
webhooks: webhooks,
ch: make(chan Event, 100),
done: make(chan struct{}),
client: &http.Client{Timeout: 10 * time.Second},
}
go n.run()
return n
}
// Notify queues an event for delivery (non-blocking).
func (n *Notifier) Notify(evt Event) {
if evt.Timestamp.IsZero() {
evt.Timestamp = time.Now()
}
select {
case n.ch <- evt:
default:
log.Printf("WARNING: webhook channel full, dropping event %s", evt.Type)
}
}
// Close drains pending events and shuts down.
func (n *Notifier) Close() {
close(n.ch)
<-n.done
}
func (n *Notifier) run() {
defer close(n.done)
for evt := range n.ch {
for _, wh := range n.webhooks {
if !n.shouldSend(wh, evt.Type) {
continue
}
n.send(wh, evt)
}
}
}
func (n *Notifier) shouldSend(wh config.WebhookConfig, eventType string) bool {
if len(wh.Events) == 0 {
return true // no filter = send all
}
for _, e := range wh.Events {
if e == eventType {
return true
}
}
return false
}
func (n *Notifier) send(wh config.WebhookConfig, evt Event) {
body, err := json.Marshal(evt)
if err != nil {
log.Printf("ERROR: webhook marshal: %v", err)
return
}
req, err := http.NewRequest(http.MethodPost, wh.URL, bytes.NewReader(body))
if err != nil {
log.Printf("ERROR: webhook request: %v", err)
return
}
req.Header.Set("Content-Type", "application/json")
if wh.Secret != "" {
mac := hmac.New(sha256.New, []byte(wh.Secret))
mac.Write(body)
sig := hex.EncodeToString(mac.Sum(nil))
req.Header.Set("X-Webhook-Signature", "sha256="+sig)
}
resp, err := n.client.Do(req)
if err != nil {
log.Printf("WARNING: webhook delivery to %s failed: %v", wh.URL, err)
return
}
resp.Body.Close()
if resp.StatusCode >= 400 {
log.Printf("WARNING: webhook %s returned %d", wh.URL, resp.StatusCode)
}
}