123 lines
2.6 KiB
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)
|
|
}
|
|
}
|