From 76a07d33a680e74f79888e7ce1b56e2566f7b4b2 Mon Sep 17 00:00:00 2001 From: Stepan Ermakov Date: Thu, 12 Jan 2023 07:29:58 +0300 Subject: [PATCH] Notification emails merging and localization Signed-off-by: Stepan Ermakov --- .../transport/smtp/EventMessageContent.java | 41 ++- .../smtp/LocalizedMessageHelper.java | 159 ++++++++++++ .../notifier/transport/smtp/MessageBody.java | 16 +- .../transport/smtp/MessageHelper.java | 11 +- .../core/notifier/transport/smtp/Smtp.java | 47 ++-- .../transport/smtp/SmtpMessageMerger.java | 240 ++++++++++++++++++ .../main/resources/smtp-messages.properties | 26 ++ .../resources/smtp-messages_ru.properties | 26 ++ .../smtp/LocalizedMessageHelperTest.java | 120 +++++++++ .../transport/smtp/SmtpMessageMergerTest.java | 240 ++++++++++++++++++ .../notifier/transport/smtp/SmtpTest.java | 56 ++++ .../ovirt-engine-notifier.conf.in | 14 + 12 files changed, 956 insertions(+), 40 deletions(-) create mode 100644 backend/manager/tools/src/main/java/org/ovirt/engine/core/notifier/transport/smtp/LocalizedMessageHelper.java create mode 100644 backend/manager/tools/src/main/java/org/ovirt/engine/core/notifier/transport/smtp/SmtpMessageMerger.java create mode 100644 backend/manager/tools/src/main/resources/smtp-messages.properties create mode 100644 backend/manager/tools/src/main/resources/smtp-messages_ru.properties create mode 100644 backend/manager/tools/src/test/java/org/ovirt/engine/core/notifier/transport/smtp/LocalizedMessageHelperTest.java create mode 100644 backend/manager/tools/src/test/java/org/ovirt/engine/core/notifier/transport/smtp/SmtpMessageMergerTest.java create mode 100644 backend/manager/tools/src/test/java/org/ovirt/engine/core/notifier/transport/smtp/SmtpTest.java diff --git a/backend/manager/tools/src/main/java/org/ovirt/engine/core/notifier/transport/smtp/EventMessageContent.java b/backend/manager/tools/src/main/java/org/ovirt/engine/core/notifier/transport/smtp/EventMessageContent.java index b117fc5dd83..0071483c844 100644 --- a/backend/manager/tools/src/main/java/org/ovirt/engine/core/notifier/transport/smtp/EventMessageContent.java +++ b/backend/manager/tools/src/main/java/org/ovirt/engine/core/notifier/transport/smtp/EventMessageContent.java @@ -1,24 +1,35 @@ package org.ovirt.engine.core.notifier.transport.smtp; -import java.util.Date; +import java.util.Locale; import org.ovirt.engine.core.notifier.filter.AuditLogEvent; /** - * Creates a simple message subject and body using helper class {@linkplain MessageHelper} to determine
- * the structure of the message subject and body + * Creates a simple message subject and body using helper class {@linkplain MessageHelper} or + * {@linkplain LocalizedMessageHelper} to determine the structure of the message subject and body */ public class EventMessageContent { private String subject; private String body; + public EventMessageContent() { + } + private void prepareMessageSubject(String hostName, - AuditLogEvent event) { - subject = MessageHelper.prepareMessageSubject(event.getType(), hostName, event.getMessage()); + AuditLogEvent event, + Locale locale) { + subject = (locale == null) ? MessageHelper.prepareMessageSubject(event.getType(), hostName, event.getMessage()) : + LocalizedMessageHelper.prepareMessageSubject(event.getType(), hostName, event.getMessage(), locale); + } + + public EventMessageContent(String subject, String body) { + this.subject = subject; + this.body = body; } private void prepareMessageBody(AuditLogEvent event, - boolean isBodyHtml) { + boolean isBodyHtml, + Locale locale) { MessageBody messageBody = new MessageBody(); messageBody.setUserInfo(event.getUserName()); messageBody.setVmInfo(event.getVmName()); @@ -26,15 +37,16 @@ private void prepareMessageBody(AuditLogEvent event, messageBody.setTemplateInfo(event.getVmTemplateName()); messageBody.setDatacenterInfo(event.getStoragePoolName()); messageBody.setStorageDomainInfo(event.getStorageDomainName()); - final Date logTime = event.getLogTime(); - messageBody.setLogTime(logTime != null ? logTime.toString() : ""); - messageBody.setSeverity(String.valueOf(event.getSeverity())); + messageBody.setLogTime(event.getLogTime()); + messageBody.setSeverity(event.getSeverity()); messageBody.setMessage(event.getMessage()); if (isBodyHtml) { - this.body = MessageHelper.prepareHTMLMessageBody(messageBody); + this.body = (locale == null) ? MessageHelper.prepareHTMLMessageBody(messageBody) : + LocalizedMessageHelper.prepareHTMLMessageBody(messageBody, locale); } else { - this.body = MessageHelper.prepareMessageBody(messageBody); + this.body = (locale == null) ? MessageHelper.prepareMessageBody(messageBody) : + LocalizedMessageHelper.prepareMessageBody(messageBody, locale); } } @@ -63,11 +75,12 @@ public String getMessageSubject() { * @param hostName the host name on which the subject will refer to * @param event associated event which the message will be created by * @param isBodyHtml defines the format of message body + * @param locale locale for the message content */ public void prepareMessage(String hostName, AuditLogEvent event, - boolean isBodyHtml) { - prepareMessageSubject(hostName, event); - prepareMessageBody(event, isBodyHtml); + boolean isBodyHtml, Locale locale) { + prepareMessageSubject(hostName, event, locale); + prepareMessageBody(event, isBodyHtml, locale); } } diff --git a/backend/manager/tools/src/main/java/org/ovirt/engine/core/notifier/transport/smtp/LocalizedMessageHelper.java b/backend/manager/tools/src/main/java/org/ovirt/engine/core/notifier/transport/smtp/LocalizedMessageHelper.java new file mode 100644 index 00000000000..f52fc1c5269 --- /dev/null +++ b/backend/manager/tools/src/main/java/org/ovirt/engine/core/notifier/transport/smtp/LocalizedMessageHelper.java @@ -0,0 +1,159 @@ +package org.ovirt.engine.core.notifier.transport.smtp; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.Map; +import java.util.MissingResourceException; +import java.util.ResourceBundle; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.commons.lang.StringUtils; +import org.ovirt.engine.core.common.AuditLogSeverity; +import org.ovirt.engine.core.notifier.filter.AuditLogEventType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A helper class designed to construct localized message parts in static structure + */ +public class LocalizedMessageHelper { + private static final Logger log = LoggerFactory.getLogger(LocalizedMessageHelper.class); + private static final Map RESOURCES = new ConcurrentHashMap<>(); + + /** + * Constructs a formatted message body based on provided message body elements content.
+ * If any of message body element is empty or missing, it will not appear in the formatted message body text + * @param messageBody + * the message body values for populate a formatted message body + * @param locale + * locale for the message content + * @return a formatted message body + */ + public static String prepareMessageBody(MessageBody messageBody, Locale locale) { + StringBuilder sb = new StringBuilder(); + + appendPlainMessage(sb, "smtp.message.body.plain.time", getLogTime(messageBody, locale), locale); + appendPlainMessage(sb, "smtp.message.body.plain.message", messageBody.getMessage(), locale); + appendPlainMessage(sb, "smtp.message.body.plain.severity", getSeverity(messageBody, locale), locale); + + appendPlainMessage(sb, "smtp.message.body.plain.user.info", messageBody.getUserInfo(), locale); + appendPlainMessage(sb, "smtp.message.body.plain.vm.info", messageBody.getVmInfo(), locale); + appendPlainMessage(sb, "smtp.message.body.plain.host.info", messageBody.getHostInfo(), locale); + appendPlainMessage(sb, "smtp.message.body.plain.template.info", messageBody.getTemplateInfo(), locale); + appendPlainMessage(sb, "smtp.message.body.plain.dc.info", messageBody.getDatacenterInfo(), locale); + appendPlainMessage(sb, "smtp.message.body.plain.storage.domain.info", messageBody.getStorageDomainInfo(), locale); + + return sb.toString(); + } + + /** + * Construct a formatted message based on predefined template:
+ * {@code "Issue Solved"/"Alert Notification" (host name), [message details]} + * @param type + * determines the prefix of the subject + * @param hostName + * the machine names associated with this event + * @param message + * the content of the message to convey + * @param locale + * locale for the message content + * @return a formatted message subject + */ + public static String prepareMessageSubject(AuditLogEventType type, String hostName, String message, Locale locale) { + String auditLogEventType = type.name(); + switch (type) { + case alertMessage: + auditLogEventType = getResourceString("smtp.message.audit.log.event.type.alert", locale, auditLogEventType); + break; + case resolveMessage: + auditLogEventType = getResourceString("smtp.message.audit.log.event.type.resolve", locale, auditLogEventType); + break; + } + return String.format("%s (%s), [%s]", auditLogEventType, hostName, message); + + } + + /** + * Constructs a formatted message body based on provided message body elements content in HTML format.
+ * If any of message body element is empty or missing, it will not appear in the formatted message body text + * @param messageBody + * the message body values for populate a formatted message body + * @param locale + * locale for the message content + * @return a formatted HTML message body + */ + public static String prepareHTMLMessageBody(MessageBody messageBody, Locale locale) { + StringBuilder sb = new StringBuilder(); + + appendHtmlMessage(sb, "smtp.message.body.html.time", getLogTime(messageBody, locale), locale); + appendHtmlMessage(sb, "smtp.message.body.html.message", messageBody.getMessage(), locale); + appendHtmlMessage(sb, "smtp.message.body.html.severity", getSeverity(messageBody, locale), locale); + + appendHtmlMessage(sb, "smtp.message.body.html.user.info", messageBody.getUserInfo(), locale); + appendHtmlMessage(sb, "smtp.message.body.html.vm.info", messageBody.getVmInfo(), locale); + appendHtmlMessage(sb, "smtp.message.body.html.host.info", messageBody.getHostInfo(), locale); + appendHtmlMessage(sb, "smtp.message.body.html.template.info", messageBody.getTemplateInfo(), locale); + appendHtmlMessage(sb, "smtp.message.body.html.dc.info", messageBody.getDatacenterInfo(), locale); + appendHtmlMessage(sb, "smtp.message.body.html.storage.domain.info", messageBody.getStorageDomainInfo(), locale); + + return sb.toString(); + } + + private static String getLogTime(MessageBody messageBody, Locale locale) { + Date logTime = messageBody.getLogTime(); + if (logTime == null) { + return ""; + } + return SimpleDateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, locale).format(logTime); + } + + private static String getSeverity(MessageBody messageBody, Locale locale) { + AuditLogSeverity severity = messageBody.getSeverity(); + if (severity == null) { + return ""; + } + String auditLogSeverity = severity.name(); + switch (severity) { + case NORMAL: + auditLogSeverity = getResourceString("smtp.message.audit.log.severity.normal", locale, auditLogSeverity); + break; + case WARNING: + auditLogSeverity = getResourceString("smtp.message.audit.log.severity.warning", locale, auditLogSeverity); + break; + case ERROR: + auditLogSeverity = getResourceString("smtp.message.audit.log.severity.error", locale, auditLogSeverity); + break; + case ALERT: + auditLogSeverity = getResourceString("smtp.message.audit.log.severity.alert", locale, auditLogSeverity); + break; + + } + return auditLogSeverity; + } + + private static void appendPlainMessage(StringBuilder body, String messageId, String messageValue, Locale locale) { + appendMessage(body, messageId, messageValue, System.lineSeparator(), locale); + } + private static void appendHtmlMessage(StringBuilder body, String messageId, String messageValue, Locale locale) { + appendMessage(body, messageId, messageValue, "
", locale); + } + private static void appendMessage(StringBuilder body, String messageId, String messageValue, String messageSuffix, Locale locale) { + if (StringUtils.isNotEmpty(messageValue)) { + String message = getResourceString(messageId, locale, ""); + body.append(String.format(message, messageValue)) + .append(messageSuffix); + } + } + + private static String getResourceString(String id, Locale locale, String defaultValue) { + try { + return RESOURCES.computeIfAbsent(locale, lc -> ResourceBundle.getBundle("smtp-messages", lc)) + .getString(id); + } catch (MissingResourceException mre) { + log.debug("Failed to load resource string {}: {}", id, mre.getMessage(), mre); + return defaultValue; + } + } +} diff --git a/backend/manager/tools/src/main/java/org/ovirt/engine/core/notifier/transport/smtp/MessageBody.java b/backend/manager/tools/src/main/java/org/ovirt/engine/core/notifier/transport/smtp/MessageBody.java index 8f7e712dd7d..a7c820ceb09 100644 --- a/backend/manager/tools/src/main/java/org/ovirt/engine/core/notifier/transport/smtp/MessageBody.java +++ b/backend/manager/tools/src/main/java/org/ovirt/engine/core/notifier/transport/smtp/MessageBody.java @@ -1,5 +1,9 @@ package org.ovirt.engine.core.notifier.transport.smtp; +import java.util.Date; + +import org.ovirt.engine.core.common.AuditLogSeverity; + /** * Describes a message content */ @@ -10,9 +14,9 @@ public class MessageBody{ private String templateInfo; private String datacenterInfo; private String storageDomainInfo; - private String logTime; + private Date logTime; private String message; - private String severity; + private AuditLogSeverity severity; public String getUserInfo() { return userInfo; @@ -53,19 +57,19 @@ public void setStorageDomainInfo(String storageDomainInfo) { this.storageDomainInfo = storageDomainInfo; } - public String getLogTime() { + public Date getLogTime() { return logTime; } - public void setLogTime(String logTime) { + public void setLogTime(Date logTime) { this.logTime = logTime; } - public String getSeverity() { + public AuditLogSeverity getSeverity() { return severity; } - public void setSeverity(String severity) { + public void setSeverity(AuditLogSeverity severity) { this.severity = severity; } diff --git a/backend/manager/tools/src/main/java/org/ovirt/engine/core/notifier/transport/smtp/MessageHelper.java b/backend/manager/tools/src/main/java/org/ovirt/engine/core/notifier/transport/smtp/MessageHelper.java index 153aa0f8bbe..55886e1b88e 100644 --- a/backend/manager/tools/src/main/java/org/ovirt/engine/core/notifier/transport/smtp/MessageHelper.java +++ b/backend/manager/tools/src/main/java/org/ovirt/engine/core/notifier/transport/smtp/MessageHelper.java @@ -1,5 +1,7 @@ package org.ovirt.engine.core.notifier.transport.smtp; +import java.util.Date; + import org.apache.commons.lang.StringUtils; import org.ovirt.engine.core.notifier.filter.AuditLogEventType; @@ -19,7 +21,7 @@ public static String prepareMessageBody(MessageBody messageBody) { StringBuilder sb = new StringBuilder(); sb.append(String.format("Time:%s%nMessage:%s%nSeverity:%s%n", - messageBody.getLogTime(), + getLogTime(messageBody), messageBody.getMessage(), messageBody.getSeverity())); @@ -77,7 +79,7 @@ public static String prepareHTMLMessageBody(MessageBody messageBody) { StringBuilder sb = new StringBuilder(); sb.append(String.format("Time: %s
Message: %s
Severity: %s

", - messageBody.getLogTime(), + getLogTime(messageBody), messageBody.getMessage(), messageBody.getSeverity())); @@ -107,4 +109,9 @@ public static String prepareHTMLMessageBody(MessageBody messageBody) { } return sb.toString(); } + + private static String getLogTime(MessageBody messageBody) { + Date logTime = messageBody.getLogTime(); + return logTime == null ? "" : logTime.toString(); + } } diff --git a/backend/manager/tools/src/main/java/org/ovirt/engine/core/notifier/transport/smtp/Smtp.java b/backend/manager/tools/src/main/java/org/ovirt/engine/core/notifier/transport/smtp/Smtp.java index 384d0bcf1b1..efb9eaea808 100644 --- a/backend/manager/tools/src/main/java/org/ovirt/engine/core/notifier/transport/smtp/Smtp.java +++ b/backend/manager/tools/src/main/java/org/ovirt/engine/core/notifier/transport/smtp/Smtp.java @@ -3,8 +3,11 @@ import java.net.InetAddress; import java.net.UnknownHostException; +import java.util.ArrayList; import java.util.Date; import java.util.Iterator; +import java.util.List; +import java.util.Locale; import java.util.Properties; import java.util.Queue; import java.util.concurrent.LinkedBlockingQueue; @@ -19,8 +22,6 @@ import javax.mail.internet.MimeMessage; import org.apache.commons.lang.StringUtils; -import org.ovirt.engine.core.common.EventNotificationMethod; -import org.ovirt.engine.core.notifier.dao.DispatchResult; import org.ovirt.engine.core.notifier.filter.AuditLogEvent; import org.ovirt.engine.core.notifier.transport.Transport; import org.ovirt.engine.core.notifier.utils.NotificationProperties; @@ -60,6 +61,7 @@ public class Smtp extends Transport { private static final String MAIL_SMTP_ENCRYPTION_TLS = "tls"; private static final String MAIL_SEND_INTERVAL = "MAIL_SEND_INTERVAL"; private static final String MAIL_RETRIES = "MAIL_RETRIES"; + private static final String MAIL_LOCALE = "MAIL_LOCALE"; private static final Logger log = LoggerFactory.getLogger(Smtp.class); private int retries; @@ -71,6 +73,8 @@ public class Smtp extends Transport { private Session session = null; private InternetAddress from = null; private InternetAddress replyTo = null; + private SmtpMessageMerger messageMerger; + private Locale locale; private boolean active = false; public Smtp(NotificationProperties props) { @@ -136,6 +140,12 @@ private void init(NotificationProperties props) { } else { session = Session.getInstance(mailSessionProps); } + + messageMerger = new SmtpMessageMerger(props); + String lc = props.getProperty(MAIL_LOCALE, true); + if (StringUtils.isNotBlank(lc)) { + locale = Locale.forLanguageTag(lc); + } } @Override @@ -162,12 +172,14 @@ public void idle() { if (lastSendInterval++ >= sendIntervals) { lastSendInterval = 0; + messageMerger.mergeSimilarEvents(sendQueue); + Iterator iterator = sendQueue.iterator(); while (iterator.hasNext()) { DispatchAttempt attempt = iterator.next(); try { - EventMessageContent message = new EventMessageContent(); - message.prepareMessage(hostName, attempt.event, isBodyHtml); + EventMessageContent message = messageMerger.prepareEMailMessageContent( + hostName, attempt, isBodyHtml, locale); log.info("Sending e-mail subject='{}' to='{}'", message.getMessageSubject(), @@ -179,15 +191,12 @@ public void idle() { message.getMessageSubject(), attempt.address ); - notifyObservers(DispatchResult.success(attempt.event, attempt.address, EventNotificationMethod.SMTP)); + messageMerger.notifyAboutSuccess(this, attempt); iterator.remove(); } catch (Exception ex) { attempt.retries++; if (attempt.retries >= retries) { - notifyObservers(DispatchResult.failure(attempt.event, - attempt.address, - EventNotificationMethod.SMTP, - ex.getMessage())); + messageMerger.notifyAboutFailure(this, attempt, ex.getMessage()); iterator.remove(); } } @@ -258,14 +267,16 @@ protected PasswordAuthentication getPasswordAuthentication() { } } - private static class DispatchAttempt { - public final AuditLogEvent event; - public final String address; - public int retries = 0; - private DispatchAttempt(AuditLogEvent event, String address) { - this.event = event; - this.address = address; - } - } + static class DispatchAttempt { + public final AuditLogEvent event; + public final String address; + public final List merged = new ArrayList<>(); + public int retries = 0; + + DispatchAttempt(AuditLogEvent event, String address) { + this.event = event; + this.address = address; + } + } } diff --git a/backend/manager/tools/src/main/java/org/ovirt/engine/core/notifier/transport/smtp/SmtpMessageMerger.java b/backend/manager/tools/src/main/java/org/ovirt/engine/core/notifier/transport/smtp/SmtpMessageMerger.java new file mode 100644 index 00000000000..d60342c7339 --- /dev/null +++ b/backend/manager/tools/src/main/java/org/ovirt/engine/core/notifier/transport/smtp/SmtpMessageMerger.java @@ -0,0 +1,240 @@ +package org.ovirt.engine.core.notifier.transport.smtp; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Date; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import org.apache.commons.lang.StringUtils; +import org.ovirt.engine.core.common.EventNotificationMethod; +import org.ovirt.engine.core.notifier.dao.DispatchResult; +import org.ovirt.engine.core.notifier.filter.AuditLogEvent; +import org.ovirt.engine.core.notifier.transport.Observable; +import org.ovirt.engine.core.notifier.utils.NotificationProperties; + +/** + * Helper class to merge different events of the same type ({@link AuditLogEvent#getLogTypeName()}) into one e-mail to + * reduce the number of similar e-mails. Also, it helps to work with the merged e-mails. + * In order to configure this behavior, the following properties should be provided: + *

    + *
  • MAIL_MERGE_LOG_TYPES comma-separated list of log type names {@link AuditLogEvent#getLogTypeName()} + * that could be merged (e.g. ENGINE_BACKUP_STARTED, ENGINE_BACKUP_FAILED, ENGINE_BACKUP_COMPLETED).
  • + *
+ * + * The following properties are optional: + *
    + *
  • MAIL_MERGE_MAX_TIME_DIFFERENCE maximum allowed log time ({@link AuditLogEvent#getLogTime()}) + * difference (milliseconds) for two events to be merged. If two events have difference in log time greater than + * specified by the parameter then the events will not be merged. Default value is 5000.
  • + *
+ */ +class SmtpMessageMerger { + private static final String MAIL_MERGE_LOG_TYPES_PROPERTY = "MAIL_MERGE_LOG_TYPES"; + private static final String MAIL_MERGE_MAX_TIME_DIFFERENCE_PROPERTY = "MAIL_MERGE_MAX_TIME_DIFFERENCE"; + private static final String MAIL_MERGE_LOG_TYPES_SEPARATOR = ","; + private static final long MAIL_MERGE_MAX_TIME_DIFFERENCE_DEFAULT = 5000L; + + private final Set mergeLogTypes = new HashSet<>(); + private final long maxTimeDifference; + + SmtpMessageMerger(NotificationProperties props) { + String mergeLogTypesProperty = props.getProperty(MAIL_MERGE_LOG_TYPES_PROPERTY, true); + if (!StringUtils.isEmpty(mergeLogTypesProperty)) { + mergeLogTypes.addAll(Arrays.stream(mergeLogTypesProperty.split(MAIL_MERGE_LOG_TYPES_SEPARATOR)) + .map(String::trim) + .filter(type -> !type.isEmpty()) + .map(String::toUpperCase) + .collect(Collectors.toList())); + } + maxTimeDifference = props.getLong( + MAIL_MERGE_MAX_TIME_DIFFERENCE_PROPERTY, MAIL_MERGE_MAX_TIME_DIFFERENCE_DEFAULT); + } + + /** + * Prepare e-mail (subject and body) content for the merged event. + * @param hostName the host name on which the subject will refer to + * @param attempt associated attempt which the message will be created by + * @param isBodyHtml defines the format of message body + * @param locale locale for the message content + * @return e-mail content built by the event. + */ + EventMessageContent prepareEMailMessageContent(String hostName, Smtp.DispatchAttempt attempt, boolean isBodyHtml, Locale locale) { + EventMessageContent message = prepareEMailMessageContent(hostName, attempt.event, isBodyHtml, locale); + if (!attempt.merged.isEmpty()) { + List mergedMessages = attempt.merged.stream() + .map(event -> prepareEMailMessageContent(hostName, event, isBodyHtml, locale)) + .collect(Collectors.toList()); + //If the message consists of multiple merged messages then we need to define a subject of the resulting + // e-mail by the following algorithm: + //1. We walk through all subjects and define common prefix and common suffix of the subject across all + // messages. + //2. Then the resulting subject would be a concatenation: common prefix + + common suffix. + String subject = message.getMessageSubject(); + String commonPrefix = null; + String commonSuffix = null; + for (EventMessageContent mergedMessage : mergedMessages) { + String anotherSubject = mergedMessage.getMessageSubject(); + String prefix = defineCommonPrefix(subject, anotherSubject); + String suffix = defineCommonSuffix(subject, anotherSubject); + commonPrefix = defineShorterString(commonPrefix, prefix); + commonSuffix = defineShorterString(commonSuffix, suffix); + } + int pr = commonPrefix.length(); + int sx = commonSuffix.length(); + StringBuilder mergedSubject = new StringBuilder(commonPrefix) + .append(subject.substring(pr, subject.length() - sx)); + for (EventMessageContent mergedMessage : mergedMessages) { + String anotherSubject = mergedMessage.getMessageSubject(); + mergedSubject.append(", ") + .append(anotherSubject.substring(pr, anotherSubject.length() - sx).trim()); + } + mergedSubject.append(commonSuffix); + + //If the message consists of multiple merged messages then the body of the resulting message will be just a + // concatenation of bodies of all messages divided by a separator. + StringBuilder mergedBody = new StringBuilder(message.getMessageBody()); + for (EventMessageContent mergedMessage : mergedMessages) { + if (isBodyHtml) { + mergedBody.append("

==========================

"); + } else { + mergedBody.append("\n==========================\n\n"); + } + mergedBody.append(mergedMessage.getMessageBody()); + } + + message = new EventMessageContent(mergedSubject.toString(), mergedBody.toString()); + } + return message; + } + + private static String defineCommonPrefix(String str1, String str2) { + int size = Math.min(str1.length(), str2.length()); + for (int i = 0; i < size; i++) { + if (str1.charAt(i) != str2.charAt(i)) { + return str1.substring(0, i); + } + } + return str1.substring(0, size); + } + + private static String defineCommonSuffix(String str1, String str2) { + int l1 = str1.length(); + int l2 = str2.length(); + int size = Math.min(l1, l2); + for (int i = 1; i <= size; i++) { + if (str1.charAt(l1 - i) != str2.charAt(l2 - i)) { + return str1.substring(l1 - i + 1); + } + } + return str1.substring(0, size); + } + + private static String defineShorterString(String initialStr, String newStr) { + return initialStr == null ? newStr : (initialStr.length() < newStr.length() ? initialStr : newStr); + } + + private EventMessageContent prepareEMailMessageContent(String hostName, AuditLogEvent event, boolean isBodyHtml, Locale locale) { + EventMessageContent message = new EventMessageContent(); + message.prepareMessage(hostName, event, isBodyHtml, locale); + return message; + } + + /** + * Notify listeners about successful e-mail sending. + * @param observable observable object with the listeners to notify. + * @param attempt e-mail that was sent successfully. + */ + void notifyAboutSuccess(Observable observable, Smtp.DispatchAttempt attempt) { + observable.notifyObservers(DispatchResult.success( + attempt.event, attempt.address, EventNotificationMethod.SMTP)); + attempt.merged.forEach(event -> observable.notifyObservers(DispatchResult.success( + event, attempt.address, EventNotificationMethod.SMTP))); + } + + /** + * Notify listeners about failed e-mail sending. + * @param observable observable object with the listeners to notify. + * @param attempt e-mail that was failed to be sent. + * @param message description of the failure. + */ + void notifyAboutFailure(Observable observable, Smtp.DispatchAttempt attempt, String message) { + observable.notifyObservers(DispatchResult.failure( + attempt.event, attempt.address, EventNotificationMethod.SMTP, message)); + attempt.merged.forEach(event -> observable.notifyObservers(DispatchResult.failure( + event, attempt.address, EventNotificationMethod.SMTP, message))); + } + + /** + * Walk through all events and merge similar events into one (to send fewer e-mails) + * @param events list of events. Could be modified (all events merged into another events would be removed from the + * list). + */ + void mergeSimilarEvents(Collection events) { + List previous = new ArrayList<>(); + for (Iterator iterator = events.iterator(); iterator.hasNext();) { + Smtp.DispatchAttempt attempt = iterator.next(); + if (previous.isEmpty()) { + previous.add(attempt); + } else { + boolean merged = false; + for (Smtp.DispatchAttempt p : previous) { + if (mergeIfSimilar(p, attempt)) { + iterator.remove(); + merged = true; + break; + } + } + if (!merged) { + previous.add(attempt); + } + } + } + } + + /** + * Merge two e-mails into one. The e-mails will be merged if and only if: + *
    + *
  • they have exactly the same type ({@link AuditLogEvent#getLogTypeName()});
  • + *
  • the type was mentioned in the MAIL_MERGE_LOG_TYPES configuration property;
  • + *
  • time difference between the event log time ({@link AuditLogEvent#getLogTime()}) is less than specified + * in the MAIL_MERGE_MAX_TIME_DIFFERENCE configuration property.
  • + *
+ * @param main e-mail that would contain result of the merge. + * @param other e-mail that would be merged into the main e-mail. + * @return true if the specified e-mails were merged. + */ + private boolean mergeIfSimilar(Smtp.DispatchAttempt main, Smtp.DispatchAttempt other) { + if (!Objects.equals(main.address, other.address)) { + return false; + } + AuditLogEvent mainEvent = main.event; + AuditLogEvent otherEvent = other.event; + if (!Objects.equals(mainEvent.getLogTypeName(), otherEvent.getLogTypeName())) { + return false; + } + + if (!mergeLogTypes.contains(mainEvent.getLogTypeName())) { + return false; + } + + long logTime = Optional.ofNullable(main.event.getLogTime()).map(Date::getTime).orElse(0L); + long otherLogTime = Optional.ofNullable(other.event.getLogTime()).map(Date::getTime).orElse(0L); + if (Math.abs(logTime - otherLogTime) > maxTimeDifference) { + return false; + } + + main.merged.add(other.event); + main.merged.addAll(other.merged); + + return true; + } +} diff --git a/backend/manager/tools/src/main/resources/smtp-messages.properties b/backend/manager/tools/src/main/resources/smtp-messages.properties new file mode 100644 index 00000000000..b40b798de90 --- /dev/null +++ b/backend/manager/tools/src/main/resources/smtp-messages.properties @@ -0,0 +1,26 @@ +smtp.message.body.plain.time=Time: %s +smtp.message.body.html.time=Time: %s +smtp.message.body.plain.message=Message: %s +smtp.message.body.html.message=Message: %s +smtp.message.body.plain.severity=Severity: %s +smtp.message.body.html.severity=Severity: %s + +smtp.message.body.plain.user.info=User Name: %s +smtp.message.body.html.user.info=User Name: %s +smtp.message.body.plain.vm.info=VM Name: %s +smtp.message.body.html.vm.info=VM Name: %s +smtp.message.body.plain.host.info=Host Name: %s +smtp.message.body.html.host.info=Host Name: %s +smtp.message.body.plain.template.info=Template Name: %s +smtp.message.body.html.template.info=Template Name: %s +smtp.message.body.plain.dc.info=Data Center Name: %s +smtp.message.body.html.dc.info=Data Center Name: %s +smtp.message.body.plain.storage.domain.info=Storage Domain Name: %s +smtp.message.body.html.storage.domain.info=Storage Domain Name: %s + +smtp.message.audit.log.event.type.resolve=resolveMessage +smtp.message.audit.log.event.type.alert=alertMessage +smtp.message.audit.log.severity.normal=NORMAL +smtp.message.audit.log.severity.warning=WARNING +smtp.message.audit.log.severity.error=ERROR +smtp.message.audit.log.severity.alert=ALERT diff --git a/backend/manager/tools/src/main/resources/smtp-messages_ru.properties b/backend/manager/tools/src/main/resources/smtp-messages_ru.properties new file mode 100644 index 00000000000..c29370931e6 --- /dev/null +++ b/backend/manager/tools/src/main/resources/smtp-messages_ru.properties @@ -0,0 +1,26 @@ +smtp.message.body.plain.time=\u0412\u0440\u0435\u043c\u044f: %s +smtp.message.body.html.time=\u0412\u0440\u0435\u043c\u044f: %s +smtp.message.body.plain.message=\u0421\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435: %s +smtp.message.body.html.message=\u0421\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435: %s +smtp.message.body.plain.severity=\u0423\u0440\u043e\u0432\u0435\u043d\u044c \u043e\u043f\u043e\u0432\u0435\u0449\u0435\u043d\u0438\u044f: %s +smtp.message.body.html.severity=\u0423\u0440\u043e\u0432\u0435\u043d\u044c \u043e\u043f\u043e\u0432\u0435\u0449\u0435\u043d\u0438\u044f: %s + +smtp.message.body.plain.user.info=\u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c: %s +smtp.message.body.html.user.info=\u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c: %s +smtp.message.body.plain.vm.info=\u0412\u041c: %s +smtp.message.body.html.vm.info=\u0412\u041c: %s +smtp.message.body.plain.host.info=\u0425\u043e\u0441\u0442: %s +smtp.message.body.html.host.info=\u0425\u043e\u0441\u0442: %s +smtp.message.body.plain.template.info=\u0428\u0430\u0431\u043b\u043e\u043d: %s +smtp.message.body.html.template.info=\u0428\u0430\u0431\u043b\u043e\u043d: %s +smtp.message.body.plain.dc.info=\u0414\u0430\u0442\u0430\u0446\u0435\u043d\u0442\u0440: %s +smtp.message.body.html.dc.info=\u0414\u0430\u0442\u0430\u0446\u0435\u043d\u0442\u0440: %s +smtp.message.body.plain.storage.domain.info=\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0445\u0440\u0430\u043d\u0435\u043d\u0438\u044f: %s +smtp.message.body.html.storage.domain.info=\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0445\u0440\u0430\u043d\u0435\u043d\u0438\u044f: %s + +smtp.message.audit.log.event.type.resolve=\u041f\u0440\u043e\u0431\u043b\u0435\u043c\u0430 \u0440\u0435\u0448\u0435\u043d\u0430 +smtp.message.audit.log.event.type.alert=\u041e\u043f\u043e\u0432\u0435\u0449\u0435\u043d\u0438\u0435 +smtp.message.audit.log.severity.normal=\u041d\u041e\u0420\u041c\u0410\u041b\u042c\u041d\u041e\u0415 +smtp.message.audit.log.severity.warning=\u041f\u0420\u0415\u0414\u0423\u041f\u0420\u0415\u0416\u0414\u0415\u041d\u0418\u0415 +smtp.message.audit.log.severity.error=\u041e\u0428\u0418\u0411\u041a\u0410 +smtp.message.audit.log.severity.alert=\u0422\u0420\u0415\u0412\u041e\u0413\u0410 diff --git a/backend/manager/tools/src/test/java/org/ovirt/engine/core/notifier/transport/smtp/LocalizedMessageHelperTest.java b/backend/manager/tools/src/test/java/org/ovirt/engine/core/notifier/transport/smtp/LocalizedMessageHelperTest.java new file mode 100644 index 00000000000..b17f5925204 --- /dev/null +++ b/backend/manager/tools/src/test/java/org/ovirt/engine/core/notifier/transport/smtp/LocalizedMessageHelperTest.java @@ -0,0 +1,120 @@ +package org.ovirt.engine.core.notifier.transport.smtp; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Calendar; +import java.util.Locale; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.ovirt.engine.core.common.AuditLogSeverity; +import org.ovirt.engine.core.notifier.filter.AuditLogEventType; + +public class LocalizedMessageHelperTest { + + private MessageBody message; + + @BeforeEach + public void init() { + message = new MessageBody(); + message.setUserInfo("user@user.com"); + message.setVmInfo("vm01"); + message.setHostInfo("host"); + message.setTemplateInfo("templ"); + message.setDatacenterInfo("dc"); + message.setStorageDomainInfo("storage"); + Calendar cal = Calendar.getInstance(); + cal.set(2022 , Calendar.DECEMBER, 31, 23, 59, 59); + message.setLogTime(cal.getTime()); + message.setSeverity(AuditLogSeverity.WARNING); + message.setMessage("message"); + + } + + @Test + public void testForEnglish() { + Locale locale = Locale.ENGLISH; + String subject = LocalizedMessageHelper.prepareMessageSubject(AuditLogEventType.alertMessage, "localhost", message.getMessage(), locale); + assertEquals("alertMessage (localhost), [message]", subject); + + String plainBody = LocalizedMessageHelper.prepareMessageBody(message, locale); + assertEquals("Time: Dec 31, 2022, 11:59:59 PM\n" + + "Message: message\n" + + "Severity: WARNING\n" + + "User Name: user@user.com\n" + + "VM Name: vm01\n" + + "Host Name: host\n" + + "Template Name: templ\n" + + "Data Center Name: dc\n" + + "Storage Domain Name: storage\n", plainBody); + + String htmlBody = LocalizedMessageHelper.prepareHTMLMessageBody(message, locale); + assertEquals("Time: Dec 31, 2022, 11:59:59 PM
" + + "Message: message
" + + "Severity: WARNING
" + + "User Name: user@user.com
" + + "VM Name: vm01
" + + "Host Name: host
" + + "Template Name: templ
" + + "Data Center Name: dc
" + + "Storage Domain Name: storage
", htmlBody); + } + + @Test + public void testForNonTranslatedLanguage() { + Locale locale = Locale.forLanguageTag("fr-FR"); + String subject = LocalizedMessageHelper.prepareMessageSubject(AuditLogEventType.alertMessage, "localhost", message.getMessage(), locale); + assertEquals("alertMessage (localhost), [message]", subject); + + String plainBody = LocalizedMessageHelper.prepareMessageBody(message, locale); + assertEquals("Time: 31 déc. 2022 à 23:59:59\n" + + "Message: message\n" + + "Severity: WARNING\n" + + "User Name: user@user.com\n" + + "VM Name: vm01\n" + + "Host Name: host\n" + + "Template Name: templ\n" + + "Data Center Name: dc\n" + + "Storage Domain Name: storage\n", plainBody); + + String htmlBody = LocalizedMessageHelper.prepareHTMLMessageBody(message, locale); + assertEquals("Time: 31 déc. 2022 à 23:59:59
" + + "Message: message
" + + "Severity: WARNING
" + + "User Name: user@user.com
" + + "VM Name: vm01
" + + "Host Name: host
" + + "Template Name: templ
" + + "Data Center Name: dc
" + + "Storage Domain Name: storage
", htmlBody); + } + + @Test + public void testForNotDefaultLanguage() { + Locale locale = Locale.forLanguageTag("ru-RU"); + String subject = LocalizedMessageHelper.prepareMessageSubject(AuditLogEventType.alertMessage, "localhost", message.getMessage(), locale); + assertEquals("Оповещение (localhost), [message]", subject); + + String plainBody = LocalizedMessageHelper.prepareMessageBody(message, locale); + assertEquals("Время: 31 дек. 2022 г., 23:59:59\n" + + "Сообщение: message\n" + + "Уровень оповещения: ПРЕДУПРЕЖДЕНИЕ\n" + + "Пользователь: user@user.com\n" + + "ВМ: vm01\n" + + "Хост: host\n" + + "Шаблон: templ\n" + + "Датацентр: dc\n" + + "Устройство хранения: storage\n", plainBody); + + String htmlBody = LocalizedMessageHelper.prepareHTMLMessageBody(message, locale); + assertEquals("Время: 31 дек. 2022 г., 23:59:59
" + + "Сообщение: message
" + + "Уровень оповещения: ПРЕДУПРЕЖДЕНИЕ
" + + "Пользователь: user@user.com
" + + "ВМ: vm01
" + + "Хост: host
" + + "Шаблон: templ
" + + "Датацентр: dc
" + + "Устройство хранения: storage
", htmlBody); + } +} diff --git a/backend/manager/tools/src/test/java/org/ovirt/engine/core/notifier/transport/smtp/SmtpMessageMergerTest.java b/backend/manager/tools/src/test/java/org/ovirt/engine/core/notifier/transport/smtp/SmtpMessageMergerTest.java new file mode 100644 index 00000000000..16ddd7477ac --- /dev/null +++ b/backend/manager/tools/src/test/java/org/ovirt/engine/core/notifier/transport/smtp/SmtpMessageMergerTest.java @@ -0,0 +1,240 @@ +package org.ovirt.engine.core.notifier.transport.smtp; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.ovirt.engine.core.common.AuditLogType; +import org.ovirt.engine.core.notifier.dao.DispatchResult; +import org.ovirt.engine.core.notifier.transport.Observable; +import org.ovirt.engine.core.notifier.transport.Observer; +import org.ovirt.engine.core.notifier.utils.NotificationProperties; + +public class SmtpMessageMergerTest { + + private List testMessages = new ArrayList<>(); + + @BeforeEach + public void initTest() { + String address = "user@user.com"; + Date date = new Date(); + Date shiftedDate = new Date(date.getTime() - 1000); + Date significantlyShiftedDate = new Date(date.getTime() - 10000); + testMessages.add(new Smtp.DispatchAttempt( + SmtpTest.prepareEvent(date, AuditLogType.ENGINE_BACKUP_COMPLETED, "engine-backup: Backup Finished, scope=files, log=/var/log/ovirt-engine-backup/ovirt-engine-backup-20221215053034.log"), + address)); + testMessages.add(new Smtp.DispatchAttempt( + SmtpTest.prepareEvent(date, AuditLogType.ENGINE_BACKUP_COMPLETED, "engine-backup: Backup Finished, scope=db, log=/var/log/ovirt-engine-backup/ovirt-engine-backup-20221215053034.log"), + address)); + testMessages.add(new Smtp.DispatchAttempt( + SmtpTest.prepareEvent(significantlyShiftedDate, AuditLogType.ENGINE_BACKUP_COMPLETED, "engine-backup: Backup Finished, scope=grafanadb, log=/var/log/ovirt-engine-backup/ovirt-engine-backup-20221215053034.log"), + address)); + + testMessages.add(new Smtp.DispatchAttempt( + SmtpTest.prepareEvent(date, AuditLogType.ENGINE_BACKUP_STARTED, "engine-backup: Backup Started, scope=files, log=/var/log/ovirt-engine-backup/ovirt-engine-backup-20221215053034.log"), + address)); + testMessages.add(new Smtp.DispatchAttempt( + SmtpTest.prepareEvent(date, AuditLogType.ENGINE_BACKUP_STARTED, "engine-backup: Backup Started, scope=db, log=/var/log/ovirt-engine-backup/ovirt-engine-backup-20221215053034.log"), + address)); + + String anotherAddress = "yetanotheruser@user.com"; + testMessages.add(new Smtp.DispatchAttempt( + SmtpTest.prepareEvent(date, AuditLogType.ENGINE_BACKUP_COMPLETED, "engine-backup: Backup Finished, scope=files, log=/var/log/ovirt-engine-backup/ovirt-engine-backup-20221215053034.log"), + anotherAddress)); + testMessages.add(new Smtp.DispatchAttempt( + SmtpTest.prepareEvent(date, AuditLogType.ENGINE_BACKUP_COMPLETED, "engine-backup: Backup Finished, scope=db, log=/var/log/ovirt-engine-backup/ovirt-engine-backup-20221215053034.log"), + anotherAddress)); + testMessages.add(new Smtp.DispatchAttempt( + SmtpTest.prepareEvent(shiftedDate, AuditLogType.ENGINE_BACKUP_COMPLETED, "engine-backup: Backup Finished, scope=grafanadb, log=/var/log/ovirt-engine-backup/ovirt-engine-backup-20221215053034.log"), + anotherAddress)); + + testMessages.add(new Smtp.DispatchAttempt( + SmtpTest.prepareEvent(shiftedDate, AuditLogType.ENGINE_BACKUP_COMPLETED, "engine-backup: Backup Finished, scope=dwhdb, log=/var/log/ovirt-engine-backup/ovirt-engine-backup-20221215053034.log"), + address)); + testMessages.add(new Smtp.DispatchAttempt( + SmtpTest.prepareEvent(shiftedDate, AuditLogType.ENGINE_BACKUP_COMPLETED, "engine-backup: Backup Finished, scope=dwhdb, log=/var/log/ovirt-engine-backup/ovirt-engine-backup-20221215053034.log"), + anotherAddress)); + } + + @Test + public void testMergeNotConfigured() { + NotificationProperties properties = mock(NotificationProperties.class); + SmtpMessageMerger messageMerger = new SmtpMessageMerger(properties); + int numberOfEventsBeforeMerge = testMessages.size(); + + messageMerger.mergeSimilarEvents(testMessages); + + assertEquals(numberOfEventsBeforeMerge, testMessages.size(), "Events were merged, but should not."); + } + + @Test + public void testMerge() { + NotificationProperties properties = mock(NotificationProperties.class); + when(properties.getProperty("MAIL_MERGE_LOG_TYPES", true)).thenReturn( + "ENGINE_BACKUP_COMPLETED"); + when(properties.getLong("MAIL_MERGE_MAX_TIME_DIFFERENCE", 5000L)).thenReturn(5000L); + SmtpMessageMerger messageMerger = new SmtpMessageMerger(properties); + + messageMerger.mergeSimilarEvents(testMessages); + + assertEquals(5, testMessages.size(), "Events were merged incorrectly"); + + //First 2 ENGINE_BACKUP_COMPLETED and second from the end events should be merged, because they have the same + //LogType, address and similar log time (within 5 seconds) + Smtp.DispatchAttempt event = testMessages.get(0); + assertEquals(AuditLogType.ENGINE_BACKUP_COMPLETED.name(), event.event.getLogTypeName()); + assertEquals(2, event.merged.size()); + + //Third ENGINE_BACKUP_COMPLETED was not merged with any other events because it has significantly different + //log time (10 seconds in the past) + event = testMessages.get(1); + assertEquals(AuditLogType.ENGINE_BACKUP_COMPLETED.name(), event.event.getLogTypeName()); + assertEquals(0, event.merged.size()); + + //Next 2 ENGINE_BACKUP_STARTED events were not merged because their type was not configured via + //MAIL_MERGE_LOG_TYPES + event = testMessages.get(2); + assertEquals(AuditLogType.ENGINE_BACKUP_STARTED.name(), event.event.getLogTypeName()); + assertEquals(0, event.merged.size()); + + event = testMessages.get(3); + assertEquals(AuditLogType.ENGINE_BACKUP_STARTED.name(), event.event.getLogTypeName()); + assertEquals(0, event.merged.size()); + + //Last 4 ENGINE_BACKUP_COMPLETED were merged into one, because they have the same LogType, address and similar + //log time (within 5 seconds) + event = testMessages.get(4); + assertEquals(AuditLogType.ENGINE_BACKUP_COMPLETED.name(), event.event.getLogTypeName()); + assertEquals(3, event.merged.size()); + } + + @Test + public void testPrepareMessageContent() { + NotificationProperties properties = mock(NotificationProperties.class); + when(properties.getProperty("MAIL_MERGE_LOG_TYPES", true)).thenReturn( + "ENGINE_BACKUP_COMPLETED,ENGINE_BACKUP_STARTED"); + when(properties.getLong("MAIL_MERGE_MAX_TIME_DIFFERENCE", 5000L)).thenReturn(5000L); + SmtpMessageMerger messageMerger = new SmtpMessageMerger(properties); + + messageMerger.mergeSimilarEvents(testMessages); + + assertEquals(4, testMessages.size(), "Events were merged incorrectly"); + + //Test Plain Text + Smtp.DispatchAttempt event = testMessages.get(0); + EventMessageContent content = messageMerger.prepareEMailMessageContent("localhost", event, false, null); + String expectedSubject = "alertMessage (localhost), [engine-backup: Backup Finished, scope=files, db, dwhdb, log=/var/log/ovirt-engine-backup/ovirt-engine-backup-20221215053034.log]"; + String expectedBody = "Time:" + event.event.getLogTime() + "\n" + + "Message:engine-backup: Backup Finished, scope=files, log=/var/log/ovirt-engine-backup/ovirt-engine-backup-20221215053034.log\n" + + "Severity:ERROR\n" + + "\n==========================\n" + + "\n" + + "Time:" + event.merged.get(0).getLogTime() + "\n" + + "Message:engine-backup: Backup Finished, scope=db, log=/var/log/ovirt-engine-backup/ovirt-engine-backup-20221215053034.log\n" + + "Severity:ERROR\n" + + "\n==========================\n" + + "\n" + + "Time:" + event.merged.get(1).getLogTime() + "\n" + + "Message:engine-backup: Backup Finished, scope=dwhdb, log=/var/log/ovirt-engine-backup/ovirt-engine-backup-20221215053034.log\n" + + "Severity:ERROR\n"; + assertEquals(expectedSubject, content.getMessageSubject()); + assertEquals(expectedBody, content.getMessageBody()); + + event = testMessages.get(1); + content = messageMerger.prepareEMailMessageContent("localhost", event, false, null); + expectedSubject = "alertMessage (localhost), [engine-backup: Backup Finished, scope=grafanadb, log=/var/log/ovirt-engine-backup/ovirt-engine-backup-20221215053034.log]"; + expectedBody = "Time:" + event.event.getLogTime() + "\n" + + "Message:engine-backup: Backup Finished, scope=grafanadb, log=/var/log/ovirt-engine-backup/ovirt-engine-backup-20221215053034.log\n" + + "Severity:ERROR\n"; + assertEquals(expectedSubject, content.getMessageSubject()); + assertEquals(expectedBody, content.getMessageBody()); + + //Test HTML + event = testMessages.get(2); + content = messageMerger.prepareEMailMessageContent("localhost", event, true, null); + expectedSubject = "alertMessage (localhost), [engine-backup: Backup Started, scope=files, db, log=/var/log/ovirt-engine-backup/ovirt-engine-backup-20221215053034.log]"; + expectedBody = "Time: " + event.event.getLogTime() + "
" + + "Message: engine-backup: Backup Started, scope=files, log=/var/log/ovirt-engine-backup/ovirt-engine-backup-20221215053034.log
" + + "Severity: ERROR


" + + "
==========================
" + + "
" + + "Time: " + event.merged.get(0).getLogTime() + "
" + + "Message: engine-backup: Backup Started, scope=db, log=/var/log/ovirt-engine-backup/ovirt-engine-backup-20221215053034.log
" + + "Severity: ERROR

"; + assertEquals(expectedSubject, content.getMessageSubject()); + assertEquals(expectedBody, content.getMessageBody()); + } + + @Test + public void testNotifyAboutSuccessOrFailure() { + NotificationProperties properties = mock(NotificationProperties.class); + when(properties.getProperty("MAIL_MERGE_LOG_TYPES", true)).thenReturn( + "ENGINE_BACKUP_COMPLETED,ENGINE_BACKUP_STARTED"); + when(properties.getLong("MAIL_MERGE_MAX_TIME_DIFFERENCE", 5000L)).thenReturn(5000L); + SmtpMessageMerger messageMerger = new SmtpMessageMerger(properties); + + assertEquals(10, testMessages.size(), "Test data was changed, the test should be adjusted accordingly"); + List notifications = new ArrayList<>(); + Observable observable = new Observable() { + @Override + public void notifyObservers(DispatchResult data) { + notifications.add(data); + } + + @Override + public void registerObserver(Observer observer) { + } + + @Override + public void removeObserver(Observer observer) { + } + }; + + //Verify notifyAboutSuccess before messages merged + for (Smtp.DispatchAttempt event : testMessages) { + messageMerger.notifyAboutSuccess(observable, event); + } + + assertEquals(10, notifications.size()); + long successes = notifications.stream().filter(DispatchResult::isSuccess).count(); + assertEquals(10L, successes); + + //Verify notifyAboutFailure before messages merged + notifications.clear(); + for (Smtp.DispatchAttempt event : testMessages) { + messageMerger.notifyAboutFailure(observable, event, "an error"); + } + assertEquals(10, notifications.size()); + successes = notifications.stream().filter(DispatchResult::isSuccess).count(); + assertEquals(0L, successes); + + messageMerger.mergeSimilarEvents(testMessages); + assertEquals(4, testMessages.size(), "Events were merged incorrectly"); + + //Verify notifyAboutSuccess when messages were merged + notifications.clear(); + for (Smtp.DispatchAttempt event : testMessages) { + messageMerger.notifyAboutSuccess(observable, event); + } + + assertEquals(10, notifications.size()); // the number of notifications should not be changed even if messages were merged + successes = notifications.stream().filter(DispatchResult::isSuccess).count(); + assertEquals(10L, successes); + + //Verify notifyAboutFailure when messages were merged + notifications.clear(); + for (Smtp.DispatchAttempt event : testMessages) { + messageMerger.notifyAboutFailure(observable, event, "yet another error"); + } + + assertEquals(10, notifications.size()); // the number of notifications should not be changed even if messages were merged + successes = notifications.stream().filter(DispatchResult::isSuccess).count(); + assertEquals(0L, successes); + } +} diff --git a/backend/manager/tools/src/test/java/org/ovirt/engine/core/notifier/transport/smtp/SmtpTest.java b/backend/manager/tools/src/test/java/org/ovirt/engine/core/notifier/transport/smtp/SmtpTest.java new file mode 100644 index 00000000000..91f57d46578 --- /dev/null +++ b/backend/manager/tools/src/test/java/org/ovirt/engine/core/notifier/transport/smtp/SmtpTest.java @@ -0,0 +1,56 @@ +package org.ovirt.engine.core.notifier.transport.smtp; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.ovirt.engine.core.common.AuditLogSeverity; +import org.ovirt.engine.core.common.AuditLogType; +import org.ovirt.engine.core.notifier.dao.DispatchResult; +import org.ovirt.engine.core.notifier.filter.AuditLogEvent; +import org.ovirt.engine.core.notifier.filter.AuditLogEventType; +import org.ovirt.engine.core.notifier.utils.NotificationProperties; + +public class SmtpTest { + @Test + public void testCombineEngineBackupMessages() { + NotificationProperties properties = mock(NotificationProperties.class); + when(properties.getProperty("MAIL_SERVER", true)).thenReturn("smtp"); + when(properties.getProperty("MAIL_SERVER")).thenReturn("smtp"); + when(properties.getProperty("MAIL_SMTP_ENCRYPTION")).thenReturn("none"); + when(properties.getProperty("MAIL_MERGE_LOG_TYPES", true)).thenReturn( + "ENGINE_BACKUP_FAILED, ENGINE_BACKUP_COMPLETED, ENGINE_BACKUP_STARTED"); + when(properties.getLong("MAIL_MERGE_MAX_TIME_DIFFERENCE", 5000L)).thenReturn(5000L); + Smtp smtp = new Smtp(properties); + Date dt = new Date(); + String address = "user@user.com"; + AuditLogEvent event = prepareEvent(dt, AuditLogType.ENGINE_BACKUP_COMPLETED, "engine-backup: Backup Finished, scope=files, log=/var/log/ovirt-engine-backup/ovirt-engine-backup-20221215053034.log"); + smtp.dispatchEvent(event, address); + event = prepareEvent(dt, AuditLogType.ENGINE_BACKUP_COMPLETED, "engine-backup: Backup Finished, scope=db, log=/var/log/ovirt-engine-backup/ovirt-engine-backup-20221215053034.log"); + smtp.dispatchEvent(event, address); + + List results = new ArrayList<>(); + smtp.registerObserver((o, data) -> results.add(data)); + smtp.idle(); + assertEquals(2, results.size()); + DispatchResult dispatchResult = results.get(0); + assertFalse(dispatchResult.isSuccess()); + assertEquals(address, dispatchResult.getAddress()); + } + + static AuditLogEvent prepareEvent(Date dt, AuditLogType type, String message) { + AuditLogEvent event = new AuditLogEvent(); + event.setLogTime(dt); + event.setType(AuditLogEventType.alertMessage); + event.setLogTypeName(type.name()); + event.setMessage(message); + event.setSeverity(AuditLogSeverity.ERROR); + return event; + } +} diff --git a/packaging/services/ovirt-engine-notifier/ovirt-engine-notifier.conf.in b/packaging/services/ovirt-engine-notifier/ovirt-engine-notifier.conf.in index 8aef3ba4943..75ecbf552a9 100644 --- a/packaging/services/ovirt-engine-notifier/ovirt-engine-notifier.conf.in +++ b/packaging/services/ovirt-engine-notifier/ovirt-engine-notifier.conf.in @@ -209,6 +209,20 @@ MAIL_SEND_INTERVAL=1 # Amount of times to attempt sending an email before failing. MAIL_RETRIES=4 +# Comma-separated list of log type names that could be merged. +# example: +# MAIL_MERGE_LOG_TYPES=ENGINE_BACKUP_STARTED,ENGINE_BACKUP_FAILED,ENGINE_BACKUP_COMPLETED +MAIL_MERGE_LOG_TYPES= + +# Maximum allowed log time difference (milliseconds) for two events to be merged. If two events have difference in log +# time greater than specified by the parameter then the events will not be merged. +MAIL_MERGE_MAX_TIME_DIFFERENCE=5000 + +# Locale to be used to create e-mail message subject and body. If not specified then all messages will be in english +# example: +# MAIL_LOCALE=cs-CZ +MAIL_LOCALE= + #-------------------------# # SNMP_TRAP Notifications # #-------------------------#