webhooks

package
v0.15.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 24, 2026 License: AGPL-3.0 Imports: 17 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func GenerateExternalID

func GenerateExternalID(title string, labels map[string]string) string

GenerateExternalID creates a stable external_id from title and labels when not provided.

Used by the Generic webhook provider when users don't specify an external_id. The generated ID is deterministic: same title + labels → same external_id.

Algorithm:

  1. Sort label keys alphabetically
  2. Create string: "title:{title}|labels:{k1}={v1}|{k2}={v2}..."
  3. SHA256 hash the string
  4. Return first 32 characters of hex hash

Example:

Title: "High CPU"
Labels: {"service": "api", "env": "prod"}
Result: "abc123..." (SHA256 of "title:High CPU|labels:env=prod|service=api")

func GetJSONSchema

func GetJSONSchema() map[string]interface{}

GetJSONSchema returns a JSON Schema document describing the generic webhook format.

This endpoint is served at GET /api/v1/webhooks/generic/schema to provide self-documenting API for users integrating with the generic webhook.

The schema can be used for:

  • Documentation generation
  • Client-side validation
  • IDE autocomplete

Types

type AlertmanagerAlert

type AlertmanagerAlert struct {
	Status       string            `json:"status" binding:"required,oneof=firing resolved"` // "firing" or "resolved"
	Labels       map[string]string `json:"labels" binding:"required"`                       // Alert labels (alertname, severity, instance, etc.)
	Annotations  map[string]string `json:"annotations"`                                     // Alert annotations (summary, description, etc.)
	StartsAt     time.Time         `json:"startsAt" binding:"required"`                     // ISO8601 timestamp when alert started
	EndsAt       time.Time         `json:"endsAt"`                                          // ISO8601 timestamp when alert ended (or zero for firing)
	GeneratorURL string            `json:"generatorURL" binding:"max=2048"`                 // Link to Prometheus expression browser
	Fingerprint  string            `json:"fingerprint" binding:"required,max=64"`           // Unique identifier for this alert (used for deduplication)
}

AlertmanagerAlert represents a single alert within an Alertmanager webhook payload

type AlertmanagerPayload

type AlertmanagerPayload struct {
	Version  string              `json:"version" binding:"max=10"`                        // Alertmanager version (e.g., "4")
	GroupKey string              `json:"groupKey" binding:"max=500"`                      // Unique key for alert group
	Status   string              `json:"status" binding:"required,oneof=firing resolved"` // "firing" or "resolved"
	Receiver string              `json:"receiver" binding:"max=200"`                      // Name of configured receiver
	Alerts   []AlertmanagerAlert `json:"alerts" binding:"required,min=1,max=100,dive"`    // Array of alerts in this notification (max 100 per webhook)
}

AlertmanagerPayload represents the top-level webhook payload sent by Prometheus Alertmanager Spec: https://prometheus.io/docs/alerting/latest/configuration/#webhook_config

type CloudWatchAlarm

type CloudWatchAlarm struct {
	// AlarmName is the human-readable alarm name (e.g., "HighCPU")
	AlarmName string `json:"AlarmName" binding:"required"`

	// AlarmDescription provides context about the alarm (optional)
	AlarmDescription string `json:"AlarmDescription"`

	// NewStateValue is the current alarm state
	// Values: "ALARM" (threshold breached), "OK" (normal), "INSUFFICIENT_DATA" (not enough data)
	NewStateValue string `json:"NewStateValue" binding:"required,oneof=ALARM OK INSUFFICIENT_DATA"`

	// NewStateReason explains why the state changed
	// Example: "Threshold Crossed: 1 datapoint [95.2 (01/01/24 00:00:00)] was greater than the threshold (80.0)"
	NewStateReason string `json:"NewStateReason" binding:"required"`

	// StateChangeTime is when the alarm changed state (ISO 8601)
	StateChangeTime string `json:"StateChangeTime" binding:"required"`

	// Region is the AWS region (e.g., "us-east-1")
	Region string `json:"Region"`

	// AlarmArn is the globally unique identifier for this alarm
	// Example: "arn:aws:cloudwatch:us-east-1:123456789012:alarm:HighCPU"
	AlarmArn string `json:"AlarmArn" binding:"required"`

	// OldStateValue is the previous alarm state (before this transition)
	OldStateValue string `json:"OldStateValue"`

	// Trigger contains metric details
	Trigger CloudWatchTrigger `json:"Trigger"`
}

