Commit 9bf54c32 by tomd8451 Committed by Johannes Edmeier

Added Microsoft Teams notifier

parent 245c256f
......@@ -333,3 +333,24 @@ To enable Let's Chat notifications you need to add the host url and add the API
| `+++"*#{application.name}* (#{application.id}) is *#{to.status}*"+++`
|
|===
[ms-teams-notifications]
==== Microsoft Teams notifications ====
To enable Microsoft Teams notifications you need to setup a connector webhook url and set the appropriate configuration property.
.Microsoft Teams notifications configuration options
|===
| Property name |Description |Default value
| spring.boot.admin.notify.ms-teams.enabled
| Enable Microsoft Teams notifications
| `true`
| spring.boot.admin.notify.ms-teams.webhook-url
| The Microsoft Teams webhook url to send the notifications to.
|
| spring.boot.admin.notify.ms-teams.*
| There are several options to customize the message title and color
|
|===
\ No newline at end of file
......@@ -17,15 +17,6 @@ package de.codecentric.boot.admin.config;
import java.util.List;
import de.codecentric.boot.admin.notify.CompositeNotifier;
import de.codecentric.boot.admin.notify.MailNotifier;
import de.codecentric.boot.admin.notify.Notifier;
import de.codecentric.boot.admin.notify.NotifierListener;
import de.codecentric.boot.admin.notify.PagerdutyNotifier;
import de.codecentric.boot.admin.notify.OpsGenieNotifier;
import de.codecentric.boot.admin.notify.HipchatNotifier;
import de.codecentric.boot.admin.notify.SlackNotifier;
import de.codecentric.boot.admin.notify.LetsChatNotifier;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
......@@ -42,6 +33,16 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.mail.MailSender;
import de.codecentric.boot.admin.notify.CompositeNotifier;
import de.codecentric.boot.admin.notify.HipchatNotifier;
import de.codecentric.boot.admin.notify.LetsChatNotifier;
import de.codecentric.boot.admin.notify.MailNotifier;
import de.codecentric.boot.admin.notify.MicrosoftTeamsNotifier;
import de.codecentric.boot.admin.notify.Notifier;
import de.codecentric.boot.admin.notify.NotifierListener;
import de.codecentric.boot.admin.notify.OpsGenieNotifier;
import de.codecentric.boot.admin.notify.PagerdutyNotifier;
import de.codecentric.boot.admin.notify.SlackNotifier;
import de.codecentric.boot.admin.notify.filter.FilteringNotifier;
import de.codecentric.boot.admin.notify.filter.web.NotificationFilterController;
import de.codecentric.boot.admin.web.PrefixHandlerMapping;
......@@ -191,4 +192,15 @@ public class NotifierConfiguration {
return new LetsChatNotifier();
}
}
@Configuration
@ConditionalOnProperty(prefix = "spring.boot.admin.notify.ms-teams", name = "webhook-url")
@AutoConfigureBefore({ NotifierListenerConfiguration.class,
CompositeNotifierConfiguration.class})
public static class MicrosoftTeamsNotifierConfiguration {
@Bean
@ConditionalOnMissingBean
@ConfigurationProperties("spring.boot.admin.notify.ms-teams")
public MicrosoftTeamsNotifier microsoftTeamsNotifier() { return new MicrosoftTeamsNotifier(); }
}
}
package de.codecentric.boot.admin.notify;
import static java.util.Collections.singletonList;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.MissingFormatArgumentException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.client.RestTemplate;
import de.codecentric.boot.admin.event.ClientApplicationDeregisteredEvent;
import de.codecentric.boot.admin.event.ClientApplicationEvent;
import de.codecentric.boot.admin.event.ClientApplicationRegisteredEvent;
import de.codecentric.boot.admin.event.ClientApplicationStatusChangedEvent;
import de.codecentric.boot.admin.model.Application;
import de.codecentric.boot.admin.model.StatusInfo;
public class MicrosoftTeamsNotifier extends AbstractEventNotifier {
private static final Logger LOGGER = LoggerFactory.getLogger(MicrosoftTeamsNotifier.class);
private static final String STATUS_KEY = "Status";
private static final String SERVICE_URL_KEY = "Service URL";
private static final String HEALTH_URL_KEY = "Health URL";
private static final String MANAGEMENT_URL_KEY = "Management URL";
private static final String SOURCE_KEY = "Source";
private RestTemplate restTemplate = new RestTemplate();
/**
* Webhook url for Microsoft Teams Channel Webhook connector (i.e.
* https://outlook.office.com/webhook/{webhook-id})
*/
private URI webhookUrl;
/**
* Theme Color is the color of the accent on the message that appears in Microsoft Teams.
* Default is Spring Green
*/
private String themeColor = "6db33f";
/**
* Message will be used as title of the Activity section of the Teams message when an app
* de-registers.
*/
private String deregisterActivitySubtitlePattern = "%s with id %s has de-registered from Spring Boot Admin";
/**
* Message will be used as title of the Activity section of the Teams message when an app
* registers
*/
private String registerActivitySubtitlePattern = "%s with id %s has registered from Spring Boot Admin";
/**
* Message will be used as title of the Activity section of the Teams message when an app
* changes status
*/
private String statusActivitySubtitlePattern = "%s with id %s changed status from %s to %s";
/**
* Title of the Teams message when an app de-registers
*/
private String deRegisteredTitle = "De-Registered";
/**
* Title of the Teams message when an app registers
*/
private String registeredTitle = "Registered";
/**
* Title of the Teams message when an app changes status
*/
private String statusChangedTitle = "Status Changed";
/**
* Summary section of every Teams message originating from Spring Boot Admin
*/
private String messageSummary = "Spring Boot Admin Notification";
@Override
protected void doNotify(ClientApplicationEvent event) throws Exception {
Message message;
if (event instanceof ClientApplicationRegisteredEvent) {
message = getRegisteredMessage(event.getApplication());
} else if (event instanceof ClientApplicationDeregisteredEvent) {
message = getDeregisteredMessage(event.getApplication());
} else if (event instanceof ClientApplicationStatusChangedEvent) {
ClientApplicationStatusChangedEvent statusChangedEvent = (ClientApplicationStatusChangedEvent) event;
message = getStatusChangedMessage(statusChangedEvent.getApplication(),
statusChangedEvent.getFrom(), statusChangedEvent.getTo());
} else {
return;
}
this.restTemplate.postForObject(webhookUrl, message, Void.class);
}
@Override
protected boolean shouldNotify(ClientApplicationEvent event) {
return event instanceof ClientApplicationRegisteredEvent
|| event instanceof ClientApplicationDeregisteredEvent
|| event instanceof ClientApplicationStatusChangedEvent;
}
protected Message getDeregisteredMessage(Application app) {
String activitySubtitle = this.safeFormat(deregisterActivitySubtitlePattern, app.getName(),
app.getId());
return createMessage(app, deRegisteredTitle, activitySubtitle);
}
protected Message getRegisteredMessage(Application app) {
String activitySubtitle = this.safeFormat(registerActivitySubtitlePattern, app.getName(),
app.getId());
return createMessage(app, registeredTitle, activitySubtitle);
}
protected Message getStatusChangedMessage(Application app, StatusInfo from, StatusInfo to) {
String activitySubtitle = this.safeFormat(statusActivitySubtitlePattern, app.getName(),
app.getId(), from.getStatus(), to.getStatus());
return createMessage(app, statusChangedTitle, activitySubtitle);
}
private String safeFormat(String format, Object... args) {
try {
return String.format(format, args);
} catch (MissingFormatArgumentException e) {
LOGGER.warn(
"Exception while trying to format the message. Falling back by using the format string.",
e);
return format;
}
}
protected Message createMessage(Application app, String registeredTitle,
String activitySubtitle) {
Message message = new Message();
message.setTitle(registeredTitle);
message.setSummary(messageSummary);
message.setThemeColor(themeColor);
Section section = new Section();
section.setActivityTitle(app.getName());
section.setActivitySubtitle(activitySubtitle);
List<Fact> facts = new ArrayList<>();
facts.add(new Fact(STATUS_KEY, app.getStatusInfo().getStatus()));
facts.add(new Fact(SERVICE_URL_KEY, app.getServiceUrl()));
facts.add(new Fact(HEALTH_URL_KEY, app.getHealthUrl()));
facts.add(new Fact(MANAGEMENT_URL_KEY, app.getManagementUrl()));
facts.add(new Fact(SOURCE_KEY, app.getSource()));
section.setFacts(facts);
message.setSections(singletonList(section));
return message;
}
public void setWebhookUrl(URI webhookUrl) {
this.webhookUrl = webhookUrl;
}
public void setThemeColor(String themeColor) {
this.themeColor = themeColor;
}
public String getDeregisterActivitySubtitlePattern() {
return deregisterActivitySubtitlePattern;
}
public void setDeregisterActivitySubtitlePattern(String deregisterActivitySubtitlePattern) {
this.deregisterActivitySubtitlePattern = deregisterActivitySubtitlePattern;
}
public String getRegisterActivitySubtitlePattern() {
return registerActivitySubtitlePattern;
}
public void setRegisterActivitySubtitlePattern(String registerActivitySubtitlePattern) {
this.registerActivitySubtitlePattern = registerActivitySubtitlePattern;
}
public String getStatusActivitySubtitlePattern() {
return statusActivitySubtitlePattern;
}
public void setStatusActivitySubtitlePattern(String statusActivitySubtitlePattern) {
this.statusActivitySubtitlePattern = statusActivitySubtitlePattern;
}
public String getDeRegisteredTitle() {
return deRegisteredTitle;
}
public void setDeRegisteredTitle(String deRegisteredTitle) {
this.deRegisteredTitle = deRegisteredTitle;
}
public String getRegisteredTitle() {
return registeredTitle;
}
public void setRegisteredTitle(String registeredTitle) {
this.registeredTitle = registeredTitle;
}
public String getStatusChangedTitle() {
return statusChangedTitle;
}
public void setStatusChangedTitle(String statusChangedTitle) {
this.statusChangedTitle = statusChangedTitle;
}
public String getMessageSummary() {
return messageSummary;
}
public void setMessageSummary(String messageSummary) {
this.messageSummary = messageSummary;
}
public void setRestTemplate(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
public static class Message {
private String summary;
private String themeColor;
private String title;
private List<Section> sections = new ArrayList<>();
public String getSummary() {
return summary;
}
public void setSummary(String summary) {
this.summary = summary;
}
public String getTitle() {
return title;
}
public String getThemeColor() {
return themeColor;
}
public void setSections(List<Section> sections) {
this.sections = sections;
}
public void setTitle(String title) {
this.title = title;
}
public void setThemeColor(String themeColor) {
this.themeColor = themeColor;
}
public List<Section> getSections() {
return sections;
}
}
public static class Section {
private String activityTitle;
private String activitySubtitle;
private List<Fact> facts = new ArrayList<>();
public String getActivityTitle() {
return activityTitle;
}
public void setActivityTitle(String activityTitle) {
this.activityTitle = activityTitle;
}
public String getActivitySubtitle() {
return activitySubtitle;
}
public void setActivitySubtitle(String activitySubtitle) {
this.activitySubtitle = activitySubtitle;
}
public void setFacts(List<Fact> facts) {
this.facts = facts;
}
public List<Fact> getFacts() {
return facts;
}
}
public static class Fact {
private final String name;
private final String value;
public Fact(String name, String value) {
this.name = name;
this.value = value;
}
public String getName() {
return name;
}
public String getValue() {
return value;
}
}
}
......@@ -25,14 +25,6 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import de.codecentric.boot.admin.notify.CompositeNotifier;
import de.codecentric.boot.admin.notify.HipchatNotifier;
import de.codecentric.boot.admin.notify.MailNotifier;
import de.codecentric.boot.admin.notify.Notifier;
import de.codecentric.boot.admin.notify.NotifierListener;
import de.codecentric.boot.admin.notify.OpsGenieNotifier;
import de.codecentric.boot.admin.notify.PagerdutyNotifier;
import de.codecentric.boot.admin.notify.SlackNotifier;
import org.junit.After;
import org.junit.Test;
import org.springframework.boot.autoconfigure.mail.MailSenderAutoConfiguration;
......@@ -45,6 +37,15 @@ import de.codecentric.boot.admin.event.ClientApplicationEvent;
import de.codecentric.boot.admin.event.ClientApplicationStatusChangedEvent;
import de.codecentric.boot.admin.model.Application;
import de.codecentric.boot.admin.model.StatusInfo;
import de.codecentric.boot.admin.notify.CompositeNotifier;
import de.codecentric.boot.admin.notify.HipchatNotifier;
import de.codecentric.boot.admin.notify.MailNotifier;
import de.codecentric.boot.admin.notify.MicrosoftTeamsNotifier;
import de.codecentric.boot.admin.notify.Notifier;
import de.codecentric.boot.admin.notify.NotifierListener;
import de.codecentric.boot.admin.notify.OpsGenieNotifier;
import de.codecentric.boot.admin.notify.PagerdutyNotifier;
import de.codecentric.boot.admin.notify.SlackNotifier;
public class NotifierConfigurationTest {
private static final ClientApplicationEvent APP_DOWN = new ClientApplicationStatusChangedEvent(
......@@ -107,6 +108,12 @@ public class NotifierConfigurationTest {
}
@Test
public void test_ms_teams() {
load(null,"spring.boot.admin.notify.ms-teams.webhook-url:http://example.com");
assertThat(context.getBean(MicrosoftTeamsNotifier.class), is(instanceOf(MicrosoftTeamsNotifier.class)));
}
@Test
public void test_multipleNotifiers() {
load(TestMultipleNotifierConfig.class);
assertThat(context.getBean(Notifier.class), is(instanceOf(CompositeNotifier.class)));
......
package de.codecentric.boot.admin.notify;
import de.codecentric.boot.admin.event.ClientApplicationDeregisteredEvent;
import de.codecentric.boot.admin.event.ClientApplicationRegisteredEvent;
import de.codecentric.boot.admin.event.ClientApplicationStatusChangedEvent;
import de.codecentric.boot.admin.model.Application;
import de.codecentric.boot.admin.model.StatusInfo;
import de.codecentric.boot.admin.notify.MicrosoftTeamsNotifier.Message;
import org.junit.Before;
import org.junit.Test;
import org.springframework.web.client.RestTemplate;
import java.net.URI;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
public class MicrosoftTeamsNotifierTest {
private static final String appName = "Test App";
private static final String appId = "TestAppId";
private static final String healthUrl = "http://health";
private static final String managementUrl = "http://management";
private static final String serviceUrl = "http://service";
private MicrosoftTeamsNotifier notifier;
private RestTemplate mockRestTemplate;
private Application application;
@Before
public void setUp() throws Exception {
mockRestTemplate = mock(RestTemplate.class);
notifier = new MicrosoftTeamsNotifier();
notifier.setRestTemplate(mockRestTemplate);
notifier.setWebhookUrl(URI.create("http://example.com"));
application = Application.create(appName).withId(appId).withHealthUrl(healthUrl)
.withManagementUrl(managementUrl).withServiceUrl(serviceUrl).build();
}
@Test
public void test_onClientApplicationDeRegisteredEvent_resolve() throws Exception {
ClientApplicationDeregisteredEvent event = new ClientApplicationDeregisteredEvent(
application);
notifier.doNotify(event);
verify(mockRestTemplate).postForObject(eq(URI.create("http://example.com")),
any(Message.class), eq(Void.class));
}
@Test
public void test_onApplicationRegisteredEvent_resolve() throws Exception {
ClientApplicationRegisteredEvent event = new ClientApplicationRegisteredEvent(application);
notifier.doNotify(event);
verify(mockRestTemplate).postForObject(eq(URI.create("http://example.com")),
any(Message.class), eq(Void.class));
}
@Test
public void test_onApplicationStatusChangedEvent_resolve() throws Exception {
ClientApplicationStatusChangedEvent event = new ClientApplicationStatusChangedEvent(
application, StatusInfo.ofDown(), StatusInfo.ofUp());
notifier.doNotify(event);
verify(mockRestTemplate).postForObject(eq(URI.create("http://example.com")),
any(Message.class), eq(Void.class));
}
@Test
public void test_shouldNotifyWithStatusChangeEventReturns_true() {
ClientApplicationStatusChangedEvent event = new ClientApplicationStatusChangedEvent(
application, StatusInfo.ofDown(), StatusInfo.ofOffline());
boolean shouldNotify = notifier.shouldNotify(event);
assertTrue("Returned false when should notify for status change", shouldNotify);
}
@Test
public void test_shouldNotifyWithRegisteredEventReturns_true() {
ClientApplicationRegisteredEvent event = new ClientApplicationRegisteredEvent(application);
boolean shouldNotify = notifier.shouldNotify(event);
assertTrue("Returned false when should notify for register", shouldNotify);
}
@Test
public void test_shouldNotifyWithDeRegisteredEventReturns_true() {
ClientApplicationDeregisteredEvent event = new ClientApplicationDeregisteredEvent(
application);
boolean shouldNotify = notifier.shouldNotify(event);
assertTrue("Returned false when should notify for de-register", shouldNotify);
}
@Test
public void test_getDeregisteredMessageForAppReturns_correctContent() {
Message message = notifier.getDeregisteredMessage(application);
assertMessage(message, notifier.getDeRegisteredTitle(), notifier.getMessageSummary(),
String.format(notifier.getDeregisterActivitySubtitlePattern(),
application.getName(), application.getId()));
}
@Test
public void test_getRegisteredMessageForAppReturns_correctContent() {
Message message = notifier.getRegisteredMessage(application);
assertMessage(message, notifier.getRegisteredTitle(), notifier.getMessageSummary(),
String.format(notifier.getRegisterActivitySubtitlePattern(), application.getName(),
application.getId()));
}
@Test
public void test_getStatusChangedMessageForAppReturns_correctContent() {
Message message = notifier.getStatusChangedMessage(application, StatusInfo.ofUp(),
StatusInfo.ofDown());
assertMessage(message, notifier.getStatusChangedTitle(), notifier.getMessageSummary(),
String.format(notifier.getStatusActivitySubtitlePattern(), application.getName(),
application.getId(), StatusInfo.ofUp().getStatus(),
StatusInfo.ofDown().getStatus()));
}
@Test
public void test_getStatusChangedMessageWithMissingFormatArgumentReturns_activitySubtitlePattern() {
String pattern = "STATUS_%s_ACTIVITY%s_PATTERN%s%s%s%s";
notifier.setStatusActivitySubtitlePattern(pattern);
Message message = notifier.getStatusChangedMessage(application, StatusInfo.ofUp(),
StatusInfo.ofDown());
assertEquals("Activity Subtitle doesn't match", pattern,
message.getSections().get(0).getActivitySubtitle());
}
@Test
public void test_getStatusChangedMessageWithExtraFormatArgumentReturns_activitySubtitlePatternWithAppName() {
notifier.setStatusActivitySubtitlePattern("STATUS_ACTIVITY_PATTERN_%s");
Message message = notifier.getStatusChangedMessage(application, StatusInfo.ofUp(),
StatusInfo.ofDown());
assertEquals("Activity Subtitle doesn't match", "STATUS_ACTIVITY_PATTERN_" + appName,
message.getSections().get(0).getActivitySubtitle());
}
@Test
public void test_getRegisterMessageWithMissingFormatArgumentReturns_activitySubtitlePattern() {
String pattern = "REGISTER_%s_ACTIVITY%s_PATTERN%s%s%s%s";
notifier.setRegisterActivitySubtitlePattern(pattern);
Message message = notifier.getRegisteredMessage(application);
assertEquals("Activity Subtitle doesn't match", pattern,
message.getSections().get(0).getActivitySubtitle());
}
@Test
public void test_getRegisterMessageWithExtraFormatArgumentReturns_activitySubtitlePatternWithAppName() {
notifier.setRegisterActivitySubtitlePattern("REGISTER_ACTIVITY_PATTERN_%s");
Message message = notifier.getRegisteredMessage(application);
assertEquals("Activity Subtitle doesn't match", "REGISTER_ACTIVITY_PATTERN_" + appName,
message.getSections().get(0).getActivitySubtitle());
}
@Test
public void test_getDeRegisterMessageWithMissingFormatArgumentReturns_activitySubtitlePattern() {
String pattern = "DEREGISTER_%s_ACTIVITY%s_PATTERN%s%s%s%s";
notifier.setDeregisterActivitySubtitlePattern(pattern);
Message message = notifier.getDeregisteredMessage(application);
assertEquals("Activity Subtitle doesn't match", pattern,
message.getSections().get(0).getActivitySubtitle());
}
@Test
public void test_getDeRegisterMessageWithExtraFormatArgumentReturns_activitySubtitlePatternWithAppName() {
notifier.setDeregisterActivitySubtitlePattern("DEREGISTER_ACTIVITY_PATTERN_%s");
Message message = notifier.getDeregisteredMessage(application);
assertEquals("Activity Subtitle doesn't match", "DEREGISTER_ACTIVITY_PATTERN_" + appName,
message.getSections().get(0).getActivitySubtitle());
}
private void assertMessage(Message message, String expectedTitle, String expectedSummary,
String expectedSubTitle) {
assertEquals("Title doesn't match", expectedTitle, message.getTitle());
assertEquals("Summary doesn't match", expectedSummary, message.getSummary());
assertEquals("Incorrect number of sections", 1, message.getSections().size());
MicrosoftTeamsNotifier.Section section = message.getSections().get(0);
assertEquals("Activity Title doesn't match", application.getName(),
section.getActivityTitle());
assertEquals("Activity Subtitle doesn't match", expectedSubTitle,
section.getActivitySubtitle());
assertEquals("Theme Color doesn't match", "6db33f", message.getThemeColor());
List<MicrosoftTeamsNotifier.Fact> facts = section.getFacts();
assertEquals("Wrong number of facts", 5, facts.size());
assertEquals("Status", facts.get(0).getName());
assertEquals("UNKNOWN", facts.get(0).getValue());
assertEquals("Service URL", facts.get(1).getName());
assertEquals(serviceUrl, facts.get(1).getValue());
assertEquals("Health URL", facts.get(2).getName());
assertEquals(healthUrl, facts.get(2).getValue());
assertEquals("Management URL", facts.get(3).getName());
assertEquals(managementUrl, facts.get(3).getValue());
assertEquals("Source", facts.get(4).getName());
assertEquals(null, facts.get(4).getValue());
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment