Skip to content

Commit

Permalink
Format webhook messages as attachments
Browse files Browse the repository at this point in the history
  • Loading branch information
Jason Campos committed Dec 16, 2016
1 parent 0f870a2 commit 2a432da
Show file tree
Hide file tree
Showing 3 changed files with 167 additions and 58 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -125,12 +127,11 @@ public void sendNotification(Check check, Subscription subscription, List<Alert>
}

private HttpEntity createJsonEntity(Check check, Subscription subscription, List<Alert> alerts) throws JsonProcessingException {
Map<String,String> payload = new HashMap<String, String>();
Map<String,Object> payload = new HashMap<String, Object>();
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()) {
Expand All @@ -157,12 +158,9 @@ private HttpEntity createFormEntity(Check check, Subscription subscription, List

private String formatForWebApi(Check check, Subscription subscription, List<Alert> alerts) {
String url = formatCheckUrl(check);
String alertsString = formatAlert(alerts);

String alertsString = formatAlertsForText(alerts);
String channel = subscription.getTarget().contains("!") ? "<!channel>" : "";

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",
Expand All @@ -177,53 +175,96 @@ private String formatForWebApi(Check check, Subscription subscription, List<Aler
);
}

private String formatForWebhook(Check check, Subscription subscription, List<Alert> 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<Map<String,Object>> formatForWebhook(Check check, Subscription subscription, List<Alert> alerts) {
List<Map<String,Object>> attachments = new ArrayList<Map<String,Object>>();
for (Alert alert: alerts) {
Map<String,Object> attachment = new HashMap<String,Object>();
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<String> extractEmojis() {
List<String> emojis = Lists.newArrayList(
Splitter.on(',').omitEmptyStrings().trimResults().split(seyrenConfig.getSlackEmojis())
);
return emojis;
private String formatAlertsForText(List<Alert> alerts) {
return Joiner.on("\n").join(transform(alerts, new Function<Alert, String>() {
@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<Map<String,Object>> formatAlertForWebhook(Check check, Alert alert) {
List<Map<String,Object>> fields = new ArrayList<Map<String,Object>>();

if (check.getDescription() != null && !check.getDescription().isEmpty()) {
Map<String,Object> description = new HashMap<String,Object>();
description.put("title", "Description");
description.put("value", check.getDescription());
description.put("short", false);
fields.add(description);
}

Map<String,Object> trigger = new HashMap<String,Object>();
trigger.put("title", "Trigger");
trigger.put("value", String.format("`%s = %s`", alert.getTarget(), alert.getValue().toString()));
trigger.put("short", false);
fields.add(trigger);

Map<String,Object> from = new HashMap<String,Object>();
from.put("title", "From");
from.put("value", alert.getFromType().toString());
from.put("short", true);
fields.add(from);

Map<String,Object> to = new HashMap<String,Object>();
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<Alert> alerts) {
String alertsString = Joiner.on("\n").join(transform(alerts, new Function<Alert, String>() {
@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<String> 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();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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", "");
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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");
Expand All @@ -189,24 +190,55 @@ public void useSlackWebHookTest() throws JsonParseException, JsonMappingExceptio
String content = bodyCapture.getContent();
assertThat(content, is(notNullValue()));

Map<String,String> map = new HashMap<String,String>();
Map<String,Object> map = new HashMap<String,Object>();
ObjectMapper mapper = new ObjectMapper();
TypeReference<HashMap<String,Object>> typeRef = new TypeReference<HashMap<String,Object>>() {};
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<Map<String,Object>> attachments = (List<Map<String,Object>>) map.get("attachments");
Map<String,Object> attachment = (Map<String, Object>) 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<Map<String,Object>> fields = (List<Map<String, Object>>) attachment.get("fields");

// There should be four fields: description, trigger, from, and to
assertThat(fields.size(), is(4));

for(Map<String,Object> 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() {
Check check = new Check()
.withId("123")
.withEnabled(true)
.withName("test-check")
.withDescription("A description")
.withState(AlertType.ERROR);
return check;
}
Expand All @@ -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)
Expand Down

0 comments on commit 2a432da

Please sign in to comment.