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 #
#-------------------------#