fix(ops): 收紧运维邮件 fallback 和去重

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
benjamin 2026-05-20 13:25:32 +08:00
parent 3fdd5cbaef
commit a6bb6d481b
2 changed files with 114 additions and 0 deletions

View File

@ -686,6 +686,21 @@ func (s *OpsAlertEvaluatorService) maybeSendAlertEmail(ctx context.Context, runt
if !s.emailLimiter.Allow(time.Now().UTC()) {
continue
}
if s.emailService.notificationEmailService != nil {
if err := s.emailService.notificationEmailService.Send(ctx, NotificationEmailSendInput{
Event: NotificationEmailEventOpsAlert,
RecipientEmail: addr,
RecipientName: emailRecipientName(addr),
SourceType: "ops_alert",
SourceID: fmt.Sprintf("%d", event.ID),
Variables: opsAlertEmailVariables(rule, event),
}); err == nil {
anySent = true
continue
} else if !shouldFallbackNotificationEmail(err) {
continue
}
}
if err := s.emailService.SendEmail(ctx, addr, subject, body); err != nil {
// Ignore per-recipient failures; continue best-effort.
continue
@ -699,6 +714,46 @@ func (s *OpsAlertEvaluatorService) maybeSendAlertEmail(ctx context.Context, runt
return anySent
}
func opsAlertEmailVariables(rule *OpsAlertRule, event *OpsAlertEvent) map[string]string {
variables := map[string]string{
"rule_name": "-",
"severity": "-",
"alert_status": "-",
"metric_type": "-",
"operator": "-",
"metric_value": "-",
"threshold_value": "-",
"triggered_at": time.Now().UTC().Format(time.RFC3339),
"alert_description": "-",
}
if rule != nil {
variables["rule_name"] = strings.TrimSpace(rule.Name)
variables["severity"] = strings.TrimSpace(rule.Severity)
variables["metric_type"] = strings.TrimSpace(rule.MetricType)
variables["operator"] = strings.TrimSpace(rule.Operator)
variables["threshold_value"] = fmt.Sprintf("%.2f", rule.Threshold)
if strings.TrimSpace(rule.Description) != "" {
variables["alert_description"] = strings.TrimSpace(rule.Description)
}
}
if event != nil {
variables["alert_status"] = strings.TrimSpace(event.Status)
if event.MetricValue != nil {
variables["metric_value"] = fmt.Sprintf("%.2f", *event.MetricValue)
}
if event.ThresholdValue != nil {
variables["threshold_value"] = fmt.Sprintf("%.2f", *event.ThresholdValue)
}
if !event.FiredAt.IsZero() {
variables["triggered_at"] = event.FiredAt.UTC().Format(time.RFC3339)
}
if strings.TrimSpace(event.Description) != "" {
variables["alert_description"] = strings.TrimSpace(event.Description)
}
}
return variables
}
func buildOpsAlertEmailBody(rule *OpsAlertRule, event *OpsAlertEvent) string {
if rule == nil || event == nil {
return ""

View File

@ -337,6 +337,7 @@ func (s *OpsScheduledReportService) runReport(ctx context.Context, report *opsSc
}
subject := fmt.Sprintf("[Ops Report] %s", strings.TrimSpace(report.Name))
templateVariables := opsScheduledReportEmailVariables(report, now)
attempts := 0
for _, to := range recipients {
@ -345,6 +346,24 @@ func (s *OpsScheduledReportService) runReport(ctx context.Context, report *opsSc
continue
}
attempts++
if s.emailService.notificationEmailService != nil {
if err := s.emailService.notificationEmailService.Send(ctx, NotificationEmailSendInput{
Event: NotificationEmailEventOpsScheduledReport,
RecipientEmail: addr,
RecipientName: emailRecipientName(addr),
SourceType: "ops_scheduled_report",
SourceID: opsScheduledReportDeliverySourceID(report),
ReminderKey: now.UTC().Format("2006-01-02T15:04"),
Variables: templateVariables,
RawHTMLVariables: map[string]string{
"report_html": content,
},
}); err == nil {
continue
} else if !shouldFallbackNotificationEmail(err) {
continue
}
}
if err := s.emailService.SendEmail(ctx, addr, subject, content); err != nil {
// Ignore per-recipient failures; continue best-effort.
continue
@ -353,6 +372,46 @@ func (s *OpsScheduledReportService) runReport(ctx context.Context, report *opsSc
return attempts, nil
}
func opsScheduledReportDeliverySourceID(report *opsScheduledReport) string {
if report == nil {
return "scheduled_report"
}
parts := []string{
strings.TrimSpace(report.ReportType),
strings.TrimSpace(report.Name),
strings.TrimSpace(report.Schedule),
}
joined := strings.Trim(strings.Join(parts, ":"), ":")
if joined == "" {
return "scheduled_report"
}
return joined
}
func opsScheduledReportEmailVariables(report *opsScheduledReport, now time.Time) map[string]string {
end := now.UTC()
start := end
name := "Ops report"
reportType := "scheduled_report"
if report != nil {
if strings.TrimSpace(report.Name) != "" {
name = strings.TrimSpace(report.Name)
}
if strings.TrimSpace(report.ReportType) != "" {
reportType = strings.TrimSpace(report.ReportType)
}
if report.TimeRange > 0 {
start = end.Add(-report.TimeRange)
}
}
return map[string]string{
"report_name": name,
"report_type": reportType,
"report_start_time": start.Format(time.RFC3339),
"report_end_time": end.Format(time.RFC3339),
}
}
func (s *OpsScheduledReportService) generateReportHTML(ctx context.Context, report *opsScheduledReport, now time.Time) (string, error) {
if s == nil || s.opsService == nil || report == nil {
return "", fmt.Errorf("service not initialized")