server/internal/handlers/webhook_watchtower.go
Rama main · réplica de lectura del repositorio original.
package handlers
import (
"encoding/json"
"io"
"log"
"net/http"
"regexp"
"strings"
"time"
"blackbox/server/internal/hub"
"blackbox/shared/types"
"github.com/oklog/ulid/v2"
"gorm.io/gorm"
)
type watchtowerPayload struct {
Title string `json:"Title"`
Message string `json:"Message"`
Level string `json:"Level"`
}
var watchtowerParensPattern = regexp.MustCompile(`\([^)]*\)`)
func WebhookWatchtower(database *gorm.DB, h *hub.Hub, incidentCh chan<- types.Entry, shutdown <-chan struct{}) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, 1<<20))
if err != nil {
RecordWebhookDelivery(database, "watchtower", "", "", "error", "failed to read payload")
writeError(w, http.StatusBadRequest, "failed to read request body")
return
}
var payload watchtowerPayload
snippet := payloadSnippet(body)
if err := json.Unmarshal(body, &payload); err != nil {
RecordWebhookDelivery(database, "watchtower", snippet, "", "error", "malformed JSON")
writeError(w, http.StatusBadRequest, "invalid JSON")
return
}
if payload.Message == "" {
RecordWebhookDelivery(database, "watchtower", snippet, "", "ignored", "Message is required")
writeError(w, http.StatusBadRequest, "Message is required")
return
}
meta := make(map[string]interface{})
if payload.Title != "" {
meta["watchtower.title"] = payload.Title
}
if payload.Level != "" {
meta["watchtower.level"] = payload.Level
}
updatedServices := extractWatchtowerServices(payload.Message)
if len(updatedServices) > 0 {
meta["watchtower.services"] = updatedServices
}
metaBytes, err := json.Marshal(meta)
if err != nil {
log.Printf("failed to marshal metadata for watchtower: %v, meta: %+v", err, meta)
metaBytes = []byte("{}")
}
serviceName := "watchtower"
entry := types.Entry{
ID: ulid.Make().String(),
Timestamp: time.Now().UTC(),
NodeName: "webhook",
Source: "webhook",
Service: serviceName,
Event: "update",
Content: payload.Message,
Metadata: string(metaBytes),
}
if err := database.Create(&entry).Error; err != nil {
RecordWebhookDelivery(database, "watchtower", snippet, "", "error", "failed to save entry")
writeError(w, http.StatusInternalServerError, "failed to save entry")
return
}
dispatchToIncidentChannelWithShutdown(incidentCh, shutdown, entry)
if h != nil {
if msg := MarshalWSMessage("entry", entry); msg != nil {
h.Broadcast(msg)
}
}
RecordWebhookDelivery(database, "watchtower", snippet, "", "processed", "")
w.WriteHeader(http.StatusCreated)
}
}
func extractWatchtowerServices(message string) []string {
message = strings.TrimSpace(message)
if message == "" {
return nil
}
candidateSection := message
idx := strings.Index(candidateSection, ":")
if idx < 0 {
return nil
}
candidateSection = candidateSection[idx+1:]
candidateSection = strings.TrimSpace(candidateSection)
if candidateSection == "" {
return nil
}
parts := strings.FieldsFunc(candidateSection, func(r rune) bool {
return r == ',' || r == '\n' || r == ';'
})
servicesSeen := make(map[string]struct{}, len(parts))
servicesList := make([]string, 0, len(parts))
for _, part := range parts {
part = watchtowerParensPattern.ReplaceAllString(part, "")
part = strings.TrimSpace(strings.Trim(part, `"'[]`))
if part == "" {
continue
}
normalized := strings.ToLower(strings.TrimSpace(part))
if normalized == "" {
continue
}
if _, ok := servicesSeen[normalized]; ok {
continue
}
servicesSeen[normalized] = struct{}{}
servicesList = append(servicesList, normalized)
}
return servicesList
}