mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-08-18 14:06:59 +00:00
add GChat as alert destination (#10109)
This commit is contained in:
parent
8b84ea2453
commit
97747d9803
@ -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.
|
||||
|
||||
|
@ -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.
|
||||
|
||||

|
||||
|
||||
## 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.
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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 "<b>%s</b>";
|
||||
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 "<b>";
|
||||
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 "</b>";
|
||||
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 "<s>";
|
||||
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 "</s>";
|
||||
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<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(
|
||||
PUBLISH_TO publishTo, ChangeDescription changeDescription, EntityInterface entity) {
|
||||
// Store a map of entityLink -> message
|
||||
|
@ -14,6 +14,7 @@
|
||||
"GenericWebhook",
|
||||
"SlackWebhook",
|
||||
"MsTeamsWebhook",
|
||||
"GChatWebhook",
|
||||
"Email",
|
||||
"ActivityFeed"
|
||||
]
|
||||
|
@ -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",
|
||||
|
@ -469,6 +469,7 @@ const AddAlertPage = () => {
|
||||
case AlertActionType.GenericWebhook:
|
||||
case AlertActionType.SlackWebhook:
|
||||
case AlertActionType.MSTeamsWebhook:
|
||||
case AlertActionType.GChatWebhook:
|
||||
return (
|
||||
<>
|
||||
<Form.Item
|
||||
|
@ -129,6 +129,8 @@ export const getAlertActionTypeDisplayName = (
|
||||
return i18next.t('label.slack');
|
||||
case AlertActionType.MSTeamsWebhook:
|
||||
return i18next.t('label.ms-team-plural');
|
||||
case AlertActionType.GChatWebhook:
|
||||
return i18next.t('label.g-chat');
|
||||
}
|
||||
};
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user