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.
|
- **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.
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
## 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.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;
|
||||||
|
@ -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.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
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
"GenericWebhook",
|
"GenericWebhook",
|
||||||
"SlackWebhook",
|
"SlackWebhook",
|
||||||
"MsTeamsWebhook",
|
"MsTeamsWebhook",
|
||||||
|
"GChatWebhook",
|
||||||
"Email",
|
"Email",
|
||||||
"ActivityFeed"
|
"ActivityFeed"
|
||||||
]
|
]
|
||||||
|
@ -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",
|
||||||
|
@ -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
|
||||||
|
@ -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');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user