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/
#### 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.
```
<dependency>
<groupId>de.codecentric</groupId>
......@@ -59,12 +58,14 @@ Each application that want to register itself to the admin application has to in
</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
```
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
##### Dashboard
......
spring.resources.cachePeriod=3600
server.port=8080
info.version=@pom.version@
info.stage=test
logging.file=/tmp/log.log
spring.application.name=@pom.artifactId@
spring.boot.admin.url=http://localhost:8080
......@@ -17,6 +17,7 @@ package de.codecentric.boot.admin.config;
import java.util.List;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
......@@ -25,6 +26,7 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter
import de.codecentric.boot.admin.controller.RegistryController;
import de.codecentric.boot.admin.service.ApplicationRegistry;
import de.codecentric.boot.admin.service.SimpleApplicationRegistry;
@Configuration
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.
*/
@Bean
public RegistryController registryController() {
return new RegistryController();
public RegistryController registryController(ApplicationRegistry registry) {
return new RegistryController(registry);
}
/**
* Registry for all registered application.
* Default registry for all registered application.
*/
@Bean
@ConditionalOnMissingBean
public ApplicationRegistry applicationRegistry() {
return new ApplicationRegistry();
return new SimpleApplicationRegistry();
}
}
......@@ -19,13 +19,12 @@ import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import de.codecentric.boot.admin.model.Application;
......@@ -39,50 +38,71 @@ public class RegistryController {
private static final Logger LOGGER = LoggerFactory.getLogger(RegistryController.class);
@Autowired
private ApplicationRegistry registry;
private final ApplicationRegistry registry;
public RegistryController(ApplicationRegistry registry) {
this.registry = registry;
}
/**
* Register an application within this admin application.
*
* @param app
* The application infos.
*
* @param app The application infos.
* @return The registered application.
*/
@RequestMapping(value = "/api/applications", method = RequestMethod.POST)
public Application register(@RequestBody Application app) {
LOGGER.info("Register application with ID '{}' and URL '{}'", app.getId(), app.getUrl());
return registry.register(app);
public ResponseEntity<Application> register(@RequestBody Application app) {
LOGGER.debug("Register application {}", app.toString());
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.
*
* @param id
* The application identifier.
*
* @param id The application identifier.
* @return The registered application.
*/
@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);
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.
*/
@ResponseStatus(value = HttpStatus.OK)
@RequestMapping(value = "/api/application/{id}", method = RequestMethod.DELETE)
public void unregister(@PathVariable String id) {
LOGGER.info("Deregister application with ID '{}'", id);
registry.unregister(id);
public ResponseEntity<Application> unregister(@PathVariable String id) {
LOGGER.info("Unregister application with ID '{}'", 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);
}
}
/**
* List all registered applications.
*
*
* @return List.
*/
@RequestMapping(value = "/api/applications", method = RequestMethod.GET)
......
......@@ -15,97 +15,43 @@
*/
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;
/**
* Registry for all applications that should be managed/administrated by the spring-boot-admin application. This
* registry is just "in-memory", so that after a restart all applications have to be registered again.
* Registry for all applications that should be managed/administrated by the
* spring-boot-admin application.
*/
@Service
public class ApplicationRegistry {
private final Map<String, Application> registry = new HashMap<>();
public interface ApplicationRegistry {
/**
* Register application.
*
* @param app
* 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.
*
* @param app The Application.
*/
private boolean checkUrl(String url) {
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);
}
void register(Application app);
/**
* Get a list of all registered applications.
*
*
* @return List.
*/
public List<Application> getApplications() {
return new ArrayList<>(registry.values());
}
List<Application> getApplications();
/**
* Get a specific application inside the registry.
*
* @param id
* Id.
*
* @param id Id.
* @return Application.
*/
public Application getApplication(String id) {
if (!isRegistered(id)) {
throw new IllegalArgumentException("Application with ID " + id + " is not registered");
}
return registry.get(id);
}
Application getApplication(String id);
/**
* Remove a specific application from registry
* @param id
* @return the unregistered Application
*/
public void unregister(String id) {
registry.remove(id);
}
Application unregister(String 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 @@
<div class="spring-logo--container">
<a class="spring-boot-logo" href="/"><span></span></a>
</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('about')}"><a ui-sref="about">About</a></li>
</ul>
......
......@@ -38,9 +38,15 @@ angular.module('springBootAdmin', [
templateUrl: 'views/about.html'
})
.state('apps', {
abstract:true,
url: '/apps/:id',
controller: 'appsCtrl',
templateUrl: 'views/apps.html',
resolve: {
application: ['$stateParams', 'Application' , function($stateParams, Application){
return Application.query({id: $stateParams.id}).$promise;
}]
}
})
.state('apps.details', {
url: '/details',
......
......@@ -32,9 +32,11 @@ angular.module('springBootAdmin.services', ['ngResource'])
}
])
.service('ApplicationOverview', ['$http', function($http) {
this.getVersion = function(app) {
this.getInfo = function(app) {
return $http.get(app.url + '/info').success(function(response) {
app.version = response.version;
delete response.version;
app.info = response;
}).error(function() {
app.version = '---';
});
......@@ -61,9 +63,6 @@ angular.module('springBootAdmin.services', ['ngResource'])
app.urlLogfile = null;
});
}
this.refresh = function(app) {
return $http.post(app.url + '/refresh');
}
}])
.service('ApplicationDetails', ['$http', function($http) {
this.getInfo = function(app) {
......
......@@ -6,7 +6,6 @@ body {
}
.container-fluid {
max-width: 1024px;
margin: 0 auto;
}
......
<h2>{{ applicationId }}
<div class="btn-group">
<a class="btn " ng-class="{active: $state.includes('apps.details')}" ui-sref="apps.details.metrics({id: applicationId})">Details</a>
<a class="btn " ui-sref-active="active" ui-sref="apps.logging({id: applicationId})" >Logging</a></label>
<a class="btn " ui-sref-active="active" ui-sref="apps.jmx({id: applicationId})">JMX</a></label>
<a class="btn " ui-sref-active="active" ui-sref="apps.threads({id: applicationId})">Threads</a></label>
<div class="container" style="margin-bottom: 20px;">
<div class="row">
<div class="span12">
<h2 style="display: inline-block;">{{ application.name }}
<small>{{ application.url }}</small>
</h2>
</div>
</div>
</h2>
<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>
<h2 >Spring-Boot applications<br>
<small>Here you'll find all Spring-Boot applications that registered itself at this admin application.</small>
</h2>
<div class="span12">
<table class="table table-striped">
<thead>
<tr>
<th>Application</th>
<th>Version</th>
<th>Status</th>
<th></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="application in applications track by application.id">
<td><span title="{{application.url}}">{{ application.id }}</span></td>
<td>{{ application.version }}</td>
<td><span class="status-{{application.status}}">{{ application.status }}</span></td>
<td>
<span class="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 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.jmx({id: application.id})" class="btn btn-success">JMX</a>
<a ui-sref="apps.threads({id: application.id})" class="btn btn-success">Threads</a>
</span>
<span class="pull-right" ng-show="application.status == null || application.status == 'OFFLINE'">
<a class="btn btn-danger" ng-click="remove(application)"><i class="icon-remove icon-white"></i>Remove</a>
</span>
</td>
</tr>
</tbody>
</table>
</div>
<table class="table table-striped">
<thead>
<tr>
<th>Application</th>
<th>URL</th>
<th>Version</th>
<th>Info</th>
<th>Status</th>
<th></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="application in applications track by application.id">
<td>{{ application.name }}</td>
<td>{{ application.url }}</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>
<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>Log</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.jmx({id: application.id})" class="btn btn-success">JMX</a>
<a ui-sref="apps.threads({id: application.id})" class="btn btn-success">Threads</a>
</div>
<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>
</div>
</td>
</tr>
</tbody>
</table>
/*
* 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
server.port=8080
info.id=spring-boot-admin-example
info.version=1.0.0
spring.boot.admin.url=http://localhost:8080
info.stage=test
logging.file=/tmp/log.log
spring.application.name=spring-boot-admin-example
\ No newline at end of file
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
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>
<td>info.id</td><td>The identifier in the registry - this property is published by the /info endpoint</td>
</tr>
<tr>
<td>info.version</td><td>The version number - also published by the /info endpoint</td>
</tr>
<tr>
<td>spring.boot.admin.url</td><td>URL of the spring-boot-admin application to register at</td>
</tr>
<tr>
<td>logging.file</td><td>File path of the logfile of the application</td>
</tr>
</table>
### Other configuration properties
Options from other spring boot features. These should be set to enable all features.
| Name | Description |
| ----------------------- | ----------- |
| spring.application.name | Name to be shown in the application list. Name of the ApplicationContext. |
| info.version | Version number to be shown in the application list. Also published via /info-endpoint. |
| logging.file | Path to the applications logfile for access via spring-boot-admin. From Spring Boot logging configuration. |
......@@ -29,5 +29,9 @@
<groupId>org.jolokia</groupId>
<artifactId>jolokia-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
</dependencies>
</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 @@
package de.codecentric.boot.admin.config;
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.Configuration;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.web.client.RestTemplate;
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;
/**
......@@ -30,14 +33,24 @@ import de.codecentric.boot.admin.web.SimpleCORSFilter;
*/
@Configuration
@ConditionalOnProperty("spring.boot.admin.url")
@EnableConfigurationProperties({ AdminProperties.class, AdminClientProperties.class })
public class SpringBootAdminClientAutoConfiguration {
/**
* Task that registers the application at the spring-boot-admin application.
*/
@Bean
public Runnable registrator() {
return new SpringBootAdminRegistratorTask();
public SpringBootAdminRegistrator registrator(AdminProperties adminProps,
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 {
* TaskRegistrar that triggers the RegistratorTask every ten seconds.
*/
@Bean
public ScheduledTaskRegistrar taskRegistrar() {
public ScheduledTaskRegistrar taskRegistrar(final SpringBootAdminRegistrator registrator, AdminProperties adminProps) {
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;
}
......
......@@ -25,6 +25,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.bind.annotation.RequestMapping;
......@@ -55,8 +56,9 @@ public class LogfileController {
LOGGER.error("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() + "\"");
try {
FileCopyUtils.copy(file.getInputStream(), response.getOutputStream());
} catch (IOException e) {
......
......@@ -16,68 +16,113 @@
package de.codecentric.boot.admin.model;
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.
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public class Application implements Serializable {
private static final long serialVersionUID = 1L;
private String id;
private String url;
private final String id;
private final String url;
private final String name;
public String getId() {
return id;
@JsonCreator
public Application(@JsonProperty("url") String url, @JsonProperty("name") String name) {
this(url.replaceFirst("/+$", ""), name, generateId(url.replaceFirst("/+$", "")));
}
public void setId(String id) {
protected Application(String url, String name, String id) {
this.url = url;
this.name = name;
this.id = id;
}
public String getUrl() {
return url;
public String getId() {
return id;
}
public void setUrl(String url) {
this.url = url;
public String getUrl() {
return url;
}
@Override
public String toString() {
return id + " : " + url;
return "[id=" + id + ", url=" + url + ", name=" + name + "]";
}
public String getName() {
return name;
}
@Override
public int hashCode() {
final int prime = 31;
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());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
if (this == obj) {
return true;
if (obj == null)
}
if (obj == null) {
return false;
if (getClass() != obj.getClass())
}
if (getClass() != obj.getClass()) {
return false;
}
Application other = (Application) obj;
if (id == null) {
if (other.id != null)
if (name == null) {
if (other.name != null) {
return false;
} else if (!id.equals(other.id))
}
}
else if (!name.equals(other.name)) {
return false;
}
if (url == null) {
if (other.url != null)
if (other.url != null) {
return false;
} else if (!url.equals(other.url))
}
}
else if (!url.equals(other.url)) {
return false;
}
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