diff --git a/README.md b/README.md index 375c1fa5358..25c7114aee3 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ Here are some of the supported features in a nutshell: - **Comprehensive Roles and Policies** - Handles complex access control use cases and hierarchical teams. -- **Webhooks** - Supports webhook integrations. Integrate with Slack, and Microsoft Teams. +- **Webhooks** - Supports webhook integrations. Integrate with Slack, Microsoft Teams and Google Chat. - **Connectors** - Supports 55 connectors to various databases, dashboards, pipelines and messaging services. diff --git a/openmetadata-docs/content/developers/webhooks.md b/openmetadata-docs/content/developers/webhooks.md index 67c105fe145..c77df4c89ad 100644 --- a/openmetadata-docs/content/developers/webhooks.md +++ b/openmetadata-docs/content/developers/webhooks.md @@ -24,10 +24,11 @@ OpenMetadata provides out-of-the-box support for webhooks. OpenMetadata also allows the user to customise the webhook with a wide range of filters to listen to only selected type of events. -## OpenMetadata supports 3 webhook types: +## OpenMetadata supports 4 webhook types: 1. **Generic** 2. **Slack** 3. **Microsoft Teams** +4. **Google Chat** ## How to Set up Generic Type Webhook: 1. **Name**: Add the name of the webhook @@ -67,3 +68,14 @@ OpenMetadata also allows the user to customise the webhook with a wide range of 8. **Secret Key**: Secret key can be used to secure the webhook connection. ![webhook-msteams](https://user-images.githubusercontent.com/83201188/188462667-bd8443ce-a07d-4742-ae5d-42da3fc2d402.png) + +## How to Set up Google Chat Type Webhook: +1. **Name**: Add the name of the webhook +2. **Description**: Describe the webhook. +3. **Endpoint URL**: Enter the GChat endpoint URL. For more on creating GChat webhooks, see [Create a Webhook](https://developers.google.com/chat/how-tos/webhooks#create_a_webhook). +4. **Activity Filter**: Can be used to activate or disable the webhook. +5. **Event Filters**: Filters are provided for all the entities and for all the events. + Event data for specific action can be achieved. +6. **Batch Size**: Enter the batch size. +7. **Connection Timeout**: Enter the desired connection timeout. +8. **Secret Key**: Secret key can be used to secure the webhook connection. \ No newline at end of file diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/alerts/AlertUtil.java b/openmetadata-service/src/main/java/org/openmetadata/service/alerts/AlertUtil.java index 3bf851ca1a8..ac8add2f241 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/alerts/AlertUtil.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/alerts/AlertUtil.java @@ -27,6 +27,7 @@ import org.openmetadata.schema.type.Function; import org.openmetadata.schema.type.ParamAdditionalContext; import org.openmetadata.service.Entity; import org.openmetadata.service.alerts.emailAlert.EmailAlertPublisher; +import org.openmetadata.service.alerts.gchat.GChatWebhookPublisher; import org.openmetadata.service.alerts.generic.GenericWebhookPublisher; import org.openmetadata.service.alerts.msteams.MSTeamsWebhookPublisher; import org.openmetadata.service.alerts.slack.SlackWebhookEventPublisher; @@ -50,6 +51,9 @@ public class AlertUtil { case MS_TEAMS_WEBHOOK: publisher = new MSTeamsWebhookPublisher(alert, alertAction); break; + case G_CHAT_WEBHOOK: + publisher = new GChatWebhookPublisher(alert, alertAction); + break; case GENERIC_WEBHOOK: publisher = new GenericWebhookPublisher(alert, alertAction); break; diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/alerts/gchat/GChatMessage.java b/openmetadata-service/src/main/java/org/openmetadata/service/alerts/gchat/GChatMessage.java new file mode 100644 index 00000000000..29ef147afa7 --- /dev/null +++ b/openmetadata-service/src/main/java/org/openmetadata/service/alerts/gchat/GChatMessage.java @@ -0,0 +1,41 @@ +package org.openmetadata.service.alerts.gchat; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +public class GChatMessage { + + @Getter @Setter private String text; + @Getter @Setter private List cardsV2; + + public static class CardsV2 { + @Getter @Setter private String cardId; + @Getter @Setter private Card card; + } + + public static class Card { + + @Getter @Setter private CardHeader header; + @Getter @Setter private List
sections; + } + + public static class CardHeader { + @Getter @Setter private String title; + @Getter @Setter private String subtitle; + } + + public static class Section { + @Getter @Setter private List widgets; + } + + public static class Widget { + @Getter @Setter private TextParagraph textParagraph; + } + + @AllArgsConstructor + public static class TextParagraph { + @Getter @Setter private String text; + } +} diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/alerts/gchat/GChatWebhookPublisher.java b/openmetadata-service/src/main/java/org/openmetadata/service/alerts/gchat/GChatWebhookPublisher.java new file mode 100644 index 00000000000..ef7f31d1b15 --- /dev/null +++ b/openmetadata-service/src/main/java/org/openmetadata/service/alerts/gchat/GChatWebhookPublisher.java @@ -0,0 +1,80 @@ +package org.openmetadata.service.alerts.gchat; + +import java.util.concurrent.TimeUnit; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.Invocation; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import lombok.extern.slf4j.Slf4j; +import org.openmetadata.schema.entity.alerts.Alert; +import org.openmetadata.schema.entity.alerts.AlertAction; +import org.openmetadata.schema.type.ChangeEvent; +import org.openmetadata.schema.type.Webhook; +import org.openmetadata.service.alerts.AlertsActionPublisher; +import org.openmetadata.service.events.errors.EventPublisherException; +import org.openmetadata.service.resources.events.EventResource; +import org.openmetadata.service.util.ChangeEventParser; +import org.openmetadata.service.util.JsonUtils; + +@Slf4j +public class GChatWebhookPublisher extends AlertsActionPublisher { + + private final Invocation.Builder target; + private final Client client; + + public GChatWebhookPublisher(Alert alert, AlertAction alertAction) { + super(alert, alertAction); + if (alertAction.getAlertActionType() == AlertAction.AlertActionType.G_CHAT_WEBHOOK) { + Webhook webhook = JsonUtils.convertValue(alertAction.getAlertActionConfig(), Webhook.class); + String gChatWebhookURL = webhook.getEndpoint().toString(); + ClientBuilder clientBuilder = ClientBuilder.newBuilder(); + clientBuilder.connectTimeout(alertAction.getTimeout(), TimeUnit.SECONDS); + clientBuilder.readTimeout(alertAction.getReadTimeout(), TimeUnit.SECONDS); + client = clientBuilder.build(); + target = client.target(gChatWebhookURL).request(); + } else { + throw new IllegalArgumentException("GChat Alert Invoked with Illegal Type and Settings."); + } + } + + @Override + protected void onStartDelegate() { + LOG.info("GChat Webhook publisher started"); + } + + @Override + protected void onShutdownDelegate() { + if (null != client) { + client.close(); + } + } + + @Override + protected void sendAlert(EventResource.ChangeEventList list) { + + for (ChangeEvent event : list.getData()) { + long attemptTime = System.currentTimeMillis(); + try { + GChatMessage gchatMessage = ChangeEventParser.buildGChatMessage(event); + Response response = + target.post(javax.ws.rs.client.Entity.entity(gchatMessage, MediaType.APPLICATION_JSON_TYPE)); + if (response.getStatus() >= 300 && response.getStatus() < 400) { + // 3xx response/redirection is not allowed for callback. Set the webhook state as in error + setErrorStatus(attemptTime, response.getStatus(), response.getStatusInfo().getReasonPhrase()); + } else if (response.getStatus() >= 300 && response.getStatus() < 600) { + // 4xx, 5xx response retry delivering events after timeout + setNextBackOff(); + setAwaitingRetry(attemptTime, response.getStatus(), response.getStatusInfo().getReasonPhrase()); + Thread.sleep(currentBackoffTime); + } else if (response.getStatus() == 200) { + setSuccessStatus(System.currentTimeMillis()); + } + } catch (Exception e) { + LOG.error("Failed to publish event {} to gchat due to {} ", event, e.getMessage()); + throw new EventPublisherException( + String.format("Failed to publish event %s to gchat due to %s ", event, e.getMessage())); + } + } + } +} diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/util/ChangeEventParser.java b/openmetadata-service/src/main/java/org/openmetadata/service/util/ChangeEventParser.java index 9039912d1b7..eb1a06d0b0d 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/util/ChangeEventParser.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/util/ChangeEventParser.java @@ -57,6 +57,7 @@ import org.openmetadata.schema.type.EntityReference; import org.openmetadata.schema.type.FieldChange; import org.openmetadata.service.Entity; import org.openmetadata.service.alerts.emailAlert.EmailMessage; +import org.openmetadata.service.alerts.gchat.GChatMessage; import org.openmetadata.service.alerts.msteams.TeamsMessage; import org.openmetadata.service.alerts.slack.SlackAttachment; import org.openmetadata.service.alerts.slack.SlackMessage; @@ -85,6 +86,7 @@ public final class ChangeEventParser { FEED, SLACK, TEAMS, + GCHAT, EMAIL } @@ -96,6 +98,8 @@ public final class ChangeEventParser { return FEED_BOLD; case SLACK: return SLACK_BOLD; + case GCHAT: + return "%s"; default: return "INVALID"; } @@ -105,7 +109,8 @@ public final class ChangeEventParser { switch (publishTo) { case FEED: case TEAMS: - // TEAMS and FEED bold formatting is same + case GCHAT: + // TEAMS, GCHAT, FEED linebreak formatting are same return FEED_LINE_BREAK; case SLACK: return SLACK_LINE_BREAK; @@ -119,10 +124,11 @@ public final class ChangeEventParser { case FEED: return FEED_SPAN_ADD; case TEAMS: - // TEAMS and FEED bold formatting is same return "**"; case SLACK: return "*"; + case GCHAT: + return ""; default: return "INVALID"; } @@ -133,10 +139,11 @@ public final class ChangeEventParser { case FEED: return FEED_SPAN_CLOSE; case TEAMS: - // TEAMS and FEED bold formatting is same return "** "; case SLACK: return "*"; + case GCHAT: + return ""; default: return "INVALID"; } @@ -147,10 +154,11 @@ public final class ChangeEventParser { case FEED: return FEED_SPAN_REMOVE; case TEAMS: - // TEAMS and FEED bold formatting is same return "~~"; case SLACK: return "~"; + case GCHAT: + return ""; default: return "INVALID"; } @@ -161,10 +169,11 @@ public final class ChangeEventParser { case FEED: return FEED_SPAN_CLOSE; case TEAMS: - // TEAMS and FEED bold formatting is same return "~~ "; case SLACK: return "~"; + case GCHAT: + return ""; default: return "INVALID"; } @@ -177,7 +186,7 @@ public final class ChangeEventParser { if (Objects.nonNull(urlInstance)) { String scheme = urlInstance.getScheme(); String host = urlInstance.getHost(); - if (publishTo == PUBLISH_TO.SLACK) { + if (publishTo == PUBLISH_TO.SLACK || publishTo == PUBLISH_TO.GCHAT) { return String.format("<%s://%s/%s/%s|%s>", scheme, host, event.getEntityType(), fqn, fqn); } else if (publishTo == PUBLISH_TO.TEAMS) { return String.format("[%s](%s://%s/%s/%s)", fqn, scheme, host, event.getEntityType(), fqn); @@ -250,6 +259,42 @@ public final class ChangeEventParser { return teamsMessage; } + public static GChatMessage buildGChatMessage(ChangeEvent event) { + GChatMessage gChatMessage = new GChatMessage(); + GChatMessage.CardsV2 cardsV2 = new GChatMessage.CardsV2(); + GChatMessage.Card card = new GChatMessage.Card(); + GChatMessage.Section section = new GChatMessage.Section(); + if (event.getEntity() != null) { + String headerTemplate = "%s posted on %s %s"; + String headerText = + String.format( + headerTemplate, event.getUserName(), event.getEntityType(), getEntityUrl(PUBLISH_TO.GCHAT, event)); + gChatMessage.setText(headerText); + GChatMessage.CardHeader cardHeader = new GChatMessage.CardHeader(); + String cardHeaderText = + String.format( + headerTemplate, + event.getUserName(), + event.getEntityType(), + ((EntityInterface) event.getEntity()).getName()); + cardHeader.setTitle(cardHeaderText); + card.setHeader(cardHeader); + } + Map messages = + getFormattedMessages(PUBLISH_TO.GCHAT, event.getChangeDescription(), (EntityInterface) event.getEntity()); + List widgets = new ArrayList<>(); + for (Entry entry : messages.entrySet()) { + GChatMessage.Widget widget = new GChatMessage.Widget(); + widget.setTextParagraph(new GChatMessage.TextParagraph(entry.getValue())); + widgets.add(widget); + } + section.setWidgets(widgets); + card.setSections(List.of(section)); + cardsV2.setCard(card); + gChatMessage.setCardsV2(List.of(cardsV2)); + return gChatMessage; + } + public static Map getFormattedMessages( PUBLISH_TO publishTo, ChangeDescription changeDescription, EntityInterface entity) { // Store a map of entityLink -> message diff --git a/openmetadata-spec/src/main/resources/json/schema/alerts/alertAction.json b/openmetadata-spec/src/main/resources/json/schema/alerts/alertAction.json index 4ef04972dcd..d74a2148a70 100644 --- a/openmetadata-spec/src/main/resources/json/schema/alerts/alertAction.json +++ b/openmetadata-spec/src/main/resources/json/schema/alerts/alertAction.json @@ -14,6 +14,7 @@ "GenericWebhook", "SlackWebhook", "MsTeamsWebhook", + "GChatWebhook", "Email", "ActivityFeed" ] diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json index ce2662c67a4..575c4dddd02 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json @@ -291,6 +291,7 @@ "from-lowercase": "from", "full-name": "Full name", "function": "Function", + "g-chat": "G Chat", "gcs-credential-path": "GCS Credentials Path", "generate": "Generate", "generate-new-token": "Generate New Token", diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/AddAlertPage/AddAlertPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/AddAlertPage/AddAlertPage.tsx index 3cbb76b8121..43b747e2b36 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/AddAlertPage/AddAlertPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/AddAlertPage/AddAlertPage.tsx @@ -469,6 +469,7 @@ const AddAlertPage = () => { case AlertActionType.GenericWebhook: case AlertActionType.SlackWebhook: case AlertActionType.MSTeamsWebhook: + case AlertActionType.GChatWebhook: return ( <>