Commit 6fdaf8c1 by Johannes Edmeier

Aggregate info on the server

The info is now fetched by the server on status change. This way the info can be used in the notifications and also improves performance when listing a huge amount of applications in the ui. closes #349
parent 497a6f3a
...@@ -63,7 +63,7 @@ module.config(function ($stateProvider) { ...@@ -63,7 +63,7 @@ module.config(function ($stateProvider) {
}); });
}); });
module.run(function ($rootScope, $state, Notification, Application, ApplicationGroups, MainViews, $timeout, $q) { module.run(function ($rootScope, $state, Notification, Application, ApplicationGroups, MainViews) {
MainViews.register({ MainViews.register({
title: 'Applications', title: 'Applications',
state: 'applications-list', state: 'applications-list',
...@@ -73,62 +73,10 @@ module.run(function ($rootScope, $state, Notification, Application, ApplicationG ...@@ -73,62 +73,10 @@ module.run(function ($rootScope, $state, Notification, Application, ApplicationG
var applicationGroups = new ApplicationGroups(); var applicationGroups = new ApplicationGroups();
$rootScope.applicationGroups = applicationGroups; $rootScope.applicationGroups = applicationGroups;
var refreshQueue = [];
var runningRefreshs = 0;
var queueRefresh = function (application) {
application.refreshing = true;
if (runningRefreshs++ >= 15) {
refreshQueue.push(application);
} else {
refresh(application);
}
};
var refresh = function (application) {
doRefresh(application).finally(function () {
application.refreshing = false;
runningRefreshs--;
if (refreshQueue.length > 0) {
refresh(refreshQueue.pop());
}
});
};
var doRefresh = function (application) {
application.info = {};
if (application.statusInfo.status === 'OFFLINE') {
return $q.reject();
}
return application.getInfo().then(function (response) {
var info = response.data;
application.version = info.version;
delete info.version;
if (info.build && info.build.version) {
application.version = info.build.version;
}
if (application.version) {
var group = application.group;
group.versionsCounter[application.version] = (group.versionsCounter[application.version] || 0) + 1;
var versions = Object.keys(group.versionsCounter);
versions.sort();
group.version = versions[0] + (versions.length > 1 ? ', ...' : '');
}
application.info = info;
});
};
Application.query(function (applications) { Application.query(function (applications) {
applications.forEach(function (application) { applications.forEach(function (application) {
applicationGroups.addApplication(application, true); applicationGroups.addApplication(application, true);
}); });
//Defer refresh to give the browser time to render
$timeout(function () {
applicationGroups.applications.forEach(function (application) {
queueRefresh(application);
});
}, 50);
}); });
if (typeof (EventSource) !== 'undefined') { if (typeof (EventSource) !== 'undefined') {
...@@ -148,14 +96,13 @@ module.run(function ($rootScope, $state, Notification, Application, ApplicationG ...@@ -148,14 +96,13 @@ module.run(function ($rootScope, $state, Notification, Application, ApplicationG
body: 'Instance ' + event.application.id + '\n' + event.application.healthUrl, body: 'Instance ' + event.application.id + '\n' + event.application.healthUrl,
icon: require('./img/unknown.png'), icon: require('./img/unknown.png'),
timeout: 10000, timeout: 10000,
url: $state.href('apps.details', { url: $state.href('applications.details', {
id: event.application.id id: event.application.id
}) })
}; };
if (event.type === 'REGISTRATION') { if (event.type === 'REGISTRATION') {
applicationGroups.addApplication(event.application, false); applicationGroups.addApplication(event.application, false);
queueRefresh(event.application);
title += ' instance registered.'; title += ' instance registered.';
options.tag = event.application.id + '-REGISTRY'; options.tag = event.application.id + '-REGISTRY';
} else if (event.type === 'DEREGISTRATION') { } else if (event.type === 'DEREGISTRATION') {
...@@ -164,7 +111,6 @@ module.run(function ($rootScope, $state, Notification, Application, ApplicationG ...@@ -164,7 +111,6 @@ module.run(function ($rootScope, $state, Notification, Application, ApplicationG
options.tag = event.application.id + '-REGISTRY'; options.tag = event.application.id + '-REGISTRY';
} else if (event.type === 'STATUS_CHANGE') { } else if (event.type === 'STATUS_CHANGE') {
applicationGroups.addApplication(event.application, true); applicationGroups.addApplication(event.application, true);
queueRefresh(event.application);
title += ' instance is ' + event.to.status; title += ' instance is ' + event.to.status;
options.tag = event.application.id + '-STATUS'; options.tag = event.application.id + '-STATUS';
options.icon = event.to.status !== 'UP' ? require('./img/error.png') : require('./img/ok.png'); options.icon = event.to.status !== 'UP' ? require('./img/error.png') : require('./img/ok.png');
......
...@@ -32,41 +32,55 @@ module.exports = function () { ...@@ -32,41 +32,55 @@ module.exports = function () {
return 'UNKNOWN'; return 'UNKNOWN';
}; };
var getGroupVersion = function (versionsCounter) {
var versions = Object.keys(versionsCounter);
versions.sort();
return versions[0] + (versions.length > 1 ? ', ...' : '');
};
this.updateStatus = function (group) { this.updateStatus = function (group) {
var statusCounter = {}; var statusCounter = {};
var versionsCounter = {};
var applicationCount = 0; var applicationCount = 0;
this.applications.forEach(function (application) { this.applications.forEach(function (application) {
if (application.name === group.name) { if (application.name === group.name) {
applicationCount++; applicationCount++;
statusCounter[application.statusInfo.status] = ++statusCounter[application.statusInfo.status] || 1; statusCounter[application.statusInfo.status] = ++statusCounter[application.statusInfo.status] || 1;
var version = (application.info.build ? application.info.build.version : application.info.version);
if (version) {
versionsCounter[version] = ++versionsCounter[version] || 1;
}
} }
}); });
group.applicationCount = applicationCount; group.applicationCount = applicationCount;
group.statusCounter = statusCounter; group.statusCounter = statusCounter;
group.status = getMaxStatus(statusCounter); group.status = getMaxStatus(statusCounter);
group.version = getGroupVersion(versionsCounter);
}; };
this.addApplication = function (application, overwrite) { this.addApplication = function (application, overwrite) {
var idx = this.applications.findIndex(function (app) { var idx = this.applications.findIndex(function (app) {
return app.id === application.id; return app.id === application.id;
}); });
this.groups[application.name] = this.groups[application.name] || { applicationCount: 0, statusCounter: {}, versionsCounter: [], name: application.name, status: 'UNKNOWN' }; application.group = this.groups[application.name] || { applicationCount: 0, statusCounter: {}, name: application.name, status: 'UNKNOWN' };
application.group = this.groups[application.name]; this.groups[application.name] = application.group;
if (idx < 0) { if (idx < 0) {
this.applications.push(application); this.applications.push(application);
} else if (overwrite) { } else if (overwrite) {
this.applications.splice(idx, 1, application); this.applications.splice(idx, 1, application);
} }
this.updateStatus(this.groups[application.name]); this.updateStatus(application.group);
}; };
this.removeApplication = function (id) { this.removeApplication = function (id) {
var idx = this.applications.findIndex(function (application) { var idx = this.applications.findIndex(function (application) {
return id === application.id; return id === application.id;
}); });
var group = this.applications[idx].group; if (idx >= 0) {
var group = (this.applications[idx]).group;
this.applications.splice(idx, 1); this.applications.splice(idx, 1);
this.updateStatus(group); this.updateStatus(group);
}
}; };
}; };
}; };
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
<th class="group-column" title="{{expandAll ? 'collapse' : 'expand'}} all" ng-click="toggleExpandAll()"><i class="fa" ng-class="{'fa-plus': !expandAll, 'fa-minus': expandAll}"></i></th> <th class="group-column" title="{{expandAll ? 'collapse' : 'expand'}} all" ng-click="toggleExpandAll()"><i class="fa" ng-class="{'fa-plus': !expandAll, 'fa-minus': expandAll}"></i></th>
<th><span class="sortable" ng-class="orderByCssClass('name')" ng-click="orderBy('name')">Application</span> / <span class="sortable" <th><span class="sortable" ng-class="orderByCssClass('name')" ng-click="orderBy('name')">Application</span> / <span class="sortable"
ng-class="orderByCssClass('healthUrl')" ng-click="orderBy('healthUrl')">URL</span></th> ng-class="orderByCssClass('healthUrl')" ng-click="orderBy('healthUrl')">URL</span></th>
<th><span class="sortable" ng-class="orderByCssClass('info.version')" ng-click="orderBy('info.version')">Version</span></th> <th><span class="sortable" ng-class="orderByCssClass('info.build.version')" ng-click="orderBy('info.build.version')">Version</span></th>
<th>Info</th> <th>Info</th>
<th>Status</th> <th>Status</th>
<th></th> <th></th>
...@@ -39,8 +39,8 @@ ...@@ -39,8 +39,8 @@
ng-click="application.group.collapsed = application.group.applicationCount > 1"><i ng-show="application.group.applicationCount > 1 &amp;&amp; (searchFilter == '' || searchFilter == undefined)" ng-click="application.group.collapsed = application.group.applicationCount > 1"><i ng-show="application.group.applicationCount > 1 &amp;&amp; (searchFilter == '' || searchFilter == undefined)"
class="fa fa-minus"></i></td> class="fa fa-minus"></i></td>
<td>{{ application.name }} ({{ application.id }})<br/> <td>{{ application.name }} ({{ application.id }})<br/>
<span class="muted">{{ application.serviceUrl || application.managementUrl || application.healthUrl }}</span></td> <span class="muted" ng-bind="application.serviceUrl || application.managementUrl || application.healthUrl"></span></td>
<td>{{ application.version }}</td> <td ng-bind="application.info.build.version || application.info.version"></td>
<td class="scroll"> <td class="scroll">
<sba-limited-text max-lines="3" bind-html="application.info | yaml | linkify:60"></sba-limited-text> <sba-limited-text max-lines="3" bind-html="application.info | yaml | linkify:60"></sba-limited-text>
</td> </td>
......
...@@ -36,6 +36,7 @@ import de.codecentric.boot.admin.registry.StatusUpdateApplicationListener; ...@@ -36,6 +36,7 @@ import de.codecentric.boot.admin.registry.StatusUpdateApplicationListener;
import de.codecentric.boot.admin.registry.StatusUpdater; import de.codecentric.boot.admin.registry.StatusUpdater;
import de.codecentric.boot.admin.registry.store.ApplicationStore; import de.codecentric.boot.admin.registry.store.ApplicationStore;
import de.codecentric.boot.admin.registry.store.SimpleApplicationStore; import de.codecentric.boot.admin.registry.store.SimpleApplicationStore;
import de.codecentric.boot.admin.web.client.ApplicationOperations;
import de.codecentric.boot.admin.web.client.BasicAuthHttpHeaderProvider; import de.codecentric.boot.admin.web.client.BasicAuthHttpHeaderProvider;
import de.codecentric.boot.admin.web.client.HttpHeadersProvider; import de.codecentric.boot.admin.web.client.HttpHeadersProvider;
...@@ -69,8 +70,8 @@ public class AdminServerCoreConfiguration { ...@@ -69,8 +70,8 @@ public class AdminServerCoreConfiguration {
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
public StatusUpdater statusUpdater(RestTemplateBuilder restTemplBuilder, public ApplicationOperations applicationOperations(RestTemplateBuilder restTemplBuilder,
ApplicationStore applicationStore) { HttpHeadersProvider headersProvider) {
RestTemplateBuilder builder = restTemplBuilder RestTemplateBuilder builder = restTemplBuilder
.messageConverters(new MappingJackson2HttpMessageConverter()) .messageConverters(new MappingJackson2HttpMessageConverter())
.errorHandler(new DefaultResponseErrorHandler() { .errorHandler(new DefaultResponseErrorHandler() {
...@@ -79,9 +80,15 @@ public class AdminServerCoreConfiguration { ...@@ -79,9 +80,15 @@ public class AdminServerCoreConfiguration {
return false; return false;
} }
}); });
return new ApplicationOperations(builder.build(), headersProvider);
};
StatusUpdater statusUpdater = new StatusUpdater(builder.build(), applicationStore, @Bean
httpHeadersProvider()); @ConditionalOnMissingBean
public StatusUpdater statusUpdater(ApplicationStore applicationStore,
ApplicationOperations applicationOperations) {
StatusUpdater statusUpdater = new StatusUpdater(applicationStore, applicationOperations);
statusUpdater.setStatusLifetime(adminServerProperties.getMonitor().getStatusLifetime()); statusUpdater.setStatusLifetime(adminServerProperties.getMonitor().getStatusLifetime());
return statusUpdater; return statusUpdater;
} }
......
...@@ -53,6 +53,7 @@ public class Application implements Serializable { ...@@ -53,6 +53,7 @@ public class Application implements Serializable {
private final String source; private final String source;
@JsonSerialize(using = Application.MetadataSerializer.class) @JsonSerialize(using = Application.MetadataSerializer.class)
private final Map<String, String> metadata; private final Map<String, String> metadata;
private final Info info;
protected Application(Builder builder) { protected Application(Builder builder) {
Assert.hasText(builder.name, "name must not be empty!"); Assert.hasText(builder.name, "name must not be empty!");
...@@ -66,6 +67,7 @@ public class Application implements Serializable { ...@@ -66,6 +67,7 @@ public class Application implements Serializable {
this.statusInfo = builder.statusInfo; this.statusInfo = builder.statusInfo;
this.source = builder.source; this.source = builder.source;
this.metadata = Collections.unmodifiableMap(new HashMap<>(builder.metadata)); this.metadata = Collections.unmodifiableMap(new HashMap<>(builder.metadata));
this.info = builder.info;
} }
public static Builder create(String name) { public static Builder create(String name) {
...@@ -85,6 +87,7 @@ public class Application implements Serializable { ...@@ -85,6 +87,7 @@ public class Application implements Serializable {
private StatusInfo statusInfo = StatusInfo.ofUnknown(); private StatusInfo statusInfo = StatusInfo.ofUnknown();
private String source; private String source;
private Map<String, String> metadata = new HashMap<>(); private Map<String, String> metadata = new HashMap<>();
private Info info = Info.empty();
private Builder(String name) { private Builder(String name) {
this.name = name; this.name = name;
...@@ -99,6 +102,7 @@ public class Application implements Serializable { ...@@ -99,6 +102,7 @@ public class Application implements Serializable {
this.statusInfo = application.statusInfo; this.statusInfo = application.statusInfo;
this.source = application.source; this.source = application.source;
this.metadata.putAll(application.getMetadata()); this.metadata.putAll(application.getMetadata());
this.info = application.info;
} }
public Builder withName(String name) { public Builder withName(String name) {
...@@ -136,13 +140,18 @@ public class Application implements Serializable { ...@@ -136,13 +140,18 @@ public class Application implements Serializable {
return this; return this;
} }
public Builder withMetadata(String key, String value) { public Builder addMetadata(String key, String value) {
this.metadata.put(key, value); this.metadata.put(key, value);
return this; return this;
} }
public Builder withMetadata(Map<String, String> metadata) { public Builder withMetadata(Map<String, String> metadata) {
this.metadata.putAll(metadata); this.metadata = metadata;
return this;
}
public Builder withInfo(Info info) {
this.info = info;
return this; return this;
} }
...@@ -182,6 +191,11 @@ public class Application implements Serializable { ...@@ -182,6 +191,11 @@ public class Application implements Serializable {
public Map<String, String> getMetadata() { public Map<String, String> getMetadata() {
return metadata; return metadata;
} }
public Info getInfo() {
return info;
}
@Override @Override
public String toString() { public String toString() {
return "Application [id=" + id + ", name=" + name + ", managementUrl=" return "Application [id=" + id + ", name=" + name + ", managementUrl="
...@@ -284,7 +298,7 @@ public class Application implements Serializable { ...@@ -284,7 +298,7 @@ public class Application implements Serializable {
Iterator<Entry<String, JsonNode>> it = node.get("metadata").fields(); Iterator<Entry<String, JsonNode>> it = node.get("metadata").fields();
while (it.hasNext()) { while (it.hasNext()) {
Entry<String, JsonNode> entry = it.next(); Entry<String, JsonNode> entry = it.next();
builder.withMetadata(entry.getKey(), entry.getValue().asText()); builder.addMetadata(entry.getKey(), entry.getValue().asText());
} }
} }
return builder.build(); return builder.build();
......
/*
* Copyright 2016 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 java.util.Collections.unmodifiableMap;
import java.io.Serializable;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonIgnore;
/**
* Represents the info fetched from the info actuator endpoint at a certain time.
*
* @author Johannes Edmeier
*/
public class Info implements Serializable {
private static final long serialVersionUID = 2L;
private static Info EMPTY = new Info(0L, null);
@JsonIgnore
private final long timestamp;
private final Map<String, ? extends Serializable> values;
protected Info(long timestamp, Map<String, ? extends Serializable> values) {
this.timestamp = timestamp;
this.values = values != null ? unmodifiableMap(new LinkedHashMap<>(values))
: Collections.<String, Serializable>emptyMap();
}
public static Info from(Map<String, ? extends Serializable> values) {
return new Info(System.currentTimeMillis(), values);
}
public static Info empty() {
return EMPTY;
}
public long getTimestamp() {
return timestamp;
}
@JsonAnyGetter
public Map<String, ? extends Serializable> getValues() {
return values;
}
@Override
public String toString() {
return "Info [timestamp=" + timestamp + ", values=" + values + "]";
}
}
/*
* Copyright 2016 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; package de.codecentric.boot.admin.model;
import java.io.Serializable; import java.io.Serializable;
...@@ -5,6 +20,8 @@ import java.util.Collections; ...@@ -5,6 +20,8 @@ import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import com.fasterxml.jackson.annotation.JsonIgnore;
/** /**
* Represents a certain status a certain time. * Represents a certain status a certain time.
* *
...@@ -30,6 +47,10 @@ public class StatusInfo implements Serializable { ...@@ -30,6 +47,10 @@ public class StatusInfo implements Serializable {
return new StatusInfo(statusCode, System.currentTimeMillis(), details); return new StatusInfo(statusCode, System.currentTimeMillis(), details);
} }
public static StatusInfo valueOf(String statusCode) {
return valueOf(statusCode, null);
}
public static StatusInfo ofUnknown() { public static StatusInfo ofUnknown() {
return valueOf("UNKNOWN", null); return valueOf("UNKNOWN", null);
} }
...@@ -70,6 +91,26 @@ public class StatusInfo implements Serializable { ...@@ -70,6 +91,26 @@ public class StatusInfo implements Serializable {
return details; return details;
} }
@JsonIgnore
public boolean isUp() {
return "UP".equals(status);
}
@JsonIgnore
public boolean isOffline() {
return "OFFLINE".equals(status);
}
@JsonIgnore
public boolean isDown() {
return "DOWN".equals(status);
}
@JsonIgnore
public boolean isUnknown() {
return "UNKNOWN".equals(status);
}
@Override @Override
public int hashCode() { public int hashCode() {
final int prime = 31; final int prime = 31;
...@@ -100,4 +141,9 @@ public class StatusInfo implements Serializable { ...@@ -100,4 +141,9 @@ public class StatusInfo implements Serializable {
return true; return true;
} }
@Override
public String toString() {
return "StatusInfo [status=" + status + ", timestamp=" + timestamp + "]";
}
} }
\ No newline at end of file
...@@ -29,7 +29,6 @@ import org.springframework.util.StringUtils; ...@@ -29,7 +29,6 @@ import org.springframework.util.StringUtils;
import de.codecentric.boot.admin.event.ClientApplicationDeregisteredEvent; import de.codecentric.boot.admin.event.ClientApplicationDeregisteredEvent;
import de.codecentric.boot.admin.event.ClientApplicationRegisteredEvent; import de.codecentric.boot.admin.event.ClientApplicationRegisteredEvent;
import de.codecentric.boot.admin.model.Application; import de.codecentric.boot.admin.model.Application;
import de.codecentric.boot.admin.model.StatusInfo;
import de.codecentric.boot.admin.registry.store.ApplicationStore; import de.codecentric.boot.admin.registry.store.ApplicationStore;
/** /**
...@@ -70,13 +69,15 @@ public class ApplicationRegistry implements ApplicationEventPublisherAware { ...@@ -70,13 +69,15 @@ public class ApplicationRegistry implements ApplicationEventPublisherAware {
String applicationId = generator.generateId(application); String applicationId = generator.generateId(application);
Assert.notNull(applicationId, "ID must not be null"); Assert.notNull(applicationId, "ID must not be null");
StatusInfo existingStatusInfo = getExistingStatusInfo(applicationId); Application.Builder builder = Application.copyOf(application).withId(applicationId);
Application existing = getApplication(applicationId);
Application registering = Application.copyOf(application).withId(applicationId) if (existing != null) {
.withStatusInfo(existingStatusInfo).build(); // Copy Status and Info from existing registration.
builder.withStatusInfo(existing.getStatusInfo()).withInfo(existing.getInfo());
}
Application registering = builder.build();
Application replaced = store.save(registering); Application replaced = store.save(registering);
if (replaced == null) { if (replaced == null) {
LOGGER.info("New Application {} registered ", registering); LOGGER.info("New Application {} registered ", registering);
publisher.publishEvent(new ClientApplicationRegisteredEvent(registering)); publisher.publishEvent(new ClientApplicationRegisteredEvent(registering));
...@@ -90,13 +91,6 @@ public class ApplicationRegistry implements ApplicationEventPublisherAware { ...@@ -90,13 +91,6 @@ public class ApplicationRegistry implements ApplicationEventPublisherAware {
return registering; return registering;
} }
private StatusInfo getExistingStatusInfo(String applicationId) {
Application existing = getApplication(applicationId);
if (existing != null) {
return existing.getStatusInfo();
}
return StatusInfo.ofUnknown();
}
/** /**
* Checks the syntax of the given URL. * Checks the syntax of the given URL.
......
...@@ -15,8 +15,6 @@ ...@@ -15,8 +15,6 @@
*/ */
package de.codecentric.boot.admin.registry; package de.codecentric.boot.admin.registry;
import static java.util.Arrays.asList;
import java.io.Serializable; import java.io.Serializable;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
...@@ -25,18 +23,14 @@ import org.slf4j.Logger; ...@@ -25,18 +23,14 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
import de.codecentric.boot.admin.event.ClientApplicationStatusChangedEvent; import de.codecentric.boot.admin.event.ClientApplicationStatusChangedEvent;
import de.codecentric.boot.admin.model.Application; import de.codecentric.boot.admin.model.Application;
import de.codecentric.boot.admin.model.Info;
import de.codecentric.boot.admin.model.StatusInfo; import de.codecentric.boot.admin.model.StatusInfo;
import de.codecentric.boot.admin.registry.store.ApplicationStore; import de.codecentric.boot.admin.registry.store.ApplicationStore;
import de.codecentric.boot.admin.web.client.HttpHeadersProvider; import de.codecentric.boot.admin.web.client.ApplicationOperations;
/** /**
* The StatusUpdater is responsible for updating the status of all or a single application querying * The StatusUpdater is responsible for updating the status of all or a single application querying
...@@ -48,16 +42,13 @@ public class StatusUpdater implements ApplicationEventPublisherAware { ...@@ -48,16 +42,13 @@ public class StatusUpdater implements ApplicationEventPublisherAware {
private static final Logger LOGGER = LoggerFactory.getLogger(StatusUpdater.class); private static final Logger LOGGER = LoggerFactory.getLogger(StatusUpdater.class);
private final ApplicationStore store; private final ApplicationStore store;
private final RestTemplate restTemplate; private final ApplicationOperations applicationOps;
private final HttpHeadersProvider httpHeadersProvider;
private ApplicationEventPublisher publisher; private ApplicationEventPublisher publisher;
private long statusLifetime = 10_000L; private long statusLifetime = 10_000L;
public StatusUpdater(RestTemplate restTemplate, ApplicationStore store, public StatusUpdater(ApplicationStore store, ApplicationOperations applicationOps) {
HttpHeadersProvider httpHeadersProvider) {
this.restTemplate = restTemplate;
this.store = store; this.store = store;
this.httpHeadersProvider = httpHeadersProvider; this.applicationOps = applicationOps;
} }
public void updateStatusForAllApplications() { public void updateStatusForAllApplications() {
...@@ -72,22 +63,45 @@ public class StatusUpdater implements ApplicationEventPublisherAware { ...@@ -72,22 +63,45 @@ public class StatusUpdater implements ApplicationEventPublisherAware {
public void updateStatus(Application application) { public void updateStatus(Application application) {
StatusInfo oldStatus = application.getStatusInfo(); StatusInfo oldStatus = application.getStatusInfo();
StatusInfo newStatus = queryStatus(application); StatusInfo newStatus = queryStatus(application);
boolean statusChanged = !newStatus.equals(oldStatus);
Application.Builder builder = Application.copyOf(application).withStatusInfo(newStatus);
Application newState = Application.copyOf(application).withStatusInfo(newStatus).build(); if (statusChanged && !newStatus.isOffline() && !newStatus.isUnknown()) {
builder.withInfo(queryInfo(application));
}
Application newState = builder.build();
store.save(newState); store.save(newState);
if (!newStatus.equals(oldStatus)) { if (statusChanged) {
publisher.publishEvent( publisher.publishEvent(
new ClientApplicationStatusChangedEvent(newState, oldStatus, newStatus)); new ClientApplicationStatusChangedEvent(newState, oldStatus, newStatus));
} }
} }
private Info queryInfo(Application application) {
try {
ResponseEntity<Map<String, Serializable>> response = applicationOps
.getInfo(application);
if (response.getStatusCode().is2xxSuccessful() && response.hasBody()) {
return Info.from(response.getBody());
} else {
LOGGER.info("Couldn't retrieve info for {}: {} - {}", application,
response.getStatusCode(), response.getBody());
return Info.empty();
}
} catch (Exception ex) {
LOGGER.warn("Couldn't retrieve info for {}", application, ex);
return Info.empty();
}
}
protected StatusInfo queryStatus(Application application) { protected StatusInfo queryStatus(Application application) {
LOGGER.trace("Updating status for {}", application); LOGGER.trace("Updating status for {}", application);
try { try {
ResponseEntity<Map<String, Serializable>> response = doGetStatus(application); ResponseEntity<Map<String, Serializable>> response = applicationOps
.getHealth(application);
if (response.hasBody() && response.getBody().get("status") instanceof String) { if (response.hasBody() && response.getBody().get("status") instanceof String) {
return StatusInfo.valueOf((String) response.getBody().get("status"), return StatusInfo.valueOf((String) response.getBody().get("status"),
response.getBody()); response.getBody());
...@@ -100,28 +114,12 @@ public class StatusUpdater implements ApplicationEventPublisherAware { ...@@ -100,28 +114,12 @@ public class StatusUpdater implements ApplicationEventPublisherAware {
if ("OFFLINE".equals(application.getStatusInfo().getStatus())) { if ("OFFLINE".equals(application.getStatusInfo().getStatus())) {
LOGGER.debug("Couldn't retrieve status for {}", application, ex); LOGGER.debug("Couldn't retrieve status for {}", application, ex);
} else { } else {
LOGGER.warn("Couldn't retrieve status for {}", application, ex); LOGGER.info("Couldn't retrieve status for {}", application, ex);
} }
return StatusInfo.ofOffline(toDetails(ex)); return StatusInfo.ofOffline(toDetails(ex));
} }
} }
private ResponseEntity<Map<String, Serializable>> doGetStatus(Application application) {
@SuppressWarnings("unchecked")
Class<Map<String, Serializable>> responseType = (Class<Map<String, Serializable>>) (Class<?>) Map.class;
HttpHeaders headers = new HttpHeaders();
headers.setAccept(asList(MediaType.APPLICATION_JSON));
headers.putAll(httpHeadersProvider.getHeaders(application));
ResponseEntity<Map<String, Serializable>> response = restTemplate.exchange(
application.getHealthUrl(), HttpMethod.GET, new HttpEntity<Void>(headers),
responseType);
LOGGER.debug("/health for {} responded with {}", application, response);
return response;
}
protected Map<String, Serializable> toDetails(Exception ex) { protected Map<String, Serializable> toDetails(Exception ex) {
Map<String, Serializable> details = new HashMap<>(); Map<String, Serializable> details = new HashMap<>();
details.put("message", ex.getMessage()); details.put("message", ex.getMessage());
...@@ -141,5 +139,4 @@ public class StatusUpdater implements ApplicationEventPublisherAware { ...@@ -141,5 +139,4 @@ public class StatusUpdater implements ApplicationEventPublisherAware {
public void setApplicationEventPublisher(ApplicationEventPublisher publisher) { public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
this.publisher = publisher; this.publisher = publisher;
} }
} }
/*
* Copyright 2016 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.web.client;
import static java.util.Arrays.asList;
import java.io.Serializable;
import java.net.URI;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import de.codecentric.boot.admin.model.Application;
/**
* Handles all rest operations invoked on a registered application.
*
* @author Johannes Edmeier
*/
public class ApplicationOperations {
private static final Logger LOGGER = LoggerFactory.getLogger(ApplicationOperations.class);
@SuppressWarnings("unchecked")
private static final Class<Map<String, Serializable>> RESPONSE_TYPE_MAP = (Class<Map<String, Serializable>>) (Class<?>) Map.class;
private final RestTemplate restTemplate;
private final HttpHeadersProvider httpHeadersProvider;
public ApplicationOperations(RestTemplate restTemplate,
HttpHeadersProvider httpHeadersProvider) {
this.restTemplate = restTemplate;
this.httpHeadersProvider = httpHeadersProvider;
}
public ResponseEntity<Map<String, Serializable>> getInfo(
Application application) {
URI uri = UriComponentsBuilder.fromHttpUrl(application.getManagementUrl())
.pathSegment("info").build().toUri();
return doGet(application, uri, RESPONSE_TYPE_MAP);
}
public ResponseEntity<Map<String, Serializable>> getHealth(
Application application) {
URI uri = UriComponentsBuilder.fromHttpUrl(application.getHealthUrl()).build().toUri();
return doGet(application, uri, RESPONSE_TYPE_MAP);
}
protected <T> ResponseEntity<T> doGet(Application application, URI uri, Class<T> responseType) {
LOGGER.debug("Fetching '{}' for {}", uri, application);
HttpHeaders headers = new HttpHeaders();
headers.setAccept(asList(MediaType.APPLICATION_JSON));
headers.putAll(httpHeadersProvider.getHeaders(application));
ResponseEntity<T> response = restTemplate.exchange(uri, HttpMethod.GET,
new HttpEntity<Void>(headers), responseType);
LOGGER.debug("'{}' responded with {}", uri, response);
return response;
}
}
...@@ -82,7 +82,7 @@ public class ApplicationTest { ...@@ -82,7 +82,7 @@ public class ApplicationTest {
@Test @Test
public void test_sanitize_metadata() throws JsonProcessingException { public void test_sanitize_metadata() throws JsonProcessingException {
Application app = Application.create("test").withHealthUrl("http://health") Application app = Application.create("test").withHealthUrl("http://health")
.withMetadata("password", "qwertz123").withMetadata("user", "humptydumpty").build(); .addMetadata("password", "qwertz123").addMetadata("user", "humptydumpty").build();
String json = objectMapper.writeValueAsString(app); String json = objectMapper.writeValueAsString(app);
assertThat(json, not(containsString("qwertz123"))); assertThat(json, not(containsString("qwertz123")));
assertThat(json, containsString("humptydumpty")); assertThat(json, containsString("humptydumpty"));
......
package de.codecentric.boot.admin.model;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import org.junit.Test;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
public class InfoTest {
private ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json().build();
@Test
public void test_json_serialize() throws Exception {
Info info = Info.from(Collections.singletonMap("foo", "bar"));
String json = objectMapper.writeValueAsString(info);
DocumentContext doc = JsonPath.parse(json);
assertThat(doc.read("$.foo", String.class)).isEqualTo("bar");
}
@Test
public void test_retain_order() {
Map<String, String> map = new LinkedHashMap<>();
map.put("z", "1");
map.put("x", "2");
Iterator<?> iter = Info.from(map).getValues().entrySet().iterator();
assertThat(iter.next()).hasFieldOrPropertyWithValue("key", "z")
.hasFieldOrPropertyWithValue("value", "1");
assertThat(iter.next()).hasFieldOrPropertyWithValue("key", "x")
.hasFieldOrPropertyWithValue("value", "2");
}
}
...@@ -19,4 +19,33 @@ public class StatusInfoTest { ...@@ -19,4 +19,33 @@ public class StatusInfoTest {
assertThat(up.hashCode(), is(up2.hashCode())); assertThat(up.hashCode(), is(up2.hashCode()));
} }
@Test
public void test_isMethods() {
assertThat(StatusInfo.valueOf("FOO").isUp(), is(false));
assertThat(StatusInfo.valueOf("FOO").isDown(), is(false));
assertThat(StatusInfo.valueOf("FOO").isUnknown(), is(false));
assertThat(StatusInfo.valueOf("FOO").isOffline(), is(false));
assertThat(StatusInfo.ofUp().isUp(), is(true));
assertThat(StatusInfo.ofUp().isDown(), is(false));
assertThat(StatusInfo.ofUp().isUnknown(), is(false));
assertThat(StatusInfo.ofUp().isOffline(), is(false));
assertThat(StatusInfo.ofDown().isUp(), is(false));
assertThat(StatusInfo.ofDown().isDown(), is(true));
assertThat(StatusInfo.ofDown().isUnknown(), is(false));
assertThat(StatusInfo.ofDown().isOffline(), is(false));
assertThat(StatusInfo.ofUnknown().isUp(), is(false));
assertThat(StatusInfo.ofUnknown().isDown(), is(false));
assertThat(StatusInfo.ofUnknown().isUnknown(), is(true));
assertThat(StatusInfo.ofUnknown().isOffline(), is(false));
assertThat(StatusInfo.ofOffline().isUp(), is(false));
assertThat(StatusInfo.ofOffline().isDown(), is(false));
assertThat(StatusInfo.ofOffline().isUnknown(), is(false));
assertThat(StatusInfo.ofOffline().isOffline(), is(true));
}
} }
\ No newline at end of file
...@@ -15,9 +15,12 @@ ...@@ -15,9 +15,12 @@
*/ */
package de.codecentric.boot.admin.registry; package de.codecentric.boot.admin.registry;
import static java.util.Collections.singletonMap;
import static org.hamcrest.Matchers.sameInstance;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import java.util.Collection; import java.util.Collection;
...@@ -27,12 +30,15 @@ import org.mockito.Mockito; ...@@ -27,12 +30,15 @@ import org.mockito.Mockito;
import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisher;
import de.codecentric.boot.admin.model.Application; import de.codecentric.boot.admin.model.Application;
import de.codecentric.boot.admin.model.Info;
import de.codecentric.boot.admin.model.StatusInfo;
import de.codecentric.boot.admin.registry.store.ApplicationStore;
import de.codecentric.boot.admin.registry.store.SimpleApplicationStore; import de.codecentric.boot.admin.registry.store.SimpleApplicationStore;
public class ApplicationRegistryTest { public class ApplicationRegistryTest {
private ApplicationStore store = new SimpleApplicationStore();
private ApplicationRegistry registry = new ApplicationRegistry(new SimpleApplicationStore(), private ApplicationIdGenerator idGenerator = new HashingApplicationUrlIdGenerator();
new HashingApplicationUrlIdGenerator()); private ApplicationRegistry registry = new ApplicationRegistry(store, idGenerator);
public ApplicationRegistryTest() { public ApplicationRegistryTest() {
registry.setApplicationEventPublisher(Mockito.mock(ApplicationEventPublisher.class)); registry.setApplicationEventPublisher(Mockito.mock(ApplicationEventPublisher.class));
...@@ -81,6 +87,25 @@ public class ApplicationRegistryTest { ...@@ -81,6 +87,25 @@ public class ApplicationRegistryTest {
} }
@Test @Test
public void refresh() throws Exception {
// Given application is already reegistered and has status and info.
StatusInfo status = StatusInfo.ofUp();
Info info = Info.from(singletonMap("foo", "bar"));
Application app = Application.create("abc").withHealthUrl("http://localhost:8080/health")
.withStatusInfo(status).withInfo(info).build();
String id = idGenerator.generateId(app);
store.save(Application.copyOf(app).withId(id).build());
// When application registers second time
Application registered = registry.register(
Application.create("abc").withHealthUrl("http://localhost:8080/health").build());
// Then info and status are retained
assertThat(registered.getInfo(), sameInstance(info));
assertThat(registered.getStatusInfo(), sameInstance(status));
}
@Test
public void getApplication() throws Exception { public void getApplication() throws Exception {
Application app = registry.register(Application.create("abc") Application app = registry.register(Application.create("abc")
.withHealthUrl("http://localhost/health") .withHealthUrl("http://localhost/health")
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package de.codecentric.boot.admin.registry; package de.codecentric.boot.admin.registry;
import static org.hamcrest.Matchers.hasEntry;
import static org.junit.Assert.assertThat; import static org.junit.Assert.assertThat;
import static org.mockito.Matchers.any; import static org.mockito.Matchers.any;
import static org.mockito.Matchers.argThat; import static org.mockito.Matchers.argThat;
...@@ -25,6 +26,7 @@ import static org.mockito.Mockito.never; ...@@ -25,6 +26,7 @@ import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import java.io.Serializable;
import java.util.Collections; import java.util.Collections;
import java.util.Map; import java.util.Map;
...@@ -32,44 +34,37 @@ import org.hamcrest.CoreMatchers; ...@@ -32,44 +34,37 @@ import org.hamcrest.CoreMatchers;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisher;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.client.ResourceAccessException; import org.springframework.web.client.ResourceAccessException;
import org.springframework.web.client.RestTemplate;
import de.codecentric.boot.admin.event.ClientApplicationStatusChangedEvent; import de.codecentric.boot.admin.event.ClientApplicationStatusChangedEvent;
import de.codecentric.boot.admin.model.Application; import de.codecentric.boot.admin.model.Application;
import de.codecentric.boot.admin.model.StatusInfo; import de.codecentric.boot.admin.model.StatusInfo;
import de.codecentric.boot.admin.registry.store.SimpleApplicationStore; import de.codecentric.boot.admin.registry.store.SimpleApplicationStore;
import de.codecentric.boot.admin.web.client.HttpHeadersProvider; import de.codecentric.boot.admin.web.client.ApplicationOperations;
public class StatusUpdaterTest { public class StatusUpdaterTest {
private ApplicationOperations applicationOps;
private StatusUpdater updater; private StatusUpdater updater;
private SimpleApplicationStore store; private SimpleApplicationStore store;
private RestTemplate template;
private ApplicationEventPublisher publisher; private ApplicationEventPublisher publisher;
@Before @Before
public void setup() { public void setup() {
HttpHeadersProvider httpHeadersProvider = mock(HttpHeadersProvider.class);
when(httpHeadersProvider.getHeaders(any(Application.class))).thenReturn(new HttpHeaders());
store = new SimpleApplicationStore(); store = new SimpleApplicationStore();
template = mock(RestTemplate.class); applicationOps = mock(ApplicationOperations.class);
updater = new StatusUpdater(template, store, httpHeadersProvider); updater = new StatusUpdater(store, applicationOps);
publisher = mock(ApplicationEventPublisher.class); publisher = mock(ApplicationEventPublisher.class);
updater.setApplicationEventPublisher(publisher); updater.setApplicationEventPublisher(publisher);
} }
@Test @Test
@SuppressWarnings("rawtypes")
public void test_update_statusChanged() { public void test_update_statusChanged() {
when(template.exchange(eq("health"), eq(HttpMethod.GET), isA(HttpEntity.class), when(applicationOps.getHealth(isA(Application.class))).thenReturn(ResponseEntity.ok()
eq(Map.class))).thenReturn( .body(Collections.<String, Serializable>singletonMap("status", "UP")));
ResponseEntity.ok().body((Map) Collections.singletonMap("status", "UP"))); when(applicationOps.getInfo(isA(Application.class))).thenReturn(ResponseEntity.ok()
.body(Collections.<String, Serializable>singletonMap("foo", "bar")));
updater.updateStatus( updater.updateStatus(
Application.create("foo").withId("id").withHealthUrl("health").build()); Application.create("foo").withId("id").withHealthUrl("health").build());
...@@ -77,30 +72,30 @@ public class StatusUpdaterTest { ...@@ -77,30 +72,30 @@ public class StatusUpdaterTest {
Application app = store.find("id"); Application app = store.find("id");
assertThat(app.getStatusInfo().getStatus(), CoreMatchers.is("UP")); assertThat(app.getStatusInfo().getStatus(), CoreMatchers.is("UP"));
assertThat((Map<String, ? extends Serializable>) app.getInfo().getValues(),
hasEntry("foo", (Serializable) "bar"));
verify(publisher) verify(publisher)
.publishEvent(argThat(CoreMatchers.isA(ClientApplicationStatusChangedEvent.class))); .publishEvent(argThat(CoreMatchers.isA(ClientApplicationStatusChangedEvent.class)));
} }
@Test @Test
@SuppressWarnings("rawtypes")
public void test_update_statusUnchanged() { public void test_update_statusUnchanged() {
when(template.exchange(eq("health"), eq(HttpMethod.GET), isA(HttpEntity.class), when(applicationOps.getHealth(any(Application.class))).thenReturn(ResponseEntity
eq(Map.class))).thenReturn( .ok(Collections.<String, Serializable>singletonMap("status", "UNKNOWN")));
ResponseEntity.ok((Map) Collections.singletonMap("status", "UNKNOWN")));
updater.updateStatus( updater.updateStatus(
Application.create("foo").withId("id").withHealthUrl("health").build()); Application.create("foo").withId("id").withHealthUrl("health").build());
verify(publisher, never()) verify(publisher, never())
.publishEvent(argThat(CoreMatchers.isA(ClientApplicationStatusChangedEvent.class))); .publishEvent(argThat(CoreMatchers.isA(ClientApplicationStatusChangedEvent.class)));
verify(applicationOps, never()).getInfo(isA(Application.class));
} }
@Test @Test
@SuppressWarnings("rawtypes")
public void test_update_noBody() { public void test_update_noBody() {
// HTTP 200 - UP // HTTP 200 - UP
when(template.exchange(eq("health"), eq(HttpMethod.GET), isA(HttpEntity.class), when(applicationOps.getHealth(any(Application.class)))
eq(Map.class))).thenReturn(ResponseEntity.ok((Map) null)); .thenReturn(ResponseEntity.ok((Map<String, Serializable>) null));
updater.updateStatus( updater.updateStatus(
Application.create("foo").withId("id").withHealthUrl("health").build()); Application.create("foo").withId("id").withHealthUrl("health").build());
...@@ -108,8 +103,8 @@ public class StatusUpdaterTest { ...@@ -108,8 +103,8 @@ public class StatusUpdaterTest {
assertThat(store.find("id").getStatusInfo().getStatus(), CoreMatchers.is("UP")); assertThat(store.find("id").getStatusInfo().getStatus(), CoreMatchers.is("UP"));
// HTTP != 200 - DOWN // HTTP != 200 - DOWN
when(template.exchange(eq("health"), eq(HttpMethod.GET), isA(HttpEntity.class), when(applicationOps.getHealth(any(Application.class)))
eq(Map.class))).thenReturn(ResponseEntity.status(503).body((Map) null)); .thenReturn(ResponseEntity.status(503).body((Map<String, Serializable>) null));
updater.updateStatus( updater.updateStatus(
Application.create("foo").withId("id").withHealthUrl("health").build()); Application.create("foo").withId("id").withHealthUrl("health").build());
...@@ -119,8 +114,8 @@ public class StatusUpdaterTest { ...@@ -119,8 +114,8 @@ public class StatusUpdaterTest {
@Test @Test
public void test_update_offline() { public void test_update_offline() {
when(template.exchange(eq("health"), eq(HttpMethod.GET), isA(HttpEntity.class), when(applicationOps.getHealth(any(Application.class)))
eq(Map.class))).thenThrow(new ResourceAccessException("error")); .thenThrow(new ResourceAccessException("error"));
Application app = Application.create("foo").withId("id").withHealthUrl("health") Application app = Application.create("foo").withId("id").withHealthUrl("health")
.withStatusInfo(StatusInfo.ofUp()).build(); .withStatusInfo(StatusInfo.ofUp()).build();
...@@ -130,21 +125,23 @@ public class StatusUpdaterTest { ...@@ -130,21 +125,23 @@ public class StatusUpdaterTest {
} }
@Test @Test
@SuppressWarnings("rawtypes")
public void test_updateStatusForApplications() throws InterruptedException { public void test_updateStatusForApplications() throws InterruptedException {
updater.setStatusLifetime(100L); updater.setStatusLifetime(100L);
store.save(Application.create("foo").withId("id-1").withHealthUrl("health-1").build()); Application app1 = Application.create("foo").withId("id-1").withHealthUrl("health-1")
.build();
store.save(app1);
Thread.sleep(120L); // Let the StatusInfo of id-1 expire Thread.sleep(120L); // Let the StatusInfo of id-1 expire
store.save(Application.create("foo").withId("id-2").withHealthUrl("health-2").build()); Application app2 = Application.create("foo").withId("id-2").withHealthUrl("health-2")
.build();
store.save(app2);
when(template.exchange(eq("health-1"), eq(HttpMethod.GET), isA(HttpEntity.class), when(applicationOps.getHealth(eq(app1)))
eq(Map.class))).thenReturn(ResponseEntity.ok((Map) null)); .thenReturn(ResponseEntity.ok((Map<String, Serializable>) null));
updater.updateStatusForAllApplications(); updater.updateStatusForAllApplications();
assertThat(store.find("id-1").getStatusInfo().getStatus(), CoreMatchers.is("UP")); assertThat(store.find("id-1").getStatusInfo().getStatus(), CoreMatchers.is("UP"));
verify(template, never()).exchange(eq("health-2"), eq(HttpMethod.GET), verify(applicationOps, never()).getHealth(eq(app2));
isA(HttpEntity.class), eq(Map.class));
} }
} }
package de.codecentric.boot.admin.web.client;
import static java.util.Arrays.asList;
import static java.util.Collections.singletonMap;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.io.Serializable;
import java.net.URI;
import java.util.Map;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
import de.codecentric.boot.admin.model.Application;
public class ApplicationOperationsTest {
private RestTemplate restTemplate = mock(RestTemplate.class);
private HttpHeadersProvider headersProvider = mock(HttpHeadersProvider.class);
private ApplicationOperations ops = new ApplicationOperations(restTemplate, headersProvider);
@Test
@SuppressWarnings("rawtypes")
public void test_getInfo() {
Application app = Application.create("test").withHealthUrl("http://health")
.withManagementUrl("http://mgmt").build();
ArgumentCaptor<HttpEntity> requestEntity = ArgumentCaptor.forClass(HttpEntity.class);
HttpHeaders headers = new HttpHeaders();
headers.add("auth", "foo:bar");
when(headersProvider.getHeaders(eq(app))).thenReturn(headers);
when(restTemplate.exchange(eq(URI.create("http://mgmt/info")), eq(HttpMethod.GET),
requestEntity.capture(), eq(Map.class)))
.thenReturn(ResponseEntity.ok().body((Map) singletonMap("foo", "bar")));
ResponseEntity<Map<String, Serializable>> response = ops.getInfo(app);
assertThat(response.getBody()).containsEntry("foo", "bar");
assertThat(requestEntity.getValue().getHeaders()).containsEntry("auth", asList("foo:bar"));
}
@Test
@SuppressWarnings("rawtypes")
public void test_getHealth() {
Application app = Application.create("test").withHealthUrl("http://health")
.withManagementUrl("http://mgmt").build();
ArgumentCaptor<HttpEntity> requestEntity = ArgumentCaptor.forClass(HttpEntity.class);
HttpHeaders headers = new HttpHeaders();
headers.add("auth", "foo:bar");
when(headersProvider.getHeaders(eq(app))).thenReturn(headers);
when(restTemplate.exchange(eq(URI.create("http://health")), eq(HttpMethod.GET),
requestEntity.capture(), eq(Map.class)))
.thenReturn(ResponseEntity.ok().body((Map) singletonMap("foo", "bar")));
ResponseEntity<Map<String, Serializable>> response = ops.getHealth(app);
assertThat(response.getBody()).containsEntry("foo", "bar");
assertThat(requestEntity.getValue().getHeaders()).containsEntry("auth", asList("foo:bar"));
}
}
...@@ -16,7 +16,7 @@ public class BasicAuthHttpHeaderProviderTest { ...@@ -16,7 +16,7 @@ public class BasicAuthHttpHeaderProviderTest {
@Test @Test
public void test_auth_header() { public void test_auth_header() {
Application app = Application.create("test").withHealthUrl("/health") Application app = Application.create("test").withHealthUrl("/health")
.withMetadata("user.name", "test").withMetadata("user.password", "drowssap") .addMetadata("user.name", "test").addMetadata("user.password", "drowssap")
.build(); .build();
assertThat(headersProvider.getHeaders(app).get(HttpHeaders.AUTHORIZATION).get(0), assertThat(headersProvider.getHeaders(app).get(HttpHeaders.AUTHORIZATION).get(0),
is("Basic dGVzdDpkcm93c3NhcA==")); is("Basic dGVzdDpkcm93c3NhcA=="));
......
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