Skip to content

Commit 4f78a3a

Browse files
authored
feat: implementation for mattermost notifier (#4821)
1 parent 1bcb081 commit 4f78a3a

File tree

5 files changed

+374
-0
lines changed

5 files changed

+374
-0
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
sidebar_custom_props:
3+
icon: 'notifications'
4+
---
5+
import metadata from "../../../../../../spring-boot-admin-server/target/classes/META-INF/spring-configuration-metadata.json";
6+
import { PropertyTable } from "../../../src/components/PropertyTable";
7+
8+
# Mattermost Notifications
9+
10+
To enable [Mattermost](https://mattermost.com/) notifications you need to add a bot account under integrations on your Mattermost server and configure it appropriately.
11+
12+
<PropertyTable
13+
title="Mattermost notifications configuration options"
14+
properties={metadata.properties}
15+
filter={['notify.mattermost']}
16+
exclusive={false}
17+
/>

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
import de.codecentric.boot.admin.server.notify.HipchatNotifier;
6161
import de.codecentric.boot.admin.server.notify.LetsChatNotifier;
6262
import de.codecentric.boot.admin.server.notify.MailNotifier;
63+
import de.codecentric.boot.admin.server.notify.MattermostNotifier;
6364
import de.codecentric.boot.admin.server.notify.MicrosoftTeamsNotifier;
6465
import de.codecentric.boot.admin.server.notify.NotificationTrigger;
6566
import de.codecentric.boot.admin.server.notify.Notifier;
@@ -222,6 +223,22 @@ public SlackNotifier slackNotifier(InstanceRepository repository, NotifierProxyP
222223

223224
}
224225

226+
@Configuration(proxyBeanMethods = false)
227+
@ConditionalOnProperty(prefix = "spring.boot.admin.notify.mattermost", name = "api-url")
228+
@AutoConfigureBefore({ NotifierTriggerConfiguration.class, CompositeNotifierConfiguration.class })
229+
@Lazy(false)
230+
public static class MattermostNotifierConfiguration {
231+
232+
@Bean
233+
@ConditionalOnMissingBean
234+
@ConfigurationProperties("spring.boot.admin.notify.mattermost")
235+
public MattermostNotifier mattermostNotifier(InstanceRepository repository,
236+
NotifierProxyProperties proxyProperties) {
237+
return new MattermostNotifier(repository, createNotifierRestTemplate(proxyProperties));
238+
}
239+
240+
}
241+
225242
@Configuration(proxyBeanMethods = false)
226243
@ConditionalOnProperty(prefix = "spring.boot.admin.notify.letschat", name = "url")
227244
@AutoConfigureBefore({ NotifierTriggerConfiguration.class, CompositeNotifierConfiguration.class })
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
/*
2+
* Copyright 2014-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package de.codecentric.boot.admin.server.notify;
18+
19+
import java.net.URI;
20+
import java.util.Collections;
21+
import java.util.HashMap;
22+
import java.util.Map;
23+
24+
import org.springframework.context.expression.MapAccessor;
25+
import org.springframework.expression.Expression;
26+
import org.springframework.expression.ParserContext;
27+
import org.springframework.expression.spel.standard.SpelExpressionParser;
28+
import org.springframework.expression.spel.support.DataBindingPropertyAccessor;
29+
import org.springframework.expression.spel.support.SimpleEvaluationContext;
30+
import org.springframework.http.HttpEntity;
31+
import org.springframework.http.HttpHeaders;
32+
import org.springframework.http.MediaType;
33+
import org.springframework.lang.Nullable;
34+
import org.springframework.web.client.RestTemplate;
35+
import reactor.core.publisher.Mono;
36+
37+
import de.codecentric.boot.admin.server.domain.entities.Instance;
38+
import de.codecentric.boot.admin.server.domain.entities.InstanceRepository;
39+
import de.codecentric.boot.admin.server.domain.events.InstanceEvent;
40+
import de.codecentric.boot.admin.server.domain.events.InstanceStatusChangedEvent;
41+
import de.codecentric.boot.admin.server.domain.values.StatusInfo;
42+
43+
/**
44+
* Notifier submitting events to Mattermost.
45+
*
46+
* @author Emir Boyaci
47+
*/
48+
public class MattermostNotifier extends AbstractStatusChangeNotifier {
49+
50+
private static final String DEFAULT_MESSAGE = "**#{instance.registration.name}** (#{instance.id}) is **#{event.statusInfo.status}**";
51+
52+
private final SpelExpressionParser parser = new SpelExpressionParser();
53+
54+
private RestTemplate restTemplate;
55+
56+
/**
57+
* API url for Mattermost (i.e. https://example.mattermost.com/api/v4/posts)
58+
*/
59+
@Nullable
60+
private URI apiUrl;
61+
62+
/**
63+
* Bot access token (i.e. dufc8q78hjgeccwsfhe37pcq1w)
64+
*/
65+
@Nullable
66+
private String botAccessToken;
67+
68+
/**
69+
* Optional channel name without # sign (i.e. h616jh436pysjpopp3259mhwxc)
70+
*/
71+
@Nullable
72+
private String channelId;
73+
74+
/**
75+
* Message formatted using Mattermost markups. SpEL template using event as root
76+
*/
77+
private Expression message;
78+
79+
public MattermostNotifier(InstanceRepository repository, RestTemplate restTemplate) {
80+
super(repository);
81+
this.restTemplate = restTemplate;
82+
this.message = parser.parseExpression(DEFAULT_MESSAGE, ParserContext.TEMPLATE_EXPRESSION);
83+
}
84+
85+
@Override
86+
protected Mono<Void> doNotify(InstanceEvent event, Instance instance) {
87+
if (apiUrl == null) {
88+
return Mono.error(new IllegalStateException("'url' must not be null."));
89+
}
90+
return Mono.fromRunnable(() -> restTemplate.postForEntity(apiUrl, createMessage(event, instance), Void.class));
91+
}
92+
93+
public void setRestTemplate(RestTemplate restTemplate) {
94+
this.restTemplate = restTemplate;
95+
}
96+
97+
protected Object createMessage(InstanceEvent event, Instance instance) {
98+
Map<String, Object> messageJson = new HashMap<>();
99+
if (channelId != null) {
100+
messageJson.put("channel_id", channelId);
101+
}
102+
103+
Map<String, Object> attachments = new HashMap<>();
104+
attachments.put("text", getText(event, instance));
105+
attachments.put("fallback", getText(event, instance));
106+
attachments.put("color", getColor(event));
107+
108+
Map<String, Object> props = new HashMap<>();
109+
props.put("attachments", Collections.singletonList(attachments));
110+
111+
messageJson.put("props", props);
112+
113+
HttpHeaders headers = new HttpHeaders();
114+
headers.setContentType(MediaType.APPLICATION_JSON);
115+
headers.setBearerAuth(botAccessToken);
116+
return new HttpEntity<>(messageJson, headers);
117+
}
118+
119+
@Nullable
120+
protected String getText(InstanceEvent event, Instance instance) {
121+
Map<String, Object> root = new HashMap<>();
122+
root.put("event", event);
123+
root.put("instance", instance);
124+
root.put("lastStatus", getLastStatus(event.getInstance()));
125+
SimpleEvaluationContext context = SimpleEvaluationContext
126+
.forPropertyAccessors(DataBindingPropertyAccessor.forReadOnlyAccess(), new MapAccessor())
127+
.withRootObject(root)
128+
.build();
129+
130+
return message.getValue(context, String.class);
131+
}
132+
133+
protected String getColor(InstanceEvent event) {
134+
if (event instanceof InstanceStatusChangedEvent statusChangedEvent) {
135+
return StatusInfo.STATUS_UP.equals(statusChangedEvent.getStatusInfo().getStatus()) ? "#2eb885" : "#a30100";
136+
}
137+
else {
138+
return "#439FE0";
139+
}
140+
}
141+
142+
@Nullable
143+
public URI getApiUrl() {
144+
return apiUrl;
145+
}
146+
147+
public void setApiUrl(@Nullable URI apiUrl) {
148+
this.apiUrl = apiUrl;
149+
}
150+
151+
@Nullable
152+
public String getChannelId() {
153+
return channelId;
154+
}
155+
156+
public void setChannelId(@Nullable String channelId) {
157+
this.channelId = channelId;
158+
}
159+
160+
@Nullable
161+
public String getBotAccessToken() {
162+
return botAccessToken;
163+
}
164+
165+
public void setBotAccessToken(@Nullable String botAccessToken) {
166+
this.botAccessToken = botAccessToken;
167+
}
168+
169+
public String getMessage() {
170+
return message.getExpressionString();
171+
}
172+
173+
public void setMessage(String message) {
174+
this.message = parser.parseExpression(message, ParserContext.TEMPLATE_EXPRESSION);
175+
}
176+
177+
}

spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/config/AdminServerNotifierAutoConfigurationTest.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import de.codecentric.boot.admin.server.notify.HipchatNotifier;
3535
import de.codecentric.boot.admin.server.notify.LetsChatNotifier;
3636
import de.codecentric.boot.admin.server.notify.MailNotifier;
37+
import de.codecentric.boot.admin.server.notify.MattermostNotifier;
3738
import de.codecentric.boot.admin.server.notify.MicrosoftTeamsNotifier;
3839
import de.codecentric.boot.admin.server.notify.NotificationTrigger;
3940
import de.codecentric.boot.admin.server.notify.Notifier;
@@ -94,6 +95,12 @@ void test_slack() {
9495
.run((context) -> assertThat(context).hasSingleBean(SlackNotifier.class));
9596
}
9697

98+
@Test
99+
void test_mattermost() {
100+
this.contextRunner.withPropertyValues("spring.boot.admin.notify.mattermost.api-url:https://example.com")
101+
.run((context) -> assertThat(context).hasSingleBean(MattermostNotifier.class));
102+
}
103+
97104
@Test
98105
void test_pagerduty() {
99106
this.contextRunner.withPropertyValues("spring.boot.admin.notify.pagerduty.service-key:foo")

0 commit comments

Comments
 (0)