CloudWatchAlarm represents the alarm data within the SNS Message field.

This is the actual CloudWatch alarm payload, JSON-encoded inside SNS Message field. We parse this after validating the SNS envelope.

Spec: https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/AlarmThatSendsEmail.html

type CloudWatchDimension

type CloudWatchDimension struct {
	Name  string `json:"name"`
	Value string `json:"value"`
}

CloudWatchDimension is a key-value tag for a metric.

type CloudWatchProvider

type CloudWatchProvider struct{}

CloudWatchProvider implements WebhookProvider for AWS CloudWatch alarms via SNS.

CloudWatch does not send webhooks directly - it uses SNS (Simple Notification Service) as a transport layer. This provider handles:

  1. SNS envelope parsing and signature verification
  2. Automatic SNS subscription confirmation (zero-config setup)
  3. CloudWatch alarm JSON extraction from SNS Message field
  4. Normalization of CloudWatch states to firing/resolved

Field Mapping:

  • Title: AlarmName
  • Description: NewStateReason (why alarm triggered)
  • Severity: ALARM→critical, INSUFFICIENT_DATA→info (OK→resolved, doesn't create alert)
  • Status: ALARM/INSUFFICIENT_DATA→firing, OK→resolved
  • ExternalID: AlarmArn (globally unique)
  • Labels: region, namespace, metric_name, dimensions (flattened)
  • Annotations: alarm_description, state_change_time, comparison, threshold

func (*CloudWatchProvider) ParsePayload

func (c *CloudWatchProvider) ParsePayload(body []byte) ([]NormalizedAlert, error)

ParsePayload handles both SNS subscription confirmation and CloudWatch alarm notifications.

Message flow:

  1. First webhook: Type="SubscriptionConfirmation" → Auto-confirm by HTTP GET to SubscribeURL
  2. Subsequent webhooks: Type="Notification" → Extract CloudWatch alarm from Message field

For notifications, we parse the CloudWatch alarm JSON and normalize it.

func (*CloudWatchProvider) Source

func (c *CloudWatchProvider) Source() string

Source returns "cloudwatch"

func (*CloudWatchProvider) ValidatePayload

func (c *CloudWatchProvider) ValidatePayload(body []byte, headers http.Header) error

ValidatePayload verifies the SNS message signature.

AWS SNS signs messages with RSA-SHA1 using X.509 certificates. We validate to ensure the message actually came from AWS and wasn't forged.

Validation steps:

  1. Verify SigningCertURL is HTTPS and from *.amazonaws.com domain (security)
  2. Download the X.509 certificate from SigningCertURL
  3. Extract RSA public key from certificate
  4. Reconstruct the canonical message string (specific fields in specific order)
  5. Verify RSA-SHA1 signature matches

Spec: https://docs.aws.amazon.com/sns/latest/dg/sns-verify-signature-of-message.html

type CloudWatchTrigger

type CloudWatchTrigger struct {
	// MetricName is the CloudWatch metric being monitored (e.g., "CPUUtilization")
	MetricName string `json:"MetricName"`

	// Namespace is the AWS service namespace (e.g., "AWS/EC2", "AWS/RDS")
	Namespace string `json:"Namespace"`

	// StatisticType is the aggregation method (e.g., "Statistic", "Metric")
	StatisticType string `json:"StatisticType"`

	// Statistic is the aggregation function (e.g., "Average", "Sum", "Maximum")
	Statistic string `json:"Statistic"`

	// Dimensions are key-value tags for the metric
	// Example: [{"name": "InstanceId", "value": "i-0123456789abcdef"}]
	Dimensions []CloudWatchDimension `json:"Dimensions"`

	// Period is the evaluation window in seconds
	Period int `json:"Period"`

	// EvaluationPeriods is how many periods must breach to trigger alarm
	EvaluationPeriods int `json:"EvaluationPeriods"`

	// ComparisonOperator is the threshold comparison (e.g., "GreaterThanThreshold")
	ComparisonOperator string `json:"ComparisonOperator"`

	// Threshold is the numeric threshold value
	Threshold float64 `json:"Threshold"`

	// TreatMissingData defines behavior for missing data (e.g., "notBreaching")
	TreatMissingData string `json:"TreatMissingData"`

	// EvaluateLowSampleCountPercentile for percentile-based alarms
	EvaluateLowSampleCountPercentile string `json:"EvaluateLowSampleCountPercentile"`
}

CloudWatchTrigger contains metric metadata for the alarm.

type GenericAlert

type GenericAlert struct {
	// Title is the only required field - a short summary of the alert
	Title string `json:"title" binding:"required,min=1,max=500"`

	// Description provides detailed context (optional)
	Description string `json:"description" binding:"max=5000"`

	// Severity must be "critical", "warning", or "info" (defaults to "warning")
	Severity string `json:"severity" binding:"omitempty,oneof=critical warning info"`

	// Status must be "firing" or "resolved" (defaults to "firing")
	Status string `json:"status" binding:"omitempty,oneof=firing resolved"`

	// ExternalID is a unique identifier for deduplication (auto-generated if not provided)
	// If provided, it must be unique within this source. If omitted, we generate a stable
	// ID from SHA256(title + sorted labels).
	ExternalID string `json:"external_id" binding:"omitempty,max=255"`

	// Labels are key-value pairs for filtering, grouping, and routing (optional)
	Labels map[string]string `json:"labels"`

	// Annotations are additional metadata not used for routing (optional)
	Annotations map[string]string `json:"annotations"`

	// StartedAt is when the alert started firing (defaults to current time)
	StartedAt *time.Time `json:"started_at"`

	// EndedAt is when the alert was resolved (nil for firing alerts)
	// Only valid if Status == "resolved"
	EndedAt *time.Time `json:"ended_at"`
}

GenericAlert represents a single alert in the generic webhook format.

Minimal example:

{"title": "High CPU on web-01"}

Full example:

{
  "title": "High Error Rate",
  "description": "Error rate > 5% on api-gateway",
  "severity": "critical",
  "status": "firing",
  "external_id": "custom-123",
  "labels": {"service": "api-gateway", "env": "production"},
  "annotations": {"runbook_url": "https://wiki.example.com/runbooks/error-rate"},
  "started_at": "2024-01-01T00:00:00Z",
  "ended_at": "2024-01-01T00:05:00Z"
}

type GenericProvider

type GenericProvider struct {
	// WebhookSecret is the shared secret for HMAC verification (optional)
	// If empty, signature verification is disabled
	WebhookSecret string
}

GenericProvider implements WebhookProvider for the Fluidify Regen-native generic webhook format.

Authentication (optional): HMAC-SHA256 signature verification

  • Server-side secret configured via WEBHOOK_SECRET environment variable
  • Client sends signature in X-Webhook-Signature header
  • Signature = HMAC-SHA256(webhook_secret, request_body)
  • Format: "sha256=<hex-encoded-signature>"

If no webhook secret is configured, authentication is disabled (URL secrecy only).

func (*GenericProvider) ParsePayload

func (g *GenericProvider) ParsePayload(body []byte) ([]NormalizedAlert, error)

ParsePayload converts a generic webhook payload to NormalizedAlerts.

This method applies sensible defaults:

  • external_id: Generated from SHA256(title + labels) if not provided
  • severity: "warning" if not provided
  • status: "firing" if not provided
  • started_at: Current time if not provided
  • labels: Empty map if not provided
  • annotations: Empty map if not provided

func (*GenericProvider) Source

func (g *GenericProvider) Source() string

Source returns "generic"

func (*GenericProvider) ValidatePayload

func (g *GenericProvider) ValidatePayload(body []byte, headers http.Header) error

ValidatePayload verifies the HMAC-SHA256 signature if a webhook secret is configured.

Header format: X-Webhook-Signature: sha256=<hex-encoded-hmac>

Algorithm:

  1. Extract signature from X-Webhook-Signature header
  2. Compute HMAC-SHA256(webhook_secret, request_body)
  3. Compare computed signature with provided signature (constant-time comparison)

Returns nil if:

  • No webhook secret configured (signature verification disabled)
  • Signature is valid

Returns error if:

  • Webhook secret is configured but signature header is missing
  • Signature format is invalid (not "sha256=...")
  • Signature does not match

type GenericWebhookPayload

type GenericWebhookPayload struct {
	Alerts []GenericAlert `json:"alerts" binding:"required,min=1,max=100,dive"` // Array of alerts (1-100)
}

GenericWebhookPayload represents the Fluidify Regen-native webhook format.

This is a simple, documented format for teams that want to send alerts from:

  • Custom monitoring scripts
  • AWS Lambda functions
  • Internal tools and services
  • Manual curl commands for testing

Design principles:

  • Only "title" is required
  • All other fields have sensible defaults
  • external_id is auto-generated if not provided
  • Schema is self-documenting (GET /webhooks/generic/schema)

type GrafanaAlert

type GrafanaAlert struct {
	Status       string            `json:"status" binding:"required,oneof=firing resolved"` // "firing" or "resolved"
	Labels       map[string]string `json:"labels" binding:"required"`                       // Alert labels (alertname, grafana_folder, etc.)
	Annotations  map[string]string `json:"annotations"`                                     // Alert annotations (summary, description, etc.)
	StartsAt     time.Time         `json:"startsAt" binding:"required"`                     // When alert started firing
	EndsAt       time.Time         `json:"endsAt"`                                          // When alert resolved (or zero for firing)
	GeneratorURL string            `json:"generatorURL"`                                    // Link to alert rule in Grafana
	Fingerprint  string            `json:"fingerprint" binding:"max=64"`                    // Unique identifier (may be empty)

	// Grafana-specific fields not present in Alertmanager
	Values      map[string]float64 `json:"values"`      // Query results (e.g., {"A": 95.2, "B": 100})
	ValueString string             `json:"valueString"` // Human-readable query result

	// Optional fields for fingerprint derivation when fingerprint field is empty
	// (Present in some Grafana versions/configurations)
	OrgID   int64  `json:"orgId,omitempty"`   // Grafana organization ID
	RuleUID string `json:"ruleUID,omitempty"` // Unique ID of alert rule
}

GrafanaAlert represents a single alert within a Grafana webhook payload

type GrafanaProvider

type GrafanaProvider struct{}

GrafanaProvider implements WebhookProvider for Grafana Unified Alerting webhooks.

Grafana Unified Alerting (v9+) is the successor to legacy Grafana alerting and uses a webhook format intentionally similar to Prometheus Alertmanager for easier migration.

Authentication: Like Prometheus, Grafana relies on webhook URL secrecy (no signature verification).

Field Mapping:

  • Title: labels["alertname"] or labels["alert_name"] (Grafana may use either)
  • Description: annotations["summary"] or annotations["description"]
  • Severity: labels["severity"] (defaults to "warning" if missing)
  • Status: "firing" or "resolved"
  • ExternalID: fingerprint field, or derived from orgId+ruleUID if fingerprint is empty
  • Labels: All labels from Grafana
  • Annotations: All annotations from Grafana (including valueString if present)

func (*GrafanaProvider) ParsePayload

func (g *GrafanaProvider) ParsePayload(body []byte) ([]NormalizedAlert, error)

ParsePayload converts a Grafana webhook payload to NormalizedAlerts.

Handles both payloads with fingerprint field and those without (deriving from orgId+ruleUID). Preserves Grafana-specific fields (values, valueString) in annotations for display in UI.

func (*GrafanaProvider) Source

func (g *GrafanaProvider) Source() string

Source returns "grafana"

func (*GrafanaProvider) ValidatePayload

func (g *GrafanaProvider) ValidatePayload(body []byte, headers http.Header) error

ValidatePayload performs no validation for Grafana.

Grafana Unified Alerting does not sign webhook payloads. Security relies on:

  • Webhook URL secrecy (configured in contact point)
  • Network-level access controls (firewall, VPN)
  • Optional basic auth (configured in contact point URL, not implemented here)

type GrafanaWebhookPayload

type GrafanaWebhookPayload struct {
	Receiver          string            `json:"receiver"`                                        // Name of contact point
	Status            string            `json:"status" binding:"required,oneof=firing resolved"` // "firing" or "resolved"
	Alerts            []GrafanaAlert    `json:"alerts" binding:"required,min=1,max=100,dive"`    // Array of alerts (max 100)
	GroupLabels       map[string]string `json:"groupLabels"`                                     // Labels used for grouping
	CommonLabels      map[string]string `json:"commonLabels"`                                    // Labels common to all alerts
	CommonAnnotations map[string]string `json:"commonAnnotations"`                               // Annotations common to all alerts
	ExternalURL       string            `json:"externalURL"`                                     // Base URL of Grafana instance
}

GrafanaWebhookPayload represents the top-level webhook payload sent by Grafana Unified Alerting Spec: https://grafana.com/docs/grafana/latest/alerting/configure-notifications/manage-contact-points/integrations/webhook-notifier/

Grafana Unified Alerting (v9+) intentionally uses a format similar to Prometheus Alertmanager for easier migration and interoperability. The main differences:

  • Adds "values" field with query results
  • May include "orgId" in some contexts
  • Fingerprint generation may differ slightly from Prometheus

type NormalizedAlert

type NormalizedAlert struct {
	// ExternalID is the source-specific unique identifier for deduplication.
	//
	// Examples:
	//   - Prometheus: fingerprint field (e.g., "a3c4e8f1234567")
	//   - Grafana: fingerprint or orgId+ruleId combo
	//   - CloudWatch: AlarmArn (e.g., "arn:aws:cloudwatch:us-east-1:123:alarm:HighCPU")
	//   - Generic: User-provided or SHA256(title + sorted labels)
	//
	// Combined with Source, forms the composite deduplication key: (source, external_id).
	// When the same alert fires again, it updates the existing alert instead of creating a duplicate.
	ExternalID string `json:"external_id"`

	// Source identifies which monitoring system sent this alert.
	// Matches the value returned by WebhookProvider.Source().
	//
	// Examples: "prometheus", "grafana", "cloudwatch", "generic"
	Source string `json:"source"`

	// Status represents the current state of the alert.
	// MUST be one of: "firing" or "resolved"
	//
	// Mapping guidelines:
	//   - Alertmanager "firing" → "firing"
	//   - Alertmanager "resolved" → "resolved"
	//   - Grafana "alerting" → "firing"
	//   - Grafana "ok" / "normal" → "resolved"
	//   - CloudWatch "ALARM" → "firing"
	//   - CloudWatch "OK" → "resolved"
	Status string `json:"status"`

	// Severity represents the urgency level of the alert.
	// MUST be one of: "critical", "warning", or "info"
	//
	// Severity affects incident auto-creation:
	//   - "critical" → Always creates incident
	//   - "warning"  → Always creates incident
	//   - "info"     → Alert stored but no incident created
	//
	// Mapping guidelines:
	//   - Map the most severe source value to "critical"
	//   - Map medium severity to "warning"
	//   - Map informational/low severity to "info"
	//   - Default to "warning" if source doesn't specify severity
	Severity string `json:"severity"`

	// Title is a short, human-readable summary of the alert.
	// Displayed in incident lists and Slack notifications.
	//
	// Examples:
	//   - "High CPU usage on web-01"
	//   - "API endpoint /users returning 5xx errors"
	//   - "Database connection pool exhausted"
	//
	// Field mapping:
	//   - Prometheus: labels["alertname"]
	//   - Grafana: title field
	//   - CloudWatch: AlarmName
	//   - Generic: title field (required)
	Title string `json:"title"`

	// Description provides detailed context about the alert.
	// Displayed in incident detail pages.
	//
	// Examples:
	//   - "CPU utilization is 95%, threshold 80%"
	//   - "Error rate: 12% (120/1000 requests in last 5 minutes)"
	//
	// Field mapping:
	//   - Prometheus: annotations["summary"] or annotations["description"]
	//   - Grafana: message field
	//   - CloudWatch: AlarmDescription + NewStateReason
	//   - Generic: description field (optional)
	Description string `json:"description"`

	// Labels are structured key-value pairs used for filtering, grouping, and routing.
	//
	// Common labels across sources:
	//   - alertname: Name of the alert rule
	//   - severity: Severity level (may differ from normalized Severity field)
	//   - instance: Server/pod/container instance
	//   - service: Application or service name
	//   - env: Environment (prod, staging, dev)
	//   - team: Owning team
	//   - region: Geographic region or availability zone
	//
	// Labels are used by:
	//   - Grouping rules: "group alerts with same service label"
	//   - Routing rules: "route alerts with team=db to #db-oncall"
	//   - Deduplication: Optional cross-source correlation
	Labels map[string]string `json:"labels"`

	// Annotations are additional metadata NOT used for routing or grouping.
	//
	// Common annotations:
	//   - summary: Short description
	//   - description: Detailed explanation
	//   - runbook_url: Link to runbook/playbook
	//   - dashboard_url: Link to monitoring dashboard
	//   - graph_url: Link to query/graph
	//
	// Annotations are displayed in the UI but don't affect alert processing logic.
	Annotations map[string]string `json:"annotations"`

	// RawPayload stores the complete original webhook payload.
	//
	// Purpose:
	//   - Debugging: Inspect exact payload when alerts don't behave as expected
	//   - Future processing: New features can extract additional fields without changing providers
	//   - Audit trail: Complete record of what the monitoring system sent
	//
	// Stored as JSONB in the database for efficient querying.
	RawPayload json.RawMessage `json:"raw_payload"`

	// StartedAt is when the alert first started firing.
	//
	// For "firing" alerts: timestamp when condition became true
	// For "resolved" alerts: original start time (not resolution time)
	//
	// Field mapping:
	//   - Prometheus: StartsAt field
	//   - Grafana: StartsAt field
	//   - CloudWatch: StateChangeTime (for ALARM state)
	//   - Generic: started_at field or current time if not provided
	StartedAt time.Time `json:"started_at"`

	// EndedAt is when the alert was resolved (nil for firing alerts).
	//
	// Only set when Status == "resolved".
	// For "firing" alerts: MUST be nil
	//
	// Field mapping:
	//   - Prometheus: EndsAt (but only if > year 1900, Alertmanager sends 0001-01-01 for firing)
	//   - Grafana: EndsAt
	//   - CloudWatch: StateChangeTime (for OK state)
	//   - Generic: ended_at field or nil if not provided
	EndedAt *time.Time `json:"ended_at,omitempty"`
}

NormalizedAlert is the canonical internal representation of an alert.

All webhook providers produce NormalizedAlerts. AlertService only consumes this type. This separation allows adding new monitoring sources without modifying core alert processing logic.

Field Mapping Philosophy:

  • Title: Human-readable alert name (e.g., "High CPU on web-01")
  • Description: Detailed context (e.g., "CPU utilization is 95%, threshold 80%")
  • Severity: critical | warning | info (normalized across all sources)
  • Status: firing | resolved (normalized across all sources)
  • Labels: Structured key-value metadata for filtering/grouping (e.g., service, env, instance)
  • Annotations: Additional metadata not used for routing (e.g., runbook_url, dashboard_url)
  • RawPayload: Complete original webhook payload for debugging and future processing

func (*NormalizedAlert) Fingerprint

func (n *NormalizedAlert) Fingerprint() string

Fingerprint generates a stable deduplication key from source and external_id.

The fingerprint is used for quick lookups and caching. The actual deduplication happens via database unique constraint on (source, external_id).

Returns: SHA256 hash of "{source}:{external_id}"

type PrometheusProvider

type PrometheusProvider struct{}

PrometheusProvider implements WebhookProvider for Prometheus Alertmanager webhooks.

Prometheus uses webhook URL secrecy for authentication (no signature verification). The fingerprint field provides stable deduplication across alert fires/resolves.

Field Mapping:

  • Title: labels["alertname"]
  • Description: annotations["summary"] or annotations["description"]
  • Severity: labels["severity"] (defaults to "warning" if missing)
  • Status: "firing" or "resolved"
  • ExternalID: fingerprint field
  • Labels: All labels from Alertmanager
  • Annotations: All annotations from Alertmanager

func (*PrometheusProvider) ParsePayload

func (p *PrometheusProvider) ParsePayload(body []byte) ([]NormalizedAlert, error)

ParsePayload converts an Alertmanager webhook payload to NormalizedAlerts.

This method contains all Prometheus-specific field mapping logic that was previously in alert_service.normalizeAlert(). Now it's encapsulated in the provider.

func (*PrometheusProvider) Source

func (p *PrometheusProvider) Source() string

Source returns "prometheus"

func (*PrometheusProvider) ValidatePayload

func (p *PrometheusProvider) ValidatePayload(body []byte, headers http.Header) error

ValidatePayload performs no validation for Prometheus.

Prometheus Alertmanager does not sign webhook payloads. Security relies on:

  • Webhook URL secrecy (long random path)
  • Network-level access controls (firewall, VPN)
  • Optional TLS client certificates (not implemented here)

type SNSMessage

type SNSMessage struct {
	// Type identifies the message purpose
	// Values: "Notification" (alarm data), "SubscriptionConfirmation", "UnsubscribeConfirmation"
	Type string `json:"Type" binding:"required"`

	// MessageId is a unique identifier for this SNS message
	MessageId string `json:"MessageId" binding:"required"`

	// TopicArn identifies the SNS topic that sent this message
	TopicArn string `json:"TopicArn" binding:"required"`

	// Subject is a short description (appears in email notifications)
	// For CloudWatch: "ALARM: \"AlarmName\" in Region"
	Subject string `json:"Subject"`

	// Message contains the actual payload (JSON-encoded CloudWatch alarm for notifications)
	Message string `json:"Message" binding:"required"`

	// Timestamp when SNS sent this message (ISO 8601)
	Timestamp string `json:"Timestamp" binding:"required"`

	// SignatureVersion is the AWS signature version (currently always "1")
	SignatureVersion string `json:"SignatureVersion" binding:"required"`

	// Signature is the base64-encoded RSA-SHA1 signature
	Signature string `json:"Signature" binding:"required"`

	// SigningCertURL is the URL to the X.509 certificate used for signing
	// MUST be HTTPS and from *.amazonaws.com domain (security check)
	SigningCertURL string `json:"SigningCertURL" binding:"required"`

	// SubscribeURL is the confirmation URL (only present in SubscriptionConfirmation messages)
	// Fluidify Regen will automatically HTTP GET this URL to confirm the subscription
	SubscribeURL string `json:"SubscribeURL,omitempty"`

	// UnsubscribeURL allows unsubscribing from the topic (we don't use this)
	UnsubscribeURL string `json:"UnsubscribeURL,omitempty"`
}

SNSMessage represents the envelope sent by AWS Simple Notification Service.

CloudWatch alarms are delivered via SNS, which wraps them in this structure. SNS is also used for subscription confirmation - the first message sent when configuring the webhook requires an HTTP GET callback to confirm.

Spec: https://docs.aws.amazon.com/sns/latest/dg/sns-message-and-json-formats.html

type WebhookProvider

type WebhookProvider interface {
	// Source returns the identifier for this monitoring source.
	// Used as the alert.source field for storage and deduplication.
	//
	// Examples: "prometheus", "grafana", "cloudwatch", "generic"
	//
	// MUST be lowercase alphanumeric with optional hyphens.
	// MUST be globally unique across all providers.
	Source() string

	// ValidatePayload verifies the authenticity of the webhook request.
	//
	// This method is called BEFORE parsing to prevent wasting CPU on forged requests.
	// Validation mechanisms vary by source:
	//   - Prometheus: No validation (relies on URL secrecy)
	//   - Grafana: No validation (relies on URL secrecy)
	//   - CloudWatch: SNS message signature verification
	//   - Generic: HMAC-SHA256 signature via X-Webhook-Secret header
	//
	// Parameters:
	//   - body: Raw webhook request body (for signature validation)
	//   - headers: HTTP request headers (for signature extraction)
	//
	// Returns nil if valid or no validation configured.
	// Returns error if authentication fails (handler will return 401).
	ValidatePayload(body []byte, headers http.Header) error

	// ParsePayload converts the source-specific webhook payload into normalized alerts.
	//
	// This is where provider-specific field mapping happens:
	//   - Extract title, description, severity from source-specific fields
	//   - Map source-specific status values to "firing" or "resolved"
	//   - Derive a stable external_id for deduplication
	//   - Convert source-specific labels/tags to normalized map[string]string
	//   - Preserve complete original payload in RawPayload for debugging
	//
	// Parameters:
	//   - body: Raw webhook request body (validated via ValidatePayload)
	//
	// Returns:
	//   - []NormalizedAlert: One or more normalized alerts (webhooks often contain multiple)
	//   - error: Parse error (handler will return 400)
	//
	// Implementation notes:
	//   - MUST return at least one NormalizedAlert or an error (not both empty)
	//   - SHOULD store complete original payload in NormalizedAlert.RawPayload
	//   - SHOULD handle malformed JSON gracefully with descriptive error messages
	ParsePayload(body []byte) ([]NormalizedAlert, error)
}

WebhookProvider defines the interface that all monitoring source webhook handlers must implement.

Each monitoring system (Prometheus, Grafana, CloudWatch, etc.) has its own payload format and authentication mechanism. WebhookProvider normalizes these differences so the AlertService can process alerts uniformly regardless of their source.

Example flow:

  1. HTTP webhook received → WebhookHandler
  2. ValidatePayload() checks signatures/authentication
  3. ParsePayload() converts source-specific JSON → []NormalizedAlert
  4. AlertService.ProcessNormalizedAlerts() handles storage and incident creation

Adding a new monitoring source requires only implementing this three-method interface.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL