Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion healthcheck/healthcheck.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ func (h *HealthChecker) performCheck(nextWait *time.Duration) {
sCode := 0

if err != nil {
evaluationRes.Reason = fmt.Sprintf("Network/Connection Error: %v", err)
evaluationRes.Reason = fmt.Sprintf("- Reason: %v", err)
evaluationRes.Type = model.NotificationNetworkError
} else if resp != nil {
sCode = resp.StatusCode
Expand Down
58 changes: 36 additions & 22 deletions model/condition.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,33 +119,43 @@ func (c *Condition) Validate(path string) error {
func (c *Condition) Evaluate(resp *http.Response, body []byte, duration time.Duration) EvaluationResult {
// 1. منطق AND
if c.And != nil {
for i, cond := range c.And {
var failures []string
var firstType NotificationType
for _, cond := range c.And {
res := cond.Evaluate(resp, body, duration)
if !res.IsHealthy {
return EvaluationResult{
IsHealthy: false,
Reason: fmt.Sprintf("AND condition failed (index %d): %s", i, res.Reason),
Type: res.Type,
if firstType == "" {
firstType = res.Type
}
failures = append(failures, res.Reason)
}
}
if len(failures) > 0 {
return EvaluationResult{
IsHealthy: false,
Reason: strings.Join(failures, "\n"),
Type: firstType,
}
}
return EvaluationResult{IsHealthy: true}
}

// 2. منطق OR
if c.Or != nil {
var reasons []string
for i, cond := range c.Or {
var subFailures []string
for _, cond := range c.Or {
res := cond.Evaluate(resp, body, duration)
if res.IsHealthy {
return EvaluationResult{IsHealthy: true}
}
reasons = append(reasons, fmt.Sprintf("Sub-condition #%d failed: %s", i, res.Reason))
// Indent sub-failures of OR
indented := " " + strings.ReplaceAll(res.Reason, "\n", "\n ")
subFailures = append(subFailures, indented)
}
return EvaluationResult{
IsHealthy: false,
Reason: fmt.Sprintf("All OR conditions failed:\n - %s", strings.Join(reasons, "\n - ")),
Type: NotificationConditionFailed, // Common case for OR
Reason: "- All OR conditions failed:\n" + strings.Join(subFailures, "\n"),
Type: NotificationConditionFailed,
}
}

Expand All @@ -155,7 +165,7 @@ func (c *Condition) Evaluate(resp *http.Response, body []byte, duration time.Dur
if res.IsHealthy {
return EvaluationResult{
IsHealthy: false,
Reason: "NOT condition failed: the forbidden condition matched successfully",
Reason: "- NOT condition failed: Forbidden condition matched successfully",
Type: NotificationConditionFailed,
}
}
Expand All @@ -168,7 +178,7 @@ func (c *Condition) Evaluate(resp *http.Response, body []byte, duration time.Dur
if !matched {
return EvaluationResult{
IsHealthy: false,
Reason: fmt.Sprintf("Regex pattern '%s' not found in body", c.Regex.Regex),
Reason: fmt.Sprintf("- Body Match: Pattern '%s' not found in response", c.Regex.Regex),
Type: NotificationConditionFailed,
}
}
Expand All @@ -178,12 +188,12 @@ func (c *Condition) Evaluate(resp *http.Response, body []byte, duration time.Dur
// 5. بررسی StatusCode
if c.StatusCode != nil {
if resp == nil {
return EvaluationResult{IsHealthy: false, Reason: "No response received", Type: NotificationHttpError}
return EvaluationResult{IsHealthy: false, Reason: "- Status Code: No response received", Type: NotificationHttpError}
}
if resp.StatusCode != c.StatusCode.Code {
return EvaluationResult{
IsHealthy: false,
Reason: fmt.Sprintf("Expected status %d, but got %d", c.StatusCode.Code, resp.StatusCode),
Reason: fmt.Sprintf("- Status Code: Expected %d, Got %d\n- Reason: %s", c.StatusCode.Code, resp.StatusCode, http.StatusText(resp.StatusCode)),
Type: NotificationHttpError,
}
}
Expand All @@ -193,16 +203,20 @@ func (c *Condition) Evaluate(resp *http.Response, body []byte, duration time.Dur
// 6. بررسی Headers
if c.Header != nil {
if resp == nil {
return EvaluationResult{IsHealthy: false, Reason: "No response headers available", Type: NotificationConditionFailed}
return EvaluationResult{IsHealthy: false, Reason: "- Header: No response headers available", Type: NotificationConditionFailed}
}
var headerFailures []string
for _, h := range *c.Header {
actual := resp.Header.Get(h.Key)
if actual != h.Value {
return EvaluationResult{
IsHealthy: false,
Reason: fmt.Sprintf("Header '%s' expected '%s', got '%s'", h.Key, h.Value, actual),
Type: NotificationConditionFailed,
}
headerFailures = append(headerFailures, fmt.Sprintf("- Header [%s]: Expected '%s', Got '%s'", h.Key, h.Value, actual))
}
}
if len(headerFailures) > 0 {
return EvaluationResult{
IsHealthy: false,
Reason: strings.Join(headerFailures, "\n"),
Type: NotificationConditionFailed,
}
}
return EvaluationResult{IsHealthy: true}
Expand All @@ -214,14 +228,14 @@ func (c *Condition) Evaluate(resp *http.Response, body []byte, duration time.Dur
if duration > max {
return EvaluationResult{
IsHealthy: false,
Reason: fmt.Sprintf("Response time %v exceeded limit %v", duration, max),
Reason: fmt.Sprintf("- Latency: Expected <%s, Got %v", c.ResponseTime.MaxDuration, duration.Round(time.Millisecond)),
Type: NotificationSlowResponse,
}
}
return EvaluationResult{IsHealthy: true}
}

return EvaluationResult{IsHealthy: false, Reason: "No valid condition defined", Type: NotificationDefault}
return EvaluationResult{IsHealthy: false, Reason: "- No valid condition defined", Type: NotificationDefault}
}

func (r *RegexCondition) Evaluate(body []byte) bool {
Expand Down
15 changes: 6 additions & 9 deletions model/detailed_reason_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,7 @@ func TestEvaluate_DetailedReasons(t *testing.T) {
if result.IsHealthy {
t.Fatal("expected failure")
}
if !strings.Contains(result.Reason, "AND condition failed (index 1)") {
t.Errorf("expected detailed AND failure message, got: %s", result.Reason)
}
if !strings.Contains(result.Reason, "Regex pattern 'UP' not found") {
if !strings.Contains(result.Reason, "- Body Match: Pattern 'UP' not found") {
t.Errorf("expected original failure message to be included, got: %s", result.Reason)
}
})
Expand All @@ -44,14 +41,14 @@ func TestEvaluate_DetailedReasons(t *testing.T) {
if result.IsHealthy {
t.Fatal("expected failure")
}
if !strings.Contains(result.Reason, "All OR conditions failed") {
if !strings.Contains(result.Reason, "- All OR conditions failed") {
t.Errorf("expected OR failure message, got: %s", result.Reason)
}
if !strings.Contains(result.Reason, "Sub-condition #0 failed: Expected status 200, but got 500") {
t.Errorf("expected reason 0, got: %s", result.Reason)
if !strings.Contains(result.Reason, "Status Code: Expected 200, Got 500") {
t.Errorf("expected status code failure, got: %s", result.Reason)
}
if !strings.Contains(result.Reason, "Sub-condition #1 failed: Regex pattern 'UP' not found in body") {
t.Errorf("expected reason 1, got: %s", result.Reason)
if !strings.Contains(result.Reason, "Body Match: Pattern 'UP' not found in response") {
t.Errorf("expected regex failure, got: %s", result.Reason)
}
})

Expand Down
12 changes: 6 additions & 6 deletions model/notification.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@ type TemplateGroup struct {
}

const (
DefaultNetworkErrorTemplate = "[🔌 Network Alert] {{.Metadata.ServiceName}} - Connection failed at {{.Metadata.Timestamp}}. Error: {{.Metadata.Reason}}"
DefaultHttpErrorTemplate = "[❌ HTTP Alert] {{.Metadata.ServiceName}} returned {{.Metadata.StatusCode}} at {{.Metadata.Timestamp}}. URL: {{.Metadata.ServiceURL}}"
DefaultSlowResponseTemplate = "[⏱️ Latency Alert] {{.Metadata.ServiceName}} is slow! Response time: {{.Metadata.ResponseTime}} (Threshold exceeded) at {{.Metadata.Timestamp}}."
DefaultConditionFailedTemplate = "[🔍 Validation Alert] {{.Metadata.ServiceName}} failed health criteria at {{.Metadata.Timestamp}}. Detail: {{.Metadata.Reason}}"
DefaultRecoveryTemplate = "[✅ Recovery] {{.Metadata.ServiceName}} is back online! Status: Healthy. Restored at: {{.Metadata.Timestamp}}."
DefaultNotificationTemplate = "[🔔 Alert] {{.Metadata.ServiceName}} status is {{.Metadata.Status}} at {{.Metadata.Timestamp}}."
DefaultNetworkErrorTemplate = "[🔌 Network Alert] {{.Metadata.ServiceName}}\n- Time: {{.Metadata.Timestamp}}\n{{.Metadata.Reason}}"
DefaultHttpErrorTemplate = "[❌ HTTP Alert] {{.Metadata.ServiceName}}\n- Time: {{.Metadata.Timestamp}}\n{{.Metadata.Reason}}"
DefaultSlowResponseTemplate = "[⏱️ Latency Alert] {{.Metadata.ServiceName}}\n- Time: {{.Metadata.Timestamp}}\n{{.Metadata.Reason}}"
DefaultConditionFailedTemplate = "[🔍 Validation Alert] {{.Metadata.ServiceName}}\n- Time: {{.Metadata.Timestamp}}\n{{.Metadata.Reason}}"
DefaultRecoveryTemplate = "[✅ Recovery] {{.Metadata.ServiceName}}\n- Time: {{.Metadata.Timestamp}}\n- Status: Service is now Healthy"
DefaultNotificationTemplate = "[🔔 Alert] {{.Metadata.ServiceName}}\n- Time: {{.Metadata.Timestamp}}\n- Status: {{.Metadata.Status}}"
)

func GetDefaultTemplate(t NotificationType) string {
Expand Down
20 changes: 10 additions & 10 deletions sample.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,12 @@ notifiers:
# Generic fallback
text: "⚠️ Alert for {{.Metadata.ServiceName}}"
templates:
network_error: '{"text": "🔌 *Network Error*: Cannot reach {{.Metadata.ServiceName}}! Reason: {{.Metadata.Reason}}"}'
http_error: '{"text": "❌ *HTTP Error*: {{.Metadata.ServiceName}} returned {{.Metadata.StatusCode}}."}'
slow_response: '{"text": "⏱️ *Latency Alert*: {{.Metadata.ServiceName}} took {{.Metadata.ResponseTime}}."}'
condition_failed: '{"text": "🔍 *Logic Failure*: {{.Metadata.ServiceName}} failed health criteria.\n```\n{{.Metadata.Reason}}\n```"}'
recovery: '{"text": "✅ *Service Restored*: {{.Metadata.ServiceName}} is back online!"}'
default: '{"text": "🔔 *Status Update*: {{.Metadata.ServiceName}} is {{.Metadata.Status}}"}'
network_error: '{"text": "🔌 *Network Alert* {{.Metadata.ServiceName}}\n- Time: {{.Metadata.Timestamp}}\n{{.Metadata.Reason}}"}'
http_error: '{"text": "❌ *HTTP Alert* {{.Metadata.ServiceName}}\n- Time: {{.Metadata.Timestamp}}\n{{.Metadata.Reason}}"}'
slow_response: '{"text": "⏱️ *Latency Alert* {{.Metadata.ServiceName}}\n- Time: {{.Metadata.Timestamp}}\n{{.Metadata.Reason}}"}'
condition_failed: '{"text": "🔍 *Validation Alert* {{.Metadata.ServiceName}}\n- Time: {{.Metadata.Timestamp}}\n{{.Metadata.Reason}}"}'
recovery: '{"text": "✅ *Recovery* {{.Metadata.ServiceName}}\n- Time: {{.Metadata.Timestamp}}\n- Status: Service is now Healthy"}'
default: '{"text": "🔔 *Status Update* {{.Metadata.ServiceName}}\n- Time: {{.Metadata.Timestamp}}\n- Status: {{.Metadata.Status}}"}'

# ------ Email (SMTP) ------
smtp:
Expand All @@ -57,8 +57,8 @@ notifiers:
server: "smtp.gmail.com"
port: "587"
templates:
recovery: "Subject: ✅ Service {{.Metadata.ServiceName}} Recovered\n\nGood news! Service is back up."
default: "Subject: 🚨 Alert: {{.Metadata.ServiceName}} is {{.Metadata.Status}}\n\nReason: {{.Metadata.Reason}}"
recovery: "Subject: ✅ Recovery {{.Metadata.ServiceName}}\n\n- Time: {{.Metadata.Timestamp}}\n- Status: Service is now Healthy"
default: "Subject: 🚨 Alert: {{.Metadata.ServiceName}}\n\n- Time: {{.Metadata.Timestamp}}\n{{.Metadata.Reason}}"

# ------ SMS (MeliPayamak) ------
meli_payamak_panel:
Expand All @@ -67,8 +67,8 @@ notifiers:
password: "mypassword"
sender: "50001234"
templates:
recovery: "✅ سرویس {{.Metadata.ServiceName}} به وضعیت عادی بازگشت."
default: "⚠️ هشدار: {{.Metadata.ServiceName}} دچار اختلال شده است. وضعیت: {{.Metadata.Status}}"
recovery: "✅ Recovery: {{.Metadata.ServiceName}}\nTime: {{.Metadata.Timestamp}}\nStatus: Healthy"
default: "⚠️ Alert: {{.Metadata.ServiceName}}\nTime: {{.Metadata.Timestamp}}\n{{.Metadata.Reason}}"

#================================================================#
# Health Check Conditions #
Expand Down