Commit 1283b1c1 by kevin peters Committed by Johannes Edmeier

Generate mails using thymeleaf templates

parent 9ed8ec4e
...@@ -131,6 +131,24 @@ spring.boot.admin.notify.mail.to=admin@example.com ...@@ -131,6 +131,24 @@ spring.boot.admin.notify.mail.to=admin@example.com
| `+++"#{application.name} (#{application.id})\nstatus changed from #{from.status} to #{to.status}\n\n#{application.healthUrl}"+++` | `+++"#{application.name} (#{application.id})\nstatus changed from #{from.status} to #{to.status}\n\n#{application.healthUrl}"+++`
|=== |===
Mail notifications will be delivered as HTML emails composed of multiple https://www.thymeleaf.org/[Thymeleaf] templates.
The default body looks like this:
....
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<span>some-service-id</span> (<span>a16d130feb17</span>)
status changed from <span>UP</span> to <span>DOWN</span>
<br />
<span>http://some.domain:8081/actuator/health</span>
</body>
</html>
....
[[pagerduty-notifications]] [[pagerduty-notifications]]
==== PagerDuty notifications ==== ==== PagerDuty notifications ====
......
...@@ -32,7 +32,11 @@ import de.codecentric.boot.admin.server.notify.TelegramNotifier; ...@@ -32,7 +32,11 @@ import de.codecentric.boot.admin.server.notify.TelegramNotifier;
import de.codecentric.boot.admin.server.notify.filter.FilteringNotifier; import de.codecentric.boot.admin.server.notify.filter.FilteringNotifier;
import de.codecentric.boot.admin.server.notify.filter.web.NotificationFilterController; import de.codecentric.boot.admin.server.notify.filter.web.NotificationFilterController;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set;
import org.apache.commons.lang.CharEncoding;
import org.reactivestreams.Publisher; import org.reactivestreams.Publisher;
import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.autoconfigure.AutoConfigureBefore;
...@@ -48,6 +52,10 @@ import org.springframework.context.annotation.Conditional; ...@@ -48,6 +52,10 @@ import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Primary;
import org.springframework.mail.MailSender; import org.springframework.mail.MailSender;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.spring5.SpringTemplateEngine;
import org.thymeleaf.templatemode.TemplateMode;
import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver;
@Configuration @Configuration
@AutoConfigureAfter({MailSenderAutoConfiguration.class}) @AutoConfigureAfter({MailSenderAutoConfiguration.class})
...@@ -109,7 +117,26 @@ public class AdminServerNotifierAutoConfiguration { ...@@ -109,7 +117,26 @@ public class AdminServerNotifierAutoConfiguration {
@ConditionalOnMissingBean @ConditionalOnMissingBean
@ConfigurationProperties("spring.boot.admin.notify.mail") @ConfigurationProperties("spring.boot.admin.notify.mail")
public MailNotifier mailNotifier(MailSender mailSender, InstanceRepository repository) { public MailNotifier mailNotifier(MailSender mailSender, InstanceRepository repository) {
return new MailNotifier(mailSender, repository); return new MailNotifier(mailSender, repository, mailNotifierTemplateEngine());
}
@Bean
public TemplateEngine mailNotifierTemplateEngine() {
final ClassLoaderTemplateResolver templateResolver = new ClassLoaderTemplateResolver();
templateResolver.setOrder(Integer.valueOf(1));
templateResolver.setPrefix("/templates/");
final Set<String> notificationTemplateNames = new HashSet<>();
notificationTemplateNames.add("notification-template-subject.*");
notificationTemplateNames.add("notification-template-body.*");
templateResolver.setResolvablePatterns(notificationTemplateNames);
templateResolver.setTemplateMode(TemplateMode.TEXT);
templateResolver.setCharacterEncoding(CharEncoding.UTF_8);
final SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.addTemplateResolver(templateResolver);
return templateEngine;
} }
} }
......
...@@ -19,18 +19,13 @@ package de.codecentric.boot.admin.server.notify; ...@@ -19,18 +19,13 @@ package de.codecentric.boot.admin.server.notify;
import de.codecentric.boot.admin.server.domain.entities.Instance; import de.codecentric.boot.admin.server.domain.entities.Instance;
import de.codecentric.boot.admin.server.domain.entities.InstanceRepository; import de.codecentric.boot.admin.server.domain.entities.InstanceRepository;
import de.codecentric.boot.admin.server.domain.events.InstanceEvent; import de.codecentric.boot.admin.server.domain.events.InstanceEvent;
import org.springframework.mail.MailSender;
import org.springframework.mail.SimpleMailMessage;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import org.springframework.context.expression.MapAccessor;
import org.springframework.expression.Expression;
import org.springframework.expression.ParserContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.mail.MailSender;
import org.springframework.mail.SimpleMailMessage;
/** /**
* Notifier sending emails. * Notifier sending emails.
...@@ -38,10 +33,6 @@ import org.springframework.mail.SimpleMailMessage; ...@@ -38,10 +33,6 @@ import org.springframework.mail.SimpleMailMessage;
* @author Johannes Edmeier * @author Johannes Edmeier
*/ */
public class MailNotifier extends AbstractStatusChangeNotifier { public class MailNotifier extends AbstractStatusChangeNotifier {
private static final String DEFAULT_SUBJECT = "#{instance.registration.name} (#{instance.id}) is #{event.statusInfo.status}";
private static final String DEFAULT_TEXT = "#{instance.registration.name} (#{instance.id})\nstatus changed from #{lastStatus} to #{event.statusInfo.status}\n\n#{instance.registration.healthUrl}";
private final SpelExpressionParser parser = new SpelExpressionParser();
private final MailSender sender; private final MailSender sender;
/** /**
...@@ -60,36 +51,33 @@ public class MailNotifier extends AbstractStatusChangeNotifier { ...@@ -60,36 +51,33 @@ public class MailNotifier extends AbstractStatusChangeNotifier {
private String from = null; private String from = null;
/** /**
* Mail Text. SpEL template using event as root; * Mail Subject. SpEL template using event as root;
*/ */
private Expression text; private String subject;
/** /**
* Mail Subject. SpEL template using event as root; * Mail Text. Is prepared via thymeleaf template;
*/ */
private Expression subject; private TemplateEngine templateEngine;
public MailNotifier(MailSender sender, InstanceRepository repository) { public MailNotifier(MailSender sender, InstanceRepository repository, TemplateEngine templateEngine) {
super(repository); super(repository);
this.sender = sender; this.sender = sender;
this.subject = parser.parseExpression(DEFAULT_SUBJECT, ParserContext.TEMPLATE_EXPRESSION); this.templateEngine = templateEngine;
this.text = parser.parseExpression(DEFAULT_TEXT, ParserContext.TEMPLATE_EXPRESSION);
} }
@Override @Override
protected Mono<Void> doNotify(InstanceEvent event, Instance instance) { protected Mono<Void> doNotify(InstanceEvent event, Instance instance) {
Map<String, Object> root = new HashMap<>(); final Context ctx = new Context();
root.put("event", event); ctx.setVariable("event", event);
root.put("instance", instance); ctx.setVariable("instance", instance);
root.put("lastStatus", getLastStatus(event.getInstance())); ctx.setVariable("lastStatus", getLastStatus(event.getInstance()));
StandardEvaluationContext context = new StandardEvaluationContext(root);
context.addPropertyAccessor(new MapAccessor()); final SimpleMailMessage message = new SimpleMailMessage();
SimpleMailMessage message = new SimpleMailMessage();
message.setTo(to); message.setTo(to);
message.setFrom(from); message.setFrom(from);
message.setSubject(subject.getValue(context, String.class)); message.setSubject(templateEngine.process("notification-template-subject.txt", ctx));
message.setText(text.getValue(context, String.class)); message.setText(templateEngine.process("notification-template-body.html", ctx));
message.setCc(cc); message.setCc(cc);
return Mono.fromRunnable(() -> sender.send(message)); return Mono.fromRunnable(() -> sender.send(message));
...@@ -120,19 +108,11 @@ public class MailNotifier extends AbstractStatusChangeNotifier { ...@@ -120,19 +108,11 @@ public class MailNotifier extends AbstractStatusChangeNotifier {
} }
public void setSubject(String subject) { public void setSubject(String subject) {
this.subject = parser.parseExpression(subject, ParserContext.TEMPLATE_EXPRESSION); this.subject = subject;
} }
public String getSubject() { public String getSubject() {
return subject.getExpressionString(); return subject;
}
public void setText(String text) {
this.text = parser.parseExpression(text, ParserContext.TEMPLATE_EXPRESSION);
}
public String getText() {
return text.getExpressionString();
} }
} }
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<span th:text="${instance.registration.name}"/> (<span th:text="${instance.id}"/>)
status changed from <span th:text="${lastStatus}"/> to <span th:text="${event.statusInfo.status}"/>
<br />
<span th:text="${instance.registration.healthUrl}"/>
</body>
</html>
[(${instance.registration.name})] ([(${instance.id})]) is [(${event.statusInfo.status})]
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
package de.codecentric.boot.admin.server.notify; package de.codecentric.boot.admin.server.notify;
import de.codecentric.boot.admin.server.config.AdminServerNotifierAutoConfiguration;
import de.codecentric.boot.admin.server.domain.entities.Instance; import de.codecentric.boot.admin.server.domain.entities.Instance;
import de.codecentric.boot.admin.server.domain.entities.InstanceRepository; import de.codecentric.boot.admin.server.domain.entities.InstanceRepository;
import de.codecentric.boot.admin.server.domain.events.InstanceEvent; import de.codecentric.boot.admin.server.domain.events.InstanceEvent;
...@@ -23,6 +24,7 @@ import de.codecentric.boot.admin.server.domain.events.InstanceStatusChangedEvent ...@@ -23,6 +24,7 @@ import de.codecentric.boot.admin.server.domain.events.InstanceStatusChangedEvent
import de.codecentric.boot.admin.server.domain.values.InstanceId; import de.codecentric.boot.admin.server.domain.values.InstanceId;
import de.codecentric.boot.admin.server.domain.values.Registration; import de.codecentric.boot.admin.server.domain.values.Registration;
import de.codecentric.boot.admin.server.domain.values.StatusInfo; import de.codecentric.boot.admin.server.domain.values.StatusInfo;
import org.thymeleaf.TemplateEngine;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.test.StepVerifier; import reactor.test.StepVerifier;
...@@ -44,14 +46,29 @@ public class MailNotifierTest { ...@@ -44,14 +46,29 @@ public class MailNotifierTest {
private MailNotifier notifier; private MailNotifier notifier;
private InstanceRepository repository; private InstanceRepository repository;
private static final String EXPECTED_SUBJECT = "App (-id-) is DOWN\n";
private static final String EXPECTED_BODY = "<!DOCTYPE html>\n" +
"<html>\n" +
"<head>\n" +
" <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n" +
"</head>\n" +
"<body>\n" +
"<span>App</span> (<span>-id-</span>)\n" +
"status changed from <span>UNKNOWN</span> to <span>DOWN</span>\n" +
"<br />\n" +
"<span>http://health</span>\n" +
"</body>\n" +
"</html>\n";
@Before @Before
public void setup() { public void setup() {
final TemplateEngine templateEngine = new AdminServerNotifierAutoConfiguration.MailNotifierConfiguration().mailNotifierTemplateEngine();
repository = mock(InstanceRepository.class); repository = mock(InstanceRepository.class);
when(repository.find(instance.getId())).thenReturn(Mono.just(instance)); when(repository.find(instance.getId())).thenReturn(Mono.just(instance));
sender = mock(MailSender.class); sender = mock(MailSender.class);
notifier = new MailNotifier(sender, repository); notifier = new MailNotifier(sender, repository, templateEngine);
notifier.setTo(new String[]{"foo@bar.com"}); notifier.setTo(new String[]{"foo@bar.com"});
notifier.setCc(new String[]{"bar@foo.com"}); notifier.setCc(new String[]{"bar@foo.com"});
notifier.setFrom("SBA <no-reply@example.com>"); notifier.setFrom("SBA <no-reply@example.com>");
...@@ -59,20 +76,18 @@ public class MailNotifierTest { ...@@ -59,20 +76,18 @@ public class MailNotifierTest {
} }
@Test @Test
public void test_onApplicationEvent() { public void test_onApplicationEvent_with_Thymeleaf_template() {
StepVerifier.create(notifier.notify( StepVerifier.create(notifier.notify(
new InstanceStatusChangedEvent(instance.getId(), instance.getVersion(), StatusInfo.ofDown()))) new InstanceStatusChangedEvent(instance.getId(), instance.getVersion(), StatusInfo.ofDown())))
.verifyComplete(); .verifyComplete();
StepVerifier.create(
notifier.notify(new InstanceStatusChangedEvent(instance.getId(), instance.getVersion(), StatusInfo.ofUp())))
.verifyComplete();
SimpleMailMessage expected = new SimpleMailMessage(); final SimpleMailMessage expected = new SimpleMailMessage();
expected.setTo(new String[]{"foo@bar.com"}); expected.setTo(new String[]{"foo@bar.com"});
expected.setCc(new String[]{"bar@foo.com"}); expected.setCc(new String[]{"bar@foo.com"});
expected.setFrom("SBA <no-reply@example.com>"); expected.setFrom("SBA <no-reply@example.com>");
expected.setText("App (-id-)\nstatus changed from DOWN to UP\n\nhttp://health");
expected.setSubject("-id- is UP"); expected.setSubject(EXPECTED_SUBJECT);
expected.setText(EXPECTED_BODY);
verify(sender).send(eq(expected)); verify(sender).send(eq(expected));
} }
......
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