Commit 54a67c01 by Johannes Stelzer

Redesigned registration.

* registry-key is derived from URL * option to specify a management-URL and other various config options see spring-boot-starter-admin-client/README.md * less chatty registration * some more test cases * show application URL in overview and headline * show application info in overview * set HTTP-Codes in RegistryController fixes #13 fixes #16 closes #17 closes #18 I didn't add the key-value-properties to the Application. Since it was somehow redundant to the /info-endpoint.
parent d5471d18
...@@ -50,7 +50,6 @@ See also the [example project](https://github.com/codecentric/spring-boot-admin/ ...@@ -50,7 +50,6 @@ See also the [example project](https://github.com/codecentric/spring-boot-admin/
#### Client applications #### Client applications
Each application that want to register itself to the admin application has to include the [spring-boot-starter-admin-client](https://github.com/codecentric/spring-boot-admin/tree/master/spring-boot-starter-admin-client) as dependency. This starter JAR includes some [AutoConfiguration](http://docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference/htmlsingle/#using-boot-auto-configuration "Spring Boot docu") features that includes registering tasks, controller, etc. Each application that want to register itself to the admin application has to include the [spring-boot-starter-admin-client](https://github.com/codecentric/spring-boot-admin/tree/master/spring-boot-starter-admin-client) as dependency. This starter JAR includes some [AutoConfiguration](http://docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference/htmlsingle/#using-boot-auto-configuration "Spring Boot docu") features that includes registering tasks, controller, etc.
``` ```
<dependency> <dependency>
<groupId>de.codecentric</groupId> <groupId>de.codecentric</groupId>
...@@ -59,12 +58,14 @@ Each application that want to register itself to the admin application has to in ...@@ -59,12 +58,14 @@ Each application that want to register itself to the admin application has to in
</dependency> </dependency>
``` ```
Inside your application.properties you also have to define the URL of the Spring Boot Admin Server, e.g. Inside your configuration (e.g. application.properties) you also have to define the URL of the Spring Boot Admin Server, e.g.
``` ```
spring.boot.admin.url=http://localhost:8080 spring.boot.admin.url=http://localhost:8080
``` ```
For all configuration options see [spring-boot-starter-admin-client](https://github.com/codecentric/spring-boot-admin/tree/master/spring-boot-starter-admin-client)
#### Screenshots #### Screenshots
##### Dashboard ##### Dashboard
......
spring.resources.cachePeriod=3600 spring.resources.cachePeriod=3600
server.port=8080 server.port=8080
info.version=@pom.version@ info.version=@pom.version@
info.stage=test
logging.file=/tmp/log.log logging.file=/tmp/log.log
spring.application.name=@pom.artifactId@ spring.application.name=@pom.artifactId@
spring.boot.admin.url=http://localhost:8080 spring.boot.admin.url=http://localhost:8080
...@@ -17,6 +17,7 @@ package de.codecentric.boot.admin.config; ...@@ -17,6 +17,7 @@ package de.codecentric.boot.admin.config;
import java.util.List; import java.util.List;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter;
...@@ -25,6 +26,7 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter ...@@ -25,6 +26,7 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter
import de.codecentric.boot.admin.controller.RegistryController; import de.codecentric.boot.admin.controller.RegistryController;
import de.codecentric.boot.admin.service.ApplicationRegistry; import de.codecentric.boot.admin.service.ApplicationRegistry;
import de.codecentric.boot.admin.service.SimpleApplicationRegistry;
@Configuration @Configuration
public class WebappConfig extends WebMvcConfigurerAdapter { public class WebappConfig extends WebMvcConfigurerAdapter {
...@@ -41,16 +43,17 @@ public class WebappConfig extends WebMvcConfigurerAdapter { ...@@ -41,16 +43,17 @@ public class WebappConfig extends WebMvcConfigurerAdapter {
* Controller with REST-API for spring-boot applications to register itself. * Controller with REST-API for spring-boot applications to register itself.
*/ */
@Bean @Bean
public RegistryController registryController() { public RegistryController registryController(ApplicationRegistry registry) {
return new RegistryController(); return new RegistryController(registry);
} }
/** /**
* Registry for all registered application. * Default registry for all registered application.
*/ */
@Bean @Bean
@ConditionalOnMissingBean
public ApplicationRegistry applicationRegistry() { public ApplicationRegistry applicationRegistry() {
return new ApplicationRegistry(); return new SimpleApplicationRegistry();
} }
} }
...@@ -19,13 +19,12 @@ import java.util.List; ...@@ -19,13 +19,12 @@ import java.util.List;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import de.codecentric.boot.admin.model.Application; import de.codecentric.boot.admin.model.Application;
...@@ -39,45 +38,66 @@ public class RegistryController { ...@@ -39,45 +38,66 @@ public class RegistryController {
private static final Logger LOGGER = LoggerFactory.getLogger(RegistryController.class); private static final Logger LOGGER = LoggerFactory.getLogger(RegistryController.class);
@Autowired private final ApplicationRegistry registry;
private ApplicationRegistry registry;
public RegistryController(ApplicationRegistry registry) {
this.registry = registry;
}
/** /**
* Register an application within this admin application. * Register an application within this admin application.
* *
* @param app * @param app The application infos.
* The application infos.
* @return The registered application. * @return The registered application.
*/ */
@RequestMapping(value = "/api/applications", method = RequestMethod.POST) @RequestMapping(value = "/api/applications", method = RequestMethod.POST)
public Application register(@RequestBody Application app) { public ResponseEntity<Application> register(@RequestBody Application app) {
LOGGER.info("Register application with ID '{}' and URL '{}'", app.getId(), app.getUrl()); LOGGER.debug("Register application {}", app.toString());
return registry.register(app);
Application registered = registry.getApplication(app.getId());
if (registered == null || registered.equals(app)) {
LOGGER.info("Application {} registered.", app.toString());
registry.register(app);
return new ResponseEntity<Application>(app, HttpStatus.CREATED);
}
else {
return new ResponseEntity<Application>(HttpStatus.CONFLICT);
}
} }
/** /**
* Get a single application out of the registry. * Get a single application out of the registry.
* *
* @param id * @param id The application identifier.
* The application identifier.
* @return The registered application. * @return The registered application.
*/ */
@RequestMapping(value = "/api/application/{id}", method = RequestMethod.GET) @RequestMapping(value = "/api/application/{id}", method = RequestMethod.GET)
public Application get(@PathVariable String id) { public ResponseEntity<Application> get(@PathVariable String id) {
LOGGER.debug("Deliver registered application with ID '{}'", id); LOGGER.debug("Deliver registered application with ID '{}'", id);
return registry.getApplication(id); Application application = registry.getApplication(id);
if (application != null) {
return new ResponseEntity<Application>(application, HttpStatus.OK);
}
else {
return new ResponseEntity<Application>(application, HttpStatus.NOT_FOUND);
}
} }
/** /**
* Deregister an application within this admin application. * Unregister an application within this admin application.
* *
* @param id The application id. * @param id The application id.
*/ */
@ResponseStatus(value = HttpStatus.OK)
@RequestMapping(value = "/api/application/{id}", method = RequestMethod.DELETE) @RequestMapping(value = "/api/application/{id}", method = RequestMethod.DELETE)
public void unregister(@PathVariable String id) { public ResponseEntity<Application> unregister(@PathVariable String id) {
LOGGER.info("Deregister application with ID '{}'", id); LOGGER.info("Unregister application with ID '{}'", id);
registry.unregister(id); Application app = registry.unregister(id);
if (app != null) {
return new ResponseEntity<Application>(app, HttpStatus.NO_CONTENT);
}
else {
return new ResponseEntity<Application>(HttpStatus.NOT_FOUND);
}
} }
/** /**
......
...@@ -15,97 +15,43 @@ ...@@ -15,97 +15,43 @@
*/ */
package de.codecentric.boot.admin.service; package de.codecentric.boot.admin.service;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.Validate;
import org.springframework.stereotype.Service;
import de.codecentric.boot.admin.model.Application; import de.codecentric.boot.admin.model.Application;
/** /**
* Registry for all applications that should be managed/administrated by the spring-boot-admin application. This * Registry for all applications that should be managed/administrated by the
* registry is just "in-memory", so that after a restart all applications have to be registered again. * spring-boot-admin application.
*/ */
@Service public interface ApplicationRegistry {
public class ApplicationRegistry {
private final Map<String, Application> registry = new HashMap<>();
/** /**
* Register application. * Register application.
* *
* @param app * @param app The Application.
* The Application.
*/
public Application register(Application app) {
Validate.notNull(app, "Application must not be null");
Validate.notNull(app.getId(), "ID must not be null");
Validate.notNull(app.getUrl(), "URL must not be null");
Validate.isTrue(checkUrl(app.getUrl()), "URL is not valid");
return registry.put(app.getId(), app);
}
/**
* Checks the syntax of the given URL.
*
* @param url
* The URL.
* @return true, if valid.
*/ */
private boolean checkUrl(String url) { void register(Application app);
try {
new URL(url);
} catch (MalformedURLException e) {
return false;
}
return true;
}
/**
* Checks, if an application is already registerd.
*
* @param id
* The application ID.
* @return exists?
*/
public boolean isRegistered(String id) {
return registry.containsKey(id);
}
/** /**
* Get a list of all registered applications. * Get a list of all registered applications.
* *
* @return List. * @return List.
*/ */
public List<Application> getApplications() { List<Application> getApplications();
return new ArrayList<>(registry.values());
}
/** /**
* Get a specific application inside the registry. * Get a specific application inside the registry.
* *
* @param id * @param id Id.
* Id.
* @return Application. * @return Application.
*/ */
public Application getApplication(String id) { Application getApplication(String id);
if (!isRegistered(id)) {
throw new IllegalArgumentException("Application with ID " + id + " is not registered");
}
return registry.get(id);
}
/** /**
* Remove a specific application from registry * Remove a specific application from registry
* @param id * @param id
* @return the unregistered Application
*/ */
public void unregister(String id) { Application unregister(String id);
registry.remove(id);
}
} }
\ 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.service;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.Validate;
import org.springframework.stereotype.Service;
import de.codecentric.boot.admin.model.Application;
/**
* This registry is just "in-memory", so that after a restart all applications have to be
* registered again.
*/
@Service
public class SimpleApplicationRegistry implements ApplicationRegistry {
private final Map<String, Application> store = new HashMap<>();
@Override
public void register(Application app) {
Validate.notNull(app, "Application must not be null");
Validate.notNull(app.getId(), "ID must not be null");
Validate.notNull(app.getUrl(), "URL must not be null");
Validate.isTrue(checkUrl(app.getUrl()), "URL is not valid");
store.put(app.getId(), app);
}
/**
* Checks the syntax of the given URL.
*
* @param url
* The URL.
* @return true, if valid.
*/
private boolean checkUrl(String url) {
try {
new URL(url);
} catch (MalformedURLException e) {
return false;
}
return true;
}
@Override
public List<Application> getApplications() {
return new ArrayList<>(store.values());
}
@Override
public Application getApplication(String id) {
return store.get(id);
}
@Override
public Application unregister(String id) {
return store.remove(id);
}
}
...@@ -29,7 +29,7 @@ ...@@ -29,7 +29,7 @@
<div class="spring-logo--container"> <div class="spring-logo--container">
<a class="spring-boot-logo" href="/"><span></span></a> <a class="spring-boot-logo" href="/"><span></span></a>
</div> </div>
<ul class="nav pull-right" ng-controller='navCtrl'> <ul class="nav pull-right">
<li class="navbar-link" ng-class="{active: $state.includes('apps') || $state.includes('overview') }"><a ui-sref="overview">Applications</a></li> <li class="navbar-link" ng-class="{active: $state.includes('apps') || $state.includes('overview') }"><a ui-sref="overview">Applications</a></li>
<li class="navbar-link" ng-class="{active: $state.includes('about')}"><a ui-sref="about">About</a></li> <li class="navbar-link" ng-class="{active: $state.includes('about')}"><a ui-sref="about">About</a></li>
</ul> </ul>
......
...@@ -38,9 +38,15 @@ angular.module('springBootAdmin', [ ...@@ -38,9 +38,15 @@ angular.module('springBootAdmin', [
templateUrl: 'views/about.html' templateUrl: 'views/about.html'
}) })
.state('apps', { .state('apps', {
abstract:true,
url: '/apps/:id', url: '/apps/:id',
controller: 'appsCtrl', controller: 'appsCtrl',
templateUrl: 'views/apps.html', templateUrl: 'views/apps.html',
resolve: {
application: ['$stateParams', 'Application' , function($stateParams, Application){
return Application.query({id: $stateParams.id}).$promise;
}]
}
}) })
.state('apps.details', { .state('apps.details', {
url: '/details', url: '/details',
......
...@@ -32,9 +32,11 @@ angular.module('springBootAdmin.services', ['ngResource']) ...@@ -32,9 +32,11 @@ angular.module('springBootAdmin.services', ['ngResource'])
} }
]) ])
.service('ApplicationOverview', ['$http', function($http) { .service('ApplicationOverview', ['$http', function($http) {
this.getVersion = function(app) { this.getInfo = function(app) {
return $http.get(app.url + '/info').success(function(response) { return $http.get(app.url + '/info').success(function(response) {
app.version = response.version; app.version = response.version;
delete response.version;
app.info = response;
}).error(function() { }).error(function() {
app.version = '---'; app.version = '---';
}); });
...@@ -61,9 +63,6 @@ angular.module('springBootAdmin.services', ['ngResource']) ...@@ -61,9 +63,6 @@ angular.module('springBootAdmin.services', ['ngResource'])
app.urlLogfile = null; app.urlLogfile = null;
}); });
} }
this.refresh = function(app) {
return $http.post(app.url + '/refresh');
}
}]) }])
.service('ApplicationDetails', ['$http', function($http) { .service('ApplicationDetails', ['$http', function($http) {
this.getInfo = function(app) { this.getInfo = function(app) {
......
...@@ -6,7 +6,6 @@ body { ...@@ -6,7 +6,6 @@ body {
} }
.container-fluid { .container-fluid {
max-width: 1024px;
margin: 0 auto; margin: 0 auto;
} }
......
<h2>{{ applicationId }} <div class="container" style="margin-bottom: 20px;">
<div class="btn-group"> <div class="row">
<a class="btn " ng-class="{active: $state.includes('apps.details')}" ui-sref="apps.details.metrics({id: applicationId})">Details</a> <div class="span12">
<a class="btn " ui-sref-active="active" ui-sref="apps.logging({id: applicationId})" >Logging</a></label> <h2 style="display: inline-block;">{{ application.name }}
<a class="btn " ui-sref-active="active" ui-sref="apps.jmx({id: applicationId})">JMX</a></label> <small>{{ application.url }}</small>
<a class="btn " ui-sref-active="active" ui-sref="apps.threads({id: applicationId})">Threads</a></label> </h2>
</div> </div>
</h2> </div>
<div class="row">
<div class="span12 btn-group text-center">
<a class="btn" ng-class="{active: $state.includes('apps.details')}" ui-sref="apps.details.metrics({id: application.id})">Details</a>
<a class="btn" ui-sref-active="active" ui-sref="apps.logging({id: application.id})" >Logging</a></label>
<a class="btn" ui-sref-active="active" ui-sref="apps.jmx({id: application.id})">JMX</a></label>
<a class="btn" ui-sref-active="active" ui-sref="apps.threads({id: application.id})">Threads</a></label>
</div>
</div>
</div>
<div ui-view></div> <div ui-view></div>
...@@ -2,7 +2,8 @@ ...@@ -2,7 +2,8 @@
<b>Error:</b> {{ error }} <b>Error:</b> {{ error }}
</div> </div>
<div class="span6"> <div class="container-fluid" style="margin: 0 auto; width: 960px;">
<div class="span6">
<table class="table"> <table class="table">
<thead><tr><th>Application</th><th><small class="pull-right"><a href="{{ application.url }}/info">raw JSON</a></small></th></tr></thead> <thead><tr><th>Application</th><th><small class="pull-right"><a href="{{ application.url }}/info">raw JSON</a></small></th></tr></thead>
<tbody> <tbody>
...@@ -11,7 +12,7 @@ ...@@ -11,7 +12,7 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="span6"> <div class="span6">
<table class="table"> <table class="table">
...@@ -44,10 +45,10 @@ ...@@ -44,10 +45,10 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="span6"> <div class="span6">
<table class="table"> <table class="table">
<thead><tr><th colspan="2">Memory</th></tr></thead> <thead><tr><th colspan="2">Memory</th></tr></thead>
<tbody> <tbody>
...@@ -82,9 +83,9 @@ ...@@ -82,9 +83,9 @@
<td>{{metrics['heap'] / 1024 | number:2}}M</td> <td>{{metrics['heap'] / 1024 | number:2}}M</td>
</tr> </tr>
</table> </table>
</div> </div>
<div class="span6"> <div class="span6">
<table class="table"> <table class="table">
<thead><tr><th>JVM</th><th><small class="pull-right"><a href="{{ application.url }}/metrics">raw JSON</a></small></th></tr></thead> <thead><tr><th>JVM</th><th><small class="pull-right"><a href="{{ application.url }}/metrics">raw JSON</a></small></th></tr></thead>
<tbody> <tbody>
...@@ -114,9 +115,9 @@ ...@@ -114,9 +115,9 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="span6"> <div class="span6">
<table class="table"> <table class="table">
<thead><tr><th colspan="2">Garbage Collection</th></tr></thead> <thead><tr><th colspan="2">Garbage Collection</th></tr></thead>
<tr ng-repeat-start="(name, value) in gcInfos track by name"> <tr ng-repeat-start="(name, value) in gcInfos track by name">
...@@ -129,9 +130,9 @@ ...@@ -129,9 +130,9 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="span6" ng-show="hasDatasources"> <div class="span6" ng-show="hasDatasources">
<table class="table"> <table class="table">
<thead><tr><th colspan="2">Datasources</th></tr></thead> <thead><tr><th colspan="2">Datasources</th></tr></thead>
<tr ng-repeat = "(name, value) in datasources track by name"> <tr ng-repeat = "(name, value) in datasources track by name">
...@@ -147,9 +148,9 @@ ...@@ -147,9 +148,9 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="span12"> <div class="span12">
<div class="main-template"> <div class="main-template">
<div id="xd-jobs" class="tab-pane active col-md-12"> <div id="xd-jobs" class="tab-pane active col-md-12">
<ul class="nav nav-tabs"> <ul class="nav nav-tabs">
...@@ -163,5 +164,5 @@ ...@@ -163,5 +164,5 @@
</div> </div>
</div> </div>
</div> </div>
</div>
</div> </div>
<h2 >Spring-Boot applications<br> <h2 >Spring-Boot applications<br>
<small>Here you'll find all Spring-Boot applications that registered itself at this admin application.</small> <small>Here you'll find all Spring-Boot applications that registered itself at this admin application.</small>
</h2> </h2>
<div class="span12"> <table class="table table-striped">
<table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th>Application</th> <th>Application</th>
<th>URL</th>
<th>Version</th> <th>Version</th>
<th>Info</th>
<th>Status</th> <th>Status</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr ng-repeat="application in applications track by application.id"> <tr ng-repeat="application in applications track by application.id">
<td><span title="{{application.url}}">{{ application.id }}</span></td> <td>{{ application.name }}</td>
<td>{{ application.url }}</td>
<td>{{ application.version }}</td> <td>{{ application.version }}</td>
<td><span ng-repeat="(name, value) in application.info track by name">{{name}}: {{value}}<br></span></td>
<td><span class="status-{{application.status}}">{{ application.status }}</span></td> <td><span class="status-{{application.status}}">{{ application.status }}</span></td>
<td> <td>
<span class="pull-right" ng-hide="application.status == null || application.status == 'OFFLINE'"> <div class="btn-group pull-right" ng-hide="application.status == null || application.status == 'OFFLINE'">
<a ng-disabled="!application.providesLogfile" target="_self" class="btn btn-success" ng-href="{{application.urlLogfile}}"><i class="icon-file icon-white"></i>Logfile</a> <a ng-disabled="!application.providesLogfile" target="_self" class="btn btn-success" ng-href="{{application.urlLogfile}}"><i class="icon-file icon-white"></i>Log</a>
<a ui-sref="apps.details.metrics({id: application.id})" class="btn btn-success">Details</a> <a ui-sref="apps.details.metrics({id: application.id})" class="btn btn-success">Details</a>
<a ui-sref="apps.logging({id: application.id})" class="btn btn-success">Logging</a> <a ui-sref="apps.logging({id: application.id})" class="btn btn-success">Logging</a>
<a ui-sref="apps.jmx({id: application.id})" class="btn btn-success">JMX</a> <a ui-sref="apps.jmx({id: application.id})" class="btn btn-success">JMX</a>
<a ui-sref="apps.threads({id: application.id})" class="btn btn-success">Threads</a> <a ui-sref="apps.threads({id: application.id})" class="btn btn-success">Threads</a>
</span> </div>
<span class="pull-right" ng-show="application.status == null || application.status == 'OFFLINE'"> <div class="btn-group pull-right" ng-show="application.status == 'UNKNOWN' || application.status == 'OFFLINE'">
<a class="btn btn-danger" ng-click="remove(application)"><i class="icon-remove icon-white"></i>Remove</a> <a class="btn btn-danger" ng-click="remove(application)"><i class="icon-remove icon-white"></i>Remove</a>
</span> </div>
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div>
/*
* 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.controller;
import static org.junit.Assert.assertEquals;
import java.util.Collections;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import de.codecentric.boot.admin.model.Application;
import de.codecentric.boot.admin.service.SimpleApplicationRegistry;
public class RegistryControllerTest {
private RegistryController controller;
@Before
public void setup() {
controller = new RegistryController(new SimpleApplicationRegistry());
}
@Test
public void register() {
ResponseEntity<?> response = controller.register(new Application("http://localhost", "test"));
assertEquals(HttpStatus.CREATED, response.getStatusCode());
assertEquals(new Application("http://localhost", "test"), response.getBody());
}
@Test
public void register_twice() {
controller.register(new Application("http://localhost", "test"));
Application app = new Application("http://localhost", "test");
ResponseEntity<?> response = controller.register(app);
assertEquals(HttpStatus.CREATED, response.getStatusCode());
assertEquals(new Application("http://localhost", "test"), response.getBody());
}
@Test
public void register_sameUrl() {
controller.register(new Application("http://localhost", "FOO"));
ResponseEntity<?> response = controller.register(new Application("http://localhost", "BAR"));
assertEquals(HttpStatus.CONFLICT, response.getStatusCode());
}
@Test
public void get() {
Application app = new Application("http://localhost", "FOO");
controller.register(app);
ResponseEntity<?> response = controller.get(app.getId());
assertEquals(HttpStatus.OK, response.getStatusCode());
assertEquals(app, response.getBody());
}
@Test
public void get_notFound() {
controller.register(new Application("http://localhost", "FOO"));
ResponseEntity<?> response = controller.get("unknown");
assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode());
}
@Test
public void unregister() {
Application app = new Application("http://localhost", "FOO");
controller.register(app);
ResponseEntity<?> response = controller.unregister(app.getId());
assertEquals(HttpStatus.NO_CONTENT, response.getStatusCode());
assertEquals(app, response.getBody());
assertEquals(HttpStatus.NOT_FOUND, controller.get(app.getId()).getStatusCode());
}
@Test
public void unregister_notFound() {
controller.register(new Application("http://localhost", "FOO"));
ResponseEntity<?> response = controller.unregister("unknown");
assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode());
}
@Test
public void applications() {
Application app = new Application("http://localhost", "FOO");
controller.register(app);
List<Application> applications = controller.applications();
assertEquals(Collections.singletonList(app), applications);
}
}
package de.codecentric.boot.admin.service;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import de.codecentric.boot.admin.TestAdminApplication;
import de.codecentric.boot.admin.model.Application;
@SpringApplicationConfiguration(classes=TestAdminApplication.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class ApplicationRegistryTest {
@Autowired
private ApplicationRegistry registry;
@Test(expected = NullPointerException.class)
public void registerFailed1() throws Exception {
registry.register(new Application());
}
@Test(expected = NullPointerException.class)
public void registerFailed2() throws Exception {
Application app = new Application();
app.setId("abc");
registry.register(app);
}
@Test(expected = IllegalArgumentException.class)
public void registerFailed3() throws Exception {
Application app = new Application();
app.setId("abc");
app.setUrl("not-an-url");
registry.register(app);
}
@Test
public void register() throws Exception {
Application app = new Application();
app.setId("abc");
app.setUrl("http://localhost:8080");
registry.register(app);
}
@Test
public void isRegistered() throws Exception {
Application app = new Application();
app.setId("abc");
app.setUrl("http://localhost:8080");
registry.register(app);
assertFalse(registry.isRegistered("xyz"));
assertTrue(registry.isRegistered("abc"));
}
@Test
public void getApplication() throws Exception {
Application app = new Application();
app.setId("abc");
app.setUrl("http://localhost:8080");
registry.register(app);
assertEquals(app, registry.getApplication("abc"));
}
@Test
public void getApplications() throws Exception {
Application app = new Application();
app.setId("abc");
app.setUrl("http://localhost:8080");
registry.register(app);
assertEquals(1, registry.getApplications().size());
assertEquals(app, registry.getApplications().get(0));
}
}
/*
* 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.service;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
import de.codecentric.boot.admin.model.Application;
public class SimpleApplicationRegistryTest {
private ApplicationRegistry registry = new SimpleApplicationRegistry();
@Test(expected = NullPointerException.class)
public void registerFailed1() throws Exception {
registry.register(new Application(null, null));
}
@Test(expected = NullPointerException.class)
public void registerFailed2() throws Exception {
Application app = new Application(null, "abc");
registry.register(app);
}
@Test(expected = IllegalArgumentException.class)
public void registerFailed3() throws Exception {
Application app = new Application("not-an-url", "abc");
registry.register(app);
}
@Test
public void register() throws Exception {
Application app = new Application("http://localhost:8080", "abc");
registry.register(app);
}
@Test
public void getApplication() throws Exception {
Application app = new Application("http://localhost:8080", "abc");
registry.register(app);
assertEquals(app, registry.getApplication(app.getId()));
}
@Test
public void getApplications() throws Exception {
Application app = new Application("http://localhost:8080", "abc");
registry.register(app);
assertEquals(1, registry.getApplications().size());
assertEquals(app, registry.getApplications().get(0));
}
}
spring.resources.cachePeriod=3600 spring.resources.cachePeriod=3600
server.port=8080 server.port=8080
info.id=spring-boot-admin-example
info.version=1.0.0 info.version=1.0.0
spring.boot.admin.url=http://localhost:8080 info.stage=test
logging.file=/tmp/log.log logging.file=/tmp/log.log
spring.application.name=spring-boot-admin-example spring.application.name=spring-boot-admin-example
spring.boot.admin.url=http://localhost:8080
\ No newline at end of file
...@@ -5,21 +5,24 @@ This [Spring-Boot starter](http://docs.spring.io/spring-boot/docs/current-SNAPSH ...@@ -5,21 +5,24 @@ This [Spring-Boot starter](http://docs.spring.io/spring-boot/docs/current-SNAPSH
This client uses the [AutoConfiguration](http://docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference/htmlsingle/#using-boot-auto-configuration "Spring Boot docu") feature of Spring Boot to register service and controller beans in the application context. This client uses the [AutoConfiguration](http://docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference/htmlsingle/#using-boot-auto-configuration "Spring Boot docu") feature of Spring Boot to register service and controller beans in the application context.
The main service that is used is a registrar that registeres the application at the spring-boot-admin application by periodically calling a REST-API to check or perform the registration of itself. The main service that is used is a registrar that registeres the application at the spring-boot-admin application by periodically calling a REST-API to perform the registration of itself.
The following properties have to be included in the environment (i.e. application.properties) to ensure all features to work properly. ##Configuration properties
### spring-boot-admin
| Name | Description |
| --------------------- | ----------- |
| spring.boot.admin.url | URL of the spring-boot-admin application to register at.<br>_Mandatory_. | |
| spring.boot.admin.contextPath | Context-path of registration point.<br>Default: api/applications |
| spring.boot.admin.period | Time period for registration repeat.<br>Default: 10000 |
| spring.boot.admin.client.url | Client-management-URL to register with. Can be overriden in case the reachable URL is different (e.g. Docker). Must be unique in registry.<br>Default: http://_hostname_:_${management.port}_/_${management.context-path}_ |
| spring.boot.admin.client.name | Name to register with. Defaults to the ApplicationContexts name. Only set when it should differ.<br>Default: _${spring.application.name}_ if set, spring-boot-application otherwise. |
<table>
<tr> ### Other configuration properties
<td>info.id</td><td>The identifier in the registry - this property is published by the /info endpoint</td> Options from other spring boot features. These should be set to enable all features.
</tr>
<tr> | Name | Description |
<td>info.version</td><td>The version number - also published by the /info endpoint</td> | ----------------------- | ----------- |
</tr> | spring.application.name | Name to be shown in the application list. Name of the ApplicationContext. |
<tr> | info.version | Version number to be shown in the application list. Also published via /info-endpoint. |
<td>spring.boot.admin.url</td><td>URL of the spring-boot-admin application to register at</td> | logging.file | Path to the applications logfile for access via spring-boot-admin. From Spring Boot logging configuration. |
</tr>
<tr>
<td>logging.file</td><td>File path of the logfile of the application</td>
</tr>
</table>
...@@ -29,5 +29,9 @@ ...@@ -29,5 +29,9 @@
<groupId>org.jolokia</groupId> <groupId>org.jolokia</groupId>
<artifactId>jolokia-core</artifactId> <artifactId>jolokia-core</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
</dependencies> </dependencies>
</project> </project>
/*
* 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.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "spring.boot.admin.client")
public class AdminClientProperties {
@Value("http://#{T(java.net.InetAddress).localHost.canonicalHostName}:${server.port:${management.port:8080}}${management.context-path:/}")
private String url;
@Value("${spring.application.name:spring-boot-application}")
private String name;
/**
* @return Client-management-URL to register with. Can be overriden in case the
* reachable URL is different (e.g. Docker). Must be unique in registry.
*/
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
/**
* @return Name to register with.
*/
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
/*
* 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.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "spring.boot.admin")
public class AdminProperties {
private String url;
private String contextPath = "api/applications";
private int period = 10000;
public void setUrl(String url) {
this.url = url;
}
/**
*
* @return the Spring Boot Admin Server's url.
*/
public String getUrl() {
return url;
}
/**
* @return the Spring Boot Admin Server's context path.
*/
public String getContextPath() {
return contextPath;
}
public void setContextPath(String contextPath) {
this.contextPath = contextPath;
}
/**
*
* @return the time interval (in ms) the registration is repeated.
*/
public int getPeriod() {
return period;
}
public void setPeriod(int period) {
this.period = period;
}
}
...@@ -16,12 +16,15 @@ ...@@ -16,12 +16,15 @@
package de.codecentric.boot.admin.config; package de.codecentric.boot.admin.config;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.scheduling.config.ScheduledTaskRegistrar; import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.web.client.RestTemplate;
import de.codecentric.boot.admin.controller.LogfileController; import de.codecentric.boot.admin.controller.LogfileController;
import de.codecentric.boot.admin.services.SpringBootAdminRegistratorTask; import de.codecentric.boot.admin.services.SpringBootAdminRegistrator;
import de.codecentric.boot.admin.web.SimpleCORSFilter; import de.codecentric.boot.admin.web.SimpleCORSFilter;
/** /**
...@@ -30,14 +33,24 @@ import de.codecentric.boot.admin.web.SimpleCORSFilter; ...@@ -30,14 +33,24 @@ import de.codecentric.boot.admin.web.SimpleCORSFilter;
*/ */
@Configuration @Configuration
@ConditionalOnProperty("spring.boot.admin.url") @ConditionalOnProperty("spring.boot.admin.url")
@EnableConfigurationProperties({ AdminProperties.class, AdminClientProperties.class })
public class SpringBootAdminClientAutoConfiguration { public class SpringBootAdminClientAutoConfiguration {
/** /**
* Task that registers the application at the spring-boot-admin application. * Task that registers the application at the spring-boot-admin application.
*/ */
@Bean @Bean
public Runnable registrator() { public SpringBootAdminRegistrator registrator(AdminProperties adminProps,
return new SpringBootAdminRegistratorTask(); AdminClientProperties clientProps) {
return new SpringBootAdminRegistrator(restTemplate(), adminProps, clientProps);
}
@Bean
public RestTemplate restTemplate() {
RestTemplate template = new RestTemplate();
template.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
return template;
} }
/** /**
...@@ -52,9 +65,17 @@ public class SpringBootAdminClientAutoConfiguration { ...@@ -52,9 +65,17 @@ public class SpringBootAdminClientAutoConfiguration {
* TaskRegistrar that triggers the RegistratorTask every ten seconds. * TaskRegistrar that triggers the RegistratorTask every ten seconds.
*/ */
@Bean @Bean
public ScheduledTaskRegistrar taskRegistrar() { public ScheduledTaskRegistrar taskRegistrar(final SpringBootAdminRegistrator registrator, AdminProperties adminProps) {
ScheduledTaskRegistrar registrar = new ScheduledTaskRegistrar(); ScheduledTaskRegistrar registrar = new ScheduledTaskRegistrar();
registrar.addFixedRateTask(registrator(), 10000);
Runnable registratorTask = new Runnable() {
@Override
public void run() {
registrator.register();
}
};
registrar.addFixedRateTask(registratorTask, adminProps.getPeriod());
return registrar; return registrar;
} }
......
...@@ -25,6 +25,7 @@ import org.springframework.beans.factory.annotation.Autowired; ...@@ -25,6 +25,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment; import org.springframework.core.env.Environment;
import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.util.FileCopyUtils; import org.springframework.util.FileCopyUtils;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
...@@ -55,8 +56,9 @@ public class LogfileController { ...@@ -55,8 +56,9 @@ public class LogfileController {
LOGGER.error("Logfile download failed for missing file at path=" + path); LOGGER.error("Logfile download failed for missing file at path=" + path);
return "Logfile download failed for missing file at path=" + path; return "Logfile download failed for missing file at path=" + path;
} }
response.setContentType("application/octet-stream"); response.setContentType(MediaType.TEXT_PLAIN_VALUE);
response.setHeader("Content-Disposition", "attachment; filename=\"" + file.getFilename() + "\""); response.setHeader("Content-Disposition", "attachment; filename=\"" + file.getFilename() + "\"");
try { try {
FileCopyUtils.copy(file.getInputStream(), response.getOutputStream()); FileCopyUtils.copy(file.getInputStream(), response.getOutputStream());
} catch (IOException e) { } catch (IOException e) {
......
...@@ -16,68 +16,113 @@ ...@@ -16,68 +16,113 @@
package de.codecentric.boot.admin.model; package de.codecentric.boot.admin.model;
import java.io.Serializable; import java.io.Serializable;
import java.nio.charset.Charset;
import java.security.MessageDigest;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
/** /**
* The domain model for all registered application at the spring boot admin application. * The domain model for all registered application at the spring boot admin application.
*/ */
@JsonIgnoreProperties(ignoreUnknown = true)
public class Application implements Serializable { public class Application implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
private String id; private final String id;
private final String url;
private final String name;
private String url; @JsonCreator
public Application(@JsonProperty("url") String url, @JsonProperty("name") String name) {
public String getId() { this(url.replaceFirst("/+$", ""), name, generateId(url.replaceFirst("/+$", "")));
return id;
} }
public void setId(String id) { protected Application(String url, String name, String id) {
this.url = url;
this.name = name;
this.id = id; this.id = id;
} }
public String getUrl() { public String getId() {
return url; return id;
} }
public void setUrl(String url) { public String getUrl() {
this.url = url; return url;
} }
@Override @Override
public String toString() { public String toString() {
return id + " : " + url; return "[id=" + id + ", url=" + url + ", name=" + name + "]";
}
public String getName() {
return name;
} }
@Override @Override
public int hashCode() { public int hashCode() {
final int prime = 31; final int prime = 31;
int result = 1; int result = 1;
result = prime * result + ((id == null) ? 0 : id.hashCode()); result = prime * result + ((name == null) ? 0 : name.hashCode());
result = prime * result + ((url == null) ? 0 : url.hashCode()); result = prime * result + ((url == null) ? 0 : url.hashCode());
return result; return result;
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {
if (this == obj) if (this == obj) {
return true; return true;
if (obj == null) }
if (obj == null) {
return false; return false;
if (getClass() != obj.getClass()) }
if (getClass() != obj.getClass()) {
return false; return false;
}
Application other = (Application) obj; Application other = (Application) obj;
if (id == null) { if (name == null) {
if (other.id != null) if (other.name != null) {
return false; return false;
} else if (!id.equals(other.id)) }
}
else if (!name.equals(other.name)) {
return false; return false;
}
if (url == null) { if (url == null) {
if (other.url != null) if (other.url != null) {
return false; return false;
} else if (!url.equals(other.url)) }
}
else if (!url.equals(other.url)) {
return false; return false;
}
return true; return true;
} }
private static final char[] HEX_CHARS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd',
'e', 'f' };
private static String generateId(String url) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-1");
byte[] bytes = digest.digest(url.getBytes(Charset.forName("UTF-8")));
return new String(encodeHex(bytes, 0, 8));
}
catch (Exception e) {
throw new IllegalStateException(e);
}
}
private static char[] encodeHex(byte[] bytes, int offset, int length) {
char chars[] = new char[length];
for (int i = 0; i < length; i = i + 2) {
byte b = bytes[offset + (i / 2)];
chars[i] = HEX_CHARS[(b >>> 0x4) & 0xf];
chars[i + 1] = HEX_CHARS[b & 0xf];
}
return chars;
}
} }
/*
* 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.services;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
import de.codecentric.boot.admin.config.AdminClientProperties;
import de.codecentric.boot.admin.config.AdminProperties;
import de.codecentric.boot.admin.model.Application;
/**
* Registers the client application at spring-boot-admin-server
*/
public class SpringBootAdminRegistrator {
private static final Logger LOGGER = LoggerFactory.getLogger(SpringBootAdminRegistrator.class);
public SpringBootAdminRegistrator(RestTemplate template, AdminProperties adminProps,
AdminClientProperties clientProps) {
this.clientProps = clientProps;
this.adminProps = adminProps;
this.template = template;
}
private AdminClientProperties clientProps;
private AdminProperties adminProps;
private final RestTemplate template;
/**
* Registers the client application at spring-boot-admin-server.
* @return true if successful
*/
public boolean register() {
Application app = createApplication();
try {
ResponseEntity<Application> response = template.postForEntity(
adminProps.getUrl() + '/' + adminProps.getContextPath(), app, Application.class);
if (response.getStatusCode().equals(HttpStatus.CREATED)) {
LOGGER.info("Application registered itself as {}", response.getBody());
return true;
}
else if (response.getStatusCode().equals(HttpStatus.CONFLICT)) {
LOGGER.warn("Application failed to registered itself as {} because of conflict in registry.", app);
}
else {
LOGGER.warn("Application failed to registered itself as {}. Response: {}", app, response.toString());
}
}
catch (Exception ex) {
LOGGER.warn("Failed to register application as {} at spring-boot-admin: {}", app, ex.getMessage());
}
return false;
}
protected Application createApplication() {
Application app = new Application(clientProps.getUrl(), clientProps.getName());
return app;
}
}
/*
* 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.services;
import java.net.InetAddress;
import java.net.URL;
import java.util.ArrayList;
import javax.annotation.PostConstruct;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.util.Assert;
import org.springframework.web.client.RestTemplate;
import de.codecentric.boot.admin.model.Application;
/**
* Scheduler that checks the registration of the application at the spring-boot-admin.
*/
public class SpringBootAdminRegistratorTask implements Runnable {
private static final Logger LOGGER = LoggerFactory.getLogger(SpringBootAdminRegistratorTask.class);
@Autowired
private Environment env;
@PostConstruct
public void check() {
Assert.notNull(env.getProperty("spring.boot.admin.url"),
"The URL of the spring-boot-admin application is mandatory");
Assert.notNull(env.getProperty("server.port"), "The server port of the application is mandatory");
Assert.notNull(env.getProperty("spring.application.name"), "The id of the application is mandatory");
}
/**
* @see java.lang.Runnable#run()
*/
@Override
public void run() {
try {
String id = env.getProperty("info.id");
int port = env.getProperty("server.port", Integer.class);
String adminUrl = env.getProperty("spring.boot.admin.url");
RestTemplate template = new RestTemplate();
template.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
ApplicationList list = template.getForObject(adminUrl + "/api/applications", ApplicationList.class);
for (Application app : list) {
if (id.equals(app.getId())) {
// the application is already registered at the admin tool
LOGGER.debug("Application already registered with ID '{}'", id);
return;
}
}
// register the application with the used URL and port
String managementPath = env.getProperty("management.context-path", "");
String url = new URL("http", InetAddress.getLocalHost().getCanonicalHostName(), port, managementPath)
.toString();
Application app = new Application();
app.setId(id);
app.setUrl(url);
template.postForObject(adminUrl + "/api/applications", app, String.class);
LOGGER.info("Application registered itself at the admin application with ID '{}' and URL '{}'", id, url);
}
catch (Exception e) {
LOGGER.warn("Failed to register application at spring-boot-admin, message={}", e.getMessage());
}
}
private static class ApplicationList extends ArrayList<Application> {
private static final long serialVersionUID = 1L;
}
}
/*
* 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.model;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import org.junit.Test;
public class ApplicationTest {
@Test
public void url_id() {
Application a = new Application("http://localhost:8080/", null);
Application b = new Application("http://localhost:8080", null);
assertEquals("same urls must have same id", a.getId(), b.getId());
Application z = new Application("http://127.0.0.1:8080", null);
assertFalse("different urls must have diffenrent Id", a.getId().equals(z.getId()));
}
public void equals() {
Application a = new Application("http://localhost:8080/", "FOO");
Application b = new Application("http://localhost:8080", "FOO");
assertEquals("same url and same name must be equals", a, b);
assertEquals("hashcode should be equals", a.hashCode(), b.hashCode());
Application z = new Application("http://127.0.0.1:8080", "FOO");
assertFalse("different urls same name must not be equals", a.equals(z));
Application y = new Application("http://localhost:8080", "BAR");
assertFalse("same urls different name must not be equals", a.getId().equals(y.getId()));
}
}
/*
* 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.services;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.eq;
import static org.mockito.Matchers.isA;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import org.junit.Test;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;
import de.codecentric.boot.admin.config.AdminClientProperties;
import de.codecentric.boot.admin.config.AdminProperties;
import de.codecentric.boot.admin.model.Application;
public class SpringBootAdminRegistratorTest {
@Test
public void register_successful() {
AdminProperties adminProps = new AdminProperties();
adminProps.setUrl("http://sba:8080");
AdminClientProperties clientProps = new AdminClientProperties();
clientProps.setUrl("http://localhost:8080");
clientProps.setName("AppName");
RestTemplate restTemplate = mock(RestTemplate.class);
when(restTemplate.postForEntity(isA(String.class), isA(Application.class), eq(Application.class))).thenReturn(
new ResponseEntity<Application>(HttpStatus.CREATED));
SpringBootAdminRegistrator registrator = new SpringBootAdminRegistrator(restTemplate, adminProps, clientProps);
boolean result = registrator.register();
assertTrue(result);
verify(restTemplate).postForEntity("http://sba:8080/api/applications",
new Application("http://localhost:8080", "AppName"), Application.class);
}
@Test
public void register_failed() {
AdminProperties adminProps = new AdminProperties();
adminProps.setUrl("http://sba:8080");
AdminClientProperties clientProps = new AdminClientProperties();
clientProps.setUrl("http://localhost:8080");
clientProps.setName("AppName");
RestTemplate restTemplate = mock(RestTemplate.class);
when(restTemplate.postForEntity(isA(String.class), isA(Application.class), eq(Application.class))).thenThrow(
new RestClientException("Error"));
SpringBootAdminRegistrator registrator = new SpringBootAdminRegistrator(restTemplate, adminProps, clientProps);
boolean result = registrator.register();
assertFalse(result);
}
}
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