From 33602fdfda0608ba03775c825c73570444d32e90 Mon Sep 17 00:00:00 2001 From: Vichheann Saing Date: Tue, 29 Mar 2016 21:41:52 +0200 Subject: [PATCH 1/4] Use Slack webhook instead of API token --- README.md | 4 +- .../SlackNotificationService.java | 60 ++++--- .../seyren/core/util/config/SeyrenConfig.java | 8 +- .../SlackNotificationServiceTest.java | 146 +++++++----------- 4 files changed, 88 insertions(+), 130 deletions(-) diff --git a/README.md b/README.md index 6766af78..c6bb5082 100644 --- a/README.md +++ b/README.md @@ -109,9 +109,9 @@ To generate a "Service API Key", see [PagerDuty Support: Adding Services](https: ##### [Slack](https://www.slack.com) -The target for a Slack subscription will be the channel name (including the `#`, for example `#channel`). You can optionally suffix the channel name with `!` and that will cause the alerts to include a `@channel` mention (for example `#channel!`). +The target for a Slack subscription will be the channel name (e.g. `#channel` or `@channel`). -* `SLACK_TOKEN` - The Slack api auth token. Default: `` +* `SLACK_WEBHOOK_URL` - The Slack webhook URL. Default: `` * `SLACK_USERNAME` - The username that messages will be sent to slack. Default: `Seyren` * `SLACK_ICON_URL` - The user icon URL. Default: `` * `SLACK_EMOJIS` - Mapping between state and emojis unicode. Default: `` 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 68e61627..670fd20e 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 @@ -13,11 +13,11 @@ */ package com.seyren.core.service.notification; -import static com.google.common.collect.Iterables.*; +import static com.google.common.collect.Iterables.transform; -import java.util.ArrayList; +import java.util.HashMap; import java.util.List; -import java.util.Locale; +import java.util.Map; import javax.inject.Inject; import javax.inject.Named; @@ -25,15 +25,17 @@ import org.apache.commons.lang.StringUtils; import org.apache.http.HttpResponse; import org.apache.http.client.HttpClient; -import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.utils.HttpClientUtils; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.BasicResponseHandler; import org.apache.http.impl.client.HttpClientBuilder; -import org.apache.http.message.BasicNameValuePair; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.base.Function; import com.google.common.base.Joiner; import com.google.common.base.Splitter; @@ -51,50 +53,33 @@ public class SlackNotificationService implements NotificationService { private static final Logger LOGGER = LoggerFactory.getLogger(SlackNotificationService.class); private final SeyrenConfig seyrenConfig; - private final String baseUrl; @Inject public SlackNotificationService(SeyrenConfig seyrenConfig) { this.seyrenConfig = seyrenConfig; - this.baseUrl = "https://slack.com"; - } - - protected SlackNotificationService(SeyrenConfig seyrenConfig, String baseUrl) { - this.seyrenConfig = seyrenConfig; - this.baseUrl = baseUrl; } @Override public void sendNotification(Check check, Subscription subscription, List alerts) throws NotificationFailedException { - String token = seyrenConfig.getSlackToken(); - String channel = subscription.getTarget(); - String username = seyrenConfig.getSlackUsername(); - String iconUrl = seyrenConfig.getSlackIconUrl(); + String webhookUrl = seyrenConfig.getSlackWebhook(); List emojis = Lists.newArrayList( Splitter.on(',').omitEmptyStrings().trimResults().split(seyrenConfig.getSlackEmojis()) ); - String url = String.format("%s/api/chat.postMessage", baseUrl); HttpClient client = HttpClientBuilder.create().useSystemProperties().build(); - HttpPost post = new HttpPost(url); + HttpPost post = new HttpPost(webhookUrl); post.addHeader("accept", "application/json"); - List parameters = new ArrayList(); - parameters.add(new BasicNameValuePair("token", token)); - parameters.add(new BasicNameValuePair("channel", StringUtils.removeEnd(channel, "!"))); - parameters.add(new BasicNameValuePair("text", formatContent(emojis, check, subscription, alerts))); - parameters.add(new BasicNameValuePair("username", username)); - parameters.add(new BasicNameValuePair("icon_url", iconUrl)); - try { - post.setEntity(new UrlEncodedFormEntity(parameters)); + String message = generateMessage(emojis, check, subscription, alerts); + post.setEntity(new StringEntity(message, ContentType.APPLICATION_JSON)); if (LOGGER.isDebugEnabled()) { - LOGGER.info("> parameters: {}", parameters); + LOGGER.info("> message: {}", message); } HttpResponse response = client.execute(post); if (LOGGER.isDebugEnabled()) { - LOGGER.info("> parameters: {}", parameters); + LOGGER.info("> message: {}", message); LOGGER.debug("Status: {}, Body: {}", response.getStatusLine(), new BasicResponseHandler().handleResponse(response)); } } catch (Exception e) { @@ -111,6 +96,17 @@ public boolean canHandle(SubscriptionType subscriptionType) { return subscriptionType == SubscriptionType.SLACK; } + private String generateMessage(List emojis, Check check, Subscription subscription, List alerts) throws JsonProcessingException { + Map payload = new HashMap(); + payload.put("channel", subscription.getTarget()); + payload.put("username", seyrenConfig.getSlackUsername()); + payload.put("text", formatContent(emojis, check, subscription, alerts)); + payload.put("icon_url", seyrenConfig.getSlackIconUrl()); + + String message = new ObjectMapper().writeValueAsString(payload); + return message; + } + private String formatContent(List emojis, Check check, Subscription subscription, List alerts) { String url = String.format("%s/#/checks/%s", seyrenConfig.getBaseUrl(), check.getId()); String alertsString = Joiner.on("\n").join(transform(alerts, new Function() { @@ -120,8 +116,6 @@ public String apply(Alert input) { } })); - String channel = subscription.getTarget().contains("!") ? "" : ""; - String description; if (StringUtils.isNotBlank(check.getDescription())) { description = String.format("\n> %s", check.getDescription()); @@ -131,15 +125,13 @@ public String apply(Alert input) { final String state = check.getState().toString(); - return String.format("%s*%s* %s [%s]%s\n```\n%s\n```\n#%s %s", + return String.format("%s *%s* %s (<%s|Open>)%s\n```\n%s\n```", Iterables.get(emojis, check.getState().ordinal(), ""), state, check.getName(), url, description, - alertsString, - state.toLowerCase(Locale.getDefault()), - channel + alertsString ); } } 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 9c221c1c..c18a9792 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 @@ -70,7 +70,7 @@ public class SeyrenConfig { private final String flowdockEmojis; private final String ircCatHost; private final String ircCatPort; - private final String slackToken; + private final String slackWebhook; private final String slackUsername; private final String slackIconUrl; private final String slackEmojis; @@ -145,7 +145,7 @@ public SeyrenConfig() { this.ircCatPort = configOrDefault("IRCCAT_PORT", "12345"); // Slack - this.slackToken = configOrDefault("SLACK_TOKEN", ""); + this.slackWebhook = configOrDefault("SLACK_WEBHOOK_URL", ""); this.slackUsername = configOrDefault("SLACK_USERNAME", "Seyren"); this.slackIconUrl = configOrDefault("SLACK_ICON_URL", ""); this.slackEmojis = configOrDefault("SLACK_EMOJIS", ""); @@ -392,8 +392,8 @@ public int getGraphiteSocketTimeout() { } @JsonIgnore - public String getSlackToken() { - return slackToken; + public String getSlackWebhook() { + return slackWebhook; } @JsonIgnore 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 00190953..bb2d4c6a 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 @@ -13,17 +13,20 @@ */ package com.seyren.core.service.notification; -import static com.github.restdriver.clientdriver.RestClientDriver.*; -import static org.hamcrest.Matchers.*; -import static org.junit.Assert.*; -import static org.mockito.Mockito.*; - -import java.io.UnsupportedEncodingException; +import static com.github.restdriver.clientdriver.RestClientDriver.giveEmptyResponse; +import static com.github.restdriver.clientdriver.RestClientDriver.onRequestTo; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.IOException; import java.math.BigDecimal; -import java.net.URLDecoder; -import java.net.URLEncoder; import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; import org.hamcrest.Matchers; import org.joda.time.DateTime; @@ -32,6 +35,10 @@ import org.junit.Rule; import org.junit.Test; +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; import com.github.restdriver.clientdriver.ClientDriverRequest; import com.github.restdriver.clientdriver.ClientDriverRule; import com.github.restdriver.clientdriver.capture.StringBodyCapture; @@ -43,6 +50,9 @@ import com.seyren.core.util.config.SeyrenConfig; public class SlackNotificationServiceTest { + private static final String USERNAME = "Seyren"; + private static final String SLACK_URI_TO_POST = "/services/SOMETHING/ANOTHERTHING/FINALTHING"; + private NotificationService notificationService; private SeyrenConfig mockSeyrenConfig; @@ -53,11 +63,11 @@ public class SlackNotificationServiceTest { public void before() { mockSeyrenConfig = mock(SeyrenConfig.class); when(mockSeyrenConfig.getBaseUrl()).thenReturn(clientDriver.getBaseUrl() + "/slack"); + when(mockSeyrenConfig.getSlackWebhook()).thenReturn(clientDriver.getBaseUrl() + SLACK_URI_TO_POST); when(mockSeyrenConfig.getSlackEmojis()).thenReturn(""); when(mockSeyrenConfig.getSlackIconUrl()).thenReturn(""); - when(mockSeyrenConfig.getSlackToken()).thenReturn(""); - when(mockSeyrenConfig.getSlackUsername()).thenReturn("Seyren"); - notificationService = new SlackNotificationService(mockSeyrenConfig, clientDriver.getBaseUrl()); + when(mockSeyrenConfig.getSlackUsername()).thenReturn(USERNAME); + notificationService = new SlackNotificationService(mockSeyrenConfig); } @After @@ -77,117 +87,73 @@ public void notifcationServiceCanOnlyHandleSlackSubscription() { } @Test - public void basicSlackTest() { - BigDecimal value = new BigDecimal("1.0"); + public void basicSlackTest() throws JsonParseException, JsonMappingException, IOException { + // Given + Check check = givenCheck(); - Check check = new Check() - .withId("123") - .withEnabled(true) - .withName("test-check") - .withState(AlertType.ERROR); - Subscription subscription = new Subscription() - .withEnabled(true) - .withType(SubscriptionType.SLACK) - .withTarget("target"); - Alert alert = new Alert() - .withValue(value) - .withTimestamp(new DateTime()) - .withFromType(AlertType.OK) - .withToType(AlertType.ERROR); + Subscription subscription = givenSubsciption(); + + Alert alert = givenAlert(); List alerts = Arrays.asList(alert); StringBodyCapture bodyCapture = new StringBodyCapture(); clientDriver.addExpectation( - onRequestTo("/api/chat.postMessage") + onRequestTo(SLACK_URI_TO_POST) .withMethod(ClientDriverRequest.Method.POST) .capturingBodyIn(bodyCapture) .withHeader("accept", "application/json"), giveEmptyResponse()); + // When notificationService.sendNotification(check, subscription, alerts); + // Then String content = bodyCapture.getContent(); - System.out.println(decode(content)); - assertThat(content, Matchers.containsString("token=")); - assertThat(content, Matchers.containsString("&channel=target")); - assertThat(content, not(Matchers.containsString(encode("")))); - assertThat(content, Matchers.containsString(encode("*ERROR* test-check"))); - assertThat(content, Matchers.containsString(encode("/#/checks/123"))); - assertThat(content, Matchers.containsString("&username=Seyren")); - assertThat(content, Matchers.containsString("&icon_url=")); + Map map = new HashMap(); + ObjectMapper mapper = new ObjectMapper(); + TypeReference> typeRef = new TypeReference>() {}; + map = mapper.readValue(content, typeRef); + + assertThat(map.get("channel"), Matchers.is(subscription.getTarget())); + assertThat(map.get("text"), Matchers.containsString("*" + check.getState().name() + "* ")); + assertThat(map.get("text"), Matchers.containsString("/#/checks/" + check.getId())); + assertThat(map.get("text"), Matchers.containsString(check.getName())); + assertThat(map.get("username"), Matchers.is(USERNAME)); + assertThat(map.get("icon_url"), Matchers.isEmptyString()); + verify(mockSeyrenConfig).getSlackWebhook(); verify(mockSeyrenConfig).getSlackEmojis(); verify(mockSeyrenConfig).getSlackIconUrl(); - verify(mockSeyrenConfig).getSlackToken(); verify(mockSeyrenConfig).getSlackUsername(); verify(mockSeyrenConfig).getBaseUrl(); } - @Test - public void mentionChannelWhenTargetContainsExclamationTest() { - BigDecimal value = new BigDecimal("1.0"); - + Check givenCheck() { Check check = new Check() .withId("123") .withEnabled(true) .withName("test-check") .withState(AlertType.ERROR); - Subscription subscription = new Subscription() + return check; + } + + Subscription givenSubsciption() { + Subscription subscription = new Subscription() .withEnabled(true) .withType(SubscriptionType.SLACK) - .withTarget("target!"); - Alert alert = new Alert() - .withValue(value) + .withTarget("target"); + return subscription; + } + + Alert givenAlert() { + Alert alert = new Alert() + .withValue(new BigDecimal("1.0")) .withTimestamp(new DateTime()) .withFromType(AlertType.OK) .withToType(AlertType.ERROR); - List alerts = Arrays.asList(alert); - - StringBodyCapture bodyCapture = new StringBodyCapture(); - - clientDriver.addExpectation( - onRequestTo("/api/chat.postMessage") - .withMethod(ClientDriverRequest.Method.POST) - .capturingBodyIn(bodyCapture) - .withHeader("accept", "application/json"), - giveEmptyResponse()); - - notificationService.sendNotification(check, subscription, alerts); - - String content = bodyCapture.getContent(); - System.out.println(decode(content)); - - assertThat(content, Matchers.containsString("token=")); - assertThat(content, Matchers.containsString("&channel=target")); - assertThat(content, Matchers.containsString(encode(""))); - assertThat(content, Matchers.containsString(encode("*ERROR* test-check"))); - assertThat(content, Matchers.containsString(encode("/#/checks/123"))); - assertThat(content, Matchers.containsString("&username=Seyren")); - assertThat(content, Matchers.containsString("&icon_url=")); - - verify(mockSeyrenConfig).getSlackEmojis(); - verify(mockSeyrenConfig).getSlackIconUrl(); - verify(mockSeyrenConfig).getSlackToken(); - verify(mockSeyrenConfig).getSlackUsername(); - verify(mockSeyrenConfig).getBaseUrl(); - } - - String encode(String data) { - try { - return URLEncoder.encode(data, "ISO-8859-1"); - } catch (UnsupportedEncodingException e) { - return null; - } - } - - String decode(String data) { - try { - return URLDecoder.decode(data, "ISO-8859-1"); - } catch (UnsupportedEncodingException e) { - return null; - } + return alert; } } From 0299968033e94c434815a99bb5a81199aa585e8b Mon Sep 17 00:00:00 2001 From: Vichheann Saing Date: Sat, 28 May 2016 19:30:10 +0200 Subject: [PATCH 2/4] Revert "Use Slack webhook instead of API token" This reverts commit a25c1dc9531405bcbfe305ea176c9d087e4ece3b. --- README.md | 4 +- .../SlackNotificationService.java | 60 +++---- .../seyren/core/util/config/SeyrenConfig.java | 8 +- .../SlackNotificationServiceTest.java | 146 +++++++++++------- 4 files changed, 130 insertions(+), 88 deletions(-) diff --git a/README.md b/README.md index c6bb5082..6766af78 100644 --- a/README.md +++ b/README.md @@ -109,9 +109,9 @@ To generate a "Service API Key", see [PagerDuty Support: Adding Services](https: ##### [Slack](https://www.slack.com) -The target for a Slack subscription will be the channel name (e.g. `#channel` or `@channel`). +The target for a Slack subscription will be the channel name (including the `#`, for example `#channel`). You can optionally suffix the channel name with `!` and that will cause the alerts to include a `@channel` mention (for example `#channel!`). -* `SLACK_WEBHOOK_URL` - The Slack webhook URL. Default: `` +* `SLACK_TOKEN` - The Slack api auth token. Default: `` * `SLACK_USERNAME` - The username that messages will be sent to slack. Default: `Seyren` * `SLACK_ICON_URL` - The user icon URL. Default: `` * `SLACK_EMOJIS` - Mapping between state and emojis unicode. Default: `` 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 670fd20e..68e61627 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 @@ -13,11 +13,11 @@ */ package com.seyren.core.service.notification; -import static com.google.common.collect.Iterables.transform; +import static com.google.common.collect.Iterables.*; -import java.util.HashMap; +import java.util.ArrayList; import java.util.List; -import java.util.Map; +import java.util.Locale; import javax.inject.Inject; import javax.inject.Named; @@ -25,17 +25,15 @@ import org.apache.commons.lang.StringUtils; import org.apache.http.HttpResponse; import org.apache.http.client.HttpClient; +import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.utils.HttpClientUtils; -import org.apache.http.entity.ContentType; -import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.BasicResponseHandler; import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.message.BasicNameValuePair; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.base.Function; import com.google.common.base.Joiner; import com.google.common.base.Splitter; @@ -53,33 +51,50 @@ public class SlackNotificationService implements NotificationService { private static final Logger LOGGER = LoggerFactory.getLogger(SlackNotificationService.class); private final SeyrenConfig seyrenConfig; + private final String baseUrl; @Inject public SlackNotificationService(SeyrenConfig seyrenConfig) { this.seyrenConfig = seyrenConfig; + this.baseUrl = "https://slack.com"; + } + + protected SlackNotificationService(SeyrenConfig seyrenConfig, String baseUrl) { + this.seyrenConfig = seyrenConfig; + this.baseUrl = baseUrl; } @Override public void sendNotification(Check check, Subscription subscription, List alerts) throws NotificationFailedException { - String webhookUrl = seyrenConfig.getSlackWebhook(); + String token = seyrenConfig.getSlackToken(); + String channel = subscription.getTarget(); + String username = seyrenConfig.getSlackUsername(); + String iconUrl = seyrenConfig.getSlackIconUrl(); List emojis = Lists.newArrayList( Splitter.on(',').omitEmptyStrings().trimResults().split(seyrenConfig.getSlackEmojis()) ); + String url = String.format("%s/api/chat.postMessage", baseUrl); HttpClient client = HttpClientBuilder.create().useSystemProperties().build(); - HttpPost post = new HttpPost(webhookUrl); + HttpPost post = new HttpPost(url); post.addHeader("accept", "application/json"); + List parameters = new ArrayList(); + parameters.add(new BasicNameValuePair("token", token)); + parameters.add(new BasicNameValuePair("channel", StringUtils.removeEnd(channel, "!"))); + parameters.add(new BasicNameValuePair("text", formatContent(emojis, check, subscription, alerts))); + parameters.add(new BasicNameValuePair("username", username)); + parameters.add(new BasicNameValuePair("icon_url", iconUrl)); + try { - String message = generateMessage(emojis, check, subscription, alerts); - post.setEntity(new StringEntity(message, ContentType.APPLICATION_JSON)); + post.setEntity(new UrlEncodedFormEntity(parameters)); if (LOGGER.isDebugEnabled()) { - LOGGER.info("> message: {}", message); + LOGGER.info("> parameters: {}", parameters); } HttpResponse response = client.execute(post); if (LOGGER.isDebugEnabled()) { - LOGGER.info("> message: {}", message); + LOGGER.info("> parameters: {}", parameters); LOGGER.debug("Status: {}, Body: {}", response.getStatusLine(), new BasicResponseHandler().handleResponse(response)); } } catch (Exception e) { @@ -96,17 +111,6 @@ public boolean canHandle(SubscriptionType subscriptionType) { return subscriptionType == SubscriptionType.SLACK; } - private String generateMessage(List emojis, Check check, Subscription subscription, List alerts) throws JsonProcessingException { - Map payload = new HashMap(); - payload.put("channel", subscription.getTarget()); - payload.put("username", seyrenConfig.getSlackUsername()); - payload.put("text", formatContent(emojis, check, subscription, alerts)); - payload.put("icon_url", seyrenConfig.getSlackIconUrl()); - - String message = new ObjectMapper().writeValueAsString(payload); - return message; - } - private String formatContent(List emojis, Check check, Subscription subscription, List alerts) { String url = String.format("%s/#/checks/%s", seyrenConfig.getBaseUrl(), check.getId()); String alertsString = Joiner.on("\n").join(transform(alerts, new Function() { @@ -116,6 +120,8 @@ public String apply(Alert input) { } })); + String channel = subscription.getTarget().contains("!") ? "" : ""; + String description; if (StringUtils.isNotBlank(check.getDescription())) { description = String.format("\n> %s", check.getDescription()); @@ -125,13 +131,15 @@ public String apply(Alert input) { final String state = check.getState().toString(); - return String.format("%s *%s* %s (<%s|Open>)%s\n```\n%s\n```", + return String.format("%s*%s* %s [%s]%s\n```\n%s\n```\n#%s %s", Iterables.get(emojis, check.getState().ordinal(), ""), state, check.getName(), url, description, - alertsString + alertsString, + state.toLowerCase(Locale.getDefault()), + channel ); } } 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 c18a9792..9c221c1c 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 @@ -70,7 +70,7 @@ public class SeyrenConfig { private final String flowdockEmojis; private final String ircCatHost; private final String ircCatPort; - private final String slackWebhook; + private final String slackToken; private final String slackUsername; private final String slackIconUrl; private final String slackEmojis; @@ -145,7 +145,7 @@ public SeyrenConfig() { this.ircCatPort = configOrDefault("IRCCAT_PORT", "12345"); // Slack - this.slackWebhook = configOrDefault("SLACK_WEBHOOK_URL", ""); + this.slackToken = configOrDefault("SLACK_TOKEN", ""); this.slackUsername = configOrDefault("SLACK_USERNAME", "Seyren"); this.slackIconUrl = configOrDefault("SLACK_ICON_URL", ""); this.slackEmojis = configOrDefault("SLACK_EMOJIS", ""); @@ -392,8 +392,8 @@ public int getGraphiteSocketTimeout() { } @JsonIgnore - public String getSlackWebhook() { - return slackWebhook; + public String getSlackToken() { + return slackToken; } @JsonIgnore 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 bb2d4c6a..00190953 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 @@ -13,20 +13,17 @@ */ package com.seyren.core.service.notification; -import static com.github.restdriver.clientdriver.RestClientDriver.giveEmptyResponse; -import static com.github.restdriver.clientdriver.RestClientDriver.onRequestTo; -import static org.hamcrest.Matchers.is; -import static org.junit.Assert.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.io.IOException; +import static com.github.restdriver.clientdriver.RestClientDriver.*; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import java.io.UnsupportedEncodingException; import java.math.BigDecimal; +import java.net.URLDecoder; +import java.net.URLEncoder; import java.util.Arrays; -import java.util.HashMap; import java.util.List; -import java.util.Map; import org.hamcrest.Matchers; import org.joda.time.DateTime; @@ -35,10 +32,6 @@ import org.junit.Rule; import org.junit.Test; -import com.fasterxml.jackson.core.JsonParseException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.JsonMappingException; -import com.fasterxml.jackson.databind.ObjectMapper; import com.github.restdriver.clientdriver.ClientDriverRequest; import com.github.restdriver.clientdriver.ClientDriverRule; import com.github.restdriver.clientdriver.capture.StringBodyCapture; @@ -50,9 +43,6 @@ import com.seyren.core.util.config.SeyrenConfig; public class SlackNotificationServiceTest { - private static final String USERNAME = "Seyren"; - private static final String SLACK_URI_TO_POST = "/services/SOMETHING/ANOTHERTHING/FINALTHING"; - private NotificationService notificationService; private SeyrenConfig mockSeyrenConfig; @@ -63,11 +53,11 @@ public class SlackNotificationServiceTest { public void before() { mockSeyrenConfig = mock(SeyrenConfig.class); when(mockSeyrenConfig.getBaseUrl()).thenReturn(clientDriver.getBaseUrl() + "/slack"); - when(mockSeyrenConfig.getSlackWebhook()).thenReturn(clientDriver.getBaseUrl() + SLACK_URI_TO_POST); when(mockSeyrenConfig.getSlackEmojis()).thenReturn(""); when(mockSeyrenConfig.getSlackIconUrl()).thenReturn(""); - when(mockSeyrenConfig.getSlackUsername()).thenReturn(USERNAME); - notificationService = new SlackNotificationService(mockSeyrenConfig); + when(mockSeyrenConfig.getSlackToken()).thenReturn(""); + when(mockSeyrenConfig.getSlackUsername()).thenReturn("Seyren"); + notificationService = new SlackNotificationService(mockSeyrenConfig, clientDriver.getBaseUrl()); } @After @@ -87,73 +77,117 @@ public void notifcationServiceCanOnlyHandleSlackSubscription() { } @Test - public void basicSlackTest() throws JsonParseException, JsonMappingException, IOException { - // Given - Check check = givenCheck(); - - Subscription subscription = givenSubsciption(); + public void basicSlackTest() { + BigDecimal value = new BigDecimal("1.0"); - Alert alert = givenAlert(); + Check check = new Check() + .withId("123") + .withEnabled(true) + .withName("test-check") + .withState(AlertType.ERROR); + Subscription subscription = new Subscription() + .withEnabled(true) + .withType(SubscriptionType.SLACK) + .withTarget("target"); + Alert alert = new Alert() + .withValue(value) + .withTimestamp(new DateTime()) + .withFromType(AlertType.OK) + .withToType(AlertType.ERROR); List alerts = Arrays.asList(alert); StringBodyCapture bodyCapture = new StringBodyCapture(); clientDriver.addExpectation( - onRequestTo(SLACK_URI_TO_POST) + onRequestTo("/api/chat.postMessage") .withMethod(ClientDriverRequest.Method.POST) .capturingBodyIn(bodyCapture) .withHeader("accept", "application/json"), giveEmptyResponse()); - // When notificationService.sendNotification(check, subscription, alerts); - // Then String content = bodyCapture.getContent(); + System.out.println(decode(content)); - Map map = new HashMap(); - ObjectMapper mapper = new ObjectMapper(); - TypeReference> typeRef = new TypeReference>() {}; - map = mapper.readValue(content, typeRef); - - assertThat(map.get("channel"), Matchers.is(subscription.getTarget())); - assertThat(map.get("text"), Matchers.containsString("*" + check.getState().name() + "* ")); - assertThat(map.get("text"), Matchers.containsString("/#/checks/" + check.getId())); - assertThat(map.get("text"), Matchers.containsString(check.getName())); - assertThat(map.get("username"), Matchers.is(USERNAME)); - assertThat(map.get("icon_url"), Matchers.isEmptyString()); + assertThat(content, Matchers.containsString("token=")); + assertThat(content, Matchers.containsString("&channel=target")); + assertThat(content, not(Matchers.containsString(encode("")))); + assertThat(content, Matchers.containsString(encode("*ERROR* test-check"))); + assertThat(content, Matchers.containsString(encode("/#/checks/123"))); + assertThat(content, Matchers.containsString("&username=Seyren")); + assertThat(content, Matchers.containsString("&icon_url=")); - verify(mockSeyrenConfig).getSlackWebhook(); verify(mockSeyrenConfig).getSlackEmojis(); verify(mockSeyrenConfig).getSlackIconUrl(); + verify(mockSeyrenConfig).getSlackToken(); verify(mockSeyrenConfig).getSlackUsername(); verify(mockSeyrenConfig).getBaseUrl(); } - Check givenCheck() { + @Test + public void mentionChannelWhenTargetContainsExclamationTest() { + BigDecimal value = new BigDecimal("1.0"); + Check check = new Check() .withId("123") .withEnabled(true) .withName("test-check") .withState(AlertType.ERROR); - return check; - } - - Subscription givenSubsciption() { - Subscription subscription = new Subscription() + Subscription subscription = new Subscription() .withEnabled(true) .withType(SubscriptionType.SLACK) - .withTarget("target"); - return subscription; - } - - Alert givenAlert() { - Alert alert = new Alert() - .withValue(new BigDecimal("1.0")) + .withTarget("target!"); + Alert alert = new Alert() + .withValue(value) .withTimestamp(new DateTime()) .withFromType(AlertType.OK) .withToType(AlertType.ERROR); - return alert; + List alerts = Arrays.asList(alert); + + StringBodyCapture bodyCapture = new StringBodyCapture(); + + clientDriver.addExpectation( + onRequestTo("/api/chat.postMessage") + .withMethod(ClientDriverRequest.Method.POST) + .capturingBodyIn(bodyCapture) + .withHeader("accept", "application/json"), + giveEmptyResponse()); + + notificationService.sendNotification(check, subscription, alerts); + + String content = bodyCapture.getContent(); + System.out.println(decode(content)); + + assertThat(content, Matchers.containsString("token=")); + assertThat(content, Matchers.containsString("&channel=target")); + assertThat(content, Matchers.containsString(encode(""))); + assertThat(content, Matchers.containsString(encode("*ERROR* test-check"))); + assertThat(content, Matchers.containsString(encode("/#/checks/123"))); + assertThat(content, Matchers.containsString("&username=Seyren")); + assertThat(content, Matchers.containsString("&icon_url=")); + + verify(mockSeyrenConfig).getSlackEmojis(); + verify(mockSeyrenConfig).getSlackIconUrl(); + verify(mockSeyrenConfig).getSlackToken(); + verify(mockSeyrenConfig).getSlackUsername(); + verify(mockSeyrenConfig).getBaseUrl(); + } + + String encode(String data) { + try { + return URLEncoder.encode(data, "ISO-8859-1"); + } catch (UnsupportedEncodingException e) { + return null; + } + } + + String decode(String data) { + try { + return URLDecoder.decode(data, "ISO-8859-1"); + } catch (UnsupportedEncodingException e) { + return null; + } } } From f6cd1631428a3948a75779adb2136e1c8b3e11d8 Mon Sep 17 00:00:00 2001 From: Vichheann Saing Date: Sat, 28 May 2016 20:38:18 +0200 Subject: [PATCH 3/4] First refactor SlackNotificationService test --- .../SlackNotificationServiceTest.java | 122 ++++++++++-------- 1 file changed, 69 insertions(+), 53 deletions(-) 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 00190953..5353712f 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 @@ -13,10 +13,15 @@ */ package com.seyren.core.service.notification; -import static com.github.restdriver.clientdriver.RestClientDriver.*; -import static org.hamcrest.Matchers.*; -import static org.junit.Assert.*; -import static org.mockito.Mockito.*; +import static com.github.restdriver.clientdriver.RestClientDriver.giveEmptyResponse; +import static com.github.restdriver.clientdriver.RestClientDriver.onRequestTo; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import java.io.UnsupportedEncodingException; import java.math.BigDecimal; @@ -25,7 +30,7 @@ import java.util.Arrays; import java.util.List; -import org.hamcrest.Matchers; +import org.apache.commons.lang.StringUtils; import org.joda.time.DateTime; import org.junit.After; import org.junit.Before; @@ -43,6 +48,9 @@ import com.seyren.core.util.config.SeyrenConfig; public class SlackNotificationServiceTest { + private static final String USERNAME = "Seyren"; + private static final String CONTENT_ENCODING = "ISO-8859-1"; + private NotificationService notificationService; private SeyrenConfig mockSeyrenConfig; @@ -56,7 +64,7 @@ public void before() { when(mockSeyrenConfig.getSlackEmojis()).thenReturn(""); when(mockSeyrenConfig.getSlackIconUrl()).thenReturn(""); when(mockSeyrenConfig.getSlackToken()).thenReturn(""); - when(mockSeyrenConfig.getSlackUsername()).thenReturn("Seyren"); + when(mockSeyrenConfig.getSlackUsername()).thenReturn(USERNAME); notificationService = new SlackNotificationService(mockSeyrenConfig, clientDriver.getBaseUrl()); } @@ -66,7 +74,7 @@ public void after() { } @Test - public void notifcationServiceCanOnlyHandleSlackSubscription() { + public void notificationServiceCanOnlyHandleSlackSubscription() { assertThat(notificationService.canHandle(SubscriptionType.SLACK), is(true)); for (SubscriptionType type : SubscriptionType.values()) { if (type == SubscriptionType.SLACK) { @@ -78,22 +86,11 @@ public void notifcationServiceCanOnlyHandleSlackSubscription() { @Test public void basicSlackTest() { - BigDecimal value = new BigDecimal("1.0"); + // Given + Check check = givenCheck(); + Subscription subscription = givenSlackSubscriptionWithTarget("target"); + Alert alert = givenAlert(); - Check check = new Check() - .withId("123") - .withEnabled(true) - .withName("test-check") - .withState(AlertType.ERROR); - Subscription subscription = new Subscription() - .withEnabled(true) - .withType(SubscriptionType.SLACK) - .withTarget("target"); - Alert alert = new Alert() - .withValue(value) - .withTimestamp(new DateTime()) - .withFromType(AlertType.OK) - .withToType(AlertType.ERROR); List alerts = Arrays.asList(alert); StringBodyCapture bodyCapture = new StringBodyCapture(); @@ -105,18 +102,16 @@ public void basicSlackTest() { .withHeader("accept", "application/json"), giveEmptyResponse()); + // When notificationService.sendNotification(check, subscription, alerts); + // Then String content = bodyCapture.getContent(); System.out.println(decode(content)); - assertThat(content, Matchers.containsString("token=")); - assertThat(content, Matchers.containsString("&channel=target")); - assertThat(content, not(Matchers.containsString(encode("")))); - assertThat(content, Matchers.containsString(encode("*ERROR* test-check"))); - assertThat(content, Matchers.containsString(encode("/#/checks/123"))); - assertThat(content, Matchers.containsString("&username=Seyren")); - assertThat(content, Matchers.containsString("&icon_url=")); + assertContent(content, check, subscription); + assertThat(content, containsString("&channel=" + subscription.getTarget())); + assertThat(content, not(containsString(encode("")))); verify(mockSeyrenConfig).getSlackEmojis(); verify(mockSeyrenConfig).getSlackIconUrl(); @@ -127,22 +122,11 @@ public void basicSlackTest() { @Test public void mentionChannelWhenTargetContainsExclamationTest() { - BigDecimal value = new BigDecimal("1.0"); + //Given + Check check = givenCheck(); + Subscription subscription = givenSlackSubscriptionWithTarget("target!"); + Alert alert = givenAlert(); - Check check = new Check() - .withId("123") - .withEnabled(true) - .withName("test-check") - .withState(AlertType.ERROR); - Subscription subscription = new Subscription() - .withEnabled(true) - .withType(SubscriptionType.SLACK) - .withTarget("target!"); - Alert alert = new Alert() - .withValue(value) - .withTimestamp(new DateTime()) - .withFromType(AlertType.OK) - .withToType(AlertType.ERROR); List alerts = Arrays.asList(alert); StringBodyCapture bodyCapture = new StringBodyCapture(); @@ -154,18 +138,16 @@ public void mentionChannelWhenTargetContainsExclamationTest() { .withHeader("accept", "application/json"), giveEmptyResponse()); + // When notificationService.sendNotification(check, subscription, alerts); + // Then String content = bodyCapture.getContent(); System.out.println(decode(content)); - assertThat(content, Matchers.containsString("token=")); - assertThat(content, Matchers.containsString("&channel=target")); - assertThat(content, Matchers.containsString(encode(""))); - assertThat(content, Matchers.containsString(encode("*ERROR* test-check"))); - assertThat(content, Matchers.containsString(encode("/#/checks/123"))); - assertThat(content, Matchers.containsString("&username=Seyren")); - assertThat(content, Matchers.containsString("&icon_url=")); + assertContent(content, check, subscription); + assertThat(content, containsString("&channel=" + StringUtils.removeEnd(subscription.getTarget(), "!"))); + assertThat(content, containsString(encode(""))); verify(mockSeyrenConfig).getSlackEmojis(); verify(mockSeyrenConfig).getSlackIconUrl(); @@ -174,9 +156,43 @@ public void mentionChannelWhenTargetContainsExclamationTest() { verify(mockSeyrenConfig).getBaseUrl(); } + Check givenCheck() { + Check check = new Check() + .withId("123") + .withEnabled(true) + .withName("test-check") + .withState(AlertType.ERROR); + return check; + } + + Subscription givenSlackSubscriptionWithTarget(String target) { + Subscription subscription = new Subscription() + .withEnabled(true) + .withType(SubscriptionType.SLACK) + .withTarget(target); + return subscription; + } + + Alert givenAlert() { + Alert alert = new Alert() + .withValue(new BigDecimal("1.0")) + .withTimestamp(new DateTime()) + .withFromType(AlertType.OK) + .withToType(AlertType.ERROR); + return alert; + } + + private void assertContent(String content, Check check, Subscription subscription) { + assertThat(content, containsString("token=")); + assertThat(content, containsString(encode("*" + check.getState().name() + "* " + check.getName()))); + assertThat(content, containsString(encode("/#/checks/" + check.getId()))); + assertThat(content, containsString("&username=" + USERNAME)); + assertThat(content, containsString("&icon_url=")); + } + String encode(String data) { try { - return URLEncoder.encode(data, "ISO-8859-1"); + return URLEncoder.encode(data, CONTENT_ENCODING); } catch (UnsupportedEncodingException e) { return null; } @@ -184,7 +200,7 @@ String encode(String data) { String decode(String data) { try { - return URLDecoder.decode(data, "ISO-8859-1"); + return URLDecoder.decode(data, CONTENT_ENCODING); } catch (UnsupportedEncodingException e) { return null; } From ae71717c3589c02dd804e25e83e8754030877ae0 Mon Sep 17 00:00:00 2001 From: Vichheann Saing Date: Sat, 28 May 2016 22:36:30 +0200 Subject: [PATCH 4/4] Use Slack webhook or Slack API token --- README.md | 4 +- .../SlackNotificationService.java | 190 ++++++++++++++---- .../seyren/core/util/config/SeyrenConfig.java | 7 + .../SlackNotificationServiceTest.java | 88 +++++++- 4 files changed, 235 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index 6766af78..8076e89a 100644 --- a/README.md +++ b/README.md @@ -109,9 +109,11 @@ To generate a "Service API Key", see [PagerDuty Support: Adding Services](https: ##### [Slack](https://www.slack.com) -The target for a Slack subscription will be the channel name (including the `#`, for example `#channel`). You can optionally suffix the channel name with `!` and that will cause the alerts to include a `@channel` mention (for example `#channel!`). +You can specify either `SLACK_TOKEN` (which will be evaluated first for compatibility reason) or `SLACK_WEBHOOK_URL` (which should be the preferred method). +If you set `SLACK_TOKEN`, the target for a Slack subscription will be the channel name (including the `#`, for example `#channel`). You can optionally suffix the channel name with `!` and that will cause the alerts to include a `@channel` mention (for example `#channel!`). If you set `SLACK_WEBHOOK_URL`, you don't need to suffix the channel, you can simply use `#channel` or `@channel`. * `SLACK_TOKEN` - The Slack api auth token. Default: `` +* `SLACK_WEBHOOK_URL` - The Slack webhook URL. Default: `` * `SLACK_USERNAME` - The username that messages will be sent to slack. Default: `Seyren` * `SLACK_ICON_URL` - The user icon URL. Default: `` * `SLACK_EMOJIS` - Mapping between state and emojis unicode. Default: `` 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 68e61627..337ae9ac 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 @@ -13,11 +13,13 @@ */ package com.seyren.core.service.notification; -import static com.google.common.collect.Iterables.*; +import static com.google.common.collect.Iterables.transform; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Locale; +import java.util.Map; import javax.inject.Inject; import javax.inject.Named; @@ -28,12 +30,16 @@ import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.utils.HttpClientUtils; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.BasicResponseHandler; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.message.BasicNameValuePair; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.base.Function; import com.google.common.base.Joiner; import com.google.common.base.Splitter; @@ -64,37 +70,144 @@ protected SlackNotificationService(SeyrenConfig seyrenConfig, String baseUrl) { this.baseUrl = baseUrl; } + @Override + public boolean canHandle(SubscriptionType subscriptionType) { + return subscriptionType == SubscriptionType.SLACK; + } + @Override public void sendNotification(Check check, Subscription subscription, List alerts) throws NotificationFailedException { - String token = seyrenConfig.getSlackToken(); - String channel = subscription.getTarget(); - String username = seyrenConfig.getSlackUsername(); - String iconUrl = seyrenConfig.getSlackIconUrl(); + if ( !seyrenConfig.getSlackToken().isEmpty() ) { + LOGGER.info("Will use API token"); + notifyUsingApiToken(check, subscription, alerts); + } + else if ( !seyrenConfig.getSlackWebhook().isEmpty() ) { + LOGGER.info("Will use Webhook"); + notifyUsingWebhook(check, subscription, alerts); + } + else + LOGGER.warn("Slack token and Slack Webhook are empty. Do nothing."); + } + + // + // API Test Token. You should really switch to Webhook. + // Just copied previous code. + // + + private void notifyUsingApiToken(Check check, Subscription subscription, List alerts) { + String token = seyrenConfig.getSlackToken(); + String channel = subscription.getTarget(); + String username = seyrenConfig.getSlackUsername(); + String iconUrl = seyrenConfig.getSlackIconUrl(); + + List emojis = extractEmojis(); + + String url = String.format("%s/api/chat.postMessage", baseUrl); + HttpClient client = HttpClientBuilder.create().useSystemProperties().build(); + HttpPost post = new HttpPost(url); + post.addHeader("accept", "application/json"); + + List parameters = new ArrayList(); + parameters.add(new BasicNameValuePair("token", token)); + parameters.add(new BasicNameValuePair("channel", StringUtils.removeEnd(channel, "!"))); + parameters.add(new BasicNameValuePair("text", formatContent(emojis, check, subscription, alerts))); + parameters.add(new BasicNameValuePair("username", username)); + parameters.add(new BasicNameValuePair("icon_url", iconUrl)); + + try { + post.setEntity(new UrlEncodedFormEntity(parameters)); + if (LOGGER.isDebugEnabled()) { + LOGGER.info("> parameters: {}", parameters); + } + HttpResponse response = client.execute(post); + if (LOGGER.isDebugEnabled()) { + LOGGER.info("> parameters: {}", parameters); + LOGGER.debug("Status: {}, Body: {}", response.getStatusLine(), new BasicResponseHandler().handleResponse(response)); + } + } catch (Exception e) { + LOGGER.warn("Error posting to Slack", e); + } finally { + post.releaseConnection(); + HttpClientUtils.closeQuietly(client); + } + } + + private String formatContent(List emojis, Check check, Subscription subscription, List alerts) { + String url = formatCheckUrl(check); + String alertsString = formatAlert(alerts); + + String channel = subscription.getTarget().contains("!") ? "" : ""; - List emojis = Lists.newArrayList( - Splitter.on(',').omitEmptyStrings().trimResults().split(seyrenConfig.getSlackEmojis()) + 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", + Iterables.get(emojis, check.getState().ordinal(), ""), + state, + check.getName(), + url, + description, + alertsString, + state.toLowerCase(Locale.getDefault()), + channel ); + } + + private List extractEmojis() { + List emojis = Lists.newArrayList( + Splitter.on(',').omitEmptyStrings().trimResults().split(seyrenConfig.getSlackEmojis()) + ); + return emojis; + } + + private String formatCheckUrl(Check check) { + String url = String.format("%s/#/checks/%s", seyrenConfig.getBaseUrl(), check.getId()); + return url; + } + + 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 String formatDescription(Check check) { + String description; + if (StringUtils.isNotBlank(check.getDescription())) { + description = String.format("\n> %s", check.getDescription()); + } else { + description = ""; + } + return description; + } + + // + // Webhook + // + + private void notifyUsingWebhook(Check check, Subscription subscription, List alerts) throws NotificationFailedException { + String webhookUrl = seyrenConfig.getSlackWebhook(); + + List emojis = extractEmojis(); - String url = String.format("%s/api/chat.postMessage", baseUrl); HttpClient client = HttpClientBuilder.create().useSystemProperties().build(); - HttpPost post = new HttpPost(url); + HttpPost post = new HttpPost(webhookUrl); post.addHeader("accept", "application/json"); - List parameters = new ArrayList(); - parameters.add(new BasicNameValuePair("token", token)); - parameters.add(new BasicNameValuePair("channel", StringUtils.removeEnd(channel, "!"))); - parameters.add(new BasicNameValuePair("text", formatContent(emojis, check, subscription, alerts))); - parameters.add(new BasicNameValuePair("username", username)); - parameters.add(new BasicNameValuePair("icon_url", iconUrl)); - try { - post.setEntity(new UrlEncodedFormEntity(parameters)); + String message = generateMessage(emojis, check, subscription, alerts); + post.setEntity(new StringEntity(message, ContentType.APPLICATION_JSON)); if (LOGGER.isDebugEnabled()) { - LOGGER.info("> parameters: {}", parameters); + LOGGER.info("> message: {}", message); } HttpResponse response = client.execute(post); if (LOGGER.isDebugEnabled()) { - LOGGER.info("> parameters: {}", parameters); + LOGGER.info("> message: {}", message); LOGGER.debug("Status: {}, Body: {}", response.getStatusLine(), new BasicResponseHandler().handleResponse(response)); } } catch (Exception e) { @@ -103,43 +216,34 @@ public void sendNotification(Check check, Subscription subscription, List post.releaseConnection(); HttpClientUtils.closeQuietly(client); } - } - @Override - public boolean canHandle(SubscriptionType subscriptionType) { - return subscriptionType == SubscriptionType.SLACK; + private String generateMessage(List emojis, Check check, Subscription subscription, List alerts) throws JsonProcessingException { + Map payload = new HashMap(); + payload.put("channel", subscription.getTarget()); + payload.put("username", seyrenConfig.getSlackUsername()); + payload.put("text", formatText(emojis, check, subscription, alerts)); + payload.put("icon_url", seyrenConfig.getSlackIconUrl()); + + String message = new ObjectMapper().writeValueAsString(payload); + return message; } - private String formatContent(List emojis, Check check, Subscription subscription, List alerts) { - String url = String.format("%s/#/checks/%s", seyrenConfig.getBaseUrl(), check.getId()); - 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()); - } - })); + private String formatText(List emojis, Check check, Subscription subscription, List alerts) { + String url = formatCheckUrl(check); + String alertsString = formatAlert(alerts); - String channel = subscription.getTarget().contains("!") ? "" : ""; - - String description; - if (StringUtils.isNotBlank(check.getDescription())) { - description = String.format("\n> %s", check.getDescription()); - } else { - description = ""; - } + 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", + return String.format("%s *%s* %s (<%s|Open>)%s\n```\n%s\n```", Iterables.get(emojis, check.getState().ordinal(), ""), state, check.getName(), url, description, - alertsString, - state.toLowerCase(Locale.getDefault()), - channel + alertsString ); } } 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 9c221c1c..8c44d79b 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 @@ -71,6 +71,7 @@ public class SeyrenConfig { private final String ircCatHost; private final String ircCatPort; private final String slackToken; + private final String slackWebhook; private final String slackUsername; private final String slackIconUrl; private final String slackEmojis; @@ -146,6 +147,7 @@ public SeyrenConfig() { // Slack this.slackToken = configOrDefault("SLACK_TOKEN", ""); + this.slackWebhook = configOrDefault("SLACK_WEBHOOK_URL", ""); this.slackUsername = configOrDefault("SLACK_USERNAME", "Seyren"); this.slackIconUrl = configOrDefault("SLACK_ICON_URL", ""); this.slackEmojis = configOrDefault("SLACK_EMOJIS", ""); @@ -396,6 +398,11 @@ public String getSlackToken() { return slackToken; } + @JsonIgnore + public String getSlackWebhook() { + return slackWebhook; + } + @JsonIgnore public String getSlackUsername() { return slackUsername; 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 5353712f..47b2c1c9 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 @@ -17,18 +17,25 @@ import static com.github.restdriver.clientdriver.RestClientDriver.onRequestTo; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.is; +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.mockito.Mockito.atLeast; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.io.IOException; import java.io.UnsupportedEncodingException; import java.math.BigDecimal; import java.net.URLDecoder; import java.net.URLEncoder; import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; import org.apache.commons.lang.StringUtils; import org.joda.time.DateTime; @@ -37,6 +44,10 @@ import org.junit.Rule; import org.junit.Test; +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; import com.github.restdriver.clientdriver.ClientDriverRequest; import com.github.restdriver.clientdriver.ClientDriverRule; import com.github.restdriver.clientdriver.capture.StringBodyCapture; @@ -48,7 +59,10 @@ import com.seyren.core.util.config.SeyrenConfig; public class SlackNotificationServiceTest { - private static final String USERNAME = "Seyren"; + private static final String SLACK_USERNAME = "Seyren"; + private static final String SLACK_TOKEN = "A_TOKEN"; + private static final String SLACK_WEBHOOK_URI_TO_POST = "/services/SOMETHING/ANOTHERTHING/FINALTHING"; + private static final String CONTENT_ENCODING = "ISO-8859-1"; private NotificationService notificationService; @@ -63,8 +77,7 @@ public void before() { when(mockSeyrenConfig.getBaseUrl()).thenReturn(clientDriver.getBaseUrl() + "/slack"); when(mockSeyrenConfig.getSlackEmojis()).thenReturn(""); when(mockSeyrenConfig.getSlackIconUrl()).thenReturn(""); - when(mockSeyrenConfig.getSlackToken()).thenReturn(""); - when(mockSeyrenConfig.getSlackUsername()).thenReturn(USERNAME); + when(mockSeyrenConfig.getSlackUsername()).thenReturn(SLACK_USERNAME); notificationService = new SlackNotificationService(mockSeyrenConfig, clientDriver.getBaseUrl()); } @@ -85,8 +98,10 @@ public void notificationServiceCanOnlyHandleSlackSubscription() { } @Test - public void basicSlackTest() { + public void useSlackApiTokenTest() { // Given + when(mockSeyrenConfig.getSlackToken()).thenReturn(SLACK_TOKEN); + Check check = givenCheck(); Subscription subscription = givenSlackSubscriptionWithTarget("target"); Alert alert = givenAlert(); @@ -107,7 +122,7 @@ public void basicSlackTest() { // Then String content = bodyCapture.getContent(); - System.out.println(decode(content)); + //System.out.println(decode(content)); assertContent(content, check, subscription); assertThat(content, containsString("&channel=" + subscription.getTarget())); @@ -115,7 +130,8 @@ public void basicSlackTest() { verify(mockSeyrenConfig).getSlackEmojis(); verify(mockSeyrenConfig).getSlackIconUrl(); - verify(mockSeyrenConfig).getSlackToken(); + verify(mockSeyrenConfig, atLeast(2)).getSlackToken(); + verify(mockSeyrenConfig, times(0)).getSlackWebhook(); verify(mockSeyrenConfig).getSlackUsername(); verify(mockSeyrenConfig).getBaseUrl(); } @@ -123,6 +139,8 @@ public void basicSlackTest() { @Test public void mentionChannelWhenTargetContainsExclamationTest() { //Given + when(mockSeyrenConfig.getSlackToken()).thenReturn(SLACK_TOKEN); + Check check = givenCheck(); Subscription subscription = givenSlackSubscriptionWithTarget("target!"); Alert alert = givenAlert(); @@ -143,7 +161,7 @@ public void mentionChannelWhenTargetContainsExclamationTest() { // Then String content = bodyCapture.getContent(); - System.out.println(decode(content)); + //System.out.println(decode(content)); assertContent(content, check, subscription); assertThat(content, containsString("&channel=" + StringUtils.removeEnd(subscription.getTarget(), "!"))); @@ -151,7 +169,57 @@ public void mentionChannelWhenTargetContainsExclamationTest() { verify(mockSeyrenConfig).getSlackEmojis(); verify(mockSeyrenConfig).getSlackIconUrl(); - verify(mockSeyrenConfig).getSlackToken(); + verify(mockSeyrenConfig, atLeast(2)).getSlackToken(); + verify(mockSeyrenConfig, times(0)).getSlackWebhook(); + verify(mockSeyrenConfig).getSlackUsername(); + verify(mockSeyrenConfig).getBaseUrl(); + } + + @Test + public void useSlackWebHookTest() throws JsonParseException, JsonMappingException, IOException { + // Given + when(mockSeyrenConfig.getSlackToken()).thenReturn(""); + when(mockSeyrenConfig.getSlackWebhook()).thenReturn(clientDriver.getBaseUrl() + SLACK_WEBHOOK_URI_TO_POST); + + Check check = givenCheck(); + + Subscription subscription = givenSlackSubscriptionWithTarget("target"); + + Alert alert = givenAlert(); + List alerts = Arrays.asList(alert); + + StringBodyCapture bodyCapture = new StringBodyCapture(); + + clientDriver.addExpectation( + onRequestTo(SLACK_WEBHOOK_URI_TO_POST) + .withMethod(ClientDriverRequest.Method.POST) + .capturingBodyIn(bodyCapture) + .withHeader("accept", "application/json"), + giveEmptyResponse()); + + // When + notificationService.sendNotification(check, subscription, alerts); + + // Then + String content = bodyCapture.getContent(); + assertThat(content, is(notNullValue())); + + 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()); + + verify(mockSeyrenConfig, atLeast(2)).getSlackWebhook(); + verify(mockSeyrenConfig, times(1)).getSlackToken(); + verify(mockSeyrenConfig).getSlackEmojis(); + verify(mockSeyrenConfig).getSlackIconUrl(); verify(mockSeyrenConfig).getSlackUsername(); verify(mockSeyrenConfig).getBaseUrl(); } @@ -183,10 +251,10 @@ Alert givenAlert() { } private void assertContent(String content, Check check, Subscription subscription) { - assertThat(content, containsString("token=")); + assertThat(content, containsString("token=" + SLACK_TOKEN)); assertThat(content, containsString(encode("*" + check.getState().name() + "* " + check.getName()))); assertThat(content, containsString(encode("/#/checks/" + check.getId()))); - assertThat(content, containsString("&username=" + USERNAME)); + assertThat(content, containsString("&username=" + SLACK_USERNAME)); assertThat(content, containsString("&icon_url=")); }