Skip to content

Commit 9bf54c3

Browse files
tomd8451joshiste
authored andcommitted
Added Microsoft Teams notifier
1 parent 245c256 commit 9bf54c3

File tree

5 files changed

+592
-17
lines changed

5 files changed

+592
-17
lines changed

spring-boot-admin-docs/src/main/asciidoc/server-notifications.adoc

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,3 +333,24 @@ To enable Let's Chat notifications you need to add the host url and add the API
333333
| `+++"*#{application.name}* (#{application.id}) is *#{to.status}*"+++`
334334
|
335335
|===
336+
337+
[ms-teams-notifications]
338+
==== Microsoft Teams notifications ====
339+
To enable Microsoft Teams notifications you need to setup a connector webhook url and set the appropriate configuration property.
340+
341+
.Microsoft Teams notifications configuration options
342+
|===
343+
| Property name |Description |Default value
344+
345+
| spring.boot.admin.notify.ms-teams.enabled
346+
| Enable Microsoft Teams notifications
347+
| `true`
348+
349+
| spring.boot.admin.notify.ms-teams.webhook-url
350+
| The Microsoft Teams webhook url to send the notifications to.
351+
|
352+
353+
| spring.boot.admin.notify.ms-teams.*
354+
| There are several options to customize the message title and color
355+
|
356+
|===

spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/config/NotifierConfiguration.java

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,6 @@
1717

1818
import java.util.List;
1919

20-
import de.codecentric.boot.admin.notify.CompositeNotifier;
21-
import de.codecentric.boot.admin.notify.MailNotifier;
22-
import de.codecentric.boot.admin.notify.Notifier;
23-
import de.codecentric.boot.admin.notify.NotifierListener;
24-
import de.codecentric.boot.admin.notify.PagerdutyNotifier;
25-
import de.codecentric.boot.admin.notify.OpsGenieNotifier;
26-
import de.codecentric.boot.admin.notify.HipchatNotifier;
27-
import de.codecentric.boot.admin.notify.SlackNotifier;
28-
import de.codecentric.boot.admin.notify.LetsChatNotifier;
2920
import org.springframework.beans.factory.annotation.Autowired;
3021
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
3122
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
@@ -42,6 +33,16 @@
4233
import org.springframework.context.annotation.Primary;
4334
import org.springframework.mail.MailSender;
4435

36+
import de.codecentric.boot.admin.notify.CompositeNotifier;
37+
import de.codecentric.boot.admin.notify.HipchatNotifier;
38+
import de.codecentric.boot.admin.notify.LetsChatNotifier;
39+
import de.codecentric.boot.admin.notify.MailNotifier;
40+
import de.codecentric.boot.admin.notify.MicrosoftTeamsNotifier;
41+
import de.codecentric.boot.admin.notify.Notifier;
42+
import de.codecentric.boot.admin.notify.NotifierListener;
43+
import de.codecentric.boot.admin.notify.OpsGenieNotifier;
44+
import de.codecentric.boot.admin.notify.PagerdutyNotifier;
45+
import de.codecentric.boot.admin.notify.SlackNotifier;
4546
import de.codecentric.boot.admin.notify.filter.FilteringNotifier;
4647
import de.codecentric.boot.admin.notify.filter.web.NotificationFilterController;
4748
import de.codecentric.boot.admin.web.PrefixHandlerMapping;
@@ -191,4 +192,15 @@ public LetsChatNotifier letsChatNotifier() {
191192
return new LetsChatNotifier();
192193
}
193194
}
195+
196+
@Configuration
197+
@ConditionalOnProperty(prefix = "spring.boot.admin.notify.ms-teams", name = "webhook-url")
198+
@AutoConfigureBefore({ NotifierListenerConfiguration.class,
199+
CompositeNotifierConfiguration.class})
200+
public static class MicrosoftTeamsNotifierConfiguration {
201+
@Bean
202+
@ConditionalOnMissingBean
203+
@ConfigurationProperties("spring.boot.admin.notify.ms-teams")
204+
public MicrosoftTeamsNotifier microsoftTeamsNotifier() { return new MicrosoftTeamsNotifier(); }
205+
}
194206
}
Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
package de.codecentric.boot.admin.notify;
2+
3+
import static java.util.Collections.singletonList;
4+
5+
import java.net.URI;
6+
import java.util.ArrayList;
7+
import java.util.List;
8+
import java.util.MissingFormatArgumentException;
9+
10+
import org.slf4j.Logger;
11+
import org.slf4j.LoggerFactory;
12+
import org.springframework.web.client.RestTemplate;
13+
14+
import de.codecentric.boot.admin.event.ClientApplicationDeregisteredEvent;
15+
import de.codecentric.boot.admin.event.ClientApplicationEvent;
16+
import de.codecentric.boot.admin.event.ClientApplicationRegisteredEvent;
17+
import de.codecentric.boot.admin.event.ClientApplicationStatusChangedEvent;
18+
import de.codecentric.boot.admin.model.Application;
19+
import de.codecentric.boot.admin.model.StatusInfo;
20+
21+
public class MicrosoftTeamsNotifier extends AbstractEventNotifier {
22+
private static final Logger LOGGER = LoggerFactory.getLogger(MicrosoftTeamsNotifier.class);
23+
private static final String STATUS_KEY = "Status";
24+
private static final String SERVICE_URL_KEY = "Service URL";
25+
private static final String HEALTH_URL_KEY = "Health URL";
26+
private static final String MANAGEMENT_URL_KEY = "Management URL";
27+
private static final String SOURCE_KEY = "Source";
28+
private RestTemplate restTemplate = new RestTemplate();
29+
30+
/**
31+
* Webhook url for Microsoft Teams Channel Webhook connector (i.e.
32+
* https://outlook.office.com/webhook/{webhook-id})
33+
*/
34+
private URI webhookUrl;
35+
36+
/**
37+
* Theme Color is the color of the accent on the message that appears in Microsoft Teams.
38+
* Default is Spring Green
39+
*/
40+
private String themeColor = "6db33f";
41+
42+
/**
43+
* Message will be used as title of the Activity section of the Teams message when an app
44+
* de-registers.
45+
*/
46+
private String deregisterActivitySubtitlePattern = "%s with id %s has de-registered from Spring Boot Admin";
47+
48+
/**
49+
* Message will be used as title of the Activity section of the Teams message when an app
50+
* registers
51+
*/
52+
private String registerActivitySubtitlePattern = "%s with id %s has registered from Spring Boot Admin";
53+
54+
/**
55+
* Message will be used as title of the Activity section of the Teams message when an app
56+
* changes status
57+
*/
58+
private String statusActivitySubtitlePattern = "%s with id %s changed status from %s to %s";
59+
60+
/**
61+
* Title of the Teams message when an app de-registers
62+
*/
63+
private String deRegisteredTitle = "De-Registered";
64+
65+
/**
66+
* Title of the Teams message when an app registers
67+
*/
68+
private String registeredTitle = "Registered";
69+
70+
/**
71+
* Title of the Teams message when an app changes status
72+
*/
73+
private String statusChangedTitle = "Status Changed";
74+
75+
/**
76+
* Summary section of every Teams message originating from Spring Boot Admin
77+
*/
78+
private String messageSummary = "Spring Boot Admin Notification";
79+
80+
@Override
81+
protected void doNotify(ClientApplicationEvent event) throws Exception {
82+
Message message;
83+
if (event instanceof ClientApplicationRegisteredEvent) {
84+
message = getRegisteredMessage(event.getApplication());
85+
} else if (event instanceof ClientApplicationDeregisteredEvent) {
86+
message = getDeregisteredMessage(event.getApplication());
87+
} else if (event instanceof ClientApplicationStatusChangedEvent) {
88+
ClientApplicationStatusChangedEvent statusChangedEvent = (ClientApplicationStatusChangedEvent) event;
89+
message = getStatusChangedMessage(statusChangedEvent.getApplication(),
90+
statusChangedEvent.getFrom(), statusChangedEvent.getTo());
91+
} else {
92+
return;
93+
}
94+
95+
this.restTemplate.postForObject(webhookUrl, message, Void.class);
96+
}
97+
98+
@Override
99+
protected boolean shouldNotify(ClientApplicationEvent event) {
100+
return event instanceof ClientApplicationRegisteredEvent
101+
|| event instanceof ClientApplicationDeregisteredEvent
102+
|| event instanceof ClientApplicationStatusChangedEvent;
103+
}
104+
105+
protected Message getDeregisteredMessage(Application app) {
106+
String activitySubtitle = this.safeFormat(deregisterActivitySubtitlePattern, app.getName(),
107+
app.getId());
108+
return createMessage(app, deRegisteredTitle, activitySubtitle);
109+
}
110+
111+
protected Message getRegisteredMessage(Application app) {
112+
String activitySubtitle = this.safeFormat(registerActivitySubtitlePattern, app.getName(),
113+
app.getId());
114+
return createMessage(app, registeredTitle, activitySubtitle);
115+
}
116+
117+
protected Message getStatusChangedMessage(Application app, StatusInfo from, StatusInfo to) {
118+
String activitySubtitle = this.safeFormat(statusActivitySubtitlePattern, app.getName(),
119+
app.getId(), from.getStatus(), to.getStatus());
120+
return createMessage(app, statusChangedTitle, activitySubtitle);
121+
}
122+
123+
private String safeFormat(String format, Object... args) {
124+
try {
125+
return String.format(format, args);
126+
} catch (MissingFormatArgumentException e) {
127+
LOGGER.warn(
128+
"Exception while trying to format the message. Falling back by using the format string.",
129+
e);
130+
return format;
131+
}
132+
}
133+
134+
protected Message createMessage(Application app, String registeredTitle,
135+
String activitySubtitle) {
136+
Message message = new Message();
137+
message.setTitle(registeredTitle);
138+
message.setSummary(messageSummary);
139+
message.setThemeColor(themeColor);
140+
141+
Section section = new Section();
142+
section.setActivityTitle(app.getName());
143+
section.setActivitySubtitle(activitySubtitle);
144+
145+
List<Fact> facts = new ArrayList<>();
146+
facts.add(new Fact(STATUS_KEY, app.getStatusInfo().getStatus()));
147+
facts.add(new Fact(SERVICE_URL_KEY, app.getServiceUrl()));
148+
facts.add(new Fact(HEALTH_URL_KEY, app.getHealthUrl()));
149+
facts.add(new Fact(MANAGEMENT_URL_KEY, app.getManagementUrl()));
150+
facts.add(new Fact(SOURCE_KEY, app.getSource()));
151+
section.setFacts(facts);
152+
message.setSections(singletonList(section));
153+
return message;
154+
}
155+
156+
public void setWebhookUrl(URI webhookUrl) {
157+
this.webhookUrl = webhookUrl;
158+
}
159+
160+
public void setThemeColor(String themeColor) {
161+
this.themeColor = themeColor;
162+
}
163+
164+
public String getDeregisterActivitySubtitlePattern() {
165+
return deregisterActivitySubtitlePattern;
166+
}
167+
168+
public void setDeregisterActivitySubtitlePattern(String deregisterActivitySubtitlePattern) {
169+
this.deregisterActivitySubtitlePattern = deregisterActivitySubtitlePattern;
170+
}
171+
172+
public String getRegisterActivitySubtitlePattern() {
173+
return registerActivitySubtitlePattern;
174+
}
175+
176+
public void setRegisterActivitySubtitlePattern(String registerActivitySubtitlePattern) {
177+
this.registerActivitySubtitlePattern = registerActivitySubtitlePattern;
178+
}
179+
180+
public String getStatusActivitySubtitlePattern() {
181+
return statusActivitySubtitlePattern;
182+
}
183+
184+
public void setStatusActivitySubtitlePattern(String statusActivitySubtitlePattern) {
185+
this.statusActivitySubtitlePattern = statusActivitySubtitlePattern;
186+
}
187+
188+
public String getDeRegisteredTitle() {
189+
return deRegisteredTitle;
190+
}
191+
192+
public void setDeRegisteredTitle(String deRegisteredTitle) {
193+
this.deRegisteredTitle = deRegisteredTitle;
194+
}
195+
196+
public String getRegisteredTitle() {
197+
return registeredTitle;
198+
}
199+
200+
public void setRegisteredTitle(String registeredTitle) {
201+
this.registeredTitle = registeredTitle;
202+
}
203+
204+
public String getStatusChangedTitle() {
205+
return statusChangedTitle;
206+
}
207+
208+
public void setStatusChangedTitle(String statusChangedTitle) {
209+
this.statusChangedTitle = statusChangedTitle;
210+
}
211+
212+
public String getMessageSummary() {
213+
return messageSummary;
214+
}
215+
216+
public void setMessageSummary(String messageSummary) {
217+
this.messageSummary = messageSummary;
218+
}
219+
220+
public void setRestTemplate(RestTemplate restTemplate) {
221+
this.restTemplate = restTemplate;
222+
}
223+
224+
public static class Message {
225+
private String summary;
226+
private String themeColor;
227+
private String title;
228+
private List<Section> sections = new ArrayList<>();
229+
230+
public String getSummary() {
231+
return summary;
232+
}
233+
234+
public void setSummary(String summary) {
235+
this.summary = summary;
236+
}
237+
238+
public String getTitle() {
239+
return title;
240+
}
241+
242+
public String getThemeColor() {
243+
return themeColor;
244+
}
245+
246+
public void setSections(List<Section> sections) {
247+
this.sections = sections;
248+
}
249+
250+
public void setTitle(String title) {
251+
this.title = title;
252+
}
253+
254+
public void setThemeColor(String themeColor) {
255+
this.themeColor = themeColor;
256+
}
257+
258+
public List<Section> getSections() {
259+
return sections;
260+
}
261+
}
262+
263+
public static class Section {
264+
private String activityTitle;
265+
private String activitySubtitle;
266+
private List<Fact> facts = new ArrayList<>();
267+
268+
public String getActivityTitle() {
269+
return activityTitle;
270+
}
271+
272+
public void setActivityTitle(String activityTitle) {
273+
this.activityTitle = activityTitle;
274+
}
275+
276+
public String getActivitySubtitle() {
277+
return activitySubtitle;
278+
}
279+
280+
public void setActivitySubtitle(String activitySubtitle) {
281+
this.activitySubtitle = activitySubtitle;
282+
}
283+
284+
public void setFacts(List<Fact> facts) {
285+
this.facts = facts;
286+
}
287+
288+
public List<Fact> getFacts() {
289+
return facts;
290+
}
291+
}
292+
293+
public static class Fact {
294+
private final String name;
295+
private final String value;
296+
297+
public Fact(String name, String value) {
298+
this.name = name;
299+
this.value = value;
300+
}
301+
302+
public String getName() {
303+
return name;
304+
}
305+
306+
public String getValue() {
307+
return value;
308+
}
309+
}
310+
}

0 commit comments

Comments
 (0)