-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Microsoft Teams Notification (#1064)
Microsoft Teams Notification Co-authored-by: S srinidhi <[email protected]> Co-authored-by: Dan Torrey <[email protected]>
- Loading branch information
1 parent
08a16fe
commit 07ca26e
Showing
15 changed files
with
1,538 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
201 changes: 201 additions & 0 deletions
201
...a/org/graylog/integrations/notifications/types/microsoftteams/TeamsEventNotification.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,201 @@ | ||
/* | ||
* Copyright (C) 2020 Graylog, Inc. | ||
* | ||
* This program is free software: you can redistribute it and/or modify | ||
* it under the terms of the Server Side Public License, version 1, | ||
* as published by MongoDB, Inc. | ||
* | ||
* This program is distributed in the hope that it will be useful, | ||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
* Server Side Public License for more details. | ||
* | ||
* You should have received a copy of the Server Side Public License | ||
* along with this program. If not, see | ||
* <http://www.mongodb.com/licensing/server-side-public-license>. | ||
*/ | ||
package org.graylog.integrations.notifications.types.microsoftteams; | ||
|
||
import com.fasterxml.jackson.databind.JsonNode; | ||
import com.fasterxml.jackson.databind.ObjectMapper; | ||
import com.floreysoft.jmte.Engine; | ||
import com.google.common.annotations.VisibleForTesting; | ||
import org.graylog.events.notifications.EventNotification; | ||
import org.graylog.events.notifications.EventNotificationContext; | ||
import org.graylog.events.notifications.EventNotificationException; | ||
import org.graylog.events.notifications.EventNotificationModelData; | ||
import org.graylog.events.notifications.EventNotificationService; | ||
import org.graylog.events.notifications.PermanentEventNotificationException; | ||
import org.graylog.events.notifications.TemporaryEventNotificationException; | ||
import org.graylog.events.processor.EventDefinitionDto; | ||
import org.graylog.integrations.notifications.types.util.RequestClient; | ||
import org.graylog2.jackson.TypeReferences; | ||
import org.graylog2.notifications.Notification; | ||
import org.graylog2.notifications.NotificationService; | ||
import org.graylog2.plugin.MessageSummary; | ||
import org.graylog2.plugin.system.NodeId; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
import javax.inject.Inject; | ||
import java.util.ArrayList; | ||
import java.util.HashMap; | ||
import java.util.List; | ||
import java.util.Locale; | ||
import java.util.Map; | ||
import java.util.stream.Collectors; | ||
|
||
import static com.google.common.base.Strings.isNullOrEmpty; | ||
import static java.util.Objects.requireNonNull; | ||
|
||
public class TeamsEventNotification implements EventNotification { | ||
|
||
private static final Logger LOG = LoggerFactory.getLogger(TeamsEventNotification.class); | ||
private final EventNotificationService notificationCallbackService; | ||
private final Engine templateEngine; | ||
private final NotificationService notificationService; | ||
private final ObjectMapper objectMapper; | ||
private final NodeId nodeId; | ||
private final RequestClient requestClient; | ||
|
||
@Inject | ||
public TeamsEventNotification(EventNotificationService notificationCallbackService, | ||
ObjectMapper objectMapper, | ||
Engine templateEngine, | ||
NotificationService notificationService, | ||
NodeId nodeId, RequestClient requestClient) { | ||
this.notificationCallbackService = notificationCallbackService; | ||
this.objectMapper = requireNonNull(objectMapper); | ||
this.templateEngine = requireNonNull(templateEngine); | ||
this.notificationService = requireNonNull(notificationService); | ||
this.nodeId = requireNonNull(nodeId); | ||
this.requestClient = requireNonNull(requestClient); | ||
} | ||
|
||
/** | ||
* @param ctx | ||
* @throws EventNotificationException is thrown when execute fails | ||
*/ | ||
@Override | ||
public void execute(EventNotificationContext ctx) throws EventNotificationException { | ||
final TeamsEventNotificationConfig config = (TeamsEventNotificationConfig) ctx.notificationConfig(); | ||
LOG.debug("TeamsEventNotification backlog size in method execute is [{}]", config.backlogSize()); | ||
|
||
try { | ||
TeamsMessage teamsMessage = createTeamsMessage(ctx, config); | ||
requestClient.send(teamsMessage.getJsonString(), config.webhookUrl()); | ||
} catch (TemporaryEventNotificationException exp) { | ||
//scheduler needs to retry a TemporaryEventNotificationException | ||
throw exp; | ||
} catch (PermanentEventNotificationException exp) { | ||
String errorMessage = String.format("Error sending the TeamsEventNotification :: %s", exp.getMessage()); | ||
final Notification systemNotification = notificationService.buildNow() | ||
.addNode(nodeId.toString()) | ||
.addType(Notification.Type.GENERIC) | ||
.addSeverity(Notification.Severity.URGENT) | ||
.addDetail("title", "TeamsEventNotification Failed") | ||
.addDetail("description", errorMessage); | ||
|
||
notificationService.publishIfFirst(systemNotification); | ||
throw exp; | ||
|
||
} catch (Exception exp) { | ||
throw new EventNotificationException("There was an exception triggering the TeamsEventNotification", exp); | ||
} | ||
} | ||
|
||
/** | ||
* @param ctx | ||
* @param config | ||
* @return | ||
* @throws PermanentEventNotificationException - throws this exception when the custom message template is invalid | ||
*/ | ||
TeamsMessage createTeamsMessage(EventNotificationContext ctx, TeamsEventNotificationConfig config) throws PermanentEventNotificationException { | ||
//Note: Link names if notify channel or else the channel tag will be plain text. | ||
String messageTitle = buildDefaultMessage(ctx); | ||
String description = buildMessageDescription(ctx); | ||
JsonNode customMessage = null; | ||
String template = config.customMessage(); | ||
if (!isNullOrEmpty(template)) { | ||
customMessage = buildCustomMessage(ctx, config, template); | ||
} | ||
|
||
return new TeamsMessage( | ||
config.color(), | ||
config.iconUrl(), | ||
messageTitle, | ||
customMessage, | ||
description | ||
); | ||
} | ||
|
||
String buildDefaultMessage(EventNotificationContext ctx) { | ||
String title = ctx.eventDefinition().map(EventDefinitionDto::title).orElse("Unnamed"); | ||
|
||
// Build Message title | ||
return String.format(Locale.ROOT, "**Alert %s triggered:**\n", title); | ||
} | ||
|
||
private String buildMessageDescription(EventNotificationContext ctx) { | ||
String description = ctx.eventDefinition().map(EventDefinitionDto::description).orElse(""); | ||
return "_" + description + "_"; | ||
} | ||
|
||
|
||
JsonNode buildCustomMessage(EventNotificationContext ctx, TeamsEventNotificationConfig config, String template) throws PermanentEventNotificationException { | ||
final List<MessageSummary> backlog = getMessageBacklog(ctx, config); | ||
Map<String, Object> model = getCustomMessageModel(ctx, config.type(), backlog); | ||
try { | ||
String facts = templateEngine.transform(template, model); | ||
JsonNode factsNode = getMessageDetails(facts); | ||
LOG.debug("customMessage: template = {} model = {}", template, model); | ||
return factsNode; | ||
} catch (Exception e) { | ||
String error = "Invalid Custom Message template."; | ||
LOG.error("{} [{}]", error, e.toString()); | ||
throw new PermanentEventNotificationException(error + e, e.getCause()); | ||
} | ||
} | ||
|
||
public JsonNode getMessageDetails(String eventFields) { | ||
String[] fields = eventFields.split("\\r?\\n"); | ||
List<Map<String, String>> event = new ArrayList<>(); | ||
for (String field : fields) { | ||
Map<String, String> facts = new HashMap<>(); | ||
String[] factFields = field.split(":"); | ||
facts.put("name", factFields[0]); | ||
facts.put("value", factFields.length == 1 ? "" : factFields[1].trim()); | ||
event.add(facts); | ||
} | ||
LOG.debug("Created list of facts"); | ||
return objectMapper.convertValue(event, JsonNode.class); | ||
|
||
} | ||
|
||
@VisibleForTesting | ||
List<MessageSummary> getMessageBacklog(EventNotificationContext ctx, TeamsEventNotificationConfig config) { | ||
List<MessageSummary> backlog = notificationCallbackService.getBacklogForEvent(ctx); | ||
if (config.backlogSize() > 0 && backlog != null) { | ||
return backlog.stream().limit(config.backlogSize()).collect(Collectors.toList()); | ||
} | ||
return backlog; | ||
} | ||
|
||
|
||
@VisibleForTesting | ||
Map<String, Object> getCustomMessageModel(EventNotificationContext ctx, String type, List<MessageSummary> backlog) { | ||
EventNotificationModelData modelData = EventNotificationModelData.of(ctx, backlog); | ||
|
||
LOG.debug("the custom message model data is {}", modelData); | ||
Map<String, Object> objectMap = objectMapper.convertValue(modelData, TypeReferences.MAP_STRING_OBJECT); | ||
objectMap.put("type", type); | ||
|
||
return objectMap; | ||
} | ||
|
||
public interface Factory extends EventNotification.Factory<TeamsEventNotification> { | ||
@Override | ||
TeamsEventNotification create(); | ||
} | ||
|
||
} |
154 changes: 154 additions & 0 deletions
154
...graylog/integrations/notifications/types/microsoftteams/TeamsEventNotificationConfig.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
/* | ||
* Copyright (C) 2020 Graylog, Inc. | ||
* | ||
* This program is free software: you can redistribute it and/or modify | ||
* it under the terms of the Server Side Public License, version 1, | ||
* as published by MongoDB, Inc. | ||
* | ||
* This program is distributed in the hope that it will be useful, | ||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
* Server Side Public License for more details. | ||
* | ||
* You should have received a copy of the Server Side Public License | ||
* along with this program. If not, see | ||
* <http://www.mongodb.com/licensing/server-side-public-license>. | ||
*/ | ||
package org.graylog.integrations.notifications.types.microsoftteams; | ||
|
||
import com.fasterxml.jackson.annotation.JsonCreator; | ||
import com.fasterxml.jackson.annotation.JsonIgnore; | ||
import com.fasterxml.jackson.annotation.JsonProperty; | ||
import com.fasterxml.jackson.annotation.JsonTypeName; | ||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize; | ||
import com.google.auto.value.AutoValue; | ||
import org.graylog.events.contentpack.entities.EventNotificationConfigEntity; | ||
import org.graylog.events.event.EventDto; | ||
import org.graylog.events.notifications.EventNotificationConfig; | ||
import org.graylog.events.notifications.EventNotificationExecutionJob; | ||
import org.graylog.scheduler.JobTriggerData; | ||
import org.graylog2.contentpacks.EntityDescriptorIds; | ||
import org.graylog2.contentpacks.model.entities.references.ValueReference; | ||
import org.graylog2.plugin.rest.ValidationResult; | ||
|
||
import javax.annotation.Nullable; | ||
import javax.validation.constraints.NotBlank; | ||
import java.net.URI; | ||
import java.util.regex.Pattern; | ||
|
||
@AutoValue | ||
@JsonTypeName(TeamsEventNotificationConfig.TYPE_NAME) | ||
@JsonDeserialize(builder = TeamsEventNotificationConfig.Builder.class) | ||
public abstract class TeamsEventNotificationConfig implements EventNotificationConfig { | ||
|
||
public static final String TYPE_NAME = "teams-notification-v1"; | ||
|
||
private static final Pattern TEAMS_PATTERN = Pattern.compile("https://.*.webhook.office.com/"); | ||
private static final String DEFAULT_HEX_COLOR = "#ff0500"; | ||
private static final String DEFAULT_CUSTOM_MESSAGE = "Graylog Teams Notification"; | ||
private static final long DEFAULT_BACKLOG_SIZE = 0; | ||
|
||
static final String INVALID_BACKLOG_ERROR_MESSAGE = "Backlog size cannot be less than zero"; | ||
static final String INVALID_WEBHOOK_ERROR_MESSAGE = "Specified Webhook URL is not a valid URL"; | ||
static final String INVALID_TEAMS_URL_ERROR_MESSAGE = "Specified Webhook URL is not a valid Teams URL"; | ||
static final String WEB_HOOK_URL = "https://teams.webhook.office.com/services/xxxx/xxxxxxxxxxxxxxxxxxx"; | ||
static final String FIELD_WEBHOOK_URL = "webhook_url"; | ||
static final String TEAMS_CUSTOM_MESSAGE = "custom_message"; | ||
static final String TEAMS_ICON_URL = "icon_url"; | ||
static final String TEAMS_BACKLOG_SIZE = "backlog_size"; | ||
static final String TEAMS_COLOR = "color"; | ||
|
||
@JsonProperty(TEAMS_BACKLOG_SIZE) | ||
public abstract long backlogSize(); | ||
|
||
@JsonProperty(TEAMS_COLOR) | ||
@NotBlank | ||
public abstract String color(); | ||
|
||
@JsonProperty(FIELD_WEBHOOK_URL) | ||
@NotBlank | ||
public abstract String webhookUrl(); | ||
|
||
@JsonProperty(TEAMS_CUSTOM_MESSAGE) | ||
public abstract String customMessage(); | ||
|
||
@JsonProperty(TEAMS_ICON_URL) | ||
@Nullable | ||
public abstract String iconUrl(); | ||
|
||
@Override | ||
@JsonIgnore | ||
public JobTriggerData toJobTriggerData(EventDto dto) { | ||
return EventNotificationExecutionJob.Data.builder().eventDto(dto).build(); | ||
} | ||
|
||
public static Builder builder() { | ||
return Builder.create(); | ||
} | ||
|
||
@Override | ||
@JsonIgnore | ||
public ValidationResult validate() { | ||
ValidationResult validation = new ValidationResult(); | ||
|
||
URI webhookUri; | ||
try { | ||
webhookUri = new URI(webhookUrl()); | ||
if (webhookUri.getHost().toLowerCase().contains("office")) { | ||
if (!TEAMS_PATTERN.matcher(webhookUrl()).find()) { | ||
validation.addError(FIELD_WEBHOOK_URL, INVALID_TEAMS_URL_ERROR_MESSAGE); | ||
} | ||
} | ||
|
||
} catch (Exception ex) { | ||
validation.addError(FIELD_WEBHOOK_URL, INVALID_WEBHOOK_ERROR_MESSAGE); | ||
} | ||
|
||
if (backlogSize() < 0) { | ||
validation.addError(TEAMS_BACKLOG_SIZE, INVALID_BACKLOG_ERROR_MESSAGE); | ||
} | ||
|
||
return validation; | ||
} | ||
|
||
@AutoValue.Builder | ||
public static abstract class Builder implements EventNotificationConfig.Builder<Builder> { | ||
@JsonCreator | ||
public static Builder create() { | ||
|
||
return new AutoValue_TeamsEventNotificationConfig.Builder() | ||
.type(TYPE_NAME) | ||
.color(DEFAULT_HEX_COLOR) | ||
.webhookUrl(WEB_HOOK_URL) | ||
.customMessage(DEFAULT_CUSTOM_MESSAGE) | ||
.backlogSize(DEFAULT_BACKLOG_SIZE); | ||
} | ||
|
||
@JsonProperty(TEAMS_COLOR) | ||
public abstract Builder color(String color); | ||
|
||
@JsonProperty(FIELD_WEBHOOK_URL) | ||
public abstract Builder webhookUrl(String webhookUrl); | ||
|
||
@JsonProperty(TEAMS_CUSTOM_MESSAGE) | ||
public abstract Builder customMessage(String customMessage); | ||
|
||
@JsonProperty(TEAMS_ICON_URL) | ||
public abstract Builder iconUrl(String iconUrl); | ||
|
||
@JsonProperty(TEAMS_BACKLOG_SIZE) | ||
public abstract Builder backlogSize(long backlogSize); | ||
|
||
public abstract TeamsEventNotificationConfig build(); | ||
} | ||
|
||
@Override | ||
public EventNotificationConfigEntity toContentPackEntity(EntityDescriptorIds entityDescriptorIds) { | ||
return TeamsEventNotificationConfigEntity.builder() | ||
.color(ValueReference.of(color())) | ||
.webhookUrl(ValueReference.of(webhookUrl())) | ||
.customMessage(ValueReference.of(customMessage())) | ||
.iconUrl(ValueReference.of(iconUrl())) | ||
.build(); | ||
} | ||
} |
Oops, something went wrong.