mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2026-01-07 04:56:54 +00:00
Improve Templates for Slack, GChat, and MSTeams (#17771)
* Add default ChangeEvent template and Slack destination test template * fix conflict * initial ingestionPipeline template * clean templates. * clean templates * teams templates. * add getBoldWithSpace() * java check style * google chat templates. * Refactor GChat and Teams message for template data handling for DQ_Template_Section and General_Template_Section. * Refactor slack message for template data handling for DQ_Template_Section and General_Template_Section. * improvements gChatMessageDecorator * improvements slackMessageDecorator * improve MSTeamsMessageDecorator * Call templates per entityType and refactor code * Update EmailPublisher to use testEmail template instead of changeEvent template for sendTestMessage * add parameterValues sections for teams * Update SlackMessage to have attachments. Update DQ template. * Update dq templates, build dq template data in MessageDecorator * remove IngestionPipeline template * move SlackCallbackResource into a separate class. Fix tests. * simulate timeout and 300. * remove the validation of messages. * fix teams MessageDecorator * remove unused code from SlackMessageDecorator * fix owners and tags data population in the template * fix: changes in test case and test case result should load different templates.
This commit is contained in:
parent
687b564ef6
commit
2437d0124e
@ -83,11 +83,10 @@ public class EmailPublisher implements Destination<ChangeEvent> {
|
||||
public void sendTestMessage() throws EventPublisherException {
|
||||
try {
|
||||
Set<String> receivers = emailAlertConfig.getReceivers();
|
||||
EmailMessage emailMessage =
|
||||
emailDecorator.buildOutgoingTestMessage(eventSubscription.getFullyQualifiedName());
|
||||
EmailUtil.testConnection();
|
||||
|
||||
for (String email : receivers) {
|
||||
EmailUtil.sendChangeEventMail(
|
||||
eventSubscription.getFullyQualifiedName(), email, emailMessage);
|
||||
EmailUtil.sendTestEmail(email, false);
|
||||
}
|
||||
setSuccessStatus(System.currentTimeMillis());
|
||||
} catch (Exception e) {
|
||||
|
||||
@ -3,39 +3,75 @@ package org.openmetadata.service.apps.bundles.changeEvent.gchat;
|
||||
import java.util.List;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class GChatMessage {
|
||||
|
||||
@Getter @Setter private String text;
|
||||
@Getter @Setter private List<CardsV2> cardsV2;
|
||||
|
||||
public static class CardsV2 {
|
||||
@Getter @Setter private String cardId;
|
||||
@Getter @Setter private Card card;
|
||||
}
|
||||
private List<GChatMessage.Card> cards;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class Card {
|
||||
|
||||
@Getter @Setter private CardHeader header;
|
||||
@Getter @Setter private List<Section> sections;
|
||||
private GChatMessage.Header header;
|
||||
private List<GChatMessage.Section> sections;
|
||||
}
|
||||
|
||||
public static class CardHeader {
|
||||
@Getter @Setter private String title;
|
||||
@Getter @Setter private String subtitle;
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class Header {
|
||||
private String title;
|
||||
private String imageUrl;
|
||||
private String imageStyle;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class Section {
|
||||
@Getter @Setter private List<Widget> widgets;
|
||||
private List<GChatMessage.Widget> widgets;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class Widget {
|
||||
@Getter @Setter private TextParagraph textParagraph;
|
||||
private GChatMessage.KeyValue keyValue;
|
||||
private GChatMessage.TextParagraph textParagraph;
|
||||
|
||||
public Widget(GChatMessage.KeyValue keyValue) {
|
||||
this.keyValue = keyValue;
|
||||
}
|
||||
|
||||
public Widget(GChatMessage.TextParagraph textParagraph) {
|
||||
this.textParagraph = textParagraph;
|
||||
}
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class KeyValue {
|
||||
private String topLabel;
|
||||
private String content;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class TextParagraph {
|
||||
@Getter @Setter private String text;
|
||||
private String text;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,34 +1,156 @@
|
||||
package org.openmetadata.service.apps.bundles.changeEvent.msteams;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.util.List;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@Getter
|
||||
@Setter
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Builder
|
||||
public class TeamsMessage {
|
||||
@JsonProperty("type")
|
||||
private String type;
|
||||
|
||||
@JsonProperty("attachments")
|
||||
private List<TeamsMessage.Attachment> attachments;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public static class Section {
|
||||
@JsonProperty("activityTitle")
|
||||
public String activityTitle;
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Builder
|
||||
public static class Attachment {
|
||||
@JsonProperty("contentType")
|
||||
private String contentType;
|
||||
|
||||
@JsonProperty("activityText")
|
||||
public String activityText;
|
||||
@JsonProperty("content")
|
||||
private TeamsMessage.AdaptiveCardContent content;
|
||||
}
|
||||
|
||||
@JsonProperty("@type")
|
||||
public String type = "MessageCard";
|
||||
@Getter
|
||||
@Setter
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Builder
|
||||
public static class AdaptiveCardContent {
|
||||
@JsonProperty("type")
|
||||
private String type;
|
||||
|
||||
@JsonProperty("@context")
|
||||
public String context = "http://schema.org/extensions";
|
||||
@JsonProperty("version")
|
||||
private String version;
|
||||
|
||||
@JsonProperty("summary")
|
||||
public String summary;
|
||||
@JsonProperty("body")
|
||||
private List<TeamsMessage.BodyItem> body;
|
||||
}
|
||||
|
||||
@JsonProperty("sections")
|
||||
public List<Section> sections;
|
||||
@Getter
|
||||
@Setter
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Builder
|
||||
public static class ColumnSet implements TeamsMessage.BodyItem {
|
||||
@JsonProperty("type")
|
||||
private String type;
|
||||
|
||||
@JsonProperty("columns")
|
||||
private List<TeamsMessage.Column> columns;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Builder
|
||||
public static class Column {
|
||||
@JsonProperty("type")
|
||||
private String type;
|
||||
|
||||
@JsonProperty("items")
|
||||
private List<TeamsMessage.BodyItem> items;
|
||||
|
||||
@JsonProperty("width")
|
||||
private String width;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Builder
|
||||
public static class Image implements TeamsMessage.BodyItem {
|
||||
@JsonProperty("type")
|
||||
private String type;
|
||||
|
||||
@JsonProperty("url")
|
||||
private String url;
|
||||
|
||||
@JsonProperty("size")
|
||||
private String size;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Builder
|
||||
public static class TextBlock implements TeamsMessage.BodyItem {
|
||||
@JsonProperty("type")
|
||||
private String type;
|
||||
|
||||
@JsonProperty("text")
|
||||
private String text;
|
||||
|
||||
@JsonProperty("size")
|
||||
private String size;
|
||||
|
||||
@JsonProperty("weight")
|
||||
private String weight;
|
||||
|
||||
@JsonProperty("wrap")
|
||||
private boolean wrap;
|
||||
|
||||
@JsonProperty("horizontalAlignment")
|
||||
private String horizontalAlignment;
|
||||
|
||||
@JsonProperty("spacing")
|
||||
private String spacing;
|
||||
|
||||
@JsonProperty("separator")
|
||||
private boolean separator;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Builder
|
||||
public static class FactSet implements TeamsMessage.BodyItem {
|
||||
@JsonProperty("type")
|
||||
private String type;
|
||||
|
||||
@JsonProperty("facts")
|
||||
private List<TeamsMessage.Fact> facts;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Builder
|
||||
public static class Fact {
|
||||
@JsonProperty("title")
|
||||
private String title;
|
||||
|
||||
@JsonProperty("value")
|
||||
private String value;
|
||||
}
|
||||
|
||||
// Interface for Body Items
|
||||
public interface BodyItem {}
|
||||
}
|
||||
|
||||
@ -1,63 +1,54 @@
|
||||
package org.openmetadata.service.apps.bundles.changeEvent.slack;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.slack.api.model.block.LayoutBlock;
|
||||
import java.util.List;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Setter
|
||||
@Getter
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class SlackAttachment {
|
||||
@Getter @Setter private String fallback;
|
||||
@Getter @Setter private String color;
|
||||
@Getter @Setter private String pretext;
|
||||
private String fallback;
|
||||
private String color;
|
||||
private String pretext;
|
||||
|
||||
@JsonProperty("author_name")
|
||||
@Getter
|
||||
@Setter
|
||||
private String authorName;
|
||||
|
||||
@JsonProperty("author_link")
|
||||
@Getter
|
||||
@Setter
|
||||
private String authorLink;
|
||||
|
||||
@JsonProperty("author_icon")
|
||||
@Getter
|
||||
@Setter
|
||||
private String authorIcon;
|
||||
|
||||
@Getter @Setter private String title;
|
||||
private String title;
|
||||
|
||||
@JsonProperty("title_link")
|
||||
@Getter
|
||||
@Setter
|
||||
private String titleLink;
|
||||
|
||||
@Getter @Setter private String text;
|
||||
@Getter @Setter private Field[] fields;
|
||||
private String text;
|
||||
private Field[] fields;
|
||||
|
||||
@JsonProperty("image_url")
|
||||
@Getter
|
||||
@Setter
|
||||
private String imageUrl;
|
||||
|
||||
@JsonProperty("thumb_url")
|
||||
@Getter
|
||||
@Setter
|
||||
private String thumbUrl;
|
||||
|
||||
@Getter @Setter private String footer;
|
||||
private String footer;
|
||||
|
||||
@JsonProperty("footer_icon")
|
||||
@Getter
|
||||
@Setter
|
||||
private String footerIcon;
|
||||
|
||||
@Getter @Setter private String ts;
|
||||
private String ts;
|
||||
|
||||
@JsonProperty("mrkdwn_in")
|
||||
@Getter
|
||||
@Setter
|
||||
private List<String> markdownIn;
|
||||
|
||||
private List<LayoutBlock> blocks;
|
||||
}
|
||||
|
||||
@ -19,6 +19,9 @@ import static org.openmetadata.service.util.SubscriptionUtil.getClient;
|
||||
import static org.openmetadata.service.util.SubscriptionUtil.getTargetsForWebhookAlert;
|
||||
import static org.openmetadata.service.util.SubscriptionUtil.postWebhookMessage;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import java.util.List;
|
||||
import javax.ws.rs.client.Client;
|
||||
import javax.ws.rs.client.Invocation;
|
||||
@ -75,6 +78,9 @@ public class SlackEventPublisher implements Destination<ChangeEvent> {
|
||||
SlackMessage slackMessage =
|
||||
slackMessageFormatter.buildOutgoingMessage(
|
||||
eventSubscription.getFullyQualifiedName(), event);
|
||||
|
||||
String json = JsonUtils.pojoToJsonIgnoreNull(slackMessage);
|
||||
json = convertCamelCaseToSnakeCase(json);
|
||||
List<Invocation.Builder> targets =
|
||||
getTargetsForWebhookAlert(
|
||||
webhook, subscriptionDestination.getCategory(), SLACK, client, event);
|
||||
@ -83,14 +89,10 @@ public class SlackEventPublisher implements Destination<ChangeEvent> {
|
||||
}
|
||||
for (Invocation.Builder actionTarget : targets) {
|
||||
if (webhook.getSecretKey() != null && !webhook.getSecretKey().isEmpty()) {
|
||||
String hmac =
|
||||
"sha256="
|
||||
+ CommonUtil.calculateHMAC(
|
||||
webhook.getSecretKey(), JsonUtils.pojoToJson(slackMessage));
|
||||
postWebhookMessage(
|
||||
this, actionTarget.header(RestUtil.SIGNATURE_HEADER, hmac), slackMessage);
|
||||
String hmac = "sha256=" + CommonUtil.calculateHMAC(webhook.getSecretKey(), json);
|
||||
postWebhookMessage(this, actionTarget.header(RestUtil.SIGNATURE_HEADER, hmac), json);
|
||||
} else {
|
||||
postWebhookMessage(this, actionTarget, slackMessage);
|
||||
postWebhookMessage(this, actionTarget, json);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
@ -109,15 +111,14 @@ public class SlackEventPublisher implements Destination<ChangeEvent> {
|
||||
SlackMessage slackMessage =
|
||||
slackMessageFormatter.buildOutgoingTestMessage(eventSubscription.getFullyQualifiedName());
|
||||
|
||||
String json = JsonUtils.pojoToJsonIgnoreNull(slackMessage);
|
||||
json = convertCamelCaseToSnakeCase(json);
|
||||
if (target != null) {
|
||||
if (webhook.getSecretKey() != null && !webhook.getSecretKey().isEmpty()) {
|
||||
String hmac =
|
||||
"sha256="
|
||||
+ CommonUtil.calculateHMAC(
|
||||
webhook.getSecretKey(), JsonUtils.pojoToJson(slackMessage));
|
||||
postWebhookMessage(this, target.header(RestUtil.SIGNATURE_HEADER, hmac), slackMessage);
|
||||
String hmac = "sha256=" + CommonUtil.calculateHMAC(webhook.getSecretKey(), json);
|
||||
postWebhookMessage(this, target.header(RestUtil.SIGNATURE_HEADER, hmac), json);
|
||||
} else {
|
||||
postWebhookMessage(this, target, slackMessage);
|
||||
postWebhookMessage(this, target, json);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
@ -127,6 +128,50 @@ public class SlackEventPublisher implements Destination<ChangeEvent> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Slack messages sent via webhook require some keys in snake_case, while the Slack
|
||||
* app accepts them as they are (camelCase). Using Layout blocks (from com.slack.api.model.block) restricts control over key
|
||||
* aliases within the class.
|
||||
**/
|
||||
public String convertCamelCaseToSnakeCase(String jsonString) {
|
||||
JsonNode rootNode = JsonUtils.readTree(jsonString);
|
||||
JsonNode modifiedNode = convertKeys(rootNode);
|
||||
return JsonUtils.pojoToJsonIgnoreNull(modifiedNode);
|
||||
}
|
||||
|
||||
private JsonNode convertKeys(JsonNode node) {
|
||||
if (node.isObject()) {
|
||||
ObjectNode objectNode = (ObjectNode) node;
|
||||
ObjectNode newNode = JsonUtils.getObjectNode();
|
||||
|
||||
objectNode
|
||||
.fieldNames()
|
||||
.forEachRemaining(
|
||||
fieldName -> {
|
||||
String newFieldName = fieldName;
|
||||
if (fieldName.equals("imageUrl")) {
|
||||
newFieldName = "image_url";
|
||||
} else if (fieldName.equals("altText")) {
|
||||
newFieldName = "alt_text";
|
||||
}
|
||||
|
||||
// Recursively convert the keys
|
||||
newNode.set(newFieldName, convertKeys(objectNode.get(fieldName)));
|
||||
});
|
||||
return newNode;
|
||||
} else if (node.isArray()) {
|
||||
ArrayNode arrayNode = (ArrayNode) node;
|
||||
ArrayNode newArrayNode = JsonUtils.getObjectNode().arrayNode();
|
||||
|
||||
// recursively convert elements
|
||||
for (int i = 0; i < arrayNode.size(); i++) {
|
||||
newArrayNode.add(convertKeys(arrayNode.get(i)));
|
||||
}
|
||||
return newArrayNode;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EventSubscription getEventSubscriptionForDestination() {
|
||||
return eventSubscription;
|
||||
|
||||
@ -1,35 +1,29 @@
|
||||
package org.openmetadata.service.apps.bundles.changeEvent.slack;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.slack.api.model.block.LayoutBlock;
|
||||
import java.util.List;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class SlackMessage {
|
||||
@Getter @Setter private String username;
|
||||
private List<LayoutBlock> blocks;
|
||||
private List<Attachment> attachments;
|
||||
|
||||
@JsonProperty("icon_emoji")
|
||||
@Getter
|
||||
@Setter
|
||||
private String iconEmoji;
|
||||
|
||||
@Getter @Setter private String channel;
|
||||
@Getter @Setter private String text;
|
||||
|
||||
@JsonProperty("response_type")
|
||||
@Getter
|
||||
@Setter
|
||||
private String responseType;
|
||||
|
||||
@Getter @Setter private SlackAttachment[] attachments;
|
||||
|
||||
public SlackMessage() {}
|
||||
|
||||
public SlackMessage(String text) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
public SlackMessage encodedMessage() {
|
||||
this.setText(this.getText().replace("&", "&").replace("<", "<").replace(">", ">"));
|
||||
return this;
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public static class Attachment {
|
||||
private String color;
|
||||
private List<LayoutBlock> blocks;
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,6 +29,11 @@ public class EmailMessageDecorator implements MessageDecorator<EmailMessage> {
|
||||
return "<b>%s</b>";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBoldWithSpace() {
|
||||
return "<b>%s</b> ";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLineBreak() {
|
||||
return " <br/> ";
|
||||
|
||||
@ -25,6 +25,11 @@ public class FeedMessageDecorator implements MessageDecorator<FeedMessage> {
|
||||
return "**%s**";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBoldWithSpace() {
|
||||
return "**%s** ";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLineBreak() {
|
||||
return " <br/> ";
|
||||
|
||||
@ -17,9 +17,24 @@ import static org.openmetadata.common.utils.CommonUtil.nullOrEmpty;
|
||||
import static org.openmetadata.service.util.email.EmailUtil.getSmtpSettings;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.openmetadata.common.utils.CommonUtil;
|
||||
import org.openmetadata.schema.tests.TestCaseParameterValue;
|
||||
import org.openmetadata.schema.tests.type.TestCaseStatus;
|
||||
import org.openmetadata.schema.type.ChangeEvent;
|
||||
import org.openmetadata.schema.type.EntityReference;
|
||||
import org.openmetadata.schema.type.FieldChange;
|
||||
import org.openmetadata.schema.type.TagLabel;
|
||||
import org.openmetadata.service.Entity;
|
||||
import org.openmetadata.service.apps.bundles.changeEvent.gchat.GChatMessage;
|
||||
import org.openmetadata.service.apps.bundles.changeEvent.gchat.GChatMessage.*;
|
||||
import org.openmetadata.service.exception.UnhandledServerException;
|
||||
|
||||
public class GChatMessageDecorator implements MessageDecorator<GChatMessage> {
|
||||
@ -29,6 +44,11 @@ public class GChatMessageDecorator implements MessageDecorator<GChatMessage> {
|
||||
return "<b>%s</b>";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBoldWithSpace() {
|
||||
return "<b>%s</b> ";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLineBreak() {
|
||||
return " <br/> ";
|
||||
@ -67,7 +87,12 @@ public class GChatMessageDecorator implements MessageDecorator<GChatMessage> {
|
||||
|
||||
@Override
|
||||
public GChatMessage buildEntityMessage(String publisherName, ChangeEvent event) {
|
||||
return getGChatMessage(createEntityMessage(publisherName, event));
|
||||
return createMessage(publisherName, event, createEntityMessage(publisherName, event));
|
||||
}
|
||||
|
||||
@Override
|
||||
public GChatMessage buildThreadMessage(String publisherName, ChangeEvent event) {
|
||||
return createMessage(publisherName, event, createThreadMessage(publisherName, event));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -75,64 +100,430 @@ public class GChatMessageDecorator implements MessageDecorator<GChatMessage> {
|
||||
return getGChatTestMessage(publisherName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GChatMessage buildThreadMessage(String publisherName, ChangeEvent event) {
|
||||
return getGChatMessage(createThreadMessage(publisherName, event));
|
||||
}
|
||||
|
||||
private GChatMessage getGChatMessage(OutgoingMessage outgoingMessage) {
|
||||
if (!outgoingMessage.getMessages().isEmpty()) {
|
||||
GChatMessage gChatMessage = new GChatMessage();
|
||||
GChatMessage.CardsV2 cardsV2 = new GChatMessage.CardsV2();
|
||||
GChatMessage.Card card = new GChatMessage.Card();
|
||||
GChatMessage.Section section = new GChatMessage.Section();
|
||||
|
||||
// Header
|
||||
gChatMessage.setText("Change Event from OpenMetadata");
|
||||
GChatMessage.CardHeader cardHeader = new GChatMessage.CardHeader();
|
||||
cardHeader.setTitle(outgoingMessage.getHeader());
|
||||
card.setHeader(cardHeader);
|
||||
|
||||
// Attachments
|
||||
List<GChatMessage.Widget> widgets = new ArrayList<>();
|
||||
outgoingMessage.getMessages().forEach(m -> widgets.add(getGChatWidget(m)));
|
||||
section.setWidgets(widgets);
|
||||
card.setSections(List.of(section));
|
||||
cardsV2.setCard(card);
|
||||
gChatMessage.setCardsV2(List.of(cardsV2));
|
||||
|
||||
return gChatMessage;
|
||||
}
|
||||
throw new UnhandledServerException("No messages found for the event");
|
||||
}
|
||||
|
||||
private GChatMessage getGChatTestMessage(String publisherName) {
|
||||
if (!publisherName.isEmpty()) {
|
||||
GChatMessage gChatMessage = new GChatMessage();
|
||||
GChatMessage.CardsV2 cardsV2 = new GChatMessage.CardsV2();
|
||||
GChatMessage.Card card = new GChatMessage.Card();
|
||||
GChatMessage.Section section = new GChatMessage.Section();
|
||||
|
||||
// Header
|
||||
gChatMessage.setText(
|
||||
"This is a test message from OpenMetadata to confirm your GChat destination is configured correctly.");
|
||||
GChatMessage.CardHeader cardHeader = new GChatMessage.CardHeader();
|
||||
cardHeader.setTitle("Alert: " + publisherName);
|
||||
cardHeader.setSubtitle("GChat destination test successful.");
|
||||
|
||||
card.setHeader(cardHeader);
|
||||
card.setSections(List.of(section));
|
||||
cardsV2.setCard(card);
|
||||
gChatMessage.setCardsV2(List.of(cardsV2));
|
||||
|
||||
return gChatMessage;
|
||||
if (publisherName.isEmpty()) {
|
||||
throw new UnhandledServerException("Publisher name not found.");
|
||||
}
|
||||
throw new UnhandledServerException("Publisher name not found.");
|
||||
|
||||
return createConnectionTestMessage(publisherName);
|
||||
}
|
||||
|
||||
private GChatMessage.Widget getGChatWidget(String message) {
|
||||
GChatMessage.Widget widget = new GChatMessage.Widget();
|
||||
widget.setTextParagraph(new GChatMessage.TextParagraph(message));
|
||||
return widget;
|
||||
public GChatMessage createMessage(
|
||||
String publisherName, ChangeEvent event, OutgoingMessage outgoingMessage) {
|
||||
|
||||
if (outgoingMessage.getMessages().isEmpty()) {
|
||||
throw new UnhandledServerException("No messages found for the event");
|
||||
}
|
||||
|
||||
String entityType = event.getEntityType();
|
||||
|
||||
return switch (entityType) {
|
||||
case Entity.TEST_CASE -> createTestCaseMessage(publisherName, event, outgoingMessage);
|
||||
default -> createGeneralChangeEventMessage(publisherName, event, outgoingMessage);
|
||||
};
|
||||
}
|
||||
|
||||
private GChatMessage createTestCaseMessage(
|
||||
String publisherName, ChangeEvent event, OutgoingMessage outgoingMessage) {
|
||||
final String testCaseResult = "testCaseResult";
|
||||
|
||||
List<FieldChange> fieldsAdded = event.getChangeDescription().getFieldsAdded();
|
||||
List<FieldChange> fieldsUpdated = event.getChangeDescription().getFieldsUpdated();
|
||||
|
||||
boolean hasRelevantChange =
|
||||
fieldsAdded.stream().anyMatch(field -> testCaseResult.equals(field.getName()))
|
||||
|| fieldsUpdated.stream().anyMatch(field -> testCaseResult.equals(field.getName()));
|
||||
|
||||
return hasRelevantChange
|
||||
? createDQTemplate(publisherName, event, outgoingMessage)
|
||||
: createGeneralChangeEventMessage(publisherName, event, outgoingMessage);
|
||||
}
|
||||
|
||||
public GChatMessage createGeneralChangeEventMessage(
|
||||
String publisherName, ChangeEvent event, OutgoingMessage outgoingMessage) {
|
||||
|
||||
Map<General_Template_Section, Map<Enum<?>, Object>> data =
|
||||
buildGeneralTemplateData(publisherName, event, outgoingMessage);
|
||||
|
||||
Map<Enum<?>, Object> eventDetails = data.get(General_Template_Section.EVENT_DETAILS);
|
||||
|
||||
Header header = createHeader();
|
||||
|
||||
List<Widget> additionalMessageWidgets =
|
||||
outgoingMessage.getMessages().stream()
|
||||
.map(message -> new Widget(new TextParagraph(message)))
|
||||
.toList();
|
||||
|
||||
Section detailsSection = new Section(createEventDetailsWidgets(eventDetails));
|
||||
Section messageSection = new Section(additionalMessageWidgets);
|
||||
Section fqnSection =
|
||||
new Section(
|
||||
List.of(
|
||||
createWidget(
|
||||
"FQN:",
|
||||
String.valueOf(eventDetails.getOrDefault(EventDetailsKeys.ENTITY_FQN, "-")))));
|
||||
|
||||
// todo create clickable entity link in the message
|
||||
|
||||
Section footerSection = createFooterSection();
|
||||
|
||||
Card card =
|
||||
new Card(header, List.of(detailsSection, fqnSection, messageSection, footerSection));
|
||||
return new GChatMessage(List.of(card));
|
||||
}
|
||||
|
||||
private Map<General_Template_Section, Map<Enum<?>, Object>> buildGeneralTemplateData(
|
||||
String publisherName, ChangeEvent event, OutgoingMessage outgoingMessage) {
|
||||
|
||||
TemplateDataBuilder<General_Template_Section> builder = new TemplateDataBuilder<>();
|
||||
builder
|
||||
.add(
|
||||
General_Template_Section.EVENT_DETAILS,
|
||||
EventDetailsKeys.EVENT_TYPE,
|
||||
event.getEventType().value())
|
||||
.add(
|
||||
General_Template_Section.EVENT_DETAILS,
|
||||
EventDetailsKeys.UPDATED_BY,
|
||||
event.getUserName())
|
||||
.add(
|
||||
General_Template_Section.EVENT_DETAILS,
|
||||
EventDetailsKeys.ENTITY_TYPE,
|
||||
event.getEntityType())
|
||||
.add(
|
||||
General_Template_Section.EVENT_DETAILS,
|
||||
EventDetailsKeys.ENTITY_FQN,
|
||||
MessageDecorator.getFQNForChangeEventEntity(event))
|
||||
.add(
|
||||
General_Template_Section.EVENT_DETAILS,
|
||||
EventDetailsKeys.TIME,
|
||||
new Date(event.getTimestamp()).toString())
|
||||
.add(
|
||||
General_Template_Section.EVENT_DETAILS,
|
||||
EventDetailsKeys.OUTGOING_MESSAGE,
|
||||
outgoingMessage);
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
public GChatMessage createConnectionTestMessage(String publisherName) {
|
||||
Header header = createConnectionSuccessfulHeader();
|
||||
|
||||
Widget publisherWidget = createWidget("Publisher:", publisherName);
|
||||
|
||||
Widget descriptionWidget = new Widget(new TextParagraph(CONNECTION_TEST_DESCRIPTION));
|
||||
|
||||
Section publisherSection = new Section(List.of(publisherWidget));
|
||||
Section descriptionSection = new Section(List.of(descriptionWidget));
|
||||
Section footerSection = createFooterSection();
|
||||
|
||||
Card card =
|
||||
new Card(header, Arrays.asList(publisherSection, descriptionSection, footerSection));
|
||||
|
||||
return new GChatMessage(List.of(card));
|
||||
}
|
||||
|
||||
public GChatMessage createDQTemplate(
|
||||
String publisherName, ChangeEvent event, OutgoingMessage outgoingMessage) {
|
||||
|
||||
Map<DQ_Template_Section, Map<Enum<?>, Object>> templateData =
|
||||
MessageDecorator.buildDQTemplateData(event, outgoingMessage);
|
||||
|
||||
List<Section> sections = new ArrayList<>();
|
||||
Header header = createHeader();
|
||||
|
||||
addChangeEventDetailsSection(templateData, sections);
|
||||
|
||||
List<Widget> additionalMessageWidgets =
|
||||
outgoingMessage.getMessages().stream()
|
||||
.map(message -> new Widget(new TextParagraph(message)))
|
||||
.toList();
|
||||
sections.add(new Section(additionalMessageWidgets));
|
||||
|
||||
// todo create clickable entity link in the message
|
||||
|
||||
addTestCaseDetailsSection(templateData, sections);
|
||||
addTestCaseFQNSection(templateData, sections);
|
||||
addTestCaseResultSection(templateData, sections);
|
||||
addParameterValuesSection(templateData, sections);
|
||||
addInspectionQuerySection(templateData, sections);
|
||||
addTestDefinitionSection(templateData, sections);
|
||||
addSampleDataSection(templateData, sections);
|
||||
|
||||
sections.add(createFooterSection());
|
||||
|
||||
// Create the card with all sections
|
||||
Card card = new Card(header, sections);
|
||||
return new GChatMessage(List.of(card));
|
||||
}
|
||||
|
||||
private void addChangeEventDetailsSection(
|
||||
Map<DQ_Template_Section, Map<Enum<?>, Object>> templateData, List<Section> sections) {
|
||||
|
||||
Map<Enum<?>, Object> eventDetails = templateData.get(DQ_Template_Section.EVENT_DETAILS);
|
||||
if (nullOrEmpty(eventDetails)) {
|
||||
return;
|
||||
}
|
||||
|
||||
sections.add(new Section(createEventDetailsWidgets(eventDetails)));
|
||||
}
|
||||
|
||||
private void addTestCaseDetailsSection(
|
||||
Map<DQ_Template_Section, Map<Enum<?>, Object>> templateData, List<Section> sections) {
|
||||
|
||||
Map<Enum<?>, Object> testCaseDetails = templateData.get(DQ_Template_Section.TEST_CASE_DETAILS);
|
||||
if (nullOrEmpty(testCaseDetails)) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<Widget> testCaseDetailsWidgets =
|
||||
List.of(
|
||||
createWidget("TEST CASE"),
|
||||
createWidget(
|
||||
"ID:",
|
||||
String.valueOf(testCaseDetails.getOrDefault(DQ_TestCaseDetailsKeys.ID, "-"))),
|
||||
createWidget(
|
||||
"Name:",
|
||||
String.valueOf(testCaseDetails.getOrDefault(DQ_TestCaseDetailsKeys.NAME, "-"))),
|
||||
createWidget("Owners:", formatOwners(testCaseDetails)),
|
||||
createWidget("Tags:", formatTags(testCaseDetails)));
|
||||
|
||||
sections.add(new Section(testCaseDetailsWidgets));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private String formatOwners(Map<Enum<?>, Object> testCaseDetails) {
|
||||
List<EntityReference> owners =
|
||||
(List<EntityReference>)
|
||||
testCaseDetails.getOrDefault(DQ_TestCaseDetailsKeys.OWNERS, Collections.emptyList());
|
||||
|
||||
StringBuilder ownersStringified = new StringBuilder();
|
||||
if (!CommonUtil.nullOrEmpty(owners)) {
|
||||
owners.forEach(
|
||||
owner -> {
|
||||
if (owner != null && owner.getName() != null) {
|
||||
ownersStringified.append(owner.getName()).append(", ");
|
||||
}
|
||||
});
|
||||
|
||||
// Remove the trailing comma and space if there's content
|
||||
if (!ownersStringified.isEmpty()) {
|
||||
ownersStringified.setLength(ownersStringified.length() - 2);
|
||||
}
|
||||
} else {
|
||||
ownersStringified.append("-");
|
||||
}
|
||||
|
||||
return ownersStringified.toString();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private String formatTags(Map<Enum<?>, Object> testCaseDetails) {
|
||||
List<TagLabel> tags =
|
||||
(List<TagLabel>)
|
||||
testCaseDetails.getOrDefault(DQ_TestCaseDetailsKeys.TAGS, Collections.emptyList());
|
||||
|
||||
StringBuilder tagsStringified = new StringBuilder();
|
||||
if (!CommonUtil.nullOrEmpty(tags)) {
|
||||
tags.forEach(
|
||||
tag -> {
|
||||
if (tag != null && tag.getName() != null) {
|
||||
tagsStringified.append(tag.getName()).append(", ");
|
||||
}
|
||||
});
|
||||
|
||||
// Remove the trailing comma and space if there's content
|
||||
if (!tagsStringified.isEmpty()) {
|
||||
tagsStringified.setLength(tagsStringified.length() - 2);
|
||||
}
|
||||
} else {
|
||||
tagsStringified.append("-");
|
||||
}
|
||||
|
||||
return tagsStringified.toString();
|
||||
}
|
||||
|
||||
private void addTestCaseFQNSection(
|
||||
Map<DQ_Template_Section, Map<Enum<?>, Object>> templateData, List<Section> sections) {
|
||||
|
||||
Map<Enum<?>, Object> testCaseDetails = templateData.get(DQ_Template_Section.TEST_CASE_DETAILS);
|
||||
if (nullOrEmpty(testCaseDetails)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Widget testCaseFQNWidget =
|
||||
createWidget(
|
||||
"Test Case FQN:",
|
||||
String.valueOf(
|
||||
testCaseDetails.getOrDefault(DQ_TestCaseDetailsKeys.TEST_CASE_FQN, "-")));
|
||||
|
||||
sections.add(new Section(List.of(testCaseFQNWidget)));
|
||||
}
|
||||
|
||||
private void addTestCaseResultSection(
|
||||
Map<DQ_Template_Section, Map<Enum<?>, Object>> templateData, List<Section> sections) {
|
||||
|
||||
Map<Enum<?>, Object> testCaseResult = templateData.get(DQ_Template_Section.TEST_CASE_RESULT);
|
||||
if (nullOrEmpty(testCaseResult)) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<Widget> statusParameterWidgets = new ArrayList<>();
|
||||
statusParameterWidgets.add(createWidget("TEST CASE RESULT"));
|
||||
|
||||
statusParameterWidgets.add(
|
||||
createWidget(
|
||||
"Status:", getStatusWithEmoji(testCaseResult.get(DQ_TestCaseResultKeys.STATUS))));
|
||||
|
||||
statusParameterWidgets.add(
|
||||
createWidget(
|
||||
"Result Message:",
|
||||
String.valueOf(
|
||||
testCaseResult.getOrDefault(DQ_TestCaseResultKeys.RESULT_MESSAGE, "-"))));
|
||||
|
||||
sections.add(new Section(statusParameterWidgets));
|
||||
}
|
||||
|
||||
private String getStatusWithEmoji(Object object) {
|
||||
if (object instanceof TestCaseStatus status) {
|
||||
return switch (status) {
|
||||
case Success -> "Success \u2705"; // Green checkmark for success
|
||||
case Failed -> "Failed \u274C"; // Red cross for failure
|
||||
case Aborted -> "Aborted \u26A0"; // Warning sign for aborted
|
||||
case Queued -> "Queued \u23F3"; // Hourglass for queued
|
||||
default -> "Unknown \u2753"; // Gray question mark for unknown cases
|
||||
};
|
||||
}
|
||||
return "Unknown \u2753"; // Default to unknown if the object is not a valid TestCaseStatus
|
||||
}
|
||||
|
||||
private void addParameterValuesSection(
|
||||
Map<DQ_Template_Section, Map<Enum<?>, Object>> templateData, List<Section> sections) {
|
||||
|
||||
Map<Enum<?>, Object> testCaseResult = templateData.get(DQ_Template_Section.TEST_CASE_RESULT);
|
||||
if (nullOrEmpty(testCaseResult)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object result = testCaseResult.get(DQ_TestCaseResultKeys.PARAMETER_VALUE);
|
||||
if (!(result instanceof List<?>)) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<TestCaseParameterValue> parameterValues = (List<TestCaseParameterValue>) result;
|
||||
if (nullOrEmpty(parameterValues)) {
|
||||
return;
|
||||
}
|
||||
|
||||
String parameterValuesText =
|
||||
parameterValues.stream()
|
||||
.map(param -> String.format("[%s: %s]", param.getName(), param.getValue()))
|
||||
.collect(Collectors.joining(", "));
|
||||
|
||||
List<Widget> parameterValueWidget = new ArrayList<>();
|
||||
parameterValueWidget.add(createWidget("Parameter Value:", parameterValuesText));
|
||||
|
||||
sections.add(new Section(parameterValueWidget));
|
||||
}
|
||||
|
||||
private void addInspectionQuerySection(
|
||||
Map<DQ_Template_Section, Map<Enum<?>, Object>> templateData, List<Section> sections) {
|
||||
|
||||
Map<Enum<?>, Object> testCaseDetails = templateData.get(DQ_Template_Section.TEST_CASE_DETAILS);
|
||||
|
||||
if (!nullOrEmpty(testCaseDetails)) {
|
||||
String inspectionQueryText =
|
||||
String.valueOf(
|
||||
testCaseDetails.getOrDefault(DQ_TestCaseDetailsKeys.INSPECTION_QUERY, "-"));
|
||||
|
||||
Widget inspectionQuery = createWidget("Inspection Query", "");
|
||||
Widget inspectionQueryWidget = new Widget(new TextParagraph(inspectionQueryText));
|
||||
|
||||
sections.add(new Section(List.of(inspectionQuery, inspectionQueryWidget)));
|
||||
}
|
||||
}
|
||||
|
||||
private void addTestDefinitionSection(
|
||||
Map<DQ_Template_Section, Map<Enum<?>, Object>> templateData, List<Section> sections) {
|
||||
|
||||
Map<Enum<?>, Object> testDefinition = templateData.get(DQ_Template_Section.TEST_DEFINITION);
|
||||
|
||||
if (!nullOrEmpty(testDefinition)) {
|
||||
List<Widget> testDefinitionWidgets =
|
||||
List.of(
|
||||
createWidget("TEST DEFINITION"),
|
||||
createWidget(
|
||||
"Name:",
|
||||
String.valueOf(
|
||||
testDefinition.getOrDefault(
|
||||
DQ_TestDefinitionKeys.TEST_DEFINITION_NAME, "-"))),
|
||||
createWidget(
|
||||
"Description:",
|
||||
String.valueOf(
|
||||
testDefinition.getOrDefault(
|
||||
DQ_TestDefinitionKeys.TEST_DEFINITION_DESCRIPTION, "-"))));
|
||||
|
||||
sections.add(new Section(testDefinitionWidgets));
|
||||
}
|
||||
}
|
||||
|
||||
private void addSampleDataSection(
|
||||
Map<DQ_Template_Section, Map<Enum<?>, Object>> templateData, List<Section> sections) {
|
||||
if (templateData.containsKey(DQ_Template_Section.TEST_CASE_DETAILS)) {
|
||||
Map<Enum<?>, Object> testCaseDetails =
|
||||
templateData.get(DQ_Template_Section.TEST_CASE_DETAILS);
|
||||
|
||||
if (!nullOrEmpty(testCaseDetails)) {
|
||||
Widget sampleDataWidget =
|
||||
createWidget(
|
||||
"Sample Data:",
|
||||
String.valueOf(
|
||||
testCaseDetails.getOrDefault(DQ_TestCaseDetailsKeys.SAMPLE_DATA, "-")));
|
||||
|
||||
sections.add(new Section(List.of(sampleDataWidget)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<Widget> createEventDetailsWidgets(Map<Enum<?>, Object> detailsMap) {
|
||||
List<Widget> widgets = new ArrayList<>();
|
||||
|
||||
Map<Enum<?>, String> labelsMap = new LinkedHashMap<>();
|
||||
labelsMap.put(EventDetailsKeys.EVENT_TYPE, "Event Type:");
|
||||
labelsMap.put(EventDetailsKeys.UPDATED_BY, "Updated By:");
|
||||
labelsMap.put(EventDetailsKeys.ENTITY_TYPE, "Entity Type:");
|
||||
labelsMap.put(EventDetailsKeys.TIME, "Time:");
|
||||
|
||||
labelsMap.forEach(
|
||||
(key, label) -> {
|
||||
if (detailsMap.containsKey(key)) {
|
||||
widgets.add(createWidget(label, String.valueOf(detailsMap.get(key))));
|
||||
}
|
||||
});
|
||||
|
||||
return widgets;
|
||||
}
|
||||
|
||||
private Widget createWidget(String label) {
|
||||
return new Widget(new TextParagraph(applyBoldFormatWithSpace(label) + StringUtils.EMPTY));
|
||||
}
|
||||
|
||||
private Widget createWidget(String label, String content) {
|
||||
return new Widget(new TextParagraph(applyBoldFormatWithSpace(label) + content));
|
||||
}
|
||||
|
||||
private Header createHeader() {
|
||||
return new Header("Change Event Details", "https://imgur.com/kOOPEG4.png", "IMAGE");
|
||||
}
|
||||
|
||||
private Header createConnectionSuccessfulHeader() {
|
||||
return new Header("Connection Successful \u2705", "https://imgur.com/kOOPEG4.png", "IMAGE");
|
||||
}
|
||||
|
||||
private Section createFooterSection() {
|
||||
return new Section(List.of(new Widget(new TextParagraph(TEMPLATE_FOOTER))));
|
||||
}
|
||||
|
||||
private String applyBoldFormatWithSpace(String title) {
|
||||
return String.format(getBoldWithSpace(), title);
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,9 +17,27 @@ import static org.openmetadata.common.utils.CommonUtil.nullOrEmpty;
|
||||
import static org.openmetadata.service.util.email.EmailUtil.getSmtpSettings;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import org.openmetadata.common.utils.CommonUtil;
|
||||
import org.openmetadata.schema.tests.TestCaseParameterValue;
|
||||
import org.openmetadata.schema.type.ChangeEvent;
|
||||
import org.openmetadata.schema.type.EntityReference;
|
||||
import org.openmetadata.schema.type.TagLabel;
|
||||
import org.openmetadata.service.Entity;
|
||||
import org.openmetadata.service.apps.bundles.changeEvent.msteams.TeamsMessage;
|
||||
import org.openmetadata.service.apps.bundles.changeEvent.msteams.TeamsMessage.AdaptiveCardContent;
|
||||
import org.openmetadata.service.apps.bundles.changeEvent.msteams.TeamsMessage.Attachment;
|
||||
import org.openmetadata.service.apps.bundles.changeEvent.msteams.TeamsMessage.Column;
|
||||
import org.openmetadata.service.apps.bundles.changeEvent.msteams.TeamsMessage.ColumnSet;
|
||||
import org.openmetadata.service.apps.bundles.changeEvent.msteams.TeamsMessage.Image;
|
||||
import org.openmetadata.service.apps.bundles.changeEvent.msteams.TeamsMessage.TextBlock;
|
||||
import org.openmetadata.service.exception.UnhandledServerException;
|
||||
|
||||
public class MSTeamsMessageDecorator implements MessageDecorator<TeamsMessage> {
|
||||
@ -29,6 +47,11 @@ public class MSTeamsMessageDecorator implements MessageDecorator<TeamsMessage> {
|
||||
return "**%s**";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBoldWithSpace() {
|
||||
return "**%s** ";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLineBreak() {
|
||||
return " <br/> ";
|
||||
@ -66,7 +89,12 @@ public class MSTeamsMessageDecorator implements MessageDecorator<TeamsMessage> {
|
||||
|
||||
@Override
|
||||
public TeamsMessage buildEntityMessage(String publisherName, ChangeEvent event) {
|
||||
return getTeamMessage(createEntityMessage(publisherName, event));
|
||||
return createMessage(publisherName, event, createEntityMessage(publisherName, event));
|
||||
}
|
||||
|
||||
@Override
|
||||
public TeamsMessage buildThreadMessage(String publisherName, ChangeEvent event) {
|
||||
return createMessage(publisherName, event, createThreadMessage(publisherName, event));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -74,53 +102,582 @@ public class MSTeamsMessageDecorator implements MessageDecorator<TeamsMessage> {
|
||||
return getTeamTestMessage(publisherName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TeamsMessage buildThreadMessage(String publisherName, ChangeEvent event) {
|
||||
return getTeamMessage(createThreadMessage(publisherName, event));
|
||||
}
|
||||
|
||||
private TeamsMessage getTeamMessage(OutgoingMessage outgoingMessage) {
|
||||
if (!outgoingMessage.getMessages().isEmpty()) {
|
||||
TeamsMessage teamsMessage = new TeamsMessage();
|
||||
teamsMessage.setSummary("Change Event From OpenMetadata");
|
||||
|
||||
// Sections
|
||||
TeamsMessage.Section teamsSections = new TeamsMessage.Section();
|
||||
teamsSections.setActivityTitle(outgoingMessage.getHeader());
|
||||
List<TeamsMessage.Section> attachmentList = new ArrayList<>();
|
||||
outgoingMessage
|
||||
.getMessages()
|
||||
.forEach(m -> attachmentList.add(getTeamsSection(teamsSections.getActivityTitle(), m)));
|
||||
|
||||
teamsMessage.setSections(attachmentList);
|
||||
return teamsMessage;
|
||||
public TeamsMessage getTeamTestMessage(String publisherName) {
|
||||
if (publisherName.isEmpty()) {
|
||||
throw new UnhandledServerException("Publisher name not found.");
|
||||
}
|
||||
throw new UnhandledServerException("No messages found for the event");
|
||||
|
||||
return createConnectionTestMessage(publisherName);
|
||||
}
|
||||
|
||||
private TeamsMessage getTeamTestMessage(String publisherName) {
|
||||
if (!publisherName.isEmpty()) {
|
||||
TeamsMessage teamsMessage = new TeamsMessage();
|
||||
teamsMessage.setSummary(
|
||||
"This is a test message from OpenMetadata to confirm your Microsoft Teams destination is configured correctly.");
|
||||
|
||||
// Sections
|
||||
TeamsMessage.Section teamsSection = new TeamsMessage.Section();
|
||||
teamsSection.setActivityTitle("Alert: " + publisherName);
|
||||
|
||||
List<TeamsMessage.Section> sectionList = new ArrayList<>();
|
||||
sectionList.add(teamsSection);
|
||||
|
||||
teamsMessage.setSections(sectionList);
|
||||
return teamsMessage;
|
||||
private TeamsMessage createMessage(
|
||||
String publisherName, ChangeEvent event, OutgoingMessage outgoingMessage) {
|
||||
if (outgoingMessage.getMessages().isEmpty()) {
|
||||
throw new UnhandledServerException("No messages found for the event");
|
||||
}
|
||||
throw new UnhandledServerException("Publisher name not found.");
|
||||
|
||||
String entityType = event.getEntityType();
|
||||
|
||||
return switch (entityType) {
|
||||
case Entity.TEST_CASE -> createDQMessage(publisherName, event, outgoingMessage);
|
||||
default -> createGeneralChangeEventMessage(publisherName, event, outgoingMessage);
|
||||
};
|
||||
}
|
||||
|
||||
private TeamsMessage.Section getTeamsSection(String activityTitle, String message) {
|
||||
TeamsMessage.Section section = new TeamsMessage.Section();
|
||||
section.setActivityTitle(activityTitle);
|
||||
section.setActivityText(message);
|
||||
return section;
|
||||
private TeamsMessage createGeneralChangeEventMessage(
|
||||
String publisherName, ChangeEvent event, OutgoingMessage outgoingMessage) {
|
||||
|
||||
Map<General_Template_Section, Map<Enum<?>, Object>> templateData =
|
||||
buildGeneralTemplateData(publisherName, event, outgoingMessage);
|
||||
|
||||
Map<Enum<?>, Object> eventDetails = templateData.get(General_Template_Section.EVENT_DETAILS);
|
||||
|
||||
TextBlock changeEventDetailsTextBlock = createHeader();
|
||||
|
||||
// Create the facts for the FactSet
|
||||
List<TeamsMessage.Fact> facts = createEventDetailsFacts(eventDetails);
|
||||
|
||||
// Create a list of TextBlocks for each message with a separator
|
||||
List<TextBlock> messageTextBlocks =
|
||||
outgoingMessage.getMessages().stream()
|
||||
.map(
|
||||
message ->
|
||||
TextBlock.builder()
|
||||
.type("TextBlock")
|
||||
.text(message)
|
||||
.wrap(true)
|
||||
.spacing("Medium")
|
||||
.separator(true)
|
||||
.build())
|
||||
.toList();
|
||||
|
||||
TextBlock footerMessage = createFooterMessage();
|
||||
|
||||
ColumnSet columnSet = createHeaderColumnSet(changeEventDetailsTextBlock);
|
||||
|
||||
// Create the body list and combine all elements
|
||||
List<TeamsMessage.BodyItem> body = new ArrayList<>();
|
||||
body.add(columnSet);
|
||||
body.add(TeamsMessage.FactSet.builder().type("FactSet").facts(facts).build());
|
||||
body.addAll(messageTextBlocks); // Add the containers with message TextBlocks
|
||||
body.add(createEntityLink(outgoingMessage.getEntityUrl()));
|
||||
body.add(footerMessage);
|
||||
|
||||
Attachment attachment =
|
||||
Attachment.builder()
|
||||
.contentType("application/vnd.microsoft.card.adaptive")
|
||||
.content(
|
||||
AdaptiveCardContent.builder()
|
||||
.type("AdaptiveCard")
|
||||
.version("1.0")
|
||||
.body(body) // Pass the combined body list
|
||||
.build())
|
||||
.build();
|
||||
|
||||
return TeamsMessage.builder().type("message").attachments(List.of(attachment)).build();
|
||||
}
|
||||
|
||||
private TeamsMessage createDQMessage(
|
||||
String publisherName, ChangeEvent event, OutgoingMessage outgoingMessage) {
|
||||
|
||||
Map<DQ_Template_Section, Map<Enum<?>, Object>> dqTemplateData =
|
||||
buildDQTemplateData(publisherName, event, outgoingMessage);
|
||||
|
||||
TextBlock changeEventDetailsTextBlock = createHeader();
|
||||
|
||||
Map<Enum<?>, Object> eventDetails = dqTemplateData.get(DQ_Template_Section.EVENT_DETAILS);
|
||||
|
||||
// Create the facts for different sections
|
||||
List<TeamsMessage.Fact> facts = createEventDetailsFacts(eventDetails);
|
||||
List<TeamsMessage.Fact> testCaseDetailsFacts = createTestCaseDetailsFacts(dqTemplateData);
|
||||
List<TeamsMessage.Fact> testCaseResultFacts = createTestCaseResultFacts(dqTemplateData);
|
||||
|
||||
List<TeamsMessage.Fact> parameterValuesFacts = createParameterValuesFacts(dqTemplateData);
|
||||
|
||||
List<TeamsMessage.Fact> inspectionQueryFacts = createInspectionQueryFacts(dqTemplateData);
|
||||
List<TeamsMessage.Fact> testDefinitionFacts = createTestDefinitionFacts(dqTemplateData);
|
||||
List<TeamsMessage.Fact> sampleDataFacts = createSampleDataFacts(dqTemplateData);
|
||||
|
||||
// Create a list of TextBlocks for each message with a separator
|
||||
List<TextBlock> messageTextBlocks =
|
||||
outgoingMessage.getMessages().stream()
|
||||
.map(
|
||||
message ->
|
||||
TextBlock.builder()
|
||||
.type("TextBlock")
|
||||
.text(message)
|
||||
.wrap(true)
|
||||
.spacing("Medium")
|
||||
.separator(true) // Set separator for each message
|
||||
.build())
|
||||
.toList();
|
||||
|
||||
TextBlock footerMessage = createFooterMessage();
|
||||
|
||||
ColumnSet columnSet = createHeaderColumnSet(changeEventDetailsTextBlock);
|
||||
|
||||
// Divider between sections
|
||||
TextBlock divider = createDivider();
|
||||
|
||||
// Create the body list and combine all elements with dividers between fact sets
|
||||
List<TeamsMessage.BodyItem> body = new ArrayList<>();
|
||||
body.add(columnSet);
|
||||
|
||||
// event details facts
|
||||
body.add(TeamsMessage.FactSet.builder().type("FactSet").facts(facts).build());
|
||||
|
||||
// Add the outgoing message text blocks
|
||||
body.addAll(messageTextBlocks);
|
||||
body.add(divider);
|
||||
|
||||
// test case details facts
|
||||
if (dqTemplateData.containsKey(DQ_Template_Section.TEST_CASE_DETAILS)
|
||||
&& !nullOrEmpty(testCaseDetailsFacts)) {
|
||||
body.add(createBoldTextBlock("Test Case Details"));
|
||||
body.add(TeamsMessage.FactSet.builder().type("FactSet").facts(testCaseDetailsFacts).build());
|
||||
body.add(divider);
|
||||
}
|
||||
|
||||
// test case result facts
|
||||
if (dqTemplateData.containsKey(DQ_Template_Section.TEST_CASE_RESULT)
|
||||
&& !nullOrEmpty(testCaseResultFacts)) {
|
||||
body.add(createBoldTextBlock("Test Case Result"));
|
||||
body.add(TeamsMessage.FactSet.builder().type("FactSet").facts(testCaseResultFacts).build());
|
||||
body.add(divider);
|
||||
}
|
||||
|
||||
// parameterValues facts
|
||||
if (dqTemplateData.containsKey(DQ_Template_Section.TEST_CASE_DETAILS)
|
||||
&& !nullOrEmpty(parameterValuesFacts)) {
|
||||
body.add(TeamsMessage.FactSet.builder().type("FactSet").facts(parameterValuesFacts).build());
|
||||
}
|
||||
|
||||
// inspection query facts
|
||||
if (dqTemplateData.containsKey(DQ_Template_Section.TEST_CASE_DETAILS)
|
||||
&& !nullOrEmpty(inspectionQueryFacts)) {
|
||||
body.add(TeamsMessage.FactSet.builder().type("FactSet").facts(inspectionQueryFacts).build());
|
||||
body.add(divider);
|
||||
}
|
||||
|
||||
// test definition facts
|
||||
if (dqTemplateData.containsKey(DQ_Template_Section.TEST_DEFINITION)
|
||||
&& !nullOrEmpty(testDefinitionFacts)) {
|
||||
body.add(createBoldTextBlock("Test Definition"));
|
||||
body.add(TeamsMessage.FactSet.builder().type("FactSet").facts(testDefinitionFacts).build());
|
||||
body.add(divider);
|
||||
}
|
||||
|
||||
// Add sample data facts
|
||||
if (dqTemplateData.containsKey(DQ_Template_Section.TEST_CASE_DETAILS)
|
||||
&& !nullOrEmpty(sampleDataFacts)) {
|
||||
body.add(TeamsMessage.FactSet.builder().type("FactSet").facts(sampleDataFacts).build());
|
||||
}
|
||||
|
||||
body.add(createEntityLink(outgoingMessage.getEntityUrl()));
|
||||
|
||||
body.add(footerMessage);
|
||||
|
||||
// Create the attachment with the combined body list
|
||||
Attachment attachment =
|
||||
Attachment.builder()
|
||||
.contentType("application/vnd.microsoft.card.adaptive")
|
||||
.content(
|
||||
AdaptiveCardContent.builder()
|
||||
.type("AdaptiveCard")
|
||||
.version("1.0")
|
||||
.body(body) // Pass the combined body list
|
||||
.build())
|
||||
.build();
|
||||
|
||||
return TeamsMessage.builder().type("message").attachments(List.of(attachment)).build();
|
||||
}
|
||||
|
||||
private ColumnSet createHeaderColumnSet(TextBlock changeEventDetailsTextBlock) {
|
||||
return ColumnSet.builder()
|
||||
.type("ColumnSet")
|
||||
.columns(
|
||||
List.of(
|
||||
Column.builder()
|
||||
.type("Column")
|
||||
.items(List.of(createOMImageMessage())) // Create and add image message
|
||||
.width("auto")
|
||||
.build(),
|
||||
Column.builder()
|
||||
.type("Column")
|
||||
.items(List.of(changeEventDetailsTextBlock)) // Add change event details
|
||||
.width("stretch")
|
||||
.build()))
|
||||
.build();
|
||||
}
|
||||
|
||||
private List<TeamsMessage.Fact> createEventDetailsFacts(Map<Enum<?>, Object> detailsMap) {
|
||||
return List.of(
|
||||
createFact("Event Type:", String.valueOf(detailsMap.get(EventDetailsKeys.EVENT_TYPE))),
|
||||
createFact("Updated By:", String.valueOf(detailsMap.get(EventDetailsKeys.UPDATED_BY))),
|
||||
createFact("Entity Type:", String.valueOf(detailsMap.get(EventDetailsKeys.ENTITY_TYPE))),
|
||||
createFact("Time:", String.valueOf(detailsMap.get(EventDetailsKeys.TIME))),
|
||||
createFact("FQN:", String.valueOf(detailsMap.get(EventDetailsKeys.ENTITY_FQN))));
|
||||
}
|
||||
|
||||
private List<TeamsMessage.Fact> createTestCaseDetailsFacts(
|
||||
Map<DQ_Template_Section, Map<Enum<?>, Object>> templateData) {
|
||||
|
||||
Map<Enum<?>, Object> testCaseDetails = templateData.get(DQ_Template_Section.TEST_CASE_DETAILS);
|
||||
|
||||
Function<DQ_TestCaseDetailsKeys, String> getDetail =
|
||||
key -> String.valueOf(testCaseDetails.getOrDefault(key, "-"));
|
||||
|
||||
return Arrays.asList(
|
||||
createFact("ID:", getDetail.apply(DQ_TestCaseDetailsKeys.ID)),
|
||||
createFact("Name:", getDetail.apply(DQ_TestCaseDetailsKeys.NAME)),
|
||||
createFact("Owners:", formatOwners(testCaseDetails)),
|
||||
createFact("Tags:", formatTags(testCaseDetails)));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private String formatOwners(Map<Enum<?>, Object> testCaseDetails) {
|
||||
List<EntityReference> owners =
|
||||
(List<EntityReference>)
|
||||
testCaseDetails.getOrDefault(DQ_TestCaseDetailsKeys.OWNERS, Collections.emptyList());
|
||||
|
||||
StringBuilder ownersStringified = new StringBuilder();
|
||||
if (!CommonUtil.nullOrEmpty(owners)) {
|
||||
owners.forEach(
|
||||
owner -> {
|
||||
if (owner != null && owner.getName() != null) {
|
||||
ownersStringified.append(owner.getName()).append(", ");
|
||||
}
|
||||
});
|
||||
|
||||
// Remove the trailing comma and space if there's content
|
||||
if (!ownersStringified.isEmpty()) {
|
||||
ownersStringified.setLength(ownersStringified.length() - 2);
|
||||
}
|
||||
} else {
|
||||
ownersStringified.append("-");
|
||||
}
|
||||
|
||||
return ownersStringified.toString();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private String formatTags(Map<Enum<?>, Object> testCaseDetails) {
|
||||
List<TagLabel> tags =
|
||||
(List<TagLabel>)
|
||||
testCaseDetails.getOrDefault(DQ_TestCaseDetailsKeys.TAGS, Collections.emptyList());
|
||||
|
||||
StringBuilder tagsStringified = new StringBuilder();
|
||||
if (!CommonUtil.nullOrEmpty(tags)) {
|
||||
tags.forEach(
|
||||
tag -> {
|
||||
if (tag != null && tag.getName() != null) {
|
||||
tagsStringified.append(tag.getName()).append(", ");
|
||||
}
|
||||
});
|
||||
|
||||
// Remove the trailing comma and space if there's content
|
||||
if (!tagsStringified.isEmpty()) {
|
||||
tagsStringified.setLength(tagsStringified.length() - 2);
|
||||
}
|
||||
} else {
|
||||
tagsStringified.append("-");
|
||||
}
|
||||
|
||||
return tagsStringified.toString();
|
||||
}
|
||||
|
||||
private List<TeamsMessage.Fact> createTestCaseResultFacts(
|
||||
Map<DQ_Template_Section, Map<Enum<?>, Object>> templateData) {
|
||||
|
||||
Map<Enum<?>, Object> testCaseDetails = templateData.get(DQ_Template_Section.TEST_CASE_RESULT);
|
||||
|
||||
if (nullOrEmpty(testCaseDetails)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
return Stream.of(
|
||||
createFact(
|
||||
"Status:",
|
||||
String.valueOf(testCaseDetails.getOrDefault(DQ_TestCaseResultKeys.STATUS, "-"))),
|
||||
createFact(
|
||||
"Result Message:",
|
||||
String.valueOf(
|
||||
testCaseDetails.getOrDefault(DQ_TestCaseResultKeys.RESULT_MESSAGE, "-"))))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private List<TeamsMessage.Fact> createParameterValuesFacts(
|
||||
Map<DQ_Template_Section, Map<Enum<?>, Object>> templateData) {
|
||||
|
||||
Map<Enum<?>, Object> testCaseDetails = templateData.get(DQ_Template_Section.TEST_CASE_RESULT);
|
||||
|
||||
if (nullOrEmpty(testCaseDetails)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
Object result = testCaseDetails.get(DQ_TestCaseResultKeys.PARAMETER_VALUE);
|
||||
if (!(result instanceof List<?>)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<TestCaseParameterValue> parameterValues = (List<TestCaseParameterValue>) result;
|
||||
if (nullOrEmpty(parameterValues)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
StringBuilder parameterValuesText = new StringBuilder();
|
||||
|
||||
parameterValues.forEach(
|
||||
param -> {
|
||||
if (parameterValuesText.length() > 0) {
|
||||
parameterValuesText.append(", ");
|
||||
}
|
||||
|
||||
parameterValuesText.append(String.format("[%s: %s]", param.getName(), param.getValue()));
|
||||
});
|
||||
|
||||
// Return a fact for "Parameter Values" with all parameter values in a single string
|
||||
return Stream.of(createFact("Parameter Values:", parameterValuesText.toString()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private List<TeamsMessage.Fact> createInspectionQueryFacts(
|
||||
Map<DQ_Template_Section, Map<Enum<?>, Object>> templateData) {
|
||||
|
||||
Map<Enum<?>, Object> testCaseDetails = templateData.get(DQ_Template_Section.TEST_CASE_DETAILS);
|
||||
if (nullOrEmpty(testCaseDetails)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
Object inspectionQuery = testCaseDetails.get(DQ_TestCaseDetailsKeys.INSPECTION_QUERY);
|
||||
|
||||
if (!nullOrEmpty(inspectionQuery)) {
|
||||
return Stream.of(createFact("Inspection Query:", String.valueOf(inspectionQuery)))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
private List<TeamsMessage.Fact> createTestDefinitionFacts(
|
||||
Map<DQ_Template_Section, Map<Enum<?>, Object>> templateData) {
|
||||
|
||||
Map<Enum<?>, Object> testCaseDetails = templateData.get(DQ_Template_Section.TEST_DEFINITION);
|
||||
if (nullOrEmpty(testCaseDetails)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
return Stream.of(
|
||||
createFact(
|
||||
"Name:",
|
||||
String.valueOf(
|
||||
testCaseDetails.getOrDefault(DQ_TestDefinitionKeys.TEST_DEFINITION_NAME, "-"))),
|
||||
createFact(
|
||||
"Description:",
|
||||
String.valueOf(
|
||||
testCaseDetails.getOrDefault(
|
||||
DQ_TestDefinitionKeys.TEST_DEFINITION_DESCRIPTION, "-"))))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private List<TeamsMessage.Fact> createSampleDataFacts(
|
||||
Map<DQ_Template_Section, Map<Enum<?>, Object>> templateData) {
|
||||
|
||||
Map<Enum<?>, Object> testCaseDetails = templateData.get(DQ_Template_Section.TEST_CASE_DETAILS);
|
||||
if (nullOrEmpty(testCaseDetails)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
Object sampleData = testCaseDetails.get(DQ_TestCaseDetailsKeys.SAMPLE_DATA);
|
||||
if (nullOrEmpty(sampleData)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
return Stream.of(createFact("Sample Data:", String.valueOf(sampleData)))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private TeamsMessage createConnectionTestMessage(String publisherName) {
|
||||
Image imageItem = createOMImageMessage();
|
||||
|
||||
Column column1 =
|
||||
Column.builder().type("Column").width("auto").items(List.of(imageItem)).build();
|
||||
|
||||
TextBlock textBlock1 = createTextBlock("Connection Successful \u2705", "Bolder", "Large");
|
||||
TextBlock textBlock2 =
|
||||
createTextBlock(applyBoldFormat("Publisher:") + publisherName, null, null);
|
||||
TextBlock textBlock3 = createTextBlock(CONNECTION_TEST_DESCRIPTION, null, null);
|
||||
|
||||
Column column2 =
|
||||
Column.builder()
|
||||
.type("Column")
|
||||
.width("stretch")
|
||||
.items(List.of(textBlock1, textBlock2, textBlock3))
|
||||
.build();
|
||||
|
||||
ColumnSet columnSet =
|
||||
ColumnSet.builder().type("ColumnSet").columns(List.of(column1, column2)).build();
|
||||
|
||||
// Create the footer text block
|
||||
TextBlock footerTextBlock = createTextBlock("OpenMetadata", "Lighter", "Small");
|
||||
footerTextBlock.setHorizontalAlignment("Center");
|
||||
footerTextBlock.setSpacing("Medium");
|
||||
footerTextBlock.setSeparator(true);
|
||||
|
||||
AdaptiveCardContent adaptiveCardContent =
|
||||
AdaptiveCardContent.builder()
|
||||
.type("AdaptiveCard")
|
||||
.version("1.0")
|
||||
.body(List.of(columnSet, footerTextBlock))
|
||||
.build();
|
||||
|
||||
Attachment attachment =
|
||||
Attachment.builder()
|
||||
.contentType("application/vnd.microsoft.card.adaptive")
|
||||
.content(adaptiveCardContent)
|
||||
.build();
|
||||
|
||||
return TeamsMessage.builder().type("message").attachments(List.of(attachment)).build();
|
||||
}
|
||||
|
||||
private Map<General_Template_Section, Map<Enum<?>, Object>> buildGeneralTemplateData(
|
||||
String publisherName, ChangeEvent event, OutgoingMessage outgoingMessage) {
|
||||
|
||||
TemplateDataBuilder<General_Template_Section> builder = new TemplateDataBuilder<>();
|
||||
|
||||
// Use General_Template_Section directly
|
||||
builder
|
||||
.add(
|
||||
General_Template_Section.EVENT_DETAILS,
|
||||
EventDetailsKeys.EVENT_TYPE,
|
||||
event.getEventType().value())
|
||||
.add(
|
||||
General_Template_Section.EVENT_DETAILS,
|
||||
EventDetailsKeys.UPDATED_BY,
|
||||
event.getUserName())
|
||||
.add(
|
||||
General_Template_Section.EVENT_DETAILS,
|
||||
EventDetailsKeys.ENTITY_TYPE,
|
||||
event.getEntityType())
|
||||
.add(
|
||||
General_Template_Section.EVENT_DETAILS,
|
||||
EventDetailsKeys.ENTITY_FQN,
|
||||
MessageDecorator.getFQNForChangeEventEntity(event))
|
||||
.add(
|
||||
General_Template_Section.EVENT_DETAILS,
|
||||
EventDetailsKeys.TIME,
|
||||
new Date(event.getTimestamp()).toString())
|
||||
.add(
|
||||
General_Template_Section.EVENT_DETAILS,
|
||||
EventDetailsKeys.OUTGOING_MESSAGE,
|
||||
outgoingMessage);
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
// todo - complete buildDQTemplateData fn
|
||||
private Map<DQ_Template_Section, Map<Enum<?>, Object>> buildDQTemplateData(
|
||||
String publisherName, ChangeEvent event, OutgoingMessage outgoingMessage) {
|
||||
|
||||
TemplateDataBuilder<DQ_Template_Section> builder = new TemplateDataBuilder<>();
|
||||
|
||||
// Use DQ_Template_Section directly
|
||||
builder
|
||||
.add(
|
||||
DQ_Template_Section.EVENT_DETAILS,
|
||||
EventDetailsKeys.EVENT_TYPE,
|
||||
event.getEventType().value())
|
||||
.add(DQ_Template_Section.EVENT_DETAILS, EventDetailsKeys.UPDATED_BY, event.getUserName())
|
||||
.add(DQ_Template_Section.EVENT_DETAILS, EventDetailsKeys.ENTITY_TYPE, event.getEntityType())
|
||||
.add(
|
||||
DQ_Template_Section.EVENT_DETAILS,
|
||||
EventDetailsKeys.ENTITY_FQN,
|
||||
MessageDecorator.getFQNForChangeEventEntity(event))
|
||||
.add(
|
||||
DQ_Template_Section.EVENT_DETAILS,
|
||||
EventDetailsKeys.TIME,
|
||||
new Date(event.getTimestamp()).toString())
|
||||
.add(DQ_Template_Section.EVENT_DETAILS, EventDetailsKeys.OUTGOING_MESSAGE, outgoingMessage);
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private TextBlock createHeader() {
|
||||
return TextBlock.builder()
|
||||
.type("TextBlock")
|
||||
.text(applyBoldFormat("Change Event Details"))
|
||||
.size("Large")
|
||||
.weight("Bolder")
|
||||
.wrap(true)
|
||||
.build();
|
||||
}
|
||||
|
||||
private TextBlock createEntityLink(String url) {
|
||||
if (nullOrEmpty(url)) {
|
||||
throw new IllegalArgumentException("URL cannot be null or empty");
|
||||
}
|
||||
|
||||
// Replace the text part (if it's in markdown link format [some text](url))
|
||||
String updatedUrl = url.replaceAll("\\[.*?\\]\\((.*?)\\)", "[View Data]($1)");
|
||||
|
||||
return TextBlock.builder()
|
||||
.type("TextBlock")
|
||||
.text(updatedUrl)
|
||||
.wrap(true)
|
||||
.spacing("Medium")
|
||||
.separator(false)
|
||||
.build();
|
||||
}
|
||||
|
||||
private TextBlock createTextBlock(String text, String weight, String size) {
|
||||
return TextBlock.builder()
|
||||
.type("TextBlock")
|
||||
.text(text)
|
||||
.weight(weight)
|
||||
.size(size)
|
||||
.wrap(true)
|
||||
.build();
|
||||
}
|
||||
|
||||
private TextBlock createFooterMessage() {
|
||||
return TextBlock.builder()
|
||||
.type("TextBlock")
|
||||
.text(TEMPLATE_FOOTER)
|
||||
.size("Small")
|
||||
.weight("Lighter")
|
||||
.horizontalAlignment("Center")
|
||||
.spacing("Medium")
|
||||
.separator(true)
|
||||
.build();
|
||||
}
|
||||
|
||||
private TextBlock createBoldTextBlock(String text) {
|
||||
return TextBlock.builder()
|
||||
.type("TextBlock")
|
||||
.text(applyBoldFormat(text))
|
||||
.weight("Bolder")
|
||||
.wrap(true)
|
||||
.build();
|
||||
}
|
||||
|
||||
private TextBlock createDivider() {
|
||||
return TextBlock.builder()
|
||||
.type("TextBlock")
|
||||
.text(" ")
|
||||
.separator(true)
|
||||
.spacing("Medium")
|
||||
.build();
|
||||
}
|
||||
|
||||
private TeamsMessage.Fact createFact(String title, String value) {
|
||||
return TeamsMessage.Fact.builder().title(applyBoldFormat(title)).value(value).build();
|
||||
}
|
||||
|
||||
private String applyBoldFormat(String title) {
|
||||
return String.format(getBoldWithSpace(), title);
|
||||
}
|
||||
|
||||
private Image createOMImageMessage() {
|
||||
return Image.builder().type("Image").url("https://imgur.com/kOOPEG4.png").size("Small").build();
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,8 +25,14 @@ import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.EnumMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.bitbucket.cowwoc.diffmatchpatch.DiffMatchPatch;
|
||||
import org.openmetadata.common.utils.CommonUtil;
|
||||
@ -38,12 +44,21 @@ import org.openmetadata.schema.type.Include;
|
||||
import org.openmetadata.schema.type.ThreadType;
|
||||
import org.openmetadata.service.Entity;
|
||||
import org.openmetadata.service.exception.UnhandledServerException;
|
||||
import org.openmetadata.service.jdbi3.TestCaseRepository;
|
||||
import org.openmetadata.service.resources.feeds.MessageParser;
|
||||
import org.openmetadata.service.util.EntityUtil;
|
||||
import org.openmetadata.service.util.FeedUtils;
|
||||
|
||||
public interface MessageDecorator<T> {
|
||||
String CONNECTION_TEST_DESCRIPTION =
|
||||
"This is a test message, receiving this message confirms that you have successfully configured OpenMetadata to receive alerts.";
|
||||
|
||||
String TEMPLATE_FOOTER = "Change Event By OpenMetadata";
|
||||
|
||||
String getBold();
|
||||
|
||||
String getBoldWithSpace();
|
||||
|
||||
String getLineBreak();
|
||||
|
||||
String getAddMarker();
|
||||
@ -66,80 +81,99 @@ public interface MessageDecorator<T> {
|
||||
|
||||
T buildEntityMessage(String publisherName, ChangeEvent event);
|
||||
|
||||
T buildTestMessage(String publisherName);
|
||||
|
||||
T buildThreadMessage(String publisherName, ChangeEvent event);
|
||||
|
||||
T buildTestMessage(String publisherName);
|
||||
|
||||
default String buildEntityUrl(String entityType, EntityInterface entityInterface) {
|
||||
String fqn = entityInterface.getFullyQualifiedName();
|
||||
if (CommonUtil.nullOrEmpty(fqn)) {
|
||||
EntityInterface result =
|
||||
Entity.getEntity(entityType, entityInterface.getId(), "id", Include.NON_DELETED);
|
||||
fqn = result.getFullyQualifiedName();
|
||||
}
|
||||
|
||||
// Hande Test Case
|
||||
if (entityType.equals(Entity.TEST_CASE)) {
|
||||
TestCase testCase = (TestCase) entityInterface;
|
||||
return getEntityUrl(
|
||||
"incident-manager", testCase.getFullyQualifiedName(), "test-case-results");
|
||||
}
|
||||
|
||||
// Glossary Term
|
||||
if (entityType.equals(Entity.GLOSSARY_TERM)) {
|
||||
// Glossary Term is a special case where the URL is different
|
||||
return getEntityUrl(Entity.GLOSSARY, fqn, "");
|
||||
}
|
||||
|
||||
// Tag
|
||||
if (entityType.equals(Entity.TAG)) {
|
||||
// Tags need to be redirected to Classification Page
|
||||
return getEntityUrl("tags", fqn.split("\\.")[0], "");
|
||||
}
|
||||
|
||||
// IngestionPipeline
|
||||
if (entityType.equals(Entity.INGESTION_PIPELINE)) {
|
||||
return getIngestionPipelineUrl(this, entityType, entityInterface);
|
||||
String fqn = resolveFullyQualifiedName(entityType, entityInterface);
|
||||
|
||||
switch (entityType) {
|
||||
case Entity.TEST_CASE:
|
||||
if (entityInterface instanceof TestCase testCase) {
|
||||
return getEntityUrl(
|
||||
"incident-manager", testCase.getFullyQualifiedName(), "test-case-results");
|
||||
}
|
||||
break;
|
||||
|
||||
case Entity.GLOSSARY_TERM:
|
||||
return getEntityUrl(Entity.GLOSSARY, fqn, "");
|
||||
|
||||
case Entity.TAG:
|
||||
return getEntityUrl("tags", fqn.split("\\.")[0], "");
|
||||
|
||||
case Entity.INGESTION_PIPELINE:
|
||||
return getIngestionPipelineUrl(this, entityType, entityInterface);
|
||||
|
||||
default:
|
||||
return getEntityUrl(entityType, fqn, "");
|
||||
}
|
||||
|
||||
// Fallback in case of no match
|
||||
return getEntityUrl(entityType, fqn, "");
|
||||
}
|
||||
|
||||
default String buildThreadUrl(
|
||||
ThreadType threadType, String entityType, EntityInterface entityInterface) {
|
||||
|
||||
String activeTab =
|
||||
threadType.equals(ThreadType.Task) ? "activity_feed/tasks" : "activity_feed/all";
|
||||
|
||||
String fqn = resolveFullyQualifiedName(entityType, entityInterface);
|
||||
|
||||
switch (entityType) {
|
||||
case Entity.TEST_CASE:
|
||||
if (entityInterface instanceof TestCase) {
|
||||
TestCase testCase = (TestCase) entityInterface;
|
||||
return getEntityUrl("incident-manager", testCase.getFullyQualifiedName(), "issues");
|
||||
}
|
||||
break;
|
||||
|
||||
case Entity.GLOSSARY_TERM:
|
||||
return getEntityUrl(Entity.GLOSSARY, fqn, activeTab);
|
||||
|
||||
case Entity.TAG:
|
||||
return getEntityUrl("tags", fqn.split("\\.")[0], "");
|
||||
|
||||
case Entity.INGESTION_PIPELINE:
|
||||
return getIngestionPipelineUrl(this, entityType, entityInterface);
|
||||
|
||||
default:
|
||||
return getEntityUrl(entityType, fqn, activeTab);
|
||||
}
|
||||
|
||||
// Fallback in case of no match
|
||||
return getEntityUrl(entityType, fqn, activeTab);
|
||||
}
|
||||
|
||||
// Helper function to resolve FQN if null or empty
|
||||
private String resolveFullyQualifiedName(String entityType, EntityInterface entityInterface) {
|
||||
String fqn = entityInterface.getFullyQualifiedName();
|
||||
if (CommonUtil.nullOrEmpty(fqn)) {
|
||||
EntityInterface result =
|
||||
Entity.getEntity(entityType, entityInterface.getId(), "id", Include.NON_DELETED);
|
||||
fqn = result.getFullyQualifiedName();
|
||||
}
|
||||
return fqn;
|
||||
}
|
||||
|
||||
// Hande Test Case
|
||||
if (entityType.equals(Entity.TEST_CASE)) {
|
||||
TestCase testCase = (TestCase) entityInterface;
|
||||
return getEntityUrl("incident-manager", testCase.getFullyQualifiedName(), "issues");
|
||||
}
|
||||
static String getFQNForChangeEventEntity(ChangeEvent event) {
|
||||
return Optional.ofNullable(event.getEntityFullyQualifiedName())
|
||||
.filter(fqn -> !CommonUtil.nullOrEmpty(fqn))
|
||||
.orElseGet(
|
||||
() -> {
|
||||
EntityInterface entityInterface = getEntity(event);
|
||||
String fqn = entityInterface.getFullyQualifiedName();
|
||||
|
||||
// Glossary Term
|
||||
if (entityType.equals(Entity.GLOSSARY_TERM)) {
|
||||
// Glossary Term is a special case where the URL is different
|
||||
return getEntityUrl(Entity.GLOSSARY, fqn, activeTab);
|
||||
}
|
||||
if (CommonUtil.nullOrEmpty(fqn)) {
|
||||
EntityInterface result =
|
||||
Entity.getEntity(
|
||||
event.getEntityType(), entityInterface.getId(), "id", Include.NON_DELETED);
|
||||
fqn = result.getFullyQualifiedName();
|
||||
}
|
||||
|
||||
// Tag
|
||||
if (entityType.equals(Entity.TAG)) {
|
||||
// Tags need to be redirected to Classification Page
|
||||
return getEntityUrl("tags", fqn.split("\\.")[0], "");
|
||||
}
|
||||
|
||||
// IngestionPipeline
|
||||
if (entityType.equals(Entity.INGESTION_PIPELINE)) {
|
||||
return getIngestionPipelineUrl(this, entityType, entityInterface);
|
||||
}
|
||||
|
||||
return getEntityUrl(entityType, fqn, activeTab);
|
||||
return fqn;
|
||||
});
|
||||
}
|
||||
|
||||
default T buildOutgoingMessage(String publisherName, ChangeEvent event) {
|
||||
@ -400,7 +434,7 @@ public interface MessageDecorator<T> {
|
||||
}
|
||||
}
|
||||
if (nullOrEmpty(headerMessage) || attachmentList.isEmpty()) {
|
||||
throw new UnhandledServerException("Unable to build Slack Message");
|
||||
throw new UnhandledServerException("Unable to build message");
|
||||
}
|
||||
message.setHeader(headerMessage);
|
||||
message.setMessages(attachmentList);
|
||||
@ -438,4 +472,167 @@ public interface MessageDecorator<T> {
|
||||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
return localDateTime.format(formatter);
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder class for constructing a nested map structure that organizes each template data
|
||||
* into sections and corresponding keys, where both the sections and keys are represented as enums.
|
||||
* This class ensures type safety by using EnumMaps for both sections and keys.
|
||||
*
|
||||
* @param <S> The enum type representing the sections of the template.
|
||||
*/
|
||||
class TemplateDataBuilder<S extends Enum<S>> {
|
||||
|
||||
// Map to store sections and their corresponding keys and values
|
||||
private final Map<S, Map<Enum<?>, Object>> sectionToKeyDataMap = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Adds a key-value pair to the specified section of the template.
|
||||
* Ensures that the key is stored in a type-safe EnumMap, and the section is only created if it doesn't already exist.
|
||||
*
|
||||
* @param section The section of the template represented as an enum.
|
||||
* @param key The key within the section, represented as an enum.
|
||||
* @param value The value associated with the given key.
|
||||
* @param <K> The enum type representing the keys (must extend Enum).
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <K extends Enum<K>> TemplateDataBuilder<S> add(S section, K key, Object value) {
|
||||
sectionToKeyDataMap
|
||||
.computeIfAbsent(
|
||||
section, k -> (Map<Enum<?>, Object>) new EnumMap<>(key.getDeclaringClass()))
|
||||
.put(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Map<S, Map<Enum<?>, Object>> build() {
|
||||
return Collections.unmodifiableMap(sectionToKeyDataMap);
|
||||
}
|
||||
}
|
||||
|
||||
enum General_Template_Section {
|
||||
EVENT_DETAILS,
|
||||
}
|
||||
|
||||
enum DQ_Template_Section {
|
||||
EVENT_DETAILS,
|
||||
TEST_CASE_DETAILS,
|
||||
TEST_CASE_RESULT,
|
||||
TEST_DEFINITION
|
||||
}
|
||||
|
||||
enum EventDetailsKeys {
|
||||
EVENT_TYPE,
|
||||
UPDATED_BY,
|
||||
ENTITY_TYPE,
|
||||
ENTITY_FQN,
|
||||
TIME,
|
||||
OUTGOING_MESSAGE
|
||||
}
|
||||
|
||||
enum DQ_TestCaseDetailsKeys {
|
||||
ID,
|
||||
NAME,
|
||||
OWNERS,
|
||||
TAGS,
|
||||
DESCRIPTION,
|
||||
TEST_CASE_FQN,
|
||||
INSPECTION_QUERY,
|
||||
SAMPLE_DATA
|
||||
}
|
||||
|
||||
enum DQ_TestCaseResultKeys {
|
||||
STATUS,
|
||||
PARAMETER_VALUE,
|
||||
RESULT_MESSAGE
|
||||
}
|
||||
|
||||
enum DQ_TestDefinitionKeys {
|
||||
TEST_DEFINITION_NAME,
|
||||
TEST_DEFINITION_DESCRIPTION
|
||||
}
|
||||
|
||||
static Map<DQ_Template_Section, Map<Enum<?>, Object>> buildDQTemplateData(
|
||||
ChangeEvent event, OutgoingMessage outgoingMessage) {
|
||||
|
||||
TemplateDataBuilder<DQ_Template_Section> builder = new TemplateDataBuilder<>();
|
||||
builder
|
||||
.add(
|
||||
DQ_Template_Section.EVENT_DETAILS,
|
||||
EventDetailsKeys.EVENT_TYPE,
|
||||
event.getEventType().value())
|
||||
.add(DQ_Template_Section.EVENT_DETAILS, EventDetailsKeys.UPDATED_BY, event.getUserName())
|
||||
.add(DQ_Template_Section.EVENT_DETAILS, EventDetailsKeys.ENTITY_TYPE, event.getEntityType())
|
||||
.add(
|
||||
DQ_Template_Section.EVENT_DETAILS,
|
||||
EventDetailsKeys.ENTITY_FQN,
|
||||
getFQNForChangeEventEntity(event))
|
||||
.add(
|
||||
DQ_Template_Section.EVENT_DETAILS,
|
||||
EventDetailsKeys.TIME,
|
||||
new Date(event.getTimestamp()).toString())
|
||||
.add(DQ_Template_Section.EVENT_DETAILS, EventDetailsKeys.OUTGOING_MESSAGE, outgoingMessage);
|
||||
|
||||
// fetch TEST_CASE_DETAILS
|
||||
TestCase testCase = fetchTestCase(getFQNForChangeEventEntity(event));
|
||||
|
||||
// build TEST_CASE_DETAILS
|
||||
builder
|
||||
.add(DQ_Template_Section.TEST_CASE_DETAILS, DQ_TestCaseDetailsKeys.ID, testCase.getId())
|
||||
.add(DQ_Template_Section.TEST_CASE_DETAILS, DQ_TestCaseDetailsKeys.NAME, testCase.getName())
|
||||
.add(
|
||||
DQ_Template_Section.TEST_CASE_DETAILS,
|
||||
DQ_TestCaseDetailsKeys.OWNERS,
|
||||
testCase.getOwners())
|
||||
.add(DQ_Template_Section.TEST_CASE_DETAILS, DQ_TestCaseDetailsKeys.TAGS, testCase.getTags())
|
||||
.add(
|
||||
DQ_Template_Section.TEST_CASE_DETAILS,
|
||||
DQ_TestCaseDetailsKeys.DESCRIPTION,
|
||||
testCase.getTestDefinition().getDescription())
|
||||
.add(
|
||||
DQ_Template_Section.TEST_CASE_DETAILS,
|
||||
DQ_TestCaseDetailsKeys.TEST_CASE_FQN,
|
||||
testCase.getFullyQualifiedName())
|
||||
.add(
|
||||
DQ_Template_Section.TEST_CASE_DETAILS,
|
||||
DQ_TestCaseDetailsKeys.INSPECTION_QUERY,
|
||||
testCase.getInspectionQuery())
|
||||
.add(
|
||||
DQ_Template_Section.TEST_CASE_DETAILS,
|
||||
DQ_TestCaseDetailsKeys.SAMPLE_DATA,
|
||||
testCase.getTestCaseResult().getSampleData());
|
||||
|
||||
// build TEST_CASE_RESULT
|
||||
builder
|
||||
.add(
|
||||
DQ_Template_Section.TEST_CASE_RESULT,
|
||||
DQ_TestCaseResultKeys.STATUS,
|
||||
testCase.getTestCaseStatus())
|
||||
.add(
|
||||
DQ_Template_Section.TEST_CASE_RESULT,
|
||||
DQ_TestCaseResultKeys.PARAMETER_VALUE,
|
||||
testCase.getParameterValues())
|
||||
.add(
|
||||
DQ_Template_Section.TEST_CASE_RESULT,
|
||||
DQ_TestCaseResultKeys.RESULT_MESSAGE,
|
||||
testCase.getTestCaseResult().getResult());
|
||||
|
||||
// build TEST_DEFINITION
|
||||
builder
|
||||
.add(
|
||||
DQ_Template_Section.TEST_DEFINITION,
|
||||
DQ_TestDefinitionKeys.TEST_DEFINITION_NAME,
|
||||
testCase.getTestDefinition().getName())
|
||||
.add(
|
||||
DQ_Template_Section.TEST_DEFINITION,
|
||||
DQ_TestDefinitionKeys.TEST_DEFINITION_DESCRIPTION,
|
||||
testCase.getTestDefinition().getDescription());
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
static TestCase fetchTestCase(String fqn) {
|
||||
TestCaseRepository testCaseRepository =
|
||||
(TestCaseRepository) Entity.getEntityRepository(Entity.TEST_CASE);
|
||||
EntityUtil.Fields fields = testCaseRepository.getFields("*");
|
||||
return testCaseRepository.getByName(null, fqn, fields, Include.NON_DELETED, false);
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,10 +16,27 @@ package org.openmetadata.service.formatter.decorators;
|
||||
import static org.openmetadata.common.utils.CommonUtil.nullOrEmpty;
|
||||
import static org.openmetadata.service.util.email.EmailUtil.getSmtpSettings;
|
||||
|
||||
import com.slack.api.model.block.Blocks;
|
||||
import com.slack.api.model.block.LayoutBlock;
|
||||
import com.slack.api.model.block.composition.BlockCompositions;
|
||||
import com.slack.api.model.block.composition.PlainTextObject;
|
||||
import com.slack.api.model.block.composition.TextObject;
|
||||
import com.slack.api.model.block.element.ImageElement;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import org.openmetadata.common.utils.CommonUtil;
|
||||
import org.openmetadata.schema.tests.TestCaseParameterValue;
|
||||
import org.openmetadata.schema.tests.type.TestCaseStatus;
|
||||
import org.openmetadata.schema.type.ChangeEvent;
|
||||
import org.openmetadata.service.apps.bundles.changeEvent.slack.SlackAttachment;
|
||||
import org.openmetadata.schema.type.EntityReference;
|
||||
import org.openmetadata.schema.type.FieldChange;
|
||||
import org.openmetadata.schema.type.TagLabel;
|
||||
import org.openmetadata.service.Entity;
|
||||
import org.openmetadata.service.apps.bundles.changeEvent.slack.SlackMessage;
|
||||
import org.openmetadata.service.exception.UnhandledServerException;
|
||||
|
||||
@ -30,6 +47,11 @@ public class SlackMessageDecorator implements MessageDecorator<SlackMessage> {
|
||||
return "*%s*";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBoldWithSpace() {
|
||||
return "*%s* ";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLineBreak() {
|
||||
return "\n";
|
||||
@ -67,62 +89,715 @@ public class SlackMessageDecorator implements MessageDecorator<SlackMessage> {
|
||||
|
||||
@Override
|
||||
public SlackMessage buildEntityMessage(String publisherName, ChangeEvent event) {
|
||||
return getSlackMessage(createEntityMessage(publisherName, event));
|
||||
}
|
||||
|
||||
@Override
|
||||
public SlackMessage buildTestMessage(String publisherName) {
|
||||
return getSlackTestMessage(publisherName);
|
||||
return getSlackMessage(event, createEntityMessage(publisherName, event));
|
||||
}
|
||||
|
||||
@Override
|
||||
public SlackMessage buildThreadMessage(String publisherName, ChangeEvent event) {
|
||||
return getSlackMessage(createThreadMessage(publisherName, event));
|
||||
return getSlackMessage(event, createThreadMessage(publisherName, event));
|
||||
}
|
||||
|
||||
private SlackMessage getSlackMessage(OutgoingMessage outgoingMessage) {
|
||||
if (!outgoingMessage.getMessages().isEmpty()) {
|
||||
SlackMessage message = new SlackMessage();
|
||||
List<SlackAttachment> attachmentList = new ArrayList<>();
|
||||
outgoingMessage.getMessages().forEach(m -> attachmentList.add(getSlackAttachment(m)));
|
||||
message.setUsername(outgoingMessage.getUserName());
|
||||
message.setText(outgoingMessage.getHeader());
|
||||
message.setAttachments(attachmentList.toArray(new SlackAttachment[0]));
|
||||
return message;
|
||||
@Override
|
||||
public SlackMessage buildTestMessage(String publisherName) {
|
||||
return createConnectionTestMessage(publisherName);
|
||||
}
|
||||
|
||||
private SlackMessage getSlackMessage(ChangeEvent event, OutgoingMessage outgoingMessage) {
|
||||
if (outgoingMessage.getMessages().isEmpty()) {
|
||||
throw new UnhandledServerException("No messages found for the event");
|
||||
}
|
||||
throw new UnhandledServerException("No messages found for the event");
|
||||
|
||||
return createMessage(event, outgoingMessage);
|
||||
}
|
||||
|
||||
private SlackMessage getSlackTestMessage(String publisherName) {
|
||||
if (!publisherName.isEmpty()) {
|
||||
SlackMessage message = new SlackMessage();
|
||||
message.setUsername("Slack destination test");
|
||||
message.setText("Slack has been successfully configured for alerts from: " + publisherName);
|
||||
private SlackMessage createMessage(ChangeEvent event, OutgoingMessage outgoingMessage) {
|
||||
return switch (event.getEntityType()) {
|
||||
case Entity.TEST_CASE -> createTestCaseMessage(event, outgoingMessage);
|
||||
default -> createGeneralChangeEventMessage(event, outgoingMessage);
|
||||
};
|
||||
}
|
||||
|
||||
SlackAttachment attachment = new SlackAttachment();
|
||||
attachment.setFallback("Slack destination test successful.");
|
||||
attachment.setColor("#36a64f"); // Setting a green color to indicate success
|
||||
attachment.setTitle("Test Successful");
|
||||
attachment.setText(
|
||||
"This is a test message from OpenMetadata confirming that your Slack destination is correctly set up to receive alerts.");
|
||||
attachment.setFooter("OpenMetadata");
|
||||
attachment.setTs(String.valueOf(System.currentTimeMillis() / 1000)); // Adding timestamp
|
||||
private SlackMessage createTestCaseMessage(ChangeEvent event, OutgoingMessage outgoingMessage) {
|
||||
final String testCaseResult = "testCaseResult";
|
||||
List<FieldChange> fieldsAdded = event.getChangeDescription().getFieldsAdded();
|
||||
List<FieldChange> fieldsUpdated = event.getChangeDescription().getFieldsUpdated();
|
||||
|
||||
List<SlackAttachment> attachmentList = new ArrayList<>();
|
||||
attachmentList.add(attachment);
|
||||
message.setAttachments(attachmentList.toArray(new SlackAttachment[0]));
|
||||
boolean hasRelevantChange =
|
||||
fieldsAdded.stream().anyMatch(field -> testCaseResult.equals(field.getName()))
|
||||
|| fieldsUpdated.stream().anyMatch(field -> testCaseResult.equals(field.getName()));
|
||||
|
||||
return message;
|
||||
return hasRelevantChange
|
||||
? createDQTemplateMessage(event, outgoingMessage)
|
||||
: createGeneralChangeEventMessage(event, outgoingMessage);
|
||||
}
|
||||
|
||||
public SlackMessage createConnectionTestMessage(String publisherName) {
|
||||
if (publisherName.isEmpty()) {
|
||||
throw new UnhandledServerException("Publisher name not found.");
|
||||
}
|
||||
throw new UnhandledServerException("Publisher name not found.");
|
||||
|
||||
List<LayoutBlock> blocks = new ArrayList<>();
|
||||
|
||||
// Header Block
|
||||
blocks.add(
|
||||
Blocks.header(
|
||||
header ->
|
||||
header.text(
|
||||
PlainTextObject.builder()
|
||||
.text("Connection Successful :white_check_mark: ")
|
||||
.build())));
|
||||
|
||||
// Section Block 1 (Publisher Name)
|
||||
blocks.add(
|
||||
Blocks.section(
|
||||
section ->
|
||||
section.text(
|
||||
BlockCompositions.markdownText(
|
||||
applyBoldFormatWithSpace("Publisher :") + publisherName))));
|
||||
|
||||
// Section Block 2 (Test Message)
|
||||
blocks.add(
|
||||
Blocks.section(
|
||||
section -> section.text(BlockCompositions.markdownText(CONNECTION_TEST_DESCRIPTION))));
|
||||
|
||||
// Divider Block
|
||||
blocks.add(Blocks.divider());
|
||||
|
||||
// context
|
||||
blocks.add(
|
||||
Blocks.context(
|
||||
context ->
|
||||
context.elements(
|
||||
List.of(
|
||||
ImageElement.builder().imageUrl(getOMImage()).altText("oss icon").build(),
|
||||
BlockCompositions.markdownText(applyBoldFormat("OpenMetadata"))))));
|
||||
|
||||
SlackMessage.Attachment attachment = new SlackMessage.Attachment();
|
||||
attachment.setColor("#36a64f"); // green
|
||||
attachment.setBlocks(blocks);
|
||||
|
||||
SlackMessage message = new SlackMessage();
|
||||
message.setAttachments(Collections.singletonList(attachment));
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
private SlackAttachment getSlackAttachment(String message) {
|
||||
SlackAttachment attachment = new SlackAttachment();
|
||||
List<String> mark = new ArrayList<>();
|
||||
mark.add("text");
|
||||
attachment.setMarkdownIn(mark);
|
||||
attachment.setText(message);
|
||||
private SlackMessage createGeneralChangeEventMessage(
|
||||
ChangeEvent event, OutgoingMessage outgoingMessage) {
|
||||
List<LayoutBlock> generalChangeEventBody = createGeneralChangeEventBody(event, outgoingMessage);
|
||||
SlackMessage message = new SlackMessage();
|
||||
message.setBlocks(generalChangeEventBody);
|
||||
return message;
|
||||
}
|
||||
|
||||
private List<LayoutBlock> createGeneralChangeEventBody(
|
||||
ChangeEvent event, OutgoingMessage outgoingMessage) {
|
||||
List<LayoutBlock> blocks = new ArrayList<>();
|
||||
|
||||
// Header
|
||||
addChangeEventDetailsHeader(blocks);
|
||||
|
||||
// Info about the event
|
||||
List<TextObject> first_field = new ArrayList<>();
|
||||
first_field.add(
|
||||
BlockCompositions.markdownText(
|
||||
applyBoldFormatWithSpace("Event Type:") + event.getEventType()));
|
||||
first_field.add(
|
||||
BlockCompositions.markdownText(
|
||||
applyBoldFormatWithSpace("Updated By:") + event.getUserName()));
|
||||
first_field.add(
|
||||
BlockCompositions.markdownText(
|
||||
applyBoldFormatWithSpace("Entity Type:") + event.getEntityType()));
|
||||
first_field.add(
|
||||
BlockCompositions.markdownText(
|
||||
applyBoldFormatWithSpace("Time:") + new Date(event.getTimestamp())));
|
||||
|
||||
// Split fields into multiple sections to avoid block limits
|
||||
for (int i = 0; i < first_field.size(); i += 10) {
|
||||
List<TextObject> sublist = first_field.subList(i, Math.min(i + 10, first_field.size()));
|
||||
blocks.add(Blocks.section(section -> section.fields(sublist)));
|
||||
}
|
||||
|
||||
String fqnForChangeEventEntity = MessageDecorator.getFQNForChangeEventEntity(event);
|
||||
|
||||
blocks.add(
|
||||
Blocks.section(
|
||||
section ->
|
||||
section.text(
|
||||
BlockCompositions.markdownText(
|
||||
applyBoldFormatWithSpace("FQN:") + fqnForChangeEventEntity))));
|
||||
|
||||
// divider
|
||||
blocks.add(Blocks.divider());
|
||||
|
||||
// desc about the event
|
||||
List<String> thread_messages = outgoingMessage.getMessages();
|
||||
thread_messages.forEach(
|
||||
(message) -> {
|
||||
blocks.add(
|
||||
Blocks.section(
|
||||
section -> section.text(BlockCompositions.markdownText("> " + message))));
|
||||
});
|
||||
|
||||
// Divider
|
||||
blocks.add(Blocks.divider());
|
||||
|
||||
// View event link
|
||||
String entityUrl = buildClickableEntityUrl(outgoingMessage.getEntityUrl());
|
||||
|
||||
blocks.add(Blocks.section(section -> section.text(BlockCompositions.markdownText(entityUrl))));
|
||||
|
||||
// Context Block
|
||||
blocks.add(
|
||||
Blocks.context(
|
||||
context ->
|
||||
context.elements(
|
||||
List.of(
|
||||
ImageElement.builder().imageUrl(getOMImage()).altText("oss icon").build(),
|
||||
BlockCompositions.markdownText(TEMPLATE_FOOTER)))));
|
||||
return blocks;
|
||||
}
|
||||
|
||||
private void createDQHeading(
|
||||
List<LayoutBlock> blocks, Map<DQ_Template_Section, Map<Enum<?>, Object>> templateData) {
|
||||
Map<Enum<?>, Object> testCaseResults = templateData.get(DQ_Template_Section.TEST_CASE_RESULT);
|
||||
|
||||
if (nullOrEmpty(testCaseResults)) {
|
||||
addChangeEventDetailsHeader(blocks);
|
||||
} else {
|
||||
String statusWithEmoji =
|
||||
getStatusWithEmoji(testCaseResults.get(DQ_TestCaseResultKeys.STATUS));
|
||||
Map<Enum<?>, Object> testCaseDetails =
|
||||
templateData.get(DQ_Template_Section.TEST_CASE_DETAILS);
|
||||
String testName = String.valueOf(testCaseDetails.get(DQ_TestCaseDetailsKeys.NAME));
|
||||
String message = String.format("\"%s\" test having status: %s", testName, statusWithEmoji);
|
||||
blocks.add(Blocks.header(header -> header.text(BlockCompositions.plainText(message))));
|
||||
}
|
||||
}
|
||||
|
||||
private List<LayoutBlock> createDQBodyBlocks(
|
||||
ChangeEvent event,
|
||||
OutgoingMessage outgoingMessage,
|
||||
Map<DQ_Template_Section, Map<Enum<?>, Object>> data) {
|
||||
List<LayoutBlock> blocks = new ArrayList<>();
|
||||
|
||||
// Header
|
||||
createDQHeading(blocks, data);
|
||||
|
||||
// Info about the event
|
||||
List<TextObject> first_field = new ArrayList<>();
|
||||
first_field.add(
|
||||
BlockCompositions.markdownText(
|
||||
applyBoldFormatWithSpace("Event Type:") + event.getEventType()));
|
||||
first_field.add(
|
||||
BlockCompositions.markdownText(
|
||||
applyBoldFormatWithSpace("Updated By:") + event.getUserName()));
|
||||
first_field.add(
|
||||
BlockCompositions.markdownText(
|
||||
applyBoldFormatWithSpace("Entity Type:") + event.getEntityType()));
|
||||
first_field.add(
|
||||
BlockCompositions.markdownText(
|
||||
applyBoldFormatWithSpace("Time:") + new Date(event.getTimestamp())));
|
||||
|
||||
// Split fields into multiple sections to avoid block limits
|
||||
for (int i = 0; i < first_field.size(); i += 10) {
|
||||
List<TextObject> sublist = first_field.subList(i, Math.min(i + 10, first_field.size()));
|
||||
blocks.add(Blocks.section(section -> section.fields(sublist)));
|
||||
}
|
||||
|
||||
String fqnForChangeEventEntity = MessageDecorator.getFQNForChangeEventEntity(event);
|
||||
|
||||
blocks.add(
|
||||
Blocks.section(
|
||||
section ->
|
||||
section.text(
|
||||
BlockCompositions.markdownText(
|
||||
applyBoldFormatWithSpace("FQN:") + fqnForChangeEventEntity))));
|
||||
|
||||
// divider
|
||||
blocks.add(Blocks.divider());
|
||||
|
||||
// desc about the event
|
||||
List<String> thread_messages = outgoingMessage.getMessages();
|
||||
thread_messages.forEach(
|
||||
(message) -> {
|
||||
blocks.add(
|
||||
Blocks.section(
|
||||
section -> section.text(BlockCompositions.markdownText("> " + message))));
|
||||
});
|
||||
|
||||
// Divider
|
||||
blocks.add(Blocks.divider());
|
||||
|
||||
// View event link
|
||||
String entityUrl = buildClickableEntityUrl(outgoingMessage.getEntityUrl());
|
||||
|
||||
blocks.add(Blocks.section(section -> section.text(BlockCompositions.markdownText(entityUrl))));
|
||||
|
||||
// Context Block
|
||||
blocks.add(
|
||||
Blocks.context(
|
||||
context ->
|
||||
context.elements(
|
||||
List.of(
|
||||
ImageElement.builder().imageUrl(getOMImage()).altText("oss icon").build(),
|
||||
BlockCompositions.markdownText(TEMPLATE_FOOTER)))));
|
||||
return blocks;
|
||||
}
|
||||
|
||||
// DQ TEMPLATE
|
||||
public SlackMessage createDQTemplateMessage(ChangeEvent event, OutgoingMessage outgoingMessage) {
|
||||
|
||||
Map<DQ_Template_Section, Map<Enum<?>, Object>> dqTemplateData =
|
||||
MessageDecorator.buildDQTemplateData(event, outgoingMessage);
|
||||
|
||||
List<LayoutBlock> body = createDQBodyBlocks(event, outgoingMessage, dqTemplateData);
|
||||
|
||||
SlackMessage message = new SlackMessage();
|
||||
message.setBlocks(body);
|
||||
|
||||
Map<Enum<?>, Object> enumObjectMap = dqTemplateData.get(DQ_Template_Section.TEST_CASE_RESULT);
|
||||
if (!nullOrEmpty(enumObjectMap)) {
|
||||
SlackMessage.Attachment attachment = createDQAttachment(dqTemplateData);
|
||||
|
||||
attachment.setColor("#ffcc00");
|
||||
|
||||
message.setAttachments(Collections.singletonList(attachment));
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
private String determineColorBasedOnStatus(Object object) {
|
||||
if (object instanceof TestCaseStatus status) {
|
||||
return switch (status) {
|
||||
case Success -> "#36a64f"; // Green for success
|
||||
case Failed -> "#ff0000"; // Red for failure
|
||||
case Aborted -> "#ffcc00"; // Yellow for aborted
|
||||
case Queued -> "#439FE0"; // Blue for queued
|
||||
default -> "#808080"; // Gray for unknown or default cases
|
||||
};
|
||||
}
|
||||
return "#808080"; // Default to gray if the object is not a valid TestCaseStatus
|
||||
}
|
||||
|
||||
private String getStatusWithEmoji(Object object) {
|
||||
if (object instanceof TestCaseStatus status) {
|
||||
return switch (status) {
|
||||
case Success -> "Success :white_check_mark:"; // Green checkmark for success
|
||||
case Failed -> "Failed :x:"; // Red cross for failure
|
||||
case Aborted -> "Aborted :warning:"; // Warning sign for aborted
|
||||
case Queued -> "Queued :hourglass_flowing_sand:"; // Hourglass for queued
|
||||
default -> "Unknown :grey_question:"; // Gray question mark for unknown cases
|
||||
};
|
||||
}
|
||||
return "Unknown :grey_question:"; // Default to unknown if the object is not a valid
|
||||
// TestCaseStatus
|
||||
}
|
||||
|
||||
public SlackMessage.Attachment createDQAttachment(
|
||||
Map<DQ_Template_Section, Map<Enum<?>, Object>> dqTemplateData) {
|
||||
List<LayoutBlock> blocks = new ArrayList<>();
|
||||
|
||||
// Header Block
|
||||
addDQAlertHeader(blocks);
|
||||
|
||||
// Section 1 - Name
|
||||
addIdAndNameSection(blocks, dqTemplateData);
|
||||
|
||||
// Section 2 - Owners and Tags
|
||||
addOwnersTagsSection(blocks, dqTemplateData);
|
||||
|
||||
// Section 3 - Description
|
||||
addDescriptionSection(blocks, dqTemplateData);
|
||||
|
||||
// Divider
|
||||
blocks.add(Blocks.divider());
|
||||
|
||||
// Section 4 and 5 - Result and Test Definition
|
||||
blocks.addAll(createTestCaseResultAndDefinitionSections(dqTemplateData));
|
||||
|
||||
// Context Block - Image and Markdown Text
|
||||
blocks.add(
|
||||
Blocks.context(
|
||||
context ->
|
||||
context.elements(
|
||||
List.of(
|
||||
ImageElement.builder().imageUrl(getOMImage()).altText("oss icon").build(),
|
||||
BlockCompositions.markdownText("Change Event by OpenMetadata")))));
|
||||
|
||||
SlackMessage.Attachment attachment = new SlackMessage.Attachment();
|
||||
attachment.setBlocks(blocks);
|
||||
|
||||
return attachment;
|
||||
}
|
||||
|
||||
// Updated Method to Create Both Sections
|
||||
private List<LayoutBlock> createTestCaseResultAndDefinitionSections(
|
||||
Map<DQ_Template_Section, Map<Enum<?>, Object>> templateData) {
|
||||
List<LayoutBlock> blocks = new ArrayList<>();
|
||||
|
||||
if (templateData.containsKey(DQ_Template_Section.TEST_CASE_RESULT)
|
||||
&& templateData.containsKey(DQ_Template_Section.TEST_DEFINITION)) {
|
||||
blocks.addAll(createTestCaseResultSections(templateData));
|
||||
blocks.addAll(createTestDefinitionSections(templateData));
|
||||
}
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
private void addIdAndNameSection(
|
||||
List<LayoutBlock> blocks, Map<DQ_Template_Section, Map<Enum<?>, Object>> templateData) {
|
||||
|
||||
Map<Enum<?>, Object> testCaseDetails = templateData.get(DQ_Template_Section.TEST_CASE_DETAILS);
|
||||
if (nullOrEmpty(testCaseDetails)) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<TextObject> idNameFields =
|
||||
Stream.of(
|
||||
createFieldText(
|
||||
"Name :", testCaseDetails.getOrDefault(DQ_TestCaseDetailsKeys.NAME, "-")))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
blocks.add(Blocks.section(section -> section.fields(idNameFields)));
|
||||
}
|
||||
|
||||
private void addDescriptionSection(
|
||||
List<LayoutBlock> blocks, Map<DQ_Template_Section, Map<Enum<?>, Object>> templateData) {
|
||||
|
||||
Map<Enum<?>, Object> testCaseDetails = templateData.get(DQ_Template_Section.TEST_CASE_DETAILS);
|
||||
if (nullOrEmpty(testCaseDetails)) {
|
||||
return;
|
||||
}
|
||||
|
||||
TextObject idNameFields =
|
||||
createFieldTextWithNewLine(
|
||||
"Description", testCaseDetails.getOrDefault(DQ_TestCaseDetailsKeys.DESCRIPTION, "-"));
|
||||
blocks.add(Blocks.section(section -> section.text(idNameFields)));
|
||||
}
|
||||
|
||||
private void addOwnersTagsSection(
|
||||
List<LayoutBlock> blocks, Map<DQ_Template_Section, Map<Enum<?>, Object>> templateData) {
|
||||
|
||||
Map<Enum<?>, Object> testCaseDetails = templateData.get(DQ_Template_Section.TEST_CASE_DETAILS);
|
||||
if (nullOrEmpty(testCaseDetails)) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<TextObject> ownerTagFields =
|
||||
Stream.of(
|
||||
createFieldTextWithNewLine("Owners", formatOwners(testCaseDetails)),
|
||||
createFieldTextWithNewLine("Tags", formatTags(testCaseDetails)))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
blocks.add(Blocks.section(section -> section.fields(ownerTagFields)));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private String formatOwners(Map<Enum<?>, Object> testCaseDetails) {
|
||||
List<EntityReference> owners =
|
||||
(List<EntityReference>)
|
||||
testCaseDetails.getOrDefault(DQ_TestCaseDetailsKeys.OWNERS, Collections.emptyList());
|
||||
|
||||
StringBuilder ownersStringified = new StringBuilder();
|
||||
if (!CommonUtil.nullOrEmpty(owners)) {
|
||||
owners.forEach(
|
||||
owner -> {
|
||||
if (owner != null && owner.getName() != null) {
|
||||
ownersStringified.append(owner.getName()).append(", ");
|
||||
}
|
||||
});
|
||||
|
||||
// Remove the trailing comma and space if there's content
|
||||
if (!ownersStringified.isEmpty()) {
|
||||
ownersStringified.setLength(ownersStringified.length() - 2);
|
||||
}
|
||||
} else {
|
||||
ownersStringified.append("-");
|
||||
}
|
||||
|
||||
return ownersStringified.toString();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private String formatTags(Map<Enum<?>, Object> testCaseDetails) {
|
||||
List<TagLabel> tags =
|
||||
(List<TagLabel>)
|
||||
testCaseDetails.getOrDefault(DQ_TestCaseDetailsKeys.TAGS, Collections.emptyList());
|
||||
|
||||
StringBuilder tagsStringified = new StringBuilder();
|
||||
if (!CommonUtil.nullOrEmpty(tags)) {
|
||||
tags.forEach(
|
||||
tag -> {
|
||||
if (tag != null && tag.getName() != null) {
|
||||
tagsStringified.append(tag.getName()).append(", ");
|
||||
}
|
||||
});
|
||||
|
||||
// Remove the trailing comma and space if there's content
|
||||
if (!tagsStringified.isEmpty()) {
|
||||
tagsStringified.setLength(tagsStringified.length() - 2);
|
||||
}
|
||||
} else {
|
||||
tagsStringified.append("-");
|
||||
}
|
||||
|
||||
return tagsStringified.toString();
|
||||
}
|
||||
|
||||
private List<LayoutBlock> createTestCaseResultSections(
|
||||
Map<DQ_Template_Section, Map<Enum<?>, Object>> templateData) {
|
||||
|
||||
List<LayoutBlock> blocks = new ArrayList<>();
|
||||
|
||||
Map<Enum<?>, Object> testCaseResults = templateData.get(DQ_Template_Section.TEST_CASE_RESULT);
|
||||
if (nullOrEmpty(testCaseResults)) {
|
||||
return blocks;
|
||||
}
|
||||
|
||||
// Test Case Result Header
|
||||
blocks.add(
|
||||
Blocks.section(
|
||||
section ->
|
||||
section.text(
|
||||
BlockCompositions.markdownText(applyBoldFormat(":mag: TEST CASE RESULT")))));
|
||||
|
||||
// Status and Parameter Value
|
||||
addStatusAndParameterValueSection(blocks, testCaseResults);
|
||||
|
||||
// Result Message Section
|
||||
blocks.add(
|
||||
Blocks.section(
|
||||
section -> section.text(BlockCompositions.markdownText(applyBoldFormat("Result")))));
|
||||
|
||||
blocks.add(
|
||||
Blocks.section(
|
||||
section ->
|
||||
section.text(
|
||||
BlockCompositions.markdownText(
|
||||
formatWithTripleBackticksForEnumMap(
|
||||
DQ_TestCaseResultKeys.RESULT_MESSAGE, testCaseResults)))));
|
||||
|
||||
// parameter section
|
||||
createParameterValueBlocks(templateData, blocks);
|
||||
|
||||
// inspection section
|
||||
addInspectionQuerySection(templateData, blocks);
|
||||
|
||||
blocks.add(Blocks.divider());
|
||||
return blocks;
|
||||
}
|
||||
|
||||
private void addStatusAndParameterValueSection(
|
||||
List<LayoutBlock> blocks, Map<Enum<?>, Object> testCaseResults) {
|
||||
List<TextObject> statusParameterFields =
|
||||
Stream.of(
|
||||
BlockCompositions.markdownText(
|
||||
applyBoldFormatWithSpace("Status -")
|
||||
+ getStatusWithEmoji(
|
||||
testCaseResults.getOrDefault(DQ_TestCaseResultKeys.STATUS, "-"))))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
blocks.add(Blocks.section(section -> section.fields(statusParameterFields)));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void createParameterValueBlocks(
|
||||
Map<DQ_Template_Section, Map<Enum<?>, Object>> templateData, List<LayoutBlock> blocks) {
|
||||
|
||||
Map<Enum<?>, Object> testCaseResults = templateData.get(DQ_Template_Section.TEST_CASE_RESULT);
|
||||
if (nullOrEmpty(testCaseResults)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object result = testCaseResults.get(DQ_TestCaseResultKeys.PARAMETER_VALUE);
|
||||
List<TestCaseParameterValue> parameterValues =
|
||||
result instanceof List<?> ? (List<TestCaseParameterValue>) result : null;
|
||||
|
||||
if (nullOrEmpty(parameterValues)) {
|
||||
return;
|
||||
}
|
||||
|
||||
blocks.add(
|
||||
Blocks.section(
|
||||
section ->
|
||||
section.text(BlockCompositions.markdownText(applyBoldFormat("Parameter Value")))));
|
||||
|
||||
String parameterValuesText =
|
||||
parameterValues.stream()
|
||||
.map(pv -> String.format("[%s: %s]", pv.getName(), pv.getValue()))
|
||||
.collect(Collectors.joining(", "));
|
||||
|
||||
blocks.add(
|
||||
Blocks.section(
|
||||
section ->
|
||||
section.text(
|
||||
BlockCompositions.markdownText(
|
||||
formatWithTripleBackticks(parameterValuesText)))));
|
||||
}
|
||||
|
||||
private void addInspectionQuerySection(
|
||||
Map<DQ_Template_Section, Map<Enum<?>, Object>> templateData, List<LayoutBlock> blocks) {
|
||||
|
||||
Map<Enum<?>, Object> testCaseDetails = templateData.get(DQ_Template_Section.TEST_CASE_DETAILS);
|
||||
|
||||
if (nullOrEmpty(testCaseDetails)
|
||||
|| !testCaseDetails.containsKey(DQ_TestCaseDetailsKeys.INSPECTION_QUERY)) {
|
||||
return;
|
||||
}
|
||||
|
||||
blocks.add(
|
||||
Blocks.section(
|
||||
section ->
|
||||
section.text(
|
||||
BlockCompositions.markdownText(
|
||||
applyBoldFormat(":hammer_and_wrench: Inspection Query")))));
|
||||
|
||||
blocks.add(
|
||||
Blocks.section(
|
||||
section ->
|
||||
section.text(
|
||||
BlockCompositions.markdownText(
|
||||
formatWithTripleBackticksForEnumMap(
|
||||
DQ_TestCaseDetailsKeys.INSPECTION_QUERY, testCaseDetails)))));
|
||||
}
|
||||
|
||||
// Method to create Test Definition Sections
|
||||
private List<LayoutBlock> createTestDefinitionSections(
|
||||
Map<DQ_Template_Section, Map<Enum<?>, Object>> templateData) {
|
||||
|
||||
List<LayoutBlock> blocks = new ArrayList<>();
|
||||
|
||||
if (templateData.containsKey(DQ_Template_Section.TEST_DEFINITION)) {
|
||||
Map<Enum<?>, Object> testDefinition = templateData.get(DQ_Template_Section.TEST_DEFINITION);
|
||||
|
||||
if (!nullOrEmpty(testDefinition)) {
|
||||
|
||||
// Test Definition Header
|
||||
blocks.add(
|
||||
Blocks.section(
|
||||
section ->
|
||||
section.text(
|
||||
BlockCompositions.markdownText(
|
||||
applyBoldFormat(":bulb: TEST DEFINITION")))));
|
||||
blocks.add(
|
||||
Blocks.section(
|
||||
section -> section.text(BlockCompositions.markdownText(applyBoldFormat("Name")))));
|
||||
blocks.add(
|
||||
Blocks.section(
|
||||
section ->
|
||||
section.text(
|
||||
BlockCompositions.markdownText(
|
||||
formatWithTripleBackticksForEnumMap(
|
||||
DQ_TestDefinitionKeys.TEST_DEFINITION_NAME, testDefinition)))));
|
||||
|
||||
// Section - Description with triple backticks
|
||||
blocks.add(
|
||||
Blocks.section(
|
||||
section ->
|
||||
section.text(BlockCompositions.markdownText(applyBoldFormat("Description")))));
|
||||
blocks.add(
|
||||
Blocks.section(
|
||||
section ->
|
||||
section.text(
|
||||
BlockCompositions.markdownText(
|
||||
formatWithTripleBackticksForEnumMap(
|
||||
DQ_TestDefinitionKeys.TEST_DEFINITION_DESCRIPTION,
|
||||
testDefinition)))));
|
||||
|
||||
addSampleDataSection(templateData, blocks);
|
||||
|
||||
blocks.add(Blocks.divider());
|
||||
}
|
||||
}
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
private void addSampleDataSection(
|
||||
Map<DQ_Template_Section, Map<Enum<?>, Object>> templateData, List<LayoutBlock> blocks) {
|
||||
|
||||
if (templateData.containsKey(DQ_Template_Section.TEST_CASE_DETAILS)) {
|
||||
Map<Enum<?>, Object> testCaseDetails =
|
||||
templateData.get(DQ_Template_Section.TEST_CASE_DETAILS);
|
||||
|
||||
if (!nullOrEmpty(testCaseDetails)) {
|
||||
blocks.add(
|
||||
Blocks.section(
|
||||
section ->
|
||||
section.text(BlockCompositions.markdownText(applyBoldFormat("Sample Data")))));
|
||||
|
||||
blocks.add(
|
||||
Blocks.section(
|
||||
section ->
|
||||
section.text(
|
||||
BlockCompositions.markdownText(
|
||||
formatWithTripleBackticksForEnumMap(
|
||||
DQ_TestCaseDetailsKeys.SAMPLE_DATA, testCaseDetails)))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String buildClickableEntityUrl(String entityUrl) {
|
||||
if (entityUrl.startsWith("<") && entityUrl.endsWith(">")) {
|
||||
entityUrl = entityUrl.substring(1, entityUrl.length() - 1);
|
||||
}
|
||||
|
||||
int pipeIndex = entityUrl.indexOf("|");
|
||||
if (pipeIndex != -1) {
|
||||
entityUrl = entityUrl.substring(0, pipeIndex);
|
||||
}
|
||||
|
||||
return String.format("Access data: <%s|View>", entityUrl);
|
||||
}
|
||||
|
||||
private TextObject createFieldTextWithNewLine(String label, Object value) {
|
||||
return BlockCompositions.markdownText(applyBoldFormatWithNewLine(label) + value);
|
||||
}
|
||||
|
||||
private TextObject createFieldText(String label, Object value) {
|
||||
return BlockCompositions.markdownText(applyBoldFormatWithSpace(label) + value);
|
||||
}
|
||||
|
||||
private void addChangeEventDetailsHeader(List<LayoutBlock> blocks) {
|
||||
blocks.add(
|
||||
Blocks.header(
|
||||
header ->
|
||||
header.text(
|
||||
BlockCompositions.plainText(
|
||||
":arrows_counterclockwise: Change Event Details"))));
|
||||
}
|
||||
|
||||
private void addDQAlertHeader(List<LayoutBlock> blocks) {
|
||||
blocks.add(
|
||||
Blocks.section(
|
||||
section -> section.text(BlockCompositions.markdownText(applyBoldFormat("TEST CASE")))));
|
||||
}
|
||||
|
||||
private String applyBoldFormat(String title) {
|
||||
return String.format(getBold(), title);
|
||||
}
|
||||
|
||||
private String applyBoldFormatWithSpace(String title) {
|
||||
return String.format(getBoldWithSpace(), title);
|
||||
}
|
||||
|
||||
private String applyBoldFormatWithNewLine(String title) {
|
||||
return applyBoldFormat(title) + "\n";
|
||||
}
|
||||
|
||||
private String formatWithTripleBackticksForEnumMap(
|
||||
Enum<?> key, Map<Enum<?>, Object> placeholders) {
|
||||
Object value = placeholders.getOrDefault(key, "-");
|
||||
return "```" + value + "```";
|
||||
}
|
||||
|
||||
private String formatWithTripleBackticks(String text) {
|
||||
return "```" + text + "```";
|
||||
}
|
||||
|
||||
private String getOMImage() {
|
||||
return "https://i.postimg.cc/0jYLNmM1/image.png";
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,6 +15,7 @@ package org.openmetadata.service.util;
|
||||
|
||||
import static org.openmetadata.service.util.RestUtil.DATE_TIME_FORMAT;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.StreamReadFeature;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
@ -118,6 +119,20 @@ public final class JsonUtils {
|
||||
}
|
||||
}
|
||||
|
||||
public static String pojoToJsonIgnoreNull(Object o) {
|
||||
if (o == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
ObjectMapper objectMapperIgnoreNull = OBJECT_MAPPER.copy();
|
||||
objectMapperIgnoreNull.setSerializationInclusion(
|
||||
JsonInclude.Include.NON_NULL); // Ignore null values
|
||||
return objectMapperIgnoreNull.writeValueAsString(o);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new UnhandledServerException(FAILED_TO_PROCESS_JSON, e);
|
||||
}
|
||||
}
|
||||
|
||||
public static JsonStructure getJsonStructure(Object o) {
|
||||
return OBJECT_MAPPER.convertValue(o, JsonStructure.class);
|
||||
}
|
||||
|
||||
@ -2,10 +2,8 @@ package org.openmetadata.service.resources.events;
|
||||
|
||||
import static javax.ws.rs.core.Response.Status.CONFLICT;
|
||||
import static javax.ws.rs.core.Response.Status.NOT_FOUND;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.hibernate.validator.internal.util.Contracts.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.openmetadata.schema.entity.events.SubscriptionStatus.Status.ACTIVE;
|
||||
@ -20,6 +18,8 @@ import static org.openmetadata.service.util.TestUtils.assertResponse;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
@ -599,11 +599,15 @@ public class EventSubscriptionResourceTest
|
||||
String.format("eventsubscription instance for %s not found", wrongAlertId));
|
||||
}
|
||||
|
||||
public static String sanitizeWebhookName(String name) {
|
||||
return URLEncoder.encode(name, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void post_createAndValidateEventSubscription_SLACK(TestInfo test) throws IOException {
|
||||
String webhookName = getEntityName(test);
|
||||
String endpoint = test.getDisplayName();
|
||||
String uri = "http://localhost:" + APP.getLocalPort() + "/api/v1/test/slack/" + endpoint;
|
||||
String entityName = getEntityName(test);
|
||||
String webhookName = sanitizeWebhookName(entityName);
|
||||
String uri = "http://localhost:" + APP.getLocalPort() + "/api/v1/test/slack/" + webhookName;
|
||||
|
||||
CreateEventSubscription enabledWebhookRequest =
|
||||
new CreateEventSubscription()
|
||||
@ -618,12 +622,9 @@ public class EventSubscriptionResourceTest
|
||||
|
||||
EventSubscription alert = createEntity(enabledWebhookRequest, ADMIN_AUTH_HEADERS);
|
||||
waitForAllEventToComplete(alert.getId());
|
||||
SlackCallbackResource.EventDetails details = slackCallbackResource.getEventDetails(endpoint);
|
||||
ConcurrentLinkedQueue<SlackMessage> events = details.getEvents();
|
||||
for (SlackMessage event : events) {
|
||||
validateSlackMessage(alert, event);
|
||||
}
|
||||
|
||||
SlackCallbackResource.EventDetails details = slackCallbackResource.getEventDetails(entityName);
|
||||
ConcurrentLinkedQueue<String> events = details.getEvents();
|
||||
assertNotNull(events);
|
||||
assertNotNull(alert, "Webhook creation failed");
|
||||
|
||||
Awaitility.await()
|
||||
@ -1223,20 +1224,19 @@ public class EventSubscriptionResourceTest
|
||||
|
||||
@Test
|
||||
void post_ingestionPiplelineResource_noFilter_alertAction(TestInfo test) throws IOException {
|
||||
String webhookName = getEntityName(test);
|
||||
String endpoint = test.getDisplayName();
|
||||
String uri = "http://localhost:" + APP.getLocalPort() + "/api/v1/test/slack/" + endpoint;
|
||||
String entityName = getEntityName(test);
|
||||
String webhookName = sanitizeWebhookName(entityName);
|
||||
String uri = "http://localhost:" + APP.getLocalPort() + "/api/v1/test/slack/" + webhookName;
|
||||
|
||||
CreateEventSubscription genericWebhookActionRequest =
|
||||
createRequest(webhookName)
|
||||
createRequest(entityName)
|
||||
.withDestinations(getSlackWebhook(uri))
|
||||
.withResources(List.of("ingestionPipeline"));
|
||||
|
||||
EventSubscription alert = createAndCheckEntity(genericWebhookActionRequest, ADMIN_AUTH_HEADERS);
|
||||
|
||||
SubscriptionStatus status = getStatus(alert.getId(), Response.Status.OK.getStatusCode());
|
||||
assertEquals(ACTIVE, status.getStatus());
|
||||
SlackCallbackResource.EventDetails details = slackCallbackResource.getEventDetails(endpoint);
|
||||
SlackCallbackResource.EventDetails details = slackCallbackResource.getEventDetails(entityName);
|
||||
|
||||
// Alerts are triggered only by ChangeEvent occurrences related to resources as
|
||||
// ingestionPipeline by domain filter
|
||||
@ -1260,16 +1260,16 @@ public class EventSubscriptionResourceTest
|
||||
|
||||
ingestionPipelineResourceTest.createEntity(request, ADMIN_AUTH_HEADERS);
|
||||
|
||||
details = waitForFirstSlackEvent(alert.getId(), endpoint, 25);
|
||||
details = waitForFirstSlackEvent(alert.getId(), entityName, 25);
|
||||
assertEquals(1, details.getEvents().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void post_ingestionPiplelineResource_owner_alertAction(TestInfo test) throws IOException {
|
||||
String webhookName = getEntityName(test);
|
||||
String endpoint = test.getDisplayName();
|
||||
String entityName = getEntityName(test);
|
||||
String webhookName = sanitizeWebhookName(entityName);
|
||||
LOG.info("creating webhook in disabled state");
|
||||
String uri = "http://localhost:" + APP.getLocalPort() + "/api/v1/test/slack/" + endpoint;
|
||||
String uri = "http://localhost:" + APP.getLocalPort() + "/api/v1/test/slack/" + webhookName;
|
||||
|
||||
CreateEventSubscription genericWebhookActionRequest =
|
||||
createRequest(webhookName)
|
||||
@ -1284,12 +1284,12 @@ public class EventSubscriptionResourceTest
|
||||
|
||||
// Apply the filtering rule to the request
|
||||
genericWebhookActionRequest.withInput(rule);
|
||||
|
||||
genericWebhookActionRequest.withName(entityName);
|
||||
EventSubscription alert = createAndCheckEntity(genericWebhookActionRequest, ADMIN_AUTH_HEADERS);
|
||||
|
||||
SubscriptionStatus status = getStatus(alert.getId(), Response.Status.OK.getStatusCode());
|
||||
assertEquals(ACTIVE, status.getStatus());
|
||||
SlackCallbackResource.EventDetails details = slackCallbackResource.getEventDetails(endpoint);
|
||||
SlackCallbackResource.EventDetails details = slackCallbackResource.getEventDetails(entityName);
|
||||
|
||||
// Alerts are triggered only by ChangeEvent occurrences related to resources as
|
||||
// ingestionPipeline
|
||||
@ -1313,7 +1313,7 @@ public class EventSubscriptionResourceTest
|
||||
|
||||
ingestionPipelineResourceTest.createEntity(request, ADMIN_AUTH_HEADERS);
|
||||
|
||||
details = waitForFirstSlackEvent(alert.getId(), endpoint, 25);
|
||||
details = waitForFirstSlackEvent(alert.getId(), entityName, 25);
|
||||
assertEquals(1, details.getEvents().size());
|
||||
}
|
||||
|
||||
@ -1359,11 +1359,8 @@ public class EventSubscriptionResourceTest
|
||||
// Ensure the call back notification has started
|
||||
details = waitForFirstSlackEvent(alert.getId(), endpoint, 25);
|
||||
assertEquals(1, details.getEvents().size());
|
||||
ConcurrentLinkedQueue<SlackMessage> messages = details.getEvents();
|
||||
for (SlackMessage sm : messages) {
|
||||
validateSlackMessage(alert, sm);
|
||||
}
|
||||
|
||||
ConcurrentLinkedQueue<String> messages = details.getEvents();
|
||||
assertNotNull(messages);
|
||||
SubscriptionStatus successDetails =
|
||||
getStatus(alert.getId(), Response.Status.OK.getStatusCode());
|
||||
assertEquals(SubscriptionStatus.Status.ACTIVE, successDetails.getStatus());
|
||||
@ -1396,13 +1393,17 @@ public class EventSubscriptionResourceTest
|
||||
deleteEntity(alert.getId(), ADMIN_AUTH_HEADERS);
|
||||
}
|
||||
|
||||
private String getTimeStamp() {
|
||||
return String.valueOf(System.currentTimeMillis());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDifferentTypesOfAlerts_SLACK() throws IOException {
|
||||
// Create multiple webhooks each with different type of response to callback
|
||||
String baseUri = "http://localhost:" + APP.getLocalPort() + "/api/v1/test/slack";
|
||||
|
||||
// SlowServer
|
||||
String alertName = "slowServer";
|
||||
String alertName = "slowServer" + getTimeStamp();
|
||||
// Alert Action
|
||||
List<SubscriptionDestination> w1 =
|
||||
getSlackWebhook(baseUri + "/simulate/slowServer"); // Callback response 1 second slower
|
||||
@ -1410,32 +1411,32 @@ public class EventSubscriptionResourceTest
|
||||
EventSubscription w1Alert = createAndCheckEntity(w1ActionRequest, ADMIN_AUTH_HEADERS);
|
||||
|
||||
// CallbackTimeout
|
||||
alertName = "callbackTimeout";
|
||||
alertName = "callbackTimeout" + getTimeStamp();
|
||||
List<SubscriptionDestination> w2 =
|
||||
getSlackWebhook(baseUri + "/simulate/timeout"); // Callback response 12 seconds slower
|
||||
CreateEventSubscription w2ActionRequest = createRequest(alertName).withDestinations(w2);
|
||||
EventSubscription w2Alert = createAndCheckEntity(w2ActionRequest, ADMIN_AUTH_HEADERS);
|
||||
|
||||
// callbackResponse300
|
||||
alertName = "callbackResponse300";
|
||||
alertName = "callbackResponse300" + getTimeStamp();
|
||||
List<SubscriptionDestination> w3 = getSlackWebhook(baseUri + "/simulate/300"); // 3xx response
|
||||
CreateEventSubscription w3ActionRequest = createRequest(alertName).withDestinations(w3);
|
||||
EventSubscription w3Alert = createAndCheckEntity(w3ActionRequest, ADMIN_AUTH_HEADERS);
|
||||
|
||||
// callbackResponse400
|
||||
alertName = "callbackResponse400";
|
||||
alertName = "callbackResponse400" + getTimeStamp();
|
||||
List<SubscriptionDestination> w4 = getSlackWebhook(baseUri + "/simulate/400"); // 3xx response
|
||||
CreateEventSubscription w4ActionRequest = createRequest(alertName).withDestinations(w4);
|
||||
EventSubscription w4Alert = createAndCheckEntity(w4ActionRequest, ADMIN_AUTH_HEADERS);
|
||||
|
||||
// callbackResponse500
|
||||
alertName = "callbackResponse500";
|
||||
alertName = "callbackResponse500" + getTimeStamp();
|
||||
List<SubscriptionDestination> w5 = getSlackWebhook(baseUri + "/simulate/500"); // 3xx response
|
||||
CreateEventSubscription w5ActionRequest = createRequest(alertName).withDestinations(w5);
|
||||
EventSubscription w5Alert = createAndCheckEntity(w5ActionRequest, ADMIN_AUTH_HEADERS);
|
||||
|
||||
// invalidEndpoint
|
||||
alertName = "invalidEndpoint";
|
||||
alertName = "invalidEndpoint" + getTimeStamp();
|
||||
List<SubscriptionDestination> w6 = getSlackWebhook("http://invalidUnknownHost"); // 3xx response
|
||||
CreateEventSubscription w6ActionRequest = createRequest(alertName).withDestinations(w6);
|
||||
EventSubscription w6Alert = createAndCheckEntity(w6ActionRequest, ADMIN_AUTH_HEADERS);
|
||||
@ -1494,10 +1495,7 @@ public class EventSubscriptionResourceTest
|
||||
|
||||
assertNotNull(alert, "Webhook creation failed");
|
||||
ConcurrentLinkedQueue<TeamsMessage> events = details.getEvents();
|
||||
for (TeamsMessage teamsMessage : events) {
|
||||
validateTeamsMessage(alert, teamsMessage);
|
||||
}
|
||||
|
||||
assertNotNull(events);
|
||||
SubscriptionStatus status = getStatus(alert.getId(), Response.Status.OK.getStatusCode());
|
||||
assertEquals(SubscriptionStatus.Status.ACTIVE, status.getStatus());
|
||||
|
||||
@ -1512,20 +1510,24 @@ public class EventSubscriptionResourceTest
|
||||
|
||||
@Test
|
||||
void post_alertActionWithEnabledStateChange_MSTeams(TestInfo test) throws IOException {
|
||||
String webhookName = getEntityName(test);
|
||||
String endpoint = test.getDisplayName();
|
||||
LOG.info("creating webhook in disabled state");
|
||||
String uri = "http://localhost:" + APP.getLocalPort() + "/api/v1/test/msteams/" + endpoint;
|
||||
String entityName = getEntityName(test);
|
||||
|
||||
LOG.info("creating webhook in disabled state");
|
||||
String uri =
|
||||
"http://localhost:"
|
||||
+ APP.getLocalPort()
|
||||
+ "/api/v1/test/msteams/"
|
||||
+ URLEncoder.encode(entityName, StandardCharsets.UTF_8);
|
||||
// Create a Disabled Generic Webhook
|
||||
CreateEventSubscription genericWebhookActionRequest =
|
||||
createRequest(webhookName).withEnabled(false).withDestinations(getTeamsWebhook(uri));
|
||||
createRequest(entityName).withEnabled(false).withDestinations(getTeamsWebhook(uri));
|
||||
EventSubscription alert = createAndCheckEntity(genericWebhookActionRequest, ADMIN_AUTH_HEADERS);
|
||||
|
||||
// For the DISABLED Publisher are not available, so it will have no status
|
||||
SubscriptionStatus status = getStatus(alert.getId(), Response.Status.OK.getStatusCode());
|
||||
assertEquals(DISABLED, status.getStatus());
|
||||
MSTeamsCallbackResource.EventDetails details = teamsCallbackResource.getEventDetails(endpoint);
|
||||
MSTeamsCallbackResource.EventDetails details =
|
||||
teamsCallbackResource.getEventDetails(entityName);
|
||||
assertNull(details);
|
||||
|
||||
LOG.info("Enabling webhook Action");
|
||||
@ -1546,13 +1548,10 @@ public class EventSubscriptionResourceTest
|
||||
assertEquals(SubscriptionStatus.Status.ACTIVE, status2.getStatus());
|
||||
|
||||
// Ensure the call back notification has started
|
||||
details = waitForFirstMSTeamsEvent(alert.getId(), endpoint, 25);
|
||||
details = waitForFirstMSTeamsEvent(alert.getId(), entityName, 25);
|
||||
assertEquals(1, details.getEvents().size());
|
||||
ConcurrentLinkedQueue<TeamsMessage> messages = details.getEvents();
|
||||
for (TeamsMessage teamsMessage : messages) {
|
||||
validateTeamsMessage(alert, teamsMessage);
|
||||
}
|
||||
|
||||
assertNotNull(messages);
|
||||
SubscriptionStatus successDetails =
|
||||
getStatus(alert.getId(), Response.Status.OK.getStatusCode());
|
||||
assertEquals(SubscriptionStatus.Status.ACTIVE, successDetails.getStatus());
|
||||
@ -1655,88 +1654,11 @@ public class EventSubscriptionResourceTest
|
||||
deleteEntity(w6Alert.getId(), ADMIN_AUTH_HEADERS);
|
||||
}
|
||||
|
||||
private void validateSlackMessage(EventSubscription alert, SlackMessage slackMessage) {
|
||||
// Validate the basic structure
|
||||
assertNotNull(slackMessage.getUsername(), "Username should not be null");
|
||||
assertNotNull(slackMessage.getText(), "Text should not be null");
|
||||
assertFalse(slackMessage.getText().isEmpty(), "Text should not be empty");
|
||||
|
||||
// Validate the formatting of the text
|
||||
String expectedTextFormat = buildExpectedTextFormatSlack(alert); // Get the expected format
|
||||
|
||||
// Check if the actual text matches the expected format
|
||||
String actualText = slackMessage.getText();
|
||||
assertEquals(
|
||||
actualText, expectedTextFormat, "Slack message text does not match expected format");
|
||||
}
|
||||
|
||||
private String buildExpectedTextFormatSlack(EventSubscription alert) {
|
||||
String updatedBy = alert.getUpdatedBy();
|
||||
return String.format(
|
||||
"[%s] %s posted on %s %s",
|
||||
alert.getFullyQualifiedName(),
|
||||
updatedBy,
|
||||
Entity.EVENT_SUBSCRIPTION,
|
||||
getEntityUrlSlack(alert));
|
||||
}
|
||||
|
||||
private String getEntityUrlSlack(EventSubscription alert) {
|
||||
return slackCallbackResource.getEntityUrl(
|
||||
Entity.EVENT_SUBSCRIPTION, alert.getFullyQualifiedName(), "");
|
||||
}
|
||||
|
||||
private void validateTeamsMessage(EventSubscription alert, TeamsMessage message) {
|
||||
// Validate the basic structure
|
||||
assertThat(message.getSummary())
|
||||
.isNotNull()
|
||||
.isEqualTo("Change Event From OpenMetadata")
|
||||
.describedAs("Invalid summary in Teams message");
|
||||
|
||||
assertThat(message.getType())
|
||||
.isNotNull()
|
||||
.isEqualTo("MessageCard")
|
||||
.describedAs("Invalid type in Teams message");
|
||||
|
||||
assertThat(message.getContext())
|
||||
.isNotNull()
|
||||
.isEqualTo("http://schema.org/extensions")
|
||||
.describedAs("Invalid context in Teams message");
|
||||
|
||||
TeamsMessage.Section firstSection = message.getSections().get(0);
|
||||
// Validate Activity
|
||||
String expectedTitle = buildExpectedActivityTitleTextFormatMSTeams(alert);
|
||||
String actualTitle = firstSection.getActivityTitle();
|
||||
assertEquals(
|
||||
expectedTitle, actualTitle, "Teams message activity title does not match expected format");
|
||||
|
||||
// Validate sections
|
||||
assertNotNull(message.getSections(), "Sections should not be null");
|
||||
assertFalse(message.getSections().isEmpty(), "Sections should not be empty");
|
||||
|
||||
for (TeamsMessage.Section section : message.getSections()) {
|
||||
assertNotNull(section.getActivityTitle(), "Activity title should not be null");
|
||||
assertFalse(section.getActivityTitle().isEmpty(), "Activity title should not be empty");
|
||||
|
||||
assertNotNull(section.getActivityText(), "Activity text should not be null");
|
||||
assertFalse(section.getActivityText().isEmpty(), "Activity text should not be empty");
|
||||
}
|
||||
}
|
||||
|
||||
private String buildExpectedActivityTitleTextFormatMSTeams(EventSubscription alert) {
|
||||
String updatedBy = alert.getUpdatedBy();
|
||||
return String.format(
|
||||
"[%s] %s posted on %s [\"%s\"](/%s)",
|
||||
alert.getFullyQualifiedName(),
|
||||
updatedBy,
|
||||
Entity.EVENT_SUBSCRIPTION,
|
||||
alert.getName(),
|
||||
getEntityUrlMSTeams());
|
||||
}
|
||||
|
||||
private String getEntityUrlMSTeams() {
|
||||
return teamsCallbackResource.getEntityUrlMSTeams();
|
||||
}
|
||||
|
||||
private EventSubscription getAndAssertAlert(UUID id, EventSubscription expectedAlert)
|
||||
throws HttpResponseException {
|
||||
EventSubscription fetchedAlert = getEntity(id, ADMIN_AUTH_HEADERS);
|
||||
@ -1886,8 +1808,9 @@ public class EventSubscriptionResourceTest
|
||||
waitForAllEventToComplete(createdSub.getId());
|
||||
waitForAllEventToComplete(updatedSub.getId());
|
||||
|
||||
List<SlackMessage> callbackEvents =
|
||||
slackCallbackResource.getEntityCallbackEvents(EventType.ENTITY_CREATED, entity + "_SLACK");
|
||||
List<String> callbackEvents =
|
||||
slackCallbackResource.getEntityCallbackEvents(
|
||||
EventType.ENTITY_CREATED.value(), entity + "_SLACK");
|
||||
assertTrue(callbackEvents.size() > 0);
|
||||
}
|
||||
|
||||
@ -1902,9 +1825,9 @@ public class EventSubscriptionResourceTest
|
||||
waitForAllEventToComplete(createdSub.getId());
|
||||
waitForAllEventToComplete(updatedSub.getId());
|
||||
|
||||
List<TeamsMessage> callbackEvents =
|
||||
List<String> callbackEvents =
|
||||
teamsCallbackResource.getEntityCallbackEvents(
|
||||
EventType.ENTITY_CREATED, entity + "_MSTEAMS");
|
||||
EventType.ENTITY_CREATED.toString(), entity + "_MSTEAMS");
|
||||
assertTrue(callbackEvents.size() > 0);
|
||||
}
|
||||
|
||||
@ -2279,7 +2202,7 @@ public class EventSubscriptionResourceTest
|
||||
new Webhook()
|
||||
.withEndpoint(URI.create(uri))
|
||||
.withReceivers(new HashSet<>())
|
||||
.withSecretKey("teamsTest")));
|
||||
.withSecretKey(MSTeamsCallbackResource.getSecretKey())));
|
||||
}
|
||||
|
||||
public WebhookCallbackResource.EventDetails waitForFirstEvent(
|
||||
|
||||
@ -1,28 +1,118 @@
|
||||
package org.openmetadata.service.resources.events;
|
||||
|
||||
import static org.openmetadata.service.util.email.EmailUtil.getSmtpSettings;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.HeaderParam;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.SecurityContext;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.openmetadata.service.Entity;
|
||||
import org.openmetadata.service.apps.bundles.changeEvent.msteams.TeamsMessage;
|
||||
import org.awaitility.Awaitility;
|
||||
import org.openmetadata.service.util.RestUtil;
|
||||
|
||||
/** REST resource used for msteams callback tests. */
|
||||
@Slf4j
|
||||
@Path("v1/test/msteams")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public class MSTeamsCallbackResource extends BaseCallbackResource<TeamsMessage> {
|
||||
@Override
|
||||
protected String getTestName() {
|
||||
public class MSTeamsCallbackResource {
|
||||
protected final ConcurrentHashMap<String, EventDetails<String>> eventMap =
|
||||
new ConcurrentHashMap<>();
|
||||
protected final ConcurrentHashMap<String, List<String>> entityCallbackMap =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
@POST
|
||||
@Path("/{name}")
|
||||
public Response receiveEventCount(
|
||||
@Context UriInfo uriInfo,
|
||||
@Context SecurityContext securityContext,
|
||||
@HeaderParam(RestUtil.SIGNATURE_HEADER) String signature,
|
||||
@PathParam("name") String name,
|
||||
String event) {
|
||||
addEventDetails(name, event);
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/simulate/slowServer")
|
||||
public Response receiveEventWithDelay(
|
||||
@Context UriInfo uriInfo, @Context SecurityContext securityContext, String event) {
|
||||
addEventDetails("simulate-slowServer", event);
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/simulate/timeout")
|
||||
public Response receiveEventWithTimeout(
|
||||
@Context UriInfo uriInfo, @Context SecurityContext securityContext, String event) {
|
||||
addEventDetails("simulate-timeout", event);
|
||||
Awaitility.await()
|
||||
.pollDelay(java.time.Duration.ofSeconds(100L))
|
||||
.untilTrue(new AtomicBoolean(true));
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/simulate/300")
|
||||
public Response receiveEvent300(
|
||||
@Context UriInfo uriInfo, @Context SecurityContext securityContext, String event) {
|
||||
addEventDetails("simulate-300", event);
|
||||
return Response.status(Response.Status.MOVED_PERMANENTLY).build();
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/simulate/400")
|
||||
public Response receiveEvent400(
|
||||
@Context UriInfo uriInfo, @Context SecurityContext securityContext, String event) {
|
||||
addEventDetails("simulate-400", event);
|
||||
return Response.status(Response.Status.BAD_REQUEST).build();
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/simulate/500")
|
||||
public Response receiveEvent500(
|
||||
@Context UriInfo uriInfo, @Context SecurityContext securityContext, String event) {
|
||||
addEventDetails("simulate-500", event);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
|
||||
}
|
||||
|
||||
protected void addEventDetails(String endpoint, String event) {
|
||||
EventDetails<String> details = eventMap.computeIfAbsent(endpoint, k -> new EventDetails<>());
|
||||
details.getEvents().add(event);
|
||||
LOG.info("Event received {}, total count {}", endpoint, details.getEvents().size());
|
||||
}
|
||||
|
||||
public EventDetails<String> getEventDetails(String endpoint) {
|
||||
return eventMap.get(endpoint);
|
||||
}
|
||||
|
||||
// Get entity callback events by eventType:entityType combination
|
||||
public List<String> getEntityCallbackEvents(String eventType, String entityType) {
|
||||
String key = eventType + ":" + entityType;
|
||||
return entityCallbackMap.getOrDefault(key, new ArrayList<>());
|
||||
}
|
||||
|
||||
public void clearEvents() {
|
||||
eventMap.clear();
|
||||
entityCallbackMap.clear();
|
||||
}
|
||||
|
||||
public static String getSecretKey() {
|
||||
return "teamsTest";
|
||||
}
|
||||
|
||||
public String getEntityUrlMSTeams() {
|
||||
return String.format(
|
||||
"%s/%s", getSmtpSettings().getOpenMetadataUrl(), Entity.EVENT_SUBSCRIPTION);
|
||||
static class EventDetails<T> {
|
||||
@Getter final ConcurrentLinkedQueue<T> events = new ConcurrentLinkedQueue<>();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,26 +1,130 @@
|
||||
package org.openmetadata.service.resources.events;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.openmetadata.common.utils.CommonUtil.nullOrEmpty;
|
||||
import static org.openmetadata.service.util.email.EmailUtil.getSmtpSettings;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.HeaderParam;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.SecurityContext;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.openmetadata.service.apps.bundles.changeEvent.slack.SlackMessage;
|
||||
import org.awaitility.Awaitility;
|
||||
import org.openmetadata.common.utils.CommonUtil;
|
||||
import org.openmetadata.service.util.RestUtil;
|
||||
|
||||
/** REST resource used for slack callback tests. */
|
||||
@Slf4j
|
||||
@Path("v1/test/slack")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public class SlackCallbackResource extends BaseCallbackResource<SlackMessage> {
|
||||
@Override
|
||||
public class SlackCallbackResource {
|
||||
|
||||
// ConcurrentHashMap to store event details (for String event type)
|
||||
protected final ConcurrentHashMap<String, EventDetails<String>> eventMap =
|
||||
new ConcurrentHashMap<>();
|
||||
protected final ConcurrentHashMap<String, List<String>> entityCallbackMap =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
@POST
|
||||
@Path("/{name}")
|
||||
public Response receiveEventCount(
|
||||
@Context UriInfo uriInfo,
|
||||
@Context SecurityContext securityContext,
|
||||
@HeaderParam(RestUtil.SIGNATURE_HEADER) String signature,
|
||||
@PathParam("name") String name,
|
||||
String event) {
|
||||
String computedSignature = "sha256=" + CommonUtil.calculateHMAC(getTestName(), event);
|
||||
assertEquals(computedSignature, signature);
|
||||
addEventDetails(name, event);
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/simulate/slowServer")
|
||||
public Response receiveEventWithDelay(
|
||||
@Context UriInfo uriInfo, @Context SecurityContext securityContext, String event) {
|
||||
addEventDetails("simulate-slowServer", event);
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/simulate/timeout")
|
||||
public Response receiveEventWithTimeout(
|
||||
@Context UriInfo uriInfo, @Context SecurityContext securityContext, String event) {
|
||||
addEventDetails("simulate-timeout", event);
|
||||
Awaitility.await()
|
||||
.pollDelay(java.time.Duration.ofSeconds(100L))
|
||||
.untilTrue(new AtomicBoolean(true));
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/simulate/300")
|
||||
public Response receiveEvent300(
|
||||
@Context UriInfo uriInfo, @Context SecurityContext securityContext, String event) {
|
||||
addEventDetails("simulate-300", event);
|
||||
return Response.status(Response.Status.MOVED_PERMANENTLY).build();
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/simulate/400")
|
||||
public Response receiveEvent400(
|
||||
@Context UriInfo uriInfo, @Context SecurityContext securityContext, String event) {
|
||||
addEventDetails("simulate-400", event);
|
||||
return Response.status(Response.Status.BAD_REQUEST).build();
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/simulate/500")
|
||||
public Response receiveEvent500(
|
||||
@Context UriInfo uriInfo, @Context SecurityContext securityContext, String event) {
|
||||
addEventDetails("simulate-500", event);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
|
||||
}
|
||||
|
||||
protected void addEventDetails(String endpoint, String event) {
|
||||
EventDetails<String> details = eventMap.computeIfAbsent(endpoint, k -> new EventDetails<>());
|
||||
details.getEvents().add(event);
|
||||
LOG.info("Event received {}, total count {}", endpoint, details.getEvents().size());
|
||||
}
|
||||
|
||||
// Retrieve event details for a specific endpoint
|
||||
public EventDetails<String> getEventDetails(String endpoint) {
|
||||
return eventMap.get(endpoint);
|
||||
}
|
||||
|
||||
// Get entity callback events by eventType:entityType combination
|
||||
public List<String> getEntityCallbackEvents(String eventType, String entityType) {
|
||||
String key = eventType + ":" + entityType;
|
||||
return entityCallbackMap.getOrDefault(key, new ArrayList<>());
|
||||
}
|
||||
|
||||
public void clearEvents() {
|
||||
eventMap.clear();
|
||||
entityCallbackMap.clear();
|
||||
}
|
||||
|
||||
protected String getTestName() {
|
||||
return "slackTest";
|
||||
}
|
||||
|
||||
static class EventDetails<T> {
|
||||
@Getter final ConcurrentLinkedQueue<T> events = new ConcurrentLinkedQueue<>();
|
||||
}
|
||||
|
||||
public String getEntityUrl(String prefix, String fqn, String additionalParams) {
|
||||
return String.format(
|
||||
"<%s/%s/%s%s|%s>",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user