From b4e5f6ec13b3a0e2d069f9519e9832469fc1f12e Mon Sep 17 00:00:00 2001 From: Mohit Yadav <105265192+mohityadav766@users.noreply.github.com> Date: Thu, 10 Nov 2022 14:22:44 +0530 Subject: [PATCH] Emailing Task and Test Notification (#8626) * Emailing Task Notification [WIP] * Emailing Test Result to Owners of table in case it is enabled --- .../service/events/ChangeEventHandler.java | 85 +----- .../service/jdbi3/CollectionDAO.java | 18 +- .../resources/settings/SettingsCache.java | 11 + .../service/socket/WebSocketManager.java | 2 +- .../openmetadata/service/util/EmailUtil.java | 56 +++- .../service/util/NotificationHandler.java | 273 ++++++++++++++++++ .../emailTemplates/taskAssignment.ftl | 96 ++++++ .../emailTemplates/testResultStatus.ftl | 92 ++++++ .../json/data/settings/settingsData.json | 14 + .../taskNotificationConfiguration.json | 16 + .../testResultNotificationConfiguration.json | 35 +++ .../json/schema/settings/settings.json | 8 +- .../resources/json/schema/tests/basic.json | 32 +- 13 files changed, 634 insertions(+), 104 deletions(-) create mode 100644 openmetadata-service/src/main/java/org/openmetadata/service/util/NotificationHandler.java create mode 100644 openmetadata-service/src/main/resources/emailTemplates/taskAssignment.ftl create mode 100644 openmetadata-service/src/main/resources/emailTemplates/testResultStatus.ftl create mode 100644 openmetadata-spec/src/main/resources/json/schema/configuration/taskNotificationConfiguration.json create mode 100644 openmetadata-spec/src/main/resources/json/schema/configuration/testResultNotificationConfiguration.json diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/events/ChangeEventHandler.java b/openmetadata-service/src/main/java/org/openmetadata/service/events/ChangeEventHandler.java index fc2829840fe..b52a6deca77 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/events/ChangeEventHandler.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/events/ChangeEventHandler.java @@ -17,12 +17,8 @@ import static org.openmetadata.common.utils.CommonUtil.listOrEmpty; import static org.openmetadata.schema.type.EventType.ENTITY_DELETED; import static org.openmetadata.schema.type.EventType.ENTITY_SOFT_DELETED; import static org.openmetadata.schema.type.EventType.ENTITY_UPDATED; -import static org.openmetadata.service.Entity.TEAM; -import static org.openmetadata.service.Entity.USER; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import java.time.Instant; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -36,27 +32,21 @@ import lombok.extern.slf4j.Slf4j; import org.jdbi.v3.core.Jdbi; import org.openmetadata.schema.EntityInterface; import org.openmetadata.schema.entity.feed.Thread; -import org.openmetadata.schema.entity.teams.Team; -import org.openmetadata.schema.entity.teams.User; -import org.openmetadata.schema.type.AnnouncementDetails; import org.openmetadata.schema.type.ChangeDescription; import org.openmetadata.schema.type.ChangeEvent; import org.openmetadata.schema.type.EntityReference; import org.openmetadata.schema.type.EventType; -import org.openmetadata.schema.type.Post; -import org.openmetadata.schema.type.Relationship; import org.openmetadata.service.Entity; import org.openmetadata.service.OpenMetadataApplicationConfig; import org.openmetadata.service.filter.FilterRegistry; import org.openmetadata.service.jdbi3.CollectionDAO; -import org.openmetadata.service.jdbi3.CollectionDAO.EntityRelationshipRecord; import org.openmetadata.service.jdbi3.FeedRepository; -import org.openmetadata.service.resources.feeds.MessageParser; import org.openmetadata.service.resources.feeds.MessageParser.EntityLink; import org.openmetadata.service.socket.WebSocketManager; import org.openmetadata.service.util.ChangeEventParser; import org.openmetadata.service.util.FilterUtil; import org.openmetadata.service.util.JsonUtils; +import org.openmetadata.service.util.NotificationHandler; import org.openmetadata.service.util.RestUtil; @Slf4j @@ -64,11 +54,13 @@ public class ChangeEventHandler implements EventHandler { private CollectionDAO dao; private FeedRepository feedDao; private ObjectMapper mapper; + private NotificationHandler notificationHandler; public void init(OpenMetadataApplicationConfig config, Jdbi jdbi) { this.dao = jdbi.onDemand(CollectionDAO.class); this.feedDao = new FeedRepository(dao); this.mapper = new ObjectMapper(); + this.notificationHandler = new NotificationHandler(jdbi.onDemand(CollectionDAO.class)); } public Void process(ContainerRequestContext requestContext, ContainerResponseContext responseContext) { @@ -76,7 +68,7 @@ public class ChangeEventHandler implements EventHandler { SecurityContext securityContext = requestContext.getSecurityContext(); String loggedInUserName = securityContext.getUserPrincipal().getName(); try { - handleWebSocket(responseContext); + notificationHandler.processNotifications(responseContext); ChangeEvent changeEvent = getChangeEvent(method, responseContext); if (changeEvent == null) { return null; @@ -134,75 +126,6 @@ public class ChangeEventHandler implements EventHandler { return null; } - private void handleWebSocket(ContainerResponseContext responseContext) { - int responseCode = responseContext.getStatus(); - if (responseCode == Status.CREATED.getStatusCode() - && responseContext.getEntity() != null - && responseContext.getEntity().getClass().equals(Thread.class)) { - Thread thread = (Thread) responseContext.getEntity(); - try { - String jsonThread = mapper.writeValueAsString(thread); - switch (thread.getType()) { - case Task: - if (thread.getPostsCount() == 0) { - List assignees = thread.getTask().getAssignees(); - assignees.forEach( - e -> { - if (Entity.USER.equals(e.getType())) { - WebSocketManager.getInstance() - .sendToOne(e.getId(), WebSocketManager.TASK_BROADCAST_CHANNEL, jsonThread); - } else if (Entity.TEAM.equals(e.getType())) { - // fetch all that are there in the team - List records = - dao.relationshipDAO() - .findTo(e.getId().toString(), TEAM, Relationship.HAS.ordinal(), Entity.USER); - WebSocketManager.getInstance() - .sendToManyWithString(records, WebSocketManager.TASK_BROADCAST_CHANNEL, jsonThread); - } - }); - } - break; - case Conversation: - WebSocketManager.getInstance().broadCastMessageToAll(WebSocketManager.FEED_BROADCAST_CHANNEL, jsonThread); - List mentions; - if (thread.getPostsCount() == 0) { - mentions = MessageParser.getEntityLinks(thread.getMessage()); - } else { - Post latestPost = thread.getPosts().get(thread.getPostsCount() - 1); - mentions = MessageParser.getEntityLinks(latestPost.getMessage()); - } - mentions.forEach( - entityLink -> { - String fqn = entityLink.getEntityFQN(); - if (USER.equals(entityLink.getEntityType())) { - User user = dao.userDAO().findEntityByName(fqn); - WebSocketManager.getInstance() - .sendToOne(user.getId(), WebSocketManager.MENTION_CHANNEL, jsonThread); - } else if (TEAM.equals(entityLink.getEntityType())) { - Team team = dao.teamDAO().findEntityByName(fqn); - // fetch all that are there in the team - List records = - dao.relationshipDAO().findTo(team.getId().toString(), TEAM, Relationship.HAS.ordinal(), USER); - WebSocketManager.getInstance() - .sendToManyWithString(records, WebSocketManager.MENTION_CHANNEL, jsonThread); - } - }); - break; - case Announcement: - AnnouncementDetails announcementDetails = thread.getAnnouncement(); - Long currentTimestamp = Instant.now().getEpochSecond(); - if (announcementDetails.getStartTime() <= currentTimestamp - && currentTimestamp <= announcementDetails.getEndTime()) { - WebSocketManager.getInstance().broadCastMessageToAll(WebSocketManager.ANNOUNCEMENT_CHANNEL, jsonThread); - } - break; - } - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - } - } - } - public ChangeEvent getChangeEvent(String method, ContainerResponseContext responseContext) { // GET operations don't produce change events if (method.equals("GET")) { diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/CollectionDAO.java b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/CollectionDAO.java index 5233e78d882..c000d6d83a4 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/CollectionDAO.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/CollectionDAO.java @@ -42,6 +42,8 @@ import org.jdbi.v3.sqlobject.customizer.BindMap; import org.jdbi.v3.sqlobject.customizer.Define; import org.jdbi.v3.sqlobject.statement.SqlQuery; import org.jdbi.v3.sqlobject.statement.SqlUpdate; +import org.openmetadata.api.configuration.airflow.TaskNotificationConfiguration; +import org.openmetadata.api.configuration.airflow.TestResultNotificationConfiguration; import org.openmetadata.common.utils.CommonUtil; import org.openmetadata.schema.TokenInterface; import org.openmetadata.schema.analytics.WebAnalyticEvent; @@ -3164,10 +3166,18 @@ public interface CollectionDAO { settings.setConfigType(configType); Object value; try { - if (configType == SettingsType.ACTIVITY_FEED_FILTER_SETTING) { - value = JsonUtils.readValue(json, new TypeReference>() {}); - } else { - throw new RuntimeException("Invalid Settings Type"); + switch (configType) { + case ACTIVITY_FEED_FILTER_SETTING: + value = JsonUtils.readValue(json, new TypeReference>() {}); + break; + case TASK_NOTIFICATION_CONFIGURATION: + value = JsonUtils.readValue(json, TaskNotificationConfiguration.class); + break; + case TEST_RESULT_NOTIFICATION_CONFIGURATION: + value = JsonUtils.readValue(json, TestResultNotificationConfiguration.class); + break; + default: + throw new RuntimeException("Invalid Settings Type"); } } catch (IOException e) { throw new RuntimeException(e); diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/settings/SettingsCache.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/settings/SettingsCache.java index 642aee3e4a0..6524ffc9d73 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/settings/SettingsCache.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/settings/SettingsCache.java @@ -24,9 +24,11 @@ import java.util.concurrent.TimeUnit; import javax.annotation.CheckForNull; import lombok.extern.slf4j.Slf4j; import org.openmetadata.schema.settings.Settings; +import org.openmetadata.schema.settings.SettingsType; import org.openmetadata.service.exception.EntityNotFoundException; import org.openmetadata.service.jdbi3.CollectionDAO; import org.openmetadata.service.jdbi3.SettingsRepository; +import org.openmetadata.service.util.JsonUtils; @Slf4j public class SettingsCache { @@ -60,6 +62,15 @@ public class SettingsCache { } } + public T getSetting(SettingsType settingName, Class clazz) throws RuntimeException { + try { + String json = JsonUtils.pojoToJson(SETTINGS_CACHE.get(settingName.toString()).getConfigValue()); + return JsonUtils.readValue(json, clazz); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + public void putSettings(Settings setting) throws RuntimeException { SETTINGS_CACHE.put(setting.getConfigType().toString(), setting); } diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/socket/WebSocketManager.java b/openmetadata-service/src/main/java/org/openmetadata/service/socket/WebSocketManager.java index c164b595db7..247e2582044 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/socket/WebSocketManager.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/socket/WebSocketManager.java @@ -102,7 +102,7 @@ public class WebSocketManager { } } - public void sendToManyWithUUID(List receivers, String event, String message) { + public void sendToManyWithUUID(HashSet receivers, String event, String message) { receivers.forEach(e -> sendToOne(e, event, message)); } diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/util/EmailUtil.java b/openmetadata-service/src/main/java/org/openmetadata/service/util/EmailUtil.java index 2a72ce088ce..f0d359730d8 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/util/EmailUtil.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/util/EmailUtil.java @@ -15,7 +15,9 @@ import java.util.Map; import lombok.extern.slf4j.Slf4j; import org.openmetadata.schema.email.EmailRequest; import org.openmetadata.schema.email.SmtpSettings; +import org.openmetadata.schema.entity.feed.Thread; import org.openmetadata.schema.entity.teams.User; +import org.openmetadata.schema.tests.type.TestCaseResult; import org.simplejavamail.api.email.Email; import org.simplejavamail.api.email.EmailPopulatingBuilder; import org.simplejavamail.api.mailer.Mailer; @@ -47,12 +49,13 @@ public class EmailUtil { public static final String ACTION_KEY = "action"; public static final String ACTION_STATUS_KEY = "actionStatus"; public static final String ACCOUNT_STATUS_TEMPLATE_FILE = "account-activity-change.ftl"; - private static final String INVITE_SUBJECT = "Welcome to %s"; - + private static final String TASK_SUBJECT = "%s : Task Assignment Notification"; + private static final String TEST_SUBJECT = "%s : Test Result Notification"; public static final String INVITE_RANDOM_PWD = "invite-randompwd.ftl"; public static final String INVITE_CREATE_PWD = "invite-createPassword.ftl"; - + public static final String TASK_NOTIFICATION_TEMPLATE = "taskAssignment.ftl"; + public static final String TEST_NOTIFICATION_TEMPLATE = "testResultStatus.ftl"; private static EmailUtil INSTANCE = null; private SmtpSettings defaultSmtpSettings = null; private Mailer mailer = null; @@ -142,6 +145,45 @@ public class EmailUtil { } } + public void sendTaskAssignmentNotificationToUser( + String assigneeName, String email, String taskLink, Thread thread, String subject, String templateFilePath) + throws IOException, TemplateException { + if (defaultSmtpSettings.getEnableSmtpServer()) { + Map templatePopulator = new HashMap<>(); + templatePopulator.put("assignee", assigneeName); + templatePopulator.put("createdBy", thread.getCreatedBy()); + templatePopulator.put("taskName", thread.getMessage()); + templatePopulator.put("taskStatus", thread.getTask().getStatus().toString()); + templatePopulator.put("taskType", thread.getTask().getType().toString()); + templatePopulator.put("fieldOldValue", thread.getTask().getOldValue()); + templatePopulator.put("fieldNewValue", thread.getTask().getSuggestion()); + templatePopulator.put("taskLink", taskLink); + + sendMail(subject, templatePopulator, email, EMAIL_TEMPLATE_BASEPATH, templateFilePath); + } + } + + public void sendTestResultEmailNotificationToUser( + String email, + String testResultLink, + String testCaseName, + TestCaseResult result, + String subject, + String templateFilePath) + throws IOException, TemplateException { + if (defaultSmtpSettings.getEnableSmtpServer()) { + Map templatePopulator = new HashMap<>(); + templatePopulator.put("receiverName", email.split("@")[0]); + templatePopulator.put("testResultName", testCaseName); + templatePopulator.put("testResultDescription", result.getResult()); + templatePopulator.put("testResultStatus", result.getTestCaseStatus().toString()); + templatePopulator.put("testResultTimestamp", result.getTimestamp().toString()); + templatePopulator.put("testResultLink", testResultLink); + + sendMail(subject, templatePopulator, email, EMAIL_TEMPLATE_BASEPATH, templateFilePath); + } + } + public Email buildEmailWithDefaultSender(EmailRequest request) { EmailPopulatingBuilder emailBuilder = EmailBuilder.startingBlank(); if (request.getRecipientMails() != null @@ -266,6 +308,14 @@ public class EmailUtil { return String.format(INVITE_SUBJECT, defaultSmtpSettings.getEmailingEntity()); } + public String getTaskAssignmentSubject() { + return String.format(TASK_SUBJECT, defaultSmtpSettings.getEmailingEntity()); + } + + public String getTestResultSubject() { + return String.format(TEST_SUBJECT, defaultSmtpSettings.getEmailingEntity()); + } + public String getEmailingEntity() { return defaultSmtpSettings.getEmailingEntity(); } diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/util/NotificationHandler.java b/openmetadata-service/src/main/java/org/openmetadata/service/util/NotificationHandler.java new file mode 100644 index 00000000000..d8fc1190529 --- /dev/null +++ b/openmetadata-service/src/main/java/org/openmetadata/service/util/NotificationHandler.java @@ -0,0 +1,273 @@ +package org.openmetadata.service.util; + +import static org.openmetadata.schema.settings.SettingsType.TASK_NOTIFICATION_CONFIGURATION; +import static org.openmetadata.schema.settings.SettingsType.TEST_RESULT_NOTIFICATION_CONFIGURATION; +import static org.openmetadata.service.Entity.TABLE; +import static org.openmetadata.service.Entity.TEAM; +import static org.openmetadata.service.Entity.TEST_CASE; +import static org.openmetadata.service.Entity.USER; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import freemarker.template.TemplateException; +import java.io.IOException; +import java.net.URI; +import java.time.Instant; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import javax.ws.rs.container.ContainerResponseContext; +import javax.ws.rs.core.Response; +import lombok.extern.slf4j.Slf4j; +import org.openmetadata.api.configuration.airflow.TaskNotificationConfiguration; +import org.openmetadata.api.configuration.airflow.TestResultNotificationConfiguration; +import org.openmetadata.schema.EntityInterface; +import org.openmetadata.schema.entity.feed.Thread; +import org.openmetadata.schema.entity.teams.Team; +import org.openmetadata.schema.entity.teams.User; +import org.openmetadata.schema.tests.TestCase; +import org.openmetadata.schema.tests.type.TestCaseResult; +import org.openmetadata.schema.type.AnnouncementDetails; +import org.openmetadata.schema.type.ChangeEvent; +import org.openmetadata.schema.type.EntityReference; +import org.openmetadata.schema.type.FieldChange; +import org.openmetadata.schema.type.Post; +import org.openmetadata.schema.type.Relationship; +import org.openmetadata.service.Entity; +import org.openmetadata.service.jdbi3.CollectionDAO; +import org.openmetadata.service.jdbi3.EntityRepository; +import org.openmetadata.service.resources.feeds.MessageParser; +import org.openmetadata.service.resources.settings.SettingsCache; +import org.openmetadata.service.socket.WebSocketManager; + +@Slf4j +public class NotificationHandler { + private final CollectionDAO dao; + private final ObjectMapper mapper; + + private final ExecutorService threadScheduler; + + public NotificationHandler(CollectionDAO dao) { + this.dao = dao; + this.mapper = new ObjectMapper(); + this.threadScheduler = Executors.newFixedThreadPool(1); + } + + public void processNotifications(ContainerResponseContext responseContext) { + threadScheduler.submit( + () -> { + try { + handleNotifications(responseContext); + } catch (JsonProcessingException e) { + LOG.error("[NotificationHandler] Failed to use mapper in converting to Json", e); + } + }); + } + + private void handleNotifications(ContainerResponseContext responseContext) throws JsonProcessingException { + int responseCode = responseContext.getStatus(); + if (responseCode == Response.Status.CREATED.getStatusCode() + && responseContext.getEntity() != null + && responseContext.getEntity().getClass().equals(Thread.class)) { + Thread thread = (Thread) responseContext.getEntity(); + switch (thread.getType()) { + case Task: + handleTaskNotification(thread); + break; + case Conversation: + handleConversationNotification(thread); + break; + case Announcement: + handleAnnouncementNotification(thread); + break; + } + } else if (responseContext.getEntity() != null + && responseContext.getEntity().getClass().equals(ChangeEvent.class)) { + ChangeEvent changeEvent = (ChangeEvent) responseContext.getEntity(); + handleTestResultEmailNotification(changeEvent); + } + } + + private void handleTaskNotification(Thread thread) throws JsonProcessingException { + String jsonThread = mapper.writeValueAsString(thread); + if (thread.getPostsCount() == 0) { + List assignees = thread.getTask().getAssignees(); + HashSet receiversList = new HashSet<>(); + assignees.forEach( + e -> { + if (Entity.USER.equals(e.getType())) { + receiversList.add(e.getId()); + } else if (Entity.TEAM.equals(e.getType())) { + // fetch all that are there in the team + List records = + dao.relationshipDAO().findTo(e.getId().toString(), TEAM, Relationship.HAS.ordinal(), Entity.USER); + records.forEach((eRecord) -> receiversList.add(eRecord.getId())); + } + }); + + // Send WebSocket Notification + WebSocketManager.getInstance() + .sendToManyWithUUID(receiversList, WebSocketManager.TASK_BROADCAST_CHANNEL, jsonThread); + + // Send Email Notification If Enabled + TaskNotificationConfiguration taskSetting = + SettingsCache.getInstance().getSetting(TASK_NOTIFICATION_CONFIGURATION, TaskNotificationConfiguration.class); + if (taskSetting.getEnabled()) { + handleEmailNotifications(receiversList, thread); + } + } + } + + private void handleAnnouncementNotification(Thread thread) throws JsonProcessingException { + String jsonThread = mapper.writeValueAsString(thread); + AnnouncementDetails announcementDetails = thread.getAnnouncement(); + Long currentTimestamp = Instant.now().getEpochSecond(); + if (announcementDetails.getStartTime() <= currentTimestamp + && currentTimestamp <= announcementDetails.getEndTime()) { + WebSocketManager.getInstance().broadCastMessageToAll(WebSocketManager.ANNOUNCEMENT_CHANNEL, jsonThread); + } + } + + private void handleConversationNotification(Thread thread) throws JsonProcessingException { + String jsonThread = mapper.writeValueAsString(thread); + WebSocketManager.getInstance().broadCastMessageToAll(WebSocketManager.FEED_BROADCAST_CHANNEL, jsonThread); + List mentions; + if (thread.getPostsCount() == 0) { + mentions = MessageParser.getEntityLinks(thread.getMessage()); + } else { + Post latestPost = thread.getPosts().get(thread.getPostsCount() - 1); + mentions = MessageParser.getEntityLinks(latestPost.getMessage()); + } + mentions.forEach( + entityLink -> { + String fqn = entityLink.getEntityFQN(); + if (USER.equals(entityLink.getEntityType())) { + User user = dao.userDAO().findEntityByName(fqn); + WebSocketManager.getInstance().sendToOne(user.getId(), WebSocketManager.MENTION_CHANNEL, jsonThread); + } else if (TEAM.equals(entityLink.getEntityType())) { + Team team = dao.teamDAO().findEntityByName(fqn); + // fetch all that are there in the team + List records = + dao.relationshipDAO().findTo(team.getId().toString(), TEAM, Relationship.HAS.ordinal(), USER); + // Notify on WebSocket for Realtime + WebSocketManager.getInstance().sendToManyWithString(records, WebSocketManager.MENTION_CHANNEL, jsonThread); + } + }); + } + + private void handleEmailNotifications(HashSet userList, Thread thread) { + EntityRepository repository = Entity.getEntityRepository(USER); + URI urlInstance = thread.getHref(); + userList.forEach( + (id) -> { + try { + User user = repository.get(null, id, repository.getFields("name,email,href")); + EmailUtil.getInstance() + .sendTaskAssignmentNotificationToUser( + user.getName(), + user.getEmail(), + String.format( + "%s://%s/users/%s/tasks", urlInstance.getScheme(), urlInstance.getHost(), user.getName()), + thread, + EmailUtil.getInstance().getTaskAssignmentSubject(), + EmailUtil.TASK_NOTIFICATION_TEMPLATE); + } catch (IOException ex) { + LOG.error("Task Email Notification Failed :", ex); + } catch (TemplateException ex) { + LOG.error("Task Email Notification Template Parsing Exception :", ex); + } + }); + } + + private void handleTestResultEmailNotification(ChangeEvent changeEvent) { + if (Objects.nonNull(changeEvent.getChangeDescription())) { + FieldChange fieldChange = changeEvent.getChangeDescription().getFieldsUpdated().get(0); + String updatedField = fieldChange.getName(); + if (updatedField.equals("testCaseResult")) { + TestCaseResult result = (TestCaseResult) fieldChange.getNewValue(); + // Send Email Notification If Enabled + TestResultNotificationConfiguration testNotificationSetting = + SettingsCache.getInstance() + .getSetting(TEST_RESULT_NOTIFICATION_CONFIGURATION, TestResultNotificationConfiguration.class); + if (testNotificationSetting.getEnabled() + && testNotificationSetting.getOnResult().contains(result.getTestCaseStatus())) { + List receivers = + testNotificationSetting.getReceivers() != null + ? testNotificationSetting.getReceivers() + : new ArrayList<>(); + if (testNotificationSetting.getSendToOwners()) { + EntityInterface entity = (TestCase) changeEvent.getEntity(); + // Find the Table that have the test case + List tableToTestRecord = + dao.relationshipDAO() + .findFrom(entity.getId().toString(), TEST_CASE, Relationship.CONTAINS.ordinal(), TABLE); + tableToTestRecord.forEach( + (tableRecord) -> { + // Find the owners owning the Table , can be a team or Users + List tableOwners = + dao.relationshipDAO() + .findFrom(tableRecord.getId().toString(), TABLE, Relationship.OWNS.ordinal()); + tableOwners.forEach( + (owner) -> { + try { + if (USER.equals(owner.getType())) { + User user = dao.userDAO().findEntityById(owner.getId()); + receivers.add(user.getEmail()); + } else if (TEAM.equals(owner.getType())) { + Team team = dao.teamDAO().findEntityById(owner.getId()); + // Fetch the users in the team + List records = + dao.relationshipDAO() + .findTo(team.getId().toString(), TEAM, Relationship.HAS.ordinal(), USER); + + records.forEach( + (userRecord) -> { + try { + User user = dao.userDAO().findEntityById(userRecord.getId()); + receivers.add(user.getEmail()); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + }); + } + sendTestResultEmailNotifications(receivers, (TestCase) changeEvent.getEntity(), result); + } + } + } + } + + private void sendTestResultEmailNotifications(List emails, TestCase testCase, TestCaseResult result) { + emails.forEach( + (email) -> { + URI urlInstance = testCase.getHref(); + String testLinkUrl = + String.format( + "%s://%s/table/%s/activity_feed", + urlInstance.getScheme(), urlInstance.getHost(), testCase.getEntityFQN()); + try { + EmailUtil.getInstance() + .sendTestResultEmailNotificationToUser( + email, + testLinkUrl, + testCase.getName(), + result, + EmailUtil.getInstance().getTestResultSubject(), + EmailUtil.TEST_NOTIFICATION_TEMPLATE); + } catch (IOException e) { + LOG.error("TestResult Email Notification Failed :", e); + } catch (TemplateException e) { + LOG.error("Task Email Notification Template Parsing Exception :", e); + } + }); + } +} diff --git a/openmetadata-service/src/main/resources/emailTemplates/taskAssignment.ftl b/openmetadata-service/src/main/resources/emailTemplates/taskAssignment.ftl new file mode 100644 index 00000000000..4bb2035af66 --- /dev/null +++ b/openmetadata-service/src/main/resources/emailTemplates/taskAssignment.ftl @@ -0,0 +1,96 @@ + + + + +
 
+ + + +
+ + + +
+

+ + + + + + +
+ + + + + + +
+ Task Notification +
+

+
+
+

+ + + + + + + + + +
+

Hello ${assignee},

+

+ ${createdBy} have assigned you a task to <#if taskType=="UpdateDescription"> + Update Description + <#else> + Update Tags + . +
+

Task Details :

+
+ + Task Name : ${taskName} +
+ + Task Status : ${taskStatus} +
+ + Current Value : ${fieldOldValue} +
+ + Suggested Value : ${fieldNewValue} +
+
+
+ + + + + + + +
+ + + View Task + +
+
+
\ No newline at end of file diff --git a/openmetadata-service/src/main/resources/emailTemplates/testResultStatus.ftl b/openmetadata-service/src/main/resources/emailTemplates/testResultStatus.ftl new file mode 100644 index 00000000000..ea134e4005d --- /dev/null +++ b/openmetadata-service/src/main/resources/emailTemplates/testResultStatus.ftl @@ -0,0 +1,92 @@ + + + + +
 
+ + + +
+ + + +
+

+ + + + + + +
+ + + + + + +
+ Test Result Notification +
+

+
+
+

+ + + + + + + + + +
+

Hello ${receiverName},

+

+ You have a new Test Result Update. +
+

Task Details :

+
+ + Name : ${testResultName} +
+ + Description : ${testResultDescription} +
+ + Status : ${testResultStatus} +
+ + Event Timestamp : ${testResultTimestamp} +
+
+
+ + + + + + + +
+ + + View Activity + +
+
+
\ No newline at end of file diff --git a/openmetadata-service/src/main/resources/json/data/settings/settingsData.json b/openmetadata-service/src/main/resources/json/data/settings/settingsData.json index c88e7a2a076..f2a36f5a800 100644 --- a/openmetadata-service/src/main/resources/json/data/settings/settingsData.json +++ b/openmetadata-service/src/main/resources/json/data/settings/settingsData.json @@ -215,5 +215,19 @@ ] } ] + }, + { + "config_type": "taskNotificationConfiguration", + "config_value": { + "enabled" : true + } + }, + { + "config_type": "testResultNotificationConfiguration", + "config_value": { + "enabled" : false, + "onResult": ["Failed", "Aborted"], + "sendToOwners": false + } } ] \ No newline at end of file diff --git a/openmetadata-spec/src/main/resources/json/schema/configuration/taskNotificationConfiguration.json b/openmetadata-spec/src/main/resources/json/schema/configuration/taskNotificationConfiguration.json new file mode 100644 index 00000000000..3a549df29ab --- /dev/null +++ b/openmetadata-spec/src/main/resources/json/schema/configuration/taskNotificationConfiguration.json @@ -0,0 +1,16 @@ +{ + "$id": "https://open-metadata.org/schema/entity/configuration/taskNotificationConfiguration.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "TaskNotificationConfiguration", + "description": "This schema defines the SSL Config.", + "type": "object", + "javaType": "org.openmetadata.api.configuration.airflow.TaskNotificationConfiguration", + "properties": { + "enabled": { + "description": "Is Task Notification Enabled?", + "type" : "boolean", + "default": false + } + }, + "additionalProperties": false +} \ No newline at end of file diff --git a/openmetadata-spec/src/main/resources/json/schema/configuration/testResultNotificationConfiguration.json b/openmetadata-spec/src/main/resources/json/schema/configuration/testResultNotificationConfiguration.json new file mode 100644 index 00000000000..5f4a2de426f --- /dev/null +++ b/openmetadata-spec/src/main/resources/json/schema/configuration/testResultNotificationConfiguration.json @@ -0,0 +1,35 @@ +{ + "$id": "https://open-metadata.org/schema/entity/configuration/testResultNotificationConfiguration.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "TestResultNotificationConfiguration", + "description": "This schema defines the SSL Config.", + "type": "object", + "javaType": "org.openmetadata.api.configuration.airflow.TestResultNotificationConfiguration", + "properties": { + "enabled": { + "description": "Is Test Notification Enabled?", + "type" : "boolean", + "default": false + }, + "onResult": { + "description": "Send notification on Success, Failed or Aborted?", + "type" : "array", + "items": { + "$ref": "../tests/basic.json#/definitions/testCaseStatus" + } + }, + "receivers": { + "description": "Send notification on the mail", + "type": "array", + "items": { + "$ref": "../type/basic.json#/definitions/email" + } + }, + "sendToOwners": { + "description": "Send notification on the mail", + "type": "boolean", + "default": false + } + }, + "additionalProperties": false +} \ No newline at end of file diff --git a/openmetadata-spec/src/main/resources/json/schema/settings/settings.json b/openmetadata-spec/src/main/resources/json/schema/settings/settings.json index 40687f622da..8bdc041feb8 100644 --- a/openmetadata-spec/src/main/resources/json/schema/settings/settings.json +++ b/openmetadata-spec/src/main/resources/json/schema/settings/settings.json @@ -22,7 +22,9 @@ "activityFeedFilterSetting", "secretsManagerConfiguration", "sandboxModeEnabled", - "slackChat" + "slackChat", + "taskNotificationConfiguration", + "testResultNotificationConfiguration" ] } }, @@ -59,7 +61,11 @@ }, { "$ref": "../configuration/slackEventPubConfiguration.json" + }, + { + "$ref": "../configuration/taskNotificationConfiguration.json" } + ] } }, diff --git a/openmetadata-spec/src/main/resources/json/schema/tests/basic.json b/openmetadata-spec/src/main/resources/json/schema/tests/basic.json index 7e23f928a8f..a2b1dac781b 100644 --- a/openmetadata-spec/src/main/resources/json/schema/tests/basic.json +++ b/openmetadata-spec/src/main/resources/json/schema/tests/basic.json @@ -19,6 +19,23 @@ } } }, + "testCaseStatus": { + "description": "Status of Test Case run.", + "javaType": "org.openmetadata.schema.tests.type.TestCaseStatus", + "type": "string", + "enum": ["Success", "Failed", "Aborted"], + "javaEnums": [ + { + "name": "Success" + }, + { + "name": "Failed" + }, + { + "name": "Aborted" + } + ] + }, "testCaseResult": { "description": "Schema to capture test case result.", "javaType": "org.openmetadata.schema.tests.type.TestCaseResult", @@ -30,20 +47,7 @@ }, "testCaseStatus": { "description": "Status of Test Case run.", - "javaType": "org.openmetadata.schema.tests.type.TestCaseStatus", - "type": "string", - "enum": ["Success", "Failed", "Aborted"], - "javaEnums": [ - { - "name": "Success" - }, - { - "name": "Failed" - }, - { - "name": "Aborted" - } - ] + "$ref": "#/definitions/testCaseStatus" }, "result": { "description": "Details of test case results.",