From 2a432da0328837b1ca51068cdbce49a05e2887d2 Mon Sep 17 00:00:00 2001 From: Jason Campos Date: Thu, 15 Dec 2016 20:41:06 -0800 Subject: [PATCH] Format webhook messages as attachments --- .../SlackNotificationService.java | 139 ++++++++++++------ .../seyren/core/util/config/SeyrenConfig.java | 35 +++++ .../SlackNotificationServiceTest.java | 51 +++++-- 3 files changed, 167 insertions(+), 58 deletions(-) diff --git a/seyren-core/src/main/java/com/seyren/core/service/notification/SlackNotificationService.java b/seyren-core/src/main/java/com/seyren/core/service/notification/SlackNotificationService.java index 1fcedc40..534e4e34 100644 --- a/seyren-core/src/main/java/com/seyren/core/service/notification/SlackNotificationService.java +++ b/seyren-core/src/main/java/com/seyren/core/service/notification/SlackNotificationService.java @@ -17,7 +17,9 @@ import java.io.UnsupportedEncodingException; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; @@ -125,12 +127,11 @@ public void sendNotification(Check check, Subscription subscription, List } private HttpEntity createJsonEntity(Check check, Subscription subscription, List alerts) throws JsonProcessingException { - Map payload = new HashMap(); + Map payload = new HashMap(); payload.put("channel", subscription.getTarget()); payload.put("username", seyrenConfig.getSlackUsername()); - payload.put("text", formatForWebhook(check, subscription, alerts)); payload.put("icon_url", seyrenConfig.getSlackIconUrl()); - + payload.put("attachments", formatForWebhook(check, subscription, alerts)); String message = new ObjectMapper().writeValueAsString(payload); if (LOGGER.isDebugEnabled()) { @@ -157,12 +158,9 @@ private HttpEntity createFormEntity(Check check, Subscription subscription, List private String formatForWebApi(Check check, Subscription subscription, List alerts) { String url = formatCheckUrl(check); - String alertsString = formatAlert(alerts); - + String alertsString = formatAlertsForText(alerts); String channel = subscription.getTarget().contains("!") ? "" : ""; - String description = formatDescription(check); - final String state = check.getState().toString(); return String.format("%s*%s* %s [%s]%s\n```\n%s\n```\n#%s %s", @@ -177,53 +175,96 @@ private String formatForWebApi(Check check, Subscription subscription, List alerts) { - String url = formatCheckUrl(check); - String alertsString = formatAlert(alerts); - - String description = formatDescription(check); - - final String state = check.getState().toString(); - - return String.format("%s *%s* %s (<%s|Open>)%s\n```\n%s\n```", - Iterables.get(extractEmojis(), check.getState().ordinal(), ""), - state, - check.getName(), - url, - description, - alertsString - ); + private List> formatForWebhook(Check check, Subscription subscription, List alerts) { + List> attachments = new ArrayList>(); + for (Alert alert: alerts) { + Map attachment = new HashMap(); + attachment.put("mrkdwn_in", Arrays.asList("fields", "text", "pretext")); + attachment.put("fallback", String.format("An alert has been triggered for '%s'",check.getName())); + attachment.put("color", formatColor(check)); + attachment.put("title", check.getName()); + attachment.put("title_link", formatCheckUrl(check)); + attachment.put("fields", formatAlertForWebhook(check, alert)); + attachments.add(attachment); + } + return attachments; } - private List extractEmojis() { - List emojis = Lists.newArrayList( - Splitter.on(',').omitEmptyStrings().trimResults().split(seyrenConfig.getSlackEmojis()) - ); - return emojis; + private String formatAlertsForText(List alerts) { + return Joiner.on("\n").join(transform(alerts, new Function() { + @Override + public String apply(Alert input) { + return String.format("%s = %s (%s to %s)", input.getTarget(), input.getValue().toString(), input.getFromType(), input.getToType()); + } + })); } + + private List> formatAlertForWebhook(Check check, Alert alert) { + List> fields = new ArrayList>(); + + if (check.getDescription() != null && !check.getDescription().isEmpty()) { + Map description = new HashMap(); + description.put("title", "Description"); + description.put("value", check.getDescription()); + description.put("short", false); + fields.add(description); + } + + Map trigger = new HashMap(); + trigger.put("title", "Trigger"); + trigger.put("value", String.format("`%s = %s`", alert.getTarget(), alert.getValue().toString())); + trigger.put("short", false); + fields.add(trigger); + + Map from = new HashMap(); + from.put("title", "From"); + from.put("value", alert.getFromType().toString()); + from.put("short", true); + fields.add(from); + + Map to = new HashMap(); + to.put("title", "To"); + to.put("value", alert.getToType().toString()); + to.put("short", true); + fields.add(to); + + return fields; + } - private String formatCheckUrl(Check check) { - String url = String.format("%s/#/checks/%s", seyrenConfig.getBaseUrl(), check.getId()); - return url; + private String formatDescription(Check check) { + String description; + if (StringUtils.isNotBlank(check.getDescription())) { + description = String.format("\n> %s", check.getDescription()); + } else { + description = ""; + } + return description; } - - private String formatAlert(List alerts) { - String alertsString = Joiner.on("\n").join(transform(alerts, new Function() { - @Override - public String apply(Alert input) { - return String.format("%s = %s (%s to %s)", input.getTarget(), input.getValue().toString(), input.getFromType(), input.getToType()); - } - })); - return alertsString; + + private List extractEmojis() { + return Lists.newArrayList( + Splitter.on(',').omitEmptyStrings().trimResults().split(seyrenConfig.getSlackEmojis()) + ); + } + + private String formatCheckUrl(Check check) { + return String.format("%s/#/checks/%s", seyrenConfig.getBaseUrl(), check.getId()); } - - private String formatDescription(Check check) { - String description; - if (StringUtils.isNotBlank(check.getDescription())) { - description = String.format("\n> %s", check.getDescription()); - } else { - description = ""; - } - return description; + + private String formatColor(Check check) { + switch(check.getState()) { + case ERROR: + return seyrenConfig.getSlackDangerColor(); + case EXCEPTION: + return seyrenConfig.getSlackExceptionColor(); + case OK: + return seyrenConfig.getSlackGoodColor(); + case UNKNOWN: + return seyrenConfig.getSlackUnknownColor(); + case WARN: + return seyrenConfig.getSlackWarningColor(); + default: + return seyrenConfig.getSlackUnknownColor(); + } } } diff --git a/seyren-core/src/main/java/com/seyren/core/util/config/SeyrenConfig.java b/seyren-core/src/main/java/com/seyren/core/util/config/SeyrenConfig.java index 8c44d79b..5d6786d9 100644 --- a/seyren-core/src/main/java/com/seyren/core/util/config/SeyrenConfig.java +++ b/seyren-core/src/main/java/com/seyren/core/util/config/SeyrenConfig.java @@ -75,6 +75,11 @@ public class SeyrenConfig { private final String slackUsername; private final String slackIconUrl; private final String slackEmojis; + private final String slackGoodColor; + private final String slackWarningColor; + private final String slackDangerColor; + private final String slackExceptionColor; + private final String slackUnknownColor; private final String pushoverAppApiToken; private final String snmpHost; private final Integer snmpPort; @@ -151,6 +156,11 @@ public SeyrenConfig() { this.slackUsername = configOrDefault("SLACK_USERNAME", "Seyren"); this.slackIconUrl = configOrDefault("SLACK_ICON_URL", ""); this.slackEmojis = configOrDefault("SLACK_EMOJIS", ""); + this.slackGoodColor = configOrDefault("SLACK_ATTACHMENT_GOOD_COLOR", "good"); + this.slackWarningColor = configOrDefault("SLACK_ATTACHMENT_WARN_COLOR", "warning"); + this.slackDangerColor = configOrDefault("SLACK_ATTACHMENT_DANGER_COLOR", "danger"); + this.slackExceptionColor = configOrDefault("SLACK_ATTACHMENT_EXCEPTION_COLOR", "danger"); + this.slackUnknownColor = configOrDefault("SLACK_ATTACHMENT_UNKNOWN_COLOR", ""); // PushOver this.pushoverAppApiToken = configOrDefault("PUSHOVER_APP_API_TOKEN", ""); @@ -417,7 +427,32 @@ public String getSlackIconUrl() { public String getSlackEmojis() { return slackEmojis; } + + @JsonIgnore + public String getSlackDangerColor() { + return slackDangerColor; + } + + @JsonIgnore + public String getSlackWarningColor() { + return slackWarningColor; + } + + @JsonIgnore + public String getSlackGoodColor() { + return slackGoodColor; + } + @JsonIgnore + public String getSlackExceptionColor() { + return slackExceptionColor; + } + + @JsonIgnore + public String getSlackUnknownColor() { + return slackUnknownColor; + } + @JsonIgnore public int getNoOfThreads() { return noOfThreads; diff --git a/seyren-core/src/test/java/com/seyren/core/service/notification/SlackNotificationServiceTest.java b/seyren-core/src/test/java/com/seyren/core/service/notification/SlackNotificationServiceTest.java index 17282f62..b0397713 100644 --- a/seyren-core/src/test/java/com/seyren/core/service/notification/SlackNotificationServiceTest.java +++ b/seyren-core/src/test/java/com/seyren/core/service/notification/SlackNotificationServiceTest.java @@ -20,7 +20,7 @@ import static org.hamcrest.Matchers.isEmptyString; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.notNullValue; -import static org.junit.Assert.assertThat; +import static org.junit.Assert.*; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -165,7 +165,8 @@ public void useSlackWebHookTest() throws JsonParseException, JsonMappingExceptio // Given when(mockSeyrenConfig.getSlackToken()).thenReturn(""); when(mockSeyrenConfig.getSlackWebhook()).thenReturn(clientDriver.getBaseUrl() + SLACK_WEBHOOK_URI_TO_POST); - + when(mockSeyrenConfig.getSlackDangerColor()).thenReturn("danger"); + Check check = givenCheck(); Subscription subscription = givenSlackSubscriptionWithTarget("target"); @@ -189,17 +190,47 @@ public void useSlackWebHookTest() throws JsonParseException, JsonMappingExceptio String content = bodyCapture.getContent(); assertThat(content, is(notNullValue())); - Map map = new HashMap(); + Map map = new HashMap(); ObjectMapper mapper = new ObjectMapper(); TypeReference> typeRef = new TypeReference>() {}; map = mapper.readValue(content, typeRef); - assertThat(map.get("channel"), is(subscription.getTarget())); - assertThat(map.get("text"), containsString("*" + check.getState().name() + "* ")); - assertThat(map.get("text"), containsString("/#/checks/" + check.getId())); - assertThat(map.get("text"), containsString(check.getName())); - assertThat(map.get("username"), is(SLACK_USERNAME)); - assertThat(map.get("icon_url"), isEmptyString()); + assertThat((String) map.get("channel"), is(subscription.getTarget())); + assertThat((String) map.get("username"), is(SLACK_USERNAME)); + assertThat((String) map.get("icon_url"), isEmptyString()); + + @SuppressWarnings("unchecked") + List> attachments = (List>) map.get("attachments"); + Map attachment = (Map) attachments.get(0); + + assertThat((String)attachment.get("fallback"), containsString(check.getName())); + assertThat((String)attachment.get("title"), is(check.getName())); + assertThat((String)attachment.get("color"), is("danger")); // AlertType.ERROR + assertThat((String)attachment.get("title_link"), containsString("/#/checks/" + check.getId())); + + @SuppressWarnings("unchecked") + List> fields = (List>) attachment.get("fields"); + + // There should be four fields: description, trigger, from, and to + assertThat(fields.size(), is(4)); + + for(Map field: fields) { + if ("Description".equals(field.get("title"))) { + assertThat((String)field.get("value"), is("A description")); + assertThat((Boolean)field.get("short"), is(false)); + } else if ("Trigger".equals(field.get("title"))) { + assertThat((String)field.get("value"), is("`some.graphite.target = 1.0`")); + assertThat((Boolean)field.get("short"), is(false)); + } else if ("From".equals(field.get("title"))) { + assertThat((String)field.get("value"), is("OK")); + assertThat((Boolean)field.get("short"), is(true)); + } else if ("To".equals(field.get("title"))) { + assertThat((String)field.get("value"), is("ERROR")); + assertThat((Boolean)field.get("short"), is(true)); + } else { + fail("Unexpected field " + field.get("title")); + } + } } Check givenCheck() { @@ -207,6 +238,7 @@ Check givenCheck() { .withId("123") .withEnabled(true) .withName("test-check") + .withDescription("A description") .withState(AlertType.ERROR); return check; } @@ -221,6 +253,7 @@ Subscription givenSlackSubscriptionWithTarget(String target) { Alert givenAlert() { Alert alert = new Alert() + .withTarget("some.graphite.target") .withValue(new BigDecimal("1.0")) .withTimestamp(new DateTime()) .withFromType(AlertType.OK)