add GChat as alert destination (#10109)

This commit is contained in:
ragul balaji ravichandran 2023-02-05 06:34:31 +05:30 committed by GitHub
parent 8b84ea2453
commit 97747d9803
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 195 additions and 8 deletions

View File

@ -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. - **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. - **Connectors** - Supports 55 connectors to various databases, dashboards, pipelines and messaging services.

View File

@ -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 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** 1. **Generic**
2. **Slack** 2. **Slack**
3. **Microsoft Teams** 3. **Microsoft Teams**
4. **Google Chat**
## How to Set up Generic Type Webhook: ## How to Set up Generic Type Webhook:
1. **Name**: Add the name of the 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. 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) ![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.

View File

@ -27,6 +27,7 @@ import org.openmetadata.schema.type.Function;
import org.openmetadata.schema.type.ParamAdditionalContext; import org.openmetadata.schema.type.ParamAdditionalContext;
import org.openmetadata.service.Entity; import org.openmetadata.service.Entity;
import org.openmetadata.service.alerts.emailAlert.EmailAlertPublisher; 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.generic.GenericWebhookPublisher;
import org.openmetadata.service.alerts.msteams.MSTeamsWebhookPublisher; import org.openmetadata.service.alerts.msteams.MSTeamsWebhookPublisher;
import org.openmetadata.service.alerts.slack.SlackWebhookEventPublisher; import org.openmetadata.service.alerts.slack.SlackWebhookEventPublisher;
@ -50,6 +51,9 @@ public class AlertUtil {
case MS_TEAMS_WEBHOOK: case MS_TEAMS_WEBHOOK:
publisher = new MSTeamsWebhookPublisher(alert, alertAction); publisher = new MSTeamsWebhookPublisher(alert, alertAction);
break; break;
case G_CHAT_WEBHOOK:
publisher = new GChatWebhookPublisher(alert, alertAction);
break;
case GENERIC_WEBHOOK: case GENERIC_WEBHOOK:
publisher = new GenericWebhookPublisher(alert, alertAction); publisher = new GenericWebhookPublisher(alert, alertAction);
break; break;

View File

@ -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> 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<Section> sections;
}
public static class CardHeader {
@Getter @Setter private String title;
@Getter @Setter private String subtitle;
}
public static class Section {
@Getter @Setter private List<Widget> widgets;
}
public static class Widget {
@Getter @Setter private TextParagraph textParagraph;
}
@AllArgsConstructor
public static class TextParagraph {
@Getter @Setter private String text;
}
}

View File

@ -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()));
}
}
}
}

View File

