Documentation
¶
Index ¶
- func GenerateExternalID(title string, labels map[string]string) string
- func GetJSONSchema() map[string]interface{}
- type AlertmanagerAlert
- type AlertmanagerPayload
- type CloudWatchAlarm
- type CloudWatchDimension
- type CloudWatchProvider
- type CloudWatchTrigger
- type GenericAlert
- type GenericProvider
- type GenericWebhookPayload
- type GrafanaAlert
- type GrafanaProvider
- type GrafanaWebhookPayload
- type NormalizedAlert
- type PrometheusProvider
- type SNSMessage
- type WebhookProvider
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func GenerateExternalID ¶
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:
- Sort label keys alphabetically
- Create string: "title:{title}|labels:{k1}={v1}|{k2}={v2}..."
- SHA256 hash the string
- 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 ¶
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:
- SNS envelope parsing and signature verification
- Automatic SNS subscription confirmation (zero-config setup)
- CloudWatch alarm JSON extraction from SNS Message field
- 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:
- First webhook: Type="SubscriptionConfirmation" → Auto-confirm by HTTP GET to SubscribeURL
- 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:
- Verify SigningCertURL is HTTPS and from *.amazonaws.com domain (security)
- Download the X.509 certificate from SigningCertURL
- Extract RSA public key from certificate
- Reconstruct the canonical message string (specific fields in specific order)
- 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) 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:
- Extract signature from X-Webhook-Signature header
- Compute HMAC-SHA256(webhook_secret, request_body)
- 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) 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:
- HTTP webhook received → WebhookHandler
- ValidatePayload() checks signatures/authentication
- ParsePayload() converts source-specific JSON → []NormalizedAlert
- AlertService.ProcessNormalizedAlerts() handles storage and incident creation
Adding a new monitoring source requires only implementing this three-method interface.