Commit 76df8b0a by Johannes Edmeier

Merge remote-tracking branch 'upstream/master' into 2.x

parents 788c9ba6 0a029fc9
...@@ -32,13 +32,13 @@ This application provides a simple UI to administrate Spring Boot applications. ...@@ -32,13 +32,13 @@ This application provides a simple UI to administrate Spring Boot applications.
## Getting Started ## Getting Started
[A quick guide](http://codecentric.github.io/spring-boot-admin/1.5.2/#getting-started) to get started can be found in our docs. [A quick guide](http://codecentric.github.io/spring-boot-admin/1.5.3/#getting-started) to get started can be found in our docs.
## Getting Help ## Getting Help
Having trouble with Spring Boot Admin? We’d like to help! Having trouble with Spring Boot Admin? We’d like to help!
* Check the [reference documentation](http://codecentric.github.io/spring-boot-admin/1.5.2/). * Check the [reference documentation](http://codecentric.github.io/spring-boot-admin/1.5.3/).
* Ask a question on [stackoverflow.com](http://stackoverflow.com/questions/tagged/spring-boot-admin) - we monitor questions tagged with `spring-boot-admin`. * Ask a question on [stackoverflow.com](http://stackoverflow.com/questions/tagged/spring-boot-admin) - we monitor questions tagged with `spring-boot-admin`.
...@@ -47,7 +47,7 @@ Having trouble with Spring Boot Admin? We’d like to help! ...@@ -47,7 +47,7 @@ Having trouble with Spring Boot Admin? We’d like to help!
* Report bugs with Spring Boot Admin at http://github.com/codecentric/spring-boot-admin/issues. * Report bugs with Spring Boot Admin at http://github.com/codecentric/spring-boot-admin/issues.
## Reference Guide ## Reference Guide
[Version 1.5.2](http://codecentric.github.io/spring-boot-admin/1.5.2/) [Version 1.5.3](http://codecentric.github.io/spring-boot-admin/1.5.3/)
[Version 1.4.6](http://codecentric.github.io/spring-boot-admin/1.4.6/) [Version 1.4.6](http://codecentric.github.io/spring-boot-admin/1.4.6/)
......
...@@ -36,12 +36,12 @@ ...@@ -36,12 +36,12 @@
<main.basedir>${basedir}</main.basedir> <main.basedir>${basedir}</main.basedir>
<spring-boot.version>2.0.0.M3</spring-boot.version> <spring-boot.version>2.0.0.M3</spring-boot.version>
<spring-cloud.version>Finchley.BUILD-SNAPSHOT</spring-cloud.version> <spring-cloud.version>Finchley.BUILD-SNAPSHOT</spring-cloud.version>
<build-plugin.jacoco.version>0.7.7.201606060606</build-plugin.jacoco.version> <build-plugin.jacoco.version>0.7.9</build-plugin.jacoco.version>
<build-plugin.coveralls.version>4.2.0</build-plugin.coveralls.version> <build-plugin.coveralls.version>4.3.0</build-plugin.coveralls.version>
<build-plugin.gpg.version>1.6</build-plugin.gpg.version> <build-plugin.gpg.version>1.6</build-plugin.gpg.version>
<build-plugin.asciidoctor.version>1.5.3</build-plugin.asciidoctor.version> <build-plugin.asciidoctor.version>1.5.5</build-plugin.asciidoctor.version>
<build-plugin.exec.version>1.4.0</build-plugin.exec.version> <build-plugin.exec.version>1.6.0</build-plugin.exec.version>
<build-plugin.git-commit-id.version>2.2.1</build-plugin.git-commit-id.version> <build-plugin.git-commit-id.version>2.2.2</build-plugin.git-commit-id.version>
</properties> </properties>
<modules> <modules>
<module>spring-boot-admin-server</module> <module>spring-boot-admin-server</module>
......
...@@ -85,7 +85,15 @@ spring.boot.admin.client.password ...@@ -85,7 +85,15 @@ spring.boot.admin.client.password
| Interval for repeating the registration (in ms). | Interval for repeating the registration (in ms).
| `10.000` | `10.000`
| spring.boot.admin.client.auto-registration | spring.boot.admin.connectTimeout
| Connect timeout for the registration (in ms).
| `5.000`
| spring.boot.admin.readTimeout
| Read timeout for the registration (in ms).
| `5.000`
| spring.boot.admin.auto-registration
| If set to true the periodic task to register the application is automatically scheduled after the application is ready. | If set to true the periodic task to register the application is automatically scheduled after the application is ready.
| `true` | `true`
......
...@@ -44,7 +44,7 @@ See also the https://github.com/codecentric/spring-boot-admin/tree/master/spring ...@@ -44,7 +44,7 @@ See also the https://github.com/codecentric/spring-boot-admin/tree/master/spring
[[register-client-applications]] [[register-client-applications]]
=== Registering client applications === === Registering client applications ===
To register your application at the SBA Server you can either include the SBA Client or use http://projects.spring.io/spring-cloud/spring-cloud.html[Spring Cloud Discovery] (e.g. Eureka) To register your application at the SBA Server you can either include the SBA Client or use http://projects.spring.io/spring-cloud/spring-cloud.html[Spring Cloud Discovery] (e.g. Eureka, Consul, ...). There is also a <<spring-cloud-discovery-static-config,simple option using a static configuration on the SBA Server side>>.
[[register-clients-via-spring-boot-admin]] [[register-clients-via-spring-boot-admin]]
==== spring-boot-admin-starter-client ==== ==== spring-boot-admin-starter-client ====
......
[[spring-cloud-discovery-support]] [[spring-cloud-discovery-support]]
=== Spring Cloud Discovery === === Spring Cloud Discovery ===
The Spring Boot Admin Server can use Spring Clouds `DiscoveryClient` to discover applications. The advantage is that the clients don't have to include the `spring-boot-admin-starter-client`. You just have to add a DiscoveryClient to your admin server - everything else is done by AutoConfiguration. The Spring Boot Admin Server can use Spring Clouds `DiscoveryClient` to discover applications. The advantage is that the clients don't have to include the `spring-boot-admin-starter-client`. You just have to add a `DiscoveryClient` implementation to your admin server - everything else is done by AutoConfiguration.
The setup is explained <<discover-clients-via-spring-cloud-discovery,above>>.
[[spring-cloud-discovery-static-config]]
==== ServiceInstanceConverter ==== ==== SimpleDiscoveryClient configuration ====
Spring Boot Admin ships with the `SimpleDiscoveryClient` included. This allows you to specify client applications via configuration, without adding the SBA Client or a DiscoveryClient implementation to your monitored applications:
[source,yml]
.application.yml
----
spring:
cloud:
discovery:
client:
simple:
instances:
test:
- uri: http://instance1.intern:8080
metadata:
management.context-path: /actuator
- uri: http://instance2.intern:8080
metadata:
management.context-path: /actuator
----
==== Other DiscoveryClient implementations (Eureka, Zookeeper, Consul, ...) ====
Spring Boot Admin supports all other implementation of Spring Cloud's `DiscoveryClient`. You need to add it to the Spring Boot Admin Server and configure it properly.
An <<discover-clients-via-spring-cloud-discovery,example setup using Eureka>> is shown above.
==== Converting ServiceInstances into monitored applications ====
The information from the service registry are converted by the `ServiceInstanceConverter`. Spring Boot Admin ships with a default and Eureka converter implementation. The correct one is selected by AutoConfiguration. The information from the service registry are converted by the `ServiceInstanceConverter`. Spring Boot Admin ships with a default and Eureka converter implementation. The correct one is selected by AutoConfiguration.
...@@ -12,6 +38,28 @@ TIP: You can modify how the information from the registry is used to register th ...@@ -12,6 +38,28 @@ TIP: You can modify how the information from the registry is used to register th
NOTE: When using Eureka, the `healthCheckUrl` known to Eureka is used for health-checking, which can be set on your client using `eureka.instance.healthCheckUrl`. NOTE: When using Eureka, the `healthCheckUrl` known to Eureka is used for health-checking, which can be set on your client using `eureka.instance.healthCheckUrl`.
.Instance metadata options
|===
| Key |Value |Default value
| user.name +
user.password
| Credentials being used to access the endpoints.
|
| management.port
| The port is substituted in the service URL and will be used for accessing the actuator endpoints.
|
| management.context-path
| The path is appended to the service URL and will be used for accessing the actuator endpoints.
| `${spring.boot.admin.discovery.converter.mangement-context-path}`
| health.path
| The path is appended to the service URL and will be used for the health-checking. Ignored by the `EurekaServiceInstanceConverter`.
| `${spring.boot.admin.discovery.converter.health-endpoint}`
|===
.Discovery configuration options .Discovery configuration options
|=== |===
| Property name |Description |Default value | Property name |Description |Default value
...@@ -36,25 +84,3 @@ NOTE: When using Eureka, the `healthCheckUrl` known to Eureka is used for health ...@@ -36,25 +84,3 @@ NOTE: When using Eureka, the `healthCheckUrl` known to Eureka is used for health
| This services will be included when using discovery and registered as application. Supports simple patterns (e.g. "foo*", "*bar", "foo*bar*"). | This services will be included when using discovery and registered as application. Supports simple patterns (e.g. "foo*", "*bar", "foo*bar*").
| `"*"` | `"*"`
|=== |===
.Instance metadata options
|===
| Key |Value |Default value
| user.name +
user.password
| Credentials being used to access the endpoints.
|
| management.port
| The port is substituted in the service URL and will be used for accessing the actuator endpoints.
|
| management.context-path
| The path is appended to the service URL and will be used for accessing the actuator endpoints.
| `${spring.boot.admin.discovery.converter.mangement-context-path}`
| health.path
| The path is appended to the service URL and will be used for the health-checking. Ignored by the `EurekaServiceInstanceConverter`.
| `${spring.boot.admin.discovery.converter.health-endpoint}`
|===
...@@ -165,6 +165,60 @@ To enable pagerduty notifications you just have to add a generic service to your ...@@ -165,6 +165,60 @@ To enable pagerduty notifications you just have to add a generic service to your
| |
|=== |===
[[opsgenie-notifications]]
==== OpsGenie notifications ====
To enable OpsGenie notifications you just have to add a new JSON Rest API integration to your OpsGenie account and set `spring.boot.admin.notify.opsgenie.api-key` to the apiKey you received.
.OpsGenie notifications configuration options
|===
| Property name |Description |Default value
| spring.boot.admin.notify.opsgenie.enabled
| Enable OpsGenie notifications
| `true`
| spring.boot.admin.notify.opsgenie.ignore-changes
| Comma-delimited list of status changes to be ignored. Format: "<from-status>:<to-status>". Wildcards allowed.
| `"UNKNOWN:UP"`
| spring.boot.admin.notify.opsgenie.api-key
| apiKey you received when creating the integration
|
| spring.boot.admin.notify.opsgenie.url
| OpsGenie Alert API url
| `+++"https://api.opsgenie.com/v1/json/alert"+++`
| spring.boot.admin.notify.opsgenie.description
| Description to use in the event. SpEL-expressions are supported
| `+++"#{application.name}/#{application.id} is #{to.status}"+++`
| spring.boot.admin.notify.opsgenie.recipients
| User, group, schedule or escalation names to calculate which users will receive the notifications of the alert.
|
| spring.boot.admin.notify.opsgenie.actions
| Comma separated list of actions that can be executed.
|
| spring.boot.admin.notify.opsgenie.source
| Field to specify source of alert. By default, it will be assigned to IP address of incoming request.
|
| spring.boot.admin.notify.opsgenie.tags
| Comma separated list of labels attached to the alert.
|
| spring.boot.admin.notify.opsgenie.entity
| The entity the alert is related to.
|
| spring.boot.admin.notify.opsgenie.user
| Default owner of the execution. If user is not specified, the system becomes owner of the execution.
|
|===
[hipchat-notifications] [hipchat-notifications]
==== Hipchat notifications ==== ==== Hipchat notifications ====
To enable Hipchat notifications you need to create an API token from you Hipchat account and set the appropriate configuration properties. To enable Hipchat notifications you need to create an API token from you Hipchat account and set the appropriate configuration properties.
......
...@@ -76,7 +76,7 @@ include::{samples-dir}/spring-boot-admin-sample-eureka/src/main/resources/applic ...@@ -76,7 +76,7 @@ include::{samples-dir}/spring-boot-admin-sample-eureka/src/main/resources/applic
==== Activiti UI Module ==== ==== Activiti UI Module ====
The Activiti module shows information from the `/activti` endpoint. The Activiti module shows information from the `/activiti` endpoint.
. Add the ui module to your classpath: . Add the ui module to your classpath:
+ +
......
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
<parent> <parent>
<groupId>de.codecentric</groupId> <groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin</artifactId> <artifactId>spring-boot-admin</artifactId>
<version>1.5.3-SNAPSHOT</version> <version>1.5.4-SNAPSHOT</version>
<relativePath>..</relativePath> <relativePath>..</relativePath>
</parent> </parent>
<artifactId>spring-boot-admin-samples</artifactId> <artifactId>spring-boot-admin-samples</artifactId>
......
...@@ -22,7 +22,7 @@ import org.springframework.boot.test.context.SpringBootTest; ...@@ -22,7 +22,7 @@ import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class) @RunWith(SpringRunner.class)
@SpringBootTest(classes = {SpringBootAdminApplication.class}) @SpringBootTest(classes = {SpringBootAdminApplication.class}, properties = {"spring.cloud.consul.enabled=false"})
public class SpringBootAdminApplicationTest { public class SpringBootAdminApplicationTest {
@Test @Test
public void contextLoads() { public void contextLoads() {
......
...@@ -22,7 +22,7 @@ import org.springframework.boot.test.context.SpringBootTest; ...@@ -22,7 +22,7 @@ import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class) @RunWith(SpringRunner.class)
@SpringBootTest(classes = {SpringBootAdminApplication.class}) @SpringBootTest(classes = {SpringBootAdminApplication.class}, properties = {"spring.cloud.zookeeper.enabled=false"})
public class SpringBootAdminApplicationTest { public class SpringBootAdminApplicationTest {
@Test @Test
public void contextLoads() { public void contextLoads() {
......
...@@ -23,6 +23,7 @@ import de.codecentric.boot.admin.server.notify.LetsChatNotifier; ...@@ -23,6 +23,7 @@ import de.codecentric.boot.admin.server.notify.LetsChatNotifier;
import de.codecentric.boot.admin.server.notify.MailNotifier; import de.codecentric.boot.admin.server.notify.MailNotifier;
import de.codecentric.boot.admin.server.notify.NotificationTrigger; import de.codecentric.boot.admin.server.notify.NotificationTrigger;
import de.codecentric.boot.admin.server.notify.Notifier; import de.codecentric.boot.admin.server.notify.Notifier;
import de.codecentric.boot.admin.server.notify.OpsGenieNotifier;
import de.codecentric.boot.admin.server.notify.SlackNotifier; import de.codecentric.boot.admin.server.notify.SlackNotifier;
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;
...@@ -153,4 +154,16 @@ public class NotifierConfiguration { ...@@ -153,4 +154,16 @@ public class NotifierConfiguration {
return new LetsChatNotifier(repository); return new LetsChatNotifier(repository);
} }
} }
@Configuration
@ConditionalOnProperty(prefix = "spring.boot.admin.notify.opsgenie", name = "api-key")
@AutoConfigureBefore({NotifierTriggerConfiguration.class, CompositeNotifierConfiguration.class})
public static class OpsGenieNotifierConfiguration {
@Bean
@ConditionalOnMissingBean
@ConfigurationProperties("spring.boot.admin.notify.opsgenie")
public OpsGenieNotifier opsgenieNotifier(ApplicationRepository repository) {
return new OpsGenieNotifier(repository);
}
}
} }
...@@ -29,6 +29,7 @@ import java.util.Set; ...@@ -29,6 +29,7 @@ import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.client.discovery.event.HeartbeatEvent; import org.springframework.cloud.client.discovery.event.HeartbeatEvent;
...@@ -73,6 +74,11 @@ public class ApplicationDiscoveryListener { ...@@ -73,6 +74,11 @@ public class ApplicationDiscoveryListener {
} }
@EventListener @EventListener
public void onApplicationReady(ApplicationReadyEvent event) {
discover();
}
@EventListener
public void onInstanceRegistered(InstanceRegisteredEvent<?> event) { public void onInstanceRegistered(InstanceRegisteredEvent<?> event) {
discover(); discover();
} }
......
/*
* Copyright 2014-2017 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.server.notify;
import de.codecentric.boot.admin.server.domain.entities.Application;
import de.codecentric.boot.admin.server.domain.entities.ApplicationRepository;
import de.codecentric.boot.admin.server.domain.events.ClientApplicationEvent;
import de.codecentric.boot.admin.server.domain.events.ClientApplicationStatusChangedEvent;
import reactor.core.publisher.Mono;
import java.net.URI;
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.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.web.client.RestTemplate;
/**
* Notifier submitting events to opsgenie.com.
*
* @author Fernando Sure
*/
public class OpsGenieNotifier extends AbstractStatusChangeNotifier {
private static final URI DEFAULT_URI = URI.create("https://api.opsgenie.com/v1/json/alert");
private static final String DEFAULT_MESSAGE = "#{application.registration.name}/#{application.id} is #{application.statusInfo.status}";
private final SpelExpressionParser parser = new SpelExpressionParser();
private RestTemplate restTemplate = new RestTemplate();
/**
* BASE URL for OpsGenie API
*/
private URI url = DEFAULT_URI;
/**
* Integration ApiKey
*/
private String apiKey;
/**
* Comma separated list of users, groups, schedules or escalation names
* to calculate which users will receive the notifications of the alert.
*/
private String recipients;
/**
* Comma separated list of actions that can be executed.
*/
private String actions;
/**
* Field to specify source of alert. By default, it will be assigned to IP address of incoming request
*/
private String source;
/**
* Comma separated list of labels attached to the alert
*/
private String tags;
/**
* The entity the alert is related to.
*/
private String entity;
/**
* Default owner of the execution. If user is not specified, the system becomes owner of the execution.
*/
private String user;
/**
* Trigger description. SpEL template using event as root;
*/
private Expression description;
public OpsGenieNotifier(ApplicationRepository repositpry) {
super(repositpry);
this.description = parser.parseExpression(DEFAULT_MESSAGE, ParserContext.TEMPLATE_EXPRESSION);
}
@Override
protected Mono<Void> doNotify(ClientApplicationEvent event, Application application) {
return Mono.fromRunnable(
() -> restTemplate.exchange(buildUrl(event), HttpMethod.POST, createRequest(event, application),
Void.class));
}
protected String buildUrl(ClientApplicationEvent event) {
if ((event instanceof ClientApplicationStatusChangedEvent) &&
("UP".equals(((ClientApplicationStatusChangedEvent) event).getStatusInfo().getStatus()))) {
return String.format("%s/close", url.toString());
}
return url.toString();
}
protected HttpEntity createRequest(ClientApplicationEvent event, Application application) {
Map<String, Object> body = new HashMap<>();
body.put("apiKey", apiKey);
body.put("message", getMessage(event, application));
body.put("alias", application.getRegistration().getName() + "/" + application.getId());
body.put("description", getDescription(event, application));
if (event instanceof ClientApplicationStatusChangedEvent &&
!"UP".equals(((ClientApplicationStatusChangedEvent) event).getStatusInfo().getStatus())) {
if (recipients != null) {
body.put("recipients", recipients);
}
if (actions != null) {
body.put("actions", actions);
}
if (source != null) {
body.put("source", source);
}
if (tags != null) {
body.put("tags", tags);
}
if (entity != null) {
body.put("entity", entity);
}
if (user != null) {
body.put("user", user);
}
Map<String, Object> details = new HashMap<>();
details.put("type", "link");
details.put("href", application.getRegistration().getHealthUrl());
details.put("text", "Application health-endpoint");
body.put("details", details);
}
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return new HttpEntity<>(body, headers);
}
protected String getMessage(ClientApplicationEvent event, Application application) {
Map<String, Object> root = new HashMap<>();
root.put("event", event);
root.put("application", application);
root.put("lastStatus", getLastStatus(event.getApplication()));
StandardEvaluationContext context = new StandardEvaluationContext(root);
context.addPropertyAccessor(new MapAccessor());
return description.getValue(context, String.class);
}
protected String getDescription(ClientApplicationEvent event, Application application) {
return String.format("Application %s (%s) went from %s to %s", application.getRegistration().getName(),
application.getId(), getLastStatus(application.getId()),
((ClientApplicationStatusChangedEvent) event).getStatusInfo().getStatus());
}
public void setApiKey(String apiKey) {
this.apiKey = apiKey;
}
public String getApiKey() {
return apiKey;
}
public void setDescription(String description) {
this.description = parser.parseExpression(description, ParserContext.TEMPLATE_EXPRESSION);
}
public String getMessage() {
return description.getExpressionString();
}
public void setRestTemplate(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
public String getRecipients() {
return recipients;
}
public void setRecipients(String recipients) {
this.recipients = recipients;
}
public String getActions() {
return actions;
}
public void setActions(String actions) {
this.actions = actions;
}
public String getSource() {
return source;
}
public void setSource(String source) {
this.source = source;
}
public String getTags() {
return tags;
}
public void setTags(String tags) {
this.tags = tags;
}
public String getEntity() {
return entity;
}
public void setEntity(String entity) {
this.entity = entity;
}
public String getUser() {
return user;
}
public void setUser(String user) {
this.user = user;
}
}
...@@ -25,6 +25,7 @@ import de.codecentric.boot.admin.server.notify.HipchatNotifier; ...@@ -25,6 +25,7 @@ import de.codecentric.boot.admin.server.notify.HipchatNotifier;
import de.codecentric.boot.admin.server.notify.MailNotifier; import de.codecentric.boot.admin.server.notify.MailNotifier;
import de.codecentric.boot.admin.server.notify.NotificationTrigger; import de.codecentric.boot.admin.server.notify.NotificationTrigger;
import de.codecentric.boot.admin.server.notify.Notifier; import de.codecentric.boot.admin.server.notify.Notifier;
import de.codecentric.boot.admin.server.notify.OpsGenieNotifier;
import de.codecentric.boot.admin.server.notify.SlackNotifier; import de.codecentric.boot.admin.server.notify.SlackNotifier;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.test.StepVerifier; import reactor.test.StepVerifier;
...@@ -34,6 +35,7 @@ import java.util.ArrayList; ...@@ -34,6 +35,7 @@ import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import org.junit.After; import org.junit.After;
import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.springframework.boot.autoconfigure.mail.MailSenderAutoConfiguration; import org.springframework.boot.autoconfigure.mail.MailSenderAutoConfiguration;
import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration; import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration;
...@@ -43,6 +45,8 @@ import org.springframework.context.annotation.Primary; ...@@ -43,6 +45,8 @@ import org.springframework.context.annotation.Primary;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.is;
public class NotifierConfigurationTest { public class NotifierConfigurationTest {
private static final ClientApplicationEvent APP_DOWN = new ClientApplicationStatusChangedEvent( private static final ClientApplicationEvent APP_DOWN = new ClientApplicationStatusChangedEvent(
...@@ -95,6 +99,13 @@ public class NotifierConfigurationTest { ...@@ -95,6 +99,13 @@ public class NotifierConfigurationTest {
assertThat(context.getBean(SlackNotifier.class)).isInstanceOf(SlackNotifier.class); assertThat(context.getBean(SlackNotifier.class)).isInstanceOf(SlackNotifier.class);
} }
@Test
public void test_opsgenie() {
load(null, "spring.boot.admin.notify.opsgenie.api-key:foo");
Assert.assertThat(context.getBean(OpsGenieNotifier.class), is(instanceOf(OpsGenieNotifier.class)));
}
@Test @Test
public void test_multipleNotifiers() { public void test_multipleNotifiers() {
load(TestMultipleNotifierConfig.class); load(TestMultipleNotifierConfig.class);
......
...@@ -23,6 +23,7 @@ import de.codecentric.boot.admin.server.services.HashingApplicationUrlIdGenerato ...@@ -23,6 +23,7 @@ import de.codecentric.boot.admin.server.services.HashingApplicationUrlIdGenerato
import reactor.test.StepVerifier; import reactor.test.StepVerifier;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
...@@ -56,6 +57,20 @@ public class ApplicationDiscoveryListenerTest { ...@@ -56,6 +57,20 @@ public class ApplicationDiscoveryListenerTest {
} }
@Test @Test
public void test_application_ready() {
when(discovery.getServices()).thenReturn(Collections.singletonList("service"));
when(discovery.getInstances("service")).thenReturn(
Collections.singletonList(new DefaultServiceInstance("service", "localhost", 80, false)));
listener.onApplicationReady(null);
StepVerifier.create(registry.getApplications())
.assertNext(a -> assertThat(a.getRegistration().getName()).isEqualTo("service"))
.verifyComplete();
}
@Test
public void test_ignore() { public void test_ignore() {
when(discovery.getServices()).thenReturn(singletonList("service")); when(discovery.getServices()).thenReturn(singletonList("service"));
when(discovery.getInstances("service")).thenReturn( when(discovery.getInstances("service")).thenReturn(
......
/*
* Copyright 2014-2017 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.server.notify;
import de.codecentric.boot.admin.server.domain.entities.Application;
import de.codecentric.boot.admin.server.domain.entities.ApplicationRepository;
import de.codecentric.boot.admin.server.domain.events.ClientApplicationStatusChangedEvent;
import de.codecentric.boot.admin.server.domain.values.ApplicationId;
import de.codecentric.boot.admin.server.domain.values.Registration;
import de.codecentric.boot.admin.server.domain.values.StatusInfo;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.util.HashMap;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.web.client.RestTemplate;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class OpsGenieNotifierTest {
private OpsGenieNotifier notifier;
private RestTemplate restTemplate;
private ApplicationRepository repository;
private static final String appName = "App";
private static final Application application = Application.create(ApplicationId.of("-id-"))
.register(Registration.create(appName, "http://health")
.build());
@Before
public void setUp() {
repository = mock(ApplicationRepository.class);
when(repository.find(application.getId())).thenReturn(Mono.just(application));
restTemplate = mock(RestTemplate.class);
notifier = new OpsGenieNotifier(repository);
notifier.setApiKey("--service--");
notifier.setRecipients("--recipients--");
notifier.setRestTemplate(restTemplate);
}
@Test
public void test_onApplicationEvent_resolve() {
StepVerifier.create(notifier.notify(
new ClientApplicationStatusChangedEvent(application.getId(), application.getVersion() + 1,
StatusInfo.ofDown()))).verifyComplete();
reset(restTemplate);
when(repository.find(application.getId())).thenReturn(Mono.just(application.withStatusInfo(StatusInfo.ofUp())));
StepVerifier.create(notifier.notify(
new ClientApplicationStatusChangedEvent(application.getId(), application.getVersion() + 2,
StatusInfo.ofUp()))).verifyComplete();
verify(restTemplate).exchange(eq("https://api.opsgenie.com/v1/json/alert/close"), eq(HttpMethod.POST),
eq(expectedRequest("DOWN", "UP")), eq(Void.class));
}
@Test
public void test_onApplicationEvent_trigger() {
StepVerifier.create(notifier.notify(
new ClientApplicationStatusChangedEvent(application.getId(), application.getVersion() + 1,
StatusInfo.ofUp()))).verifyComplete();
reset(restTemplate);
when(repository.find(application.getId())).thenReturn(
Mono.just(application.withStatusInfo(StatusInfo.ofDown())));
StepVerifier.create(notifier.notify(
new ClientApplicationStatusChangedEvent(application.getId(), application.getVersion() + 2,
StatusInfo.ofDown()))).verifyComplete();
verify(restTemplate).exchange(eq("https://api.opsgenie.com/v1/json/alert"), eq(HttpMethod.POST),
eq(expectedRequest("UP", "DOWN")), eq(Void.class));
}
private String getMessage(String expectedStatus) {
return String.format("App/-id- is %s", expectedStatus);
}
private String getDescription(String expectedOldStatus, String expectedNewStatus) {
return String.format("Application App (-id-) went from %s to %s", expectedOldStatus, expectedNewStatus);
}
private HttpEntity expectedRequest(String expectedOldStatus, String expectedNewStatus) {
Map<String, Object> expected = new HashMap<>();
expected.put("apiKey", "--service--");
expected.put("message", getMessage(expectedNewStatus));
expected.put("alias", "App/-id-");
expected.put("description", getDescription(expectedOldStatus, expectedNewStatus));
if (!"UP".equals(expectedNewStatus)) {
expected.put("recipients", "--recipients--");
Map<String, Object> details = new HashMap<>();
details.put("type", "link");
details.put("href", "http://health");
details.put("text", "Application health-endpoint");
expected.put("details", details);
}
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return new HttpEntity<>(expected, headers);
}
}
...@@ -37,6 +37,16 @@ public class ClientProperties { ...@@ -37,6 +37,16 @@ public class ClientProperties {
private long period = 10_000L; private long period = 10_000L;
/** /**
* Connect timeout (in ms) for the registration.
*/
private int connectTimeout = 5_000;
/**
* Read timeout (in ms) for the registration.
*/
private int readTimeout = 5_000;
/**
* Username for basic authentication on admin server * Username for basic authentication on admin server
*/ */
private String username; private String username;
......
...@@ -19,10 +19,9 @@ import de.codecentric.boot.admin.client.registration.ApplicationFactory; ...@@ -19,10 +19,9 @@ import de.codecentric.boot.admin.client.registration.ApplicationFactory;
import de.codecentric.boot.admin.client.registration.ApplicationRegistrator; import de.codecentric.boot.admin.client.registration.ApplicationRegistrator;
import de.codecentric.boot.admin.client.registration.DefaultApplicationFactory; import de.codecentric.boot.admin.client.registration.DefaultApplicationFactory;
import de.codecentric.boot.admin.client.registration.RegistrationApplicationListener; import de.codecentric.boot.admin.client.registration.RegistrationApplicationListener;
import javax.servlet.ServletContext;
import de.codecentric.boot.admin.client.registration.ServletApplicationFactory; import de.codecentric.boot.admin.client.registration.ServletApplicationFactory;
import javax.servlet.ServletContext;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.actuate.autoconfigure.ManagementServerProperties; import org.springframework.boot.actuate.autoconfigure.ManagementServerProperties;
...@@ -66,7 +65,9 @@ public class SpringBootAdminClientAutoConfiguration { ...@@ -66,7 +65,9 @@ public class SpringBootAdminClientAutoConfiguration {
ApplicationFactory applicationFactory, ApplicationFactory applicationFactory,
RestTemplateBuilder restTemplBuilder) { RestTemplateBuilder restTemplBuilder) {
RestTemplateBuilder builder = restTemplBuilder.messageConverters(new MappingJackson2HttpMessageConverter()) RestTemplateBuilder builder = restTemplBuilder.messageConverters(new MappingJackson2HttpMessageConverter())
.requestFactory(SimpleClientHttpRequestFactory.class); .requestFactory(SimpleClientHttpRequestFactory.class)
.setConnectTimeout(client.getConnectTimeout())
.setReadTimeout(client.getReadTimeout());
if (client.getUsername() != null) { if (client.getUsername() != null) {
builder = builder.basicAuthorization(client.getUsername(), client.getPassword()); builder = builder.basicAuthorization(client.getUsername(), client.getPassword());
} }
......
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