@ -57,6 +57,7 @@ import org.openmetadata.schema.type.EntityReference;
import org.openmetadata.schema.type.FieldChange; import org.openmetadata.schema.type.FieldChange;
import org.openmetadata.service.Entity; import org.openmetadata.service.Entity;
import org.openmetadata.service.alerts.emailAlert.EmailMessage; 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.msteams.TeamsMessage;
import org.openmetadata.service.alerts.slack.SlackAttachment; import org.openmetadata.service.alerts.slack.SlackAttachment;
import org.openmetadata.service.alerts.slack.SlackMessage; import org.openmetadata.service.alerts.slack.SlackMessage;
@ -85,6 +86,7 @@ public final class ChangeEventParser {
FEED, FEED,
SLACK, SLACK,
TEAMS, TEAMS,
GCHAT,
EMAIL EMAIL
} }
@ -96,6 +98,8 @@ public final class ChangeEventParser {
return FEED_BOLD; return FEED_BOLD;
case SLACK: case SLACK:
return SLACK_BOLD; return SLACK_BOLD;
case GCHAT:
return "<b>%s</b>";
default: default:
return "INVALID"; return "INVALID";
} }
@ -105,7 +109,8 @@ public final class ChangeEventParser {
switch (publishTo) { switch (publishTo) {
case FEED: case FEED:
case TEAMS: case TEAMS:
// TEAMS and FEED bold formatting is same case GCHAT:
// TEAMS, GCHAT, FEED linebreak formatting are same
return FEED_LINE_BREAK; return FEED_LINE_BREAK;
case SLACK: case SLACK:
return SLACK_LINE_BREAK; return SLACK_LINE_BREAK;
@ -119,10 +124,11 @@ public final class ChangeEventParser {
case FEED: case FEED:
return FEED_SPAN_ADD; return FEED_SPAN_ADD;
case TEAMS: case TEAMS:
// TEAMS and FEED bold formatting is same
return "**"; return "**";
case SLACK: case SLACK:
return "*"; return "*";
case GCHAT:
return "<b>";
default: default:
return "INVALID"; return "INVALID";
} }
@ -133,10 +139,11 @@ public final class ChangeEventParser {
case FEED: case FEED:
return FEED_SPAN_CLOSE; return FEED_SPAN_CLOSE;
case TEAMS: case TEAMS:
// TEAMS and FEED bold formatting is same
return "** "; return "** ";
case SLACK: case SLACK:
return "*"; return "*";
case GCHAT:
return "</b>";
default: default:
return "INVALID"; return "INVALID";
} }
@ -147,10 +154,11 @@ public final class ChangeEventParser {
case FEED: case FEED:
return FEED_SPAN_REMOVE; return FEED_SPAN_REMOVE;
case TEAMS: case TEAMS:
// TEAMS and FEED bold formatting is same
return "~~"; return "~~";
case SLACK: case SLACK:
return "~"; return "~";
case GCHAT:
return "<s>";
default: default:
return "INVALID"; return "INVALID";
} }
@ -161,10 +169,11 @@ public final class ChangeEventParser {
case FEED: case FEED:
return FEED_SPAN_CLOSE; return FEED_SPAN_CLOSE;
case TEAMS: case TEAMS:
// TEAMS and FEED bold formatting is same
return "~~ "; return "~~ ";
case SLACK: case SLACK:
return "~"; return "~";
case GCHAT:
return "</s>";
default: default:
return "INVALID"; return "INVALID";
} }
@ -177,7 +186,7 @@ public final class ChangeEventParser {
if (Objects.nonNull(urlInstance)) { if (Objects.nonNull(urlInstance)) {
String scheme = urlInstance.getScheme(); String scheme = urlInstance.getScheme();
String host = urlInstance.getHost(); 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); return String.format("<%s://%s/%s/%s|%s>", scheme, host, event.getEntityType(), fqn, fqn);
} else if (publishTo == PUBLISH_TO.TEAMS) { } else if (publishTo == PUBLISH_TO.TEAMS) {
return String.format("[%s](%s://%s/%s/%s)", fqn, scheme, host, event.getEntityType(), fqn); return String.format("[%s](%s://%s/%s/%s)", fqn, scheme, host, event.getEntityType(), fqn);
@ -250,6 +259,42 @@ public final class ChangeEventParser {
return teamsMessage; 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<EntityLink, String> messages =
getFormattedMessages(PUBLISH_TO.GCHAT, event.getChangeDescription(), (EntityInterface) event.getEntity());
List<GChatMessage.Widget> widgets = new ArrayList<>();
for (Entry<EntityLink, String> 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<EntityLink, String> getFormattedMessages( public static Map<EntityLink, String> getFormattedMessages(
PUBLISH_TO publishTo, ChangeDescription changeDescription, EntityInterface entity) { PUBLISH_TO publishTo, ChangeDescription changeDescription, EntityInterface entity) {
// Store a map of entityLink -> message // Store a map of entityLink -> message

View File

@ -14,6 +14,7 @@
"GenericWebhook", "GenericWebhook",
"SlackWebhook", "SlackWebhook",
"MsTeamsWebhook", "MsTeamsWebhook",
"GChatWebhook",
"Email", "Email",
"ActivityFeed" "ActivityFeed"
] ]

View File

@ -291,6 +291,7 @@
"from-lowercase": "from", "from-lowercase": "from",
"full-name": "Full name", "full-name": "Full name",
"function": "Function", "function": "Function",
"g-chat": "G Chat",
"gcs-credential-path": "GCS Credentials Path", "gcs-credential-path": "GCS Credentials Path",
"generate": "Generate", "generate": "Generate",
"generate-new-token": "Generate New Token", "generate-new-token": "Generate New Token",

View File

@ -469,6 +469,7 @@ const AddAlertPage = () => {
case AlertActionType.GenericWebhook: case AlertActionType.GenericWebhook:
case AlertActionType.SlackWebhook: case AlertActionType.SlackWebhook:
case AlertActionType.MSTeamsWebhook: case AlertActionType.MSTeamsWebhook:
case AlertActionType.GChatWebhook:
return ( return (
<> <>
<Form.Item <Form.Item

View File

@ -129,6 +129,8 @@ export const getAlertActionTypeDisplayName = (
return i18next.t('label.slack'); return i18next.t('label.slack');
case AlertActionType.MSTeamsWebhook: case AlertActionType.MSTeamsWebhook:
return i18next.t('label.ms-team-plural'); return i18next.t('label.ms-team-plural');
case AlertActionType.GChatWebhook:
return i18next.t('label.g-chat');
} }
}; };