Commit 79c850ac by Johannes Edmeier

Add RemindingNotifier

The RemindingNotifier sends periodic notifications using a delegate. It must be added explicitly to the ApplicationContext
parent 02f842c1
......@@ -103,7 +103,7 @@ Also have a look at the http://projects.spring.io/spring-cloud/spring-cloud.html
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
<version>1.0.4.RELEASE</version>
<version>1.0.6.RELEASE</version>
</dependency>
----
......@@ -228,7 +228,7 @@ spring.boot.admin.password
| `${spring.application.name}` if set, `"spring-boot-application"` otherwise.
| spring.boot.admin.client.prefer-ip
| Use the ip-address rather then the hostname in the guessed urls. If `server.address` / `management.address` is set they get used otherwise the IP address returned from `InetAddress.getLocalHost()` gets used.
| Use the ip-address rather then the hostname in the guessed urls. If `server.address` / `management.address` is set, it get used. Otherwise the IP address returned from `InetAddress.getLocalHost()` gets used.
| `false`
|===
......@@ -332,8 +332,10 @@ public class SpringBootAdminApplication {
| `"spring-boot-admin-event-store"`
|===
=== Notifications ===
[[mail-notifications]]
=== Mail notifications ===
==== Mail notifications ====
Configure a `JavaMailSender` using `spring-boot-starter-mail` and set a recipient.
......@@ -385,9 +387,8 @@ 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}"+++`
|===
[[pagerduty-notifications]]
=== Pagerduty notifications ===
==== Pagerduty notifications ====
To enable pagerduty notifications you just have to add a generic service to your pagerduty-account and set `spring.boot.admin.notify.pagerduty.service-key` to the service-key you received.
.Pagerduty notifications configuration options
......@@ -423,6 +424,36 @@ To enable pagerduty notifications you just have to add a generic service to your
|
|===
[reminder-notifactaions]
==== Reminder notifications ====
To get reminders for down/offline applications you can add a `RemindingNotifier` to your `ApplicationContext`. The `RemindingNotifier` uses another `Notifier` as delegate to send the reminders.
.How to configure reminders
[source,java]
----
@Configuration
public class ReminderConfiguration {
@Autowired
private Notifier notifier;
@Bean
@Primary
public RemindingNotifier remindingNotifier() {
RemindingNotifier remindingNotifier = new RemindingNotifier(notifier);
remindingNotifier.setReminderPeriod(TimeUnit.MINUTES.toMillis(5)); // <1>
return remindingNotifier;
}
@Scheduled(fixedRate = 6000L) // <2>
public void remind() {
remindingNotifier().sendReminders();
}
}
----
<1> The reminders will be sent every 5 minutes.
<2> Schedules sending of due reminders every 60 seconds.
[[faqs]]
== FAQs ==
[qanda]
......
......@@ -28,12 +28,11 @@ public class AdminServerImportSelector implements DeferredImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[] { MailNotifierConfiguration.class.getCanonicalName(),
return new String[] { NotifierConfiguration.class.getCanonicalName(),
HazelcastStoreConfiguration.class.getCanonicalName(),
AdminServerWebConfiguration.class.getCanonicalName(),
DiscoveryClientConfiguration.class.getCanonicalName(),
RevereseZuulProxyConfiguration.class.getCanonicalName(),
PagerdutyNotifierConfiguration.class.getCanonicalName() };
RevereseZuulProxyConfiguration.class.getCanonicalName() };
}
}
/*
* Copyright 2014 the original author or authors.
* Copyright 2013-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
......@@ -17,6 +17,7 @@ package de.codecentric.boot.admin.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
......@@ -27,20 +28,53 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.mail.MailSender;
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;
@Configuration
@ConditionalOnBean(MailSender.class)
@AutoConfigureAfter({ MailSenderAutoConfiguration.class })
public class MailNotifierConfiguration {
@Autowired
private MailSender mailSender;
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = "spring.boot.admin.notify.mail", name = "enabled", matchIfMissing = true)
@ConfigurationProperties("spring.boot.admin.notify.mail")
public MailNotifier mailNotifier() {
return new MailNotifier(mailSender);
public class NotifierConfiguration {
@Configuration
@ConditionalOnBean(Notifier.class)
public static class NotifierListenerConfiguration {
@Autowired
public Notifier notifier;
@Bean
@ConditionalOnMissingBean
public NotifierListener notifierListener() {
return new NotifierListener(notifier);
}
}
@Configuration
@ConditionalOnBean(MailSender.class)
@AutoConfigureAfter({ MailSenderAutoConfiguration.class })
@AutoConfigureBefore({ NotifierListenerConfiguration.class })
public static class MailNotifierConfiguration {
@Autowired
private MailSender mailSender;
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = "spring.boot.admin.notify.mail", name = "enabled", matchIfMissing = true)
@ConfigurationProperties("spring.boot.admin.notify.mail")
public MailNotifier mailNotifier() {
return new MailNotifier(mailSender);
}
}
@Configuration
@ConditionalOnProperty(prefix = "spring.boot.admin.notify.pagerduty", name = "service-key")
@AutoConfigureBefore({ NotifierListenerConfiguration.class })
public static class PagerdutyNotifierConfiguration {
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = "spring.boot.admin.notify.pagerduty", name = "enabled", matchIfMissing = true)
@ConfigurationProperties("spring.boot.admin.notify.pagerduty")
public PagerdutyNotifier pagerdutyNotifier() {
return new PagerdutyNotifier();
}
}
}
\ No newline at end of file
}
/*
* Copyright 2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.codecentric.boot.admin.config;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import de.codecentric.boot.admin.notify.PagerdutyNotifier;
@Configuration
@ConditionalOnProperty("spring.boot.admin.notify.pagerduty.service-key")
public class PagerdutyNotifierConfiguration {
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = "spring.boot.admin.notify.pagerduty", name = "enabled", matchIfMissing = true)
@ConfigurationProperties("spring.boot.admin.notify.pagerduty")
public PagerdutyNotifier pagerdutyNotifier() {
return new PagerdutyNotifier();
}
}
\ No newline at end of file
/*
* Copyright 2013-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.codecentric.boot.admin.notify;
import java.util.Arrays;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.event.EventListener;
import de.codecentric.boot.admin.event.ClientApplicationEvent;
import de.codecentric.boot.admin.event.ClientApplicationStatusChangedEvent;
public abstract class AbstractNotifier {
/**
* Abstract Notifier for status change which allows filtering of certain status changes.
*
* @author Johannes Edmeier
*/
public abstract class AbstractStatusChangeNotifier implements Notifier {
/**
* List of changes to ignore. Must be in Format OLD:NEW, for any status use * as wildcard, e.g.
......@@ -21,13 +41,17 @@ public abstract class AbstractNotifier {
*/
private boolean enabled = true;
@EventListener
public void onClientApplicationStatusChanged(ClientApplicationStatusChangedEvent event) {
if (enabled && shouldNotify(event.getFrom().getStatus(), event.getTo().getStatus())) {
try {
notify(event);
} catch (Exception ex) {
getLogger().error("Couldn't notify for status change {} ", event, ex);
@Override
public void notify(ClientApplicationEvent event) {
if (event instanceof ClientApplicationStatusChangedEvent) {
ClientApplicationStatusChangedEvent statusChange = (ClientApplicationStatusChangedEvent) event;
if (enabled && shouldNotify(statusChange.getFrom().getStatus(),
statusChange.getTo().getStatus())) {
try {
doNotify(statusChange);
} catch (Exception ex) {
getLogger().error("Couldn't notify for status change {} ", statusChange, ex);
}
}
}
}
......@@ -38,7 +62,7 @@ public abstract class AbstractNotifier {
&& Arrays.binarySearch(ignoreChanges, (from + ":*")) < 0;
}
protected abstract void notify(ClientApplicationStatusChangedEvent event) throws Exception;
protected abstract void doNotify(ClientApplicationStatusChangedEvent event) throws Exception;
private Logger getLogger() {
return LoggerFactory.getLogger(this.getClass());
......
......@@ -27,7 +27,12 @@ import org.springframework.mail.SimpleMailMessage;
import de.codecentric.boot.admin.event.ClientApplicationStatusChangedEvent;
public class MailNotifier extends AbstractNotifier {
/**
* Notifier sending emails.
*
* @author Johannes Edmeier
*/
public class MailNotifier extends AbstractStatusChangeNotifier {
private final static String DEFAULT_SUBJECT = "#{application.name} (#{application.id}) is #{to.status}";
private final static String DEFAULT_TEXT = "#{application.name} (#{application.id})\nstatus changed from #{from.status} to #{to.status}\n\n#{application.healthUrl}";
......@@ -66,7 +71,7 @@ public class MailNotifier extends AbstractNotifier {
}
@Override
protected void notify(ClientApplicationStatusChangedEvent event) {
protected void doNotify(ClientApplicationStatusChangedEvent event) {
EvaluationContext context = new StandardEvaluationContext(event);
SimpleMailMessage message = new SimpleMailMessage();
......
/*
* Copyright 2013-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.codecentric.boot.admin.notify;
import de.codecentric.boot.admin.event.ClientApplicationEvent;
/**
* Interface for components which emits notifications upon status changes in clients
*
* @author Johannes Edmeier
*/
public interface Notifier {
void notify(ClientApplicationEvent event);
}
\ No newline at end of file
package de.codecentric.boot.admin.notify;
import org.springframework.context.event.EventListener;
import de.codecentric.boot.admin.event.ClientApplicationEvent;
public class NotifierListener {
private final Notifier notifier;
public NotifierListener(Notifier notifier) {
this.notifier = notifier;
}
@EventListener
public void onClientApplicationEvent(ClientApplicationEvent event) {
notifier.notify(event);
}
}
......@@ -27,7 +27,12 @@ import org.springframework.web.client.RestTemplate;
import de.codecentric.boot.admin.event.ClientApplicationStatusChangedEvent;
public class PagerdutyNotifier extends AbstractNotifier {
/**
* Notifier submitting events to Pagerduty.
*
* @author Johannes Edmeier
*/
public class PagerdutyNotifier extends AbstractStatusChangeNotifier {
public static final URI DEFAULT_URI = URI
.create("https://events.pagerduty.com/generic/2010-04-15/create_event.json");
......@@ -67,7 +72,7 @@ public class PagerdutyNotifier extends AbstractNotifier {
}
@Override
protected void notify(ClientApplicationStatusChangedEvent event) throws Exception {
protected void doNotify(ClientApplicationStatusChangedEvent event) throws Exception {
restTemplate.postForEntity(url, createPagerdutyEvent(event), Void.class);
}
......
/*
* Copyright 2013-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.codecentric.boot.admin.notify;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import de.codecentric.boot.admin.event.ClientApplicationDeregisteredEvent;
import de.codecentric.boot.admin.event.ClientApplicationEvent;
import de.codecentric.boot.admin.event.ClientApplicationStatusChangedEvent;
/**
* Notifier that reminds certain statuses to send reminder notification using a delegate.
*
* @author Johannes Edmeier
*/
public class RemindingNotifier implements Notifier {
private final ConcurrentHashMap<String, Reminder> reminders = new ConcurrentHashMap<String, Reminder>();
private long reminderPeriod = TimeUnit.MINUTES.toMillis(10L);
private String[] reminderStatuses = { "DOWN", "OFFLINE" };
private final Notifier delegate;
public RemindingNotifier(Notifier delegate) {
this.delegate = delegate;
}
@Override
public void notify(ClientApplicationEvent event) {
delegate.notify(event);
if (shouldEndReminder(event)) {
reminders.remove(event.getApplication().getId());
} else if (shouldStartReminder(event)) {
reminders.putIfAbsent(event.getApplication().getId(), new Reminder(event));
}
}
public void sendReminders() {
long now = System.currentTimeMillis();
for (Reminder reminder : new ArrayList<Reminder>(reminders.values())) {
if (now - reminder.getLastNotification() > reminderPeriod) {
reminder.setLastNotification(now);
delegate.notify(reminder.getEvent());
}
}
}
protected boolean shouldStartReminder(ClientApplicationEvent event) {
if (event instanceof ClientApplicationStatusChangedEvent) {
return Arrays.binarySearch(reminderStatuses,
event.getApplication().getStatusInfo().getStatus()) >= 0;
}
return false;
}
protected boolean shouldEndReminder(ClientApplicationEvent event) {
if (event instanceof ClientApplicationDeregisteredEvent) {
return true;
}
if (event instanceof ClientApplicationStatusChangedEvent) {
return Arrays.binarySearch(reminderStatuses,
event.getApplication().getStatusInfo().getStatus()) < 0;
}
return false;
}
public void setReminderPeriod(long reminderPeriod) {
this.reminderPeriod = reminderPeriod;
}
public void setReminderStatuses(String[] reminderStatuses) {
String[] copy = Arrays.copyOf(reminderStatuses, reminderStatuses.length);
Arrays.sort(copy);
this.reminderStatuses = copy;
}
private static class Reminder {
private final ClientApplicationEvent event;
private long lastNotification;
public Reminder(ClientApplicationEvent event) {
this.event = event;
this.lastNotification = event.getTimestamp();
}
public void setLastNotification(long lastNotification) {
this.lastNotification = lastNotification;
}
public long getLastNotification() {
return lastNotification;
}
public ClientApplicationEvent getEvent() {
return event;
}
}
}
......@@ -150,8 +150,7 @@ public class AdminServerWebConfigurationTest {
applicationContext.register(ServerPropertiesAutoConfiguration.class);
applicationContext.register(MailSenderAutoConfiguration.class);
applicationContext.register(HazelcastAutoConfiguration.class);
applicationContext.register(MailNotifierConfiguration.class);
applicationContext.register(PagerdutyNotifierConfiguration.class);
applicationContext.register(NotifierConfiguration.class);
applicationContext.register(HazelcastStoreConfiguration.class);
applicationContext.register(DiscoveryClientConfiguration.class);
applicationContext.register(AdminServerWebConfiguration.class);
......
......@@ -47,7 +47,7 @@ public class MailNotifierTest {
@Test
public void test_onApplicationEvent() {
notifier.onClientApplicationStatusChanged(new ClientApplicationStatusChangedEvent(
notifier.notify(new ClientApplicationStatusChangedEvent(
Application.create("App").withId("-id-").withHealthUrl("http://health").build(),
StatusInfo.ofDown(), StatusInfo.ofUp()));
......@@ -66,7 +66,7 @@ public class MailNotifierTest {
@Test
public void test_onApplicationEvent_disbaled() {
notifier.setEnabled(false);
notifier.onClientApplicationStatusChanged(new ClientApplicationStatusChangedEvent(
notifier.notify(new ClientApplicationStatusChangedEvent(
Application.create("App").withId("-id-").withHealthUrl("http://health").build(),
StatusInfo.ofDown(), StatusInfo.ofUp()));
......@@ -75,7 +75,7 @@ public class MailNotifierTest {
@Test
public void test_onApplicationEvent_noSend() {
notifier.onClientApplicationStatusChanged(new ClientApplicationStatusChangedEvent(
notifier.notify(new ClientApplicationStatusChangedEvent(
Application.create("App").withId("-id-").withHealthUrl("http://health").build(),
StatusInfo.ofUnknown(), StatusInfo.ofUp()));
......@@ -85,7 +85,7 @@ public class MailNotifierTest {
@Test
public void test_onApplicationEvent_noSend_wildcard() {
notifier.setIgnoreChanges(new String[] { "*:UP" });
notifier.onClientApplicationStatusChanged(new ClientApplicationStatusChangedEvent(
notifier.notify(new ClientApplicationStatusChangedEvent(
Application.create("App").withId("-id-").withHealthUrl("http://health").build(),
StatusInfo.ofOffline(), StatusInfo.ofUp()));
......@@ -94,13 +94,13 @@ public class MailNotifierTest {
@Test
public void test_onApplicationEvent_throw_doesnt_propagate() {
AbstractNotifier notifier = new AbstractNotifier() {
Notifier notifier = new AbstractStatusChangeNotifier() {
@Override
protected void notify(ClientApplicationStatusChangedEvent event) throws Exception {
protected void doNotify(ClientApplicationStatusChangedEvent event) throws Exception {
throw new RuntimeException();
}
};
notifier.onClientApplicationStatusChanged(new ClientApplicationStatusChangedEvent(
notifier.notify(new ClientApplicationStatusChangedEvent(
Application.create("App").withId("-id-").withHealthUrl("http://health").build(),
StatusInfo.ofOffline(), StatusInfo.ofUp()));
}
......
......@@ -52,7 +52,7 @@ public class PagerdutyNotifierTest {
StatusInfo infoDown = StatusInfo.ofDown();
StatusInfo infoUp = StatusInfo.ofUp();
notifier.onClientApplicationStatusChanged(new ClientApplicationStatusChangedEvent(
notifier.notify(new ClientApplicationStatusChangedEvent(
Application.create("App").withId("-id-").withHealthUrl("http://health").build(),
infoDown, infoUp));
......@@ -75,7 +75,7 @@ public class PagerdutyNotifierTest {
StatusInfo infoDown = StatusInfo.ofDown();
StatusInfo infoUp = StatusInfo.ofUp();
notifier.onClientApplicationStatusChanged(new ClientApplicationStatusChangedEvent(
notifier.notify(new ClientApplicationStatusChangedEvent(
Application.create("App").withId("-id-").withHealthUrl("http://health").build(),
infoUp, infoDown));
......
package de.codecentric.boot.admin.notify;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.junit.Test;
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;
public class RemindingNotifierTest {
private static final ClientApplicationEvent APP_DOWN = new ClientApplicationStatusChangedEvent(
Application.create("App").withId("id-1").withHealthUrl("http://health")
.withStatusInfo(StatusInfo.ofDown()).build(),
StatusInfo.ofUp(), StatusInfo.ofDown());
private static final ClientApplicationEvent APP_UP = new ClientApplicationStatusChangedEvent(
Application.create("App").withId("id-1").withHealthUrl("http://health")
.withStatusInfo(StatusInfo.ofUp()).build(),
StatusInfo.ofDown(), StatusInfo.ofUp());
private static final ClientApplicationEvent OTHER_APP_UP = new ClientApplicationStatusChangedEvent(
Application.create("App").withId("id-2").withHealthUrl("http://health")
.withStatusInfo(StatusInfo.ofUp()).build(),
StatusInfo.ofDown(), StatusInfo.ofUp());
@Test
public void test_remind() throws Exception {
TestNotifier notifier = new TestNotifier();
RemindingNotifier reminder = new RemindingNotifier(notifier);
reminder.setReminderPeriod(-1000L);
reminder.notify(APP_DOWN);
reminder.notify(OTHER_APP_UP);
reminder.sendReminders();
reminder.sendReminders();
assertThat(notifier.getEvents(),
is(Arrays.asList(APP_DOWN, OTHER_APP_UP, APP_DOWN, APP_DOWN)));
}
@Test
public void test_no_remind_after_up() throws Exception {
TestNotifier notifier = new TestNotifier();
RemindingNotifier reminder = new RemindingNotifier(notifier);
reminder.setReminderPeriod(0L);
reminder.notify(APP_DOWN);
reminder.notify(APP_UP);
reminder.sendReminders();
assertThat(notifier.getEvents(), is(Arrays.asList(APP_DOWN, APP_UP)));
}
@Test
public void test_no_remind_before_end() throws Exception {
TestNotifier notifier = new TestNotifier();
RemindingNotifier reminder = new RemindingNotifier(notifier);
reminder.setReminderPeriod(Long.MAX_VALUE);
reminder.notify(APP_DOWN);
reminder.sendReminders();
assertThat(notifier.getEvents(), is(Arrays.asList(APP_DOWN)));
}
private static class TestNotifier implements Notifier {
private List<ClientApplicationEvent> events = new ArrayList<ClientApplicationEvent>();
@Override
public void notify(ClientApplicationEvent event) {
this.events.add(event);
}
public List<ClientApplicationEvent> getEvents() {
return events;
}
}
}
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