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) {
});
});
module.run(function ($rootScope, $state, Notification, Application, ApplicationGroups, MainViews, $timeout, $q) {
module.run(function ($rootScope, $state, Notification, Application, ApplicationGroups, MainViews) {
MainViews.register({
title: 'Applications',
state: 'applications-list',
......@@ -73,62 +73,10 @@ module.run(function ($rootScope, $state, Notification, Application, ApplicationG
var applicationGroups = new 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) {
applications.forEach(function (application) {
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') {
......@@ -148,14 +96,13 @@ module.run(function ($rootScope, $state, Notification, Application, ApplicationG
body: 'Instance ' + event.application.id + '\n' + event.application.healthUrl,
icon: require('./img/unknown.png'),
timeout: 10000,
url: $state.href('apps.details', {
url: $state.href('applications.details', {
id: event.application.id
})
};
if (event.type === 'REGISTRATION') {
applicationGroups.addApplication(event.application, false);
queueRefresh(event.application);
title += ' instance registered.';
options.tag = event.application.id + '-REGISTRY';
} else if (event.type === 'DEREGISTRATION') {
......@@ -164,7 +111,6 @@ module.run(function ($rootScope, $state, Notification, Application, ApplicationG
options.tag = event.application.id + '-REGISTRY';
} else if (event.type === 'STATUS_CHANGE') {
applicationGroups.addApplication(event.application, true);
queueRefresh(event.application);
title += ' instance is ' + event.to.status;
options.tag = event.application.id + '-STATUS';
options.icon = event.to.status !== 'UP' ? require('./img/error.png') : require('./img/ok.png');
......
......@@ -32,41 +32,55 @@ module.exports = function () {
return 'UNKNOWN';
};
var getGroupVersion = function (versionsCounter) {
var versions = Object.keys(versionsCounter);
versions.sort();
return versions[0] + (versions.length > 1 ? ', ...' : '');
};
this.updateStatus = function (group) {
var statusCounter = {};
var versionsCounter = {};
var applicationCount = 0;
this.applications.forEach(function (application) {
if (application.name === group.name) {
applicationCount++;
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.statusCounter = statusCounter;
group.status = getMaxStatus(statusCounter);
group.version = getGroupVersion(versionsCounter);
};
this.addApplication = function (application, overwrite) {
var idx = this.applications.findIndex(function (app) {
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];
application.group = this.groups[application.name] || { applicationCount: 0, statusCounter: {}, name: application.name, status: 'UNKNOWN' };
this.groups[application.name] = application.group;
if (idx < 0) {
this.applications.push(application);
} else if (overwrite) {
this.applications.splice(idx, 1, application);
}
this.updateStatus(this.groups[application.name]);
this.updateStatus(application.group);
};
this.removeApplication = function (id) {
var idx = this.applications.findIndex(function (application) {
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.updateStatus(group);
}
};
};
};
......@@ -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><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>
<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>Status</th>
<th></th>
......@@ -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)"
class="fa fa-minus"></i></td>
<td>{{ application.name }} ({{ application.id }})<br/>
<span class="muted">{{ application.serviceUrl || application.managementUrl || application.healthUrl }}</span></td>
<td>{{ application.version }}</td>
<span class="muted" ng-bind="application.serviceUrl || application.managementUrl || application.healthUrl"></span></td>
<td ng-bind="application.info.build.version || application.info.version"></td>
<td class="scroll">
<sba-limited-text max-lines="3" bind-html="application.info | yaml | linkify:60"></sba-limited-text>
</td>
......
......@@ -36,6 +36,7 @@ import de.codecentric.boot.admin.registry.StatusUpdateApplicationListener;
import de.codecentric.boot.admin.registry.StatusUpdater;
import de.codecentric.boot.admin.registry.store.ApplicationStore;
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.HttpHeadersProvider;
......@@ -69,8 +70,8 @@ public class AdminServerCoreConfiguration {
@Bean
@ConditionalOnMissingBean
public StatusUpdater statusUpdater(RestTemplateBuilder restTemplBuilder,
ApplicationStore applicationStore) {
public ApplicationOperations applicationOperations(RestTemplateBuilder restTemplBuilder,
HttpHeadersProvider headersProvider) {
RestTemplateBuilder builder = restTemplBuilder
.messageConverters(new MappingJackson2HttpMessageConverter())
.errorHandler(new DefaultResponseErrorHandler() {
......@@ -79,9 +80,15 @@ public class AdminServerCoreConfiguration {
return false;
}
});
return new ApplicationOperations(builder.build(), headersProvider);
};
StatusUpdater statusUpdater = new StatusUpdater(builder.build(), applicationStore,
httpHeadersProvider());
@Bean
@ConditionalOnMissingBean
public StatusUpdater statusUpdater(ApplicationStore applicationStore,
ApplicationOperations applicationOperations) {
StatusUpdater statusUpdater = new StatusUpdater(applicationStore, applicationOperations);
statusUpdater.setStatusLifetime(adminServerProperties.getMonitor().getStatusLifetime());
return statusUpdater;
}
......
......@@ -53,6 +53,7 @@ public class Application implements Serializable {
private final String source;
@JsonSerialize(using = Application.MetadataSerializer.class)
private final Map<String, String> metadata;
private final Info info;
protected Application(Builder builder) {
Assert.hasText(builder.name, "name must not be empty!");
......@@ -66,6 +67,7 @@ public class Application implements Serializable {
this.statusInfo = builder.statusInfo;
this.source = builder.source;
this.metadata = Collections.unmodifiableMap(new HashMap<>(builder.metadata));
this.info = builder.info;
}
public static Builder create(String name) {
......@@ -85,6 +87,7 @@ public class Application implements Serializable {
private StatusInfo statusInfo = StatusInfo.ofUnknown();
private String source;
private Map<String, String> metadata = new HashMap<>();
private Info info = Info.empty();
private Builder(String name) {
this.name = name;
......@@ -99,6 +102,7 @@ public class Application implements Serializable {
this.statusInfo = application.statusInfo;
this.source = application.source;
this.metadata.putAll(application.getMetadata());
this.info = application.info;
}
public Builder withName(String name) {
......@@ -136,13 +140,18 @@ public class Application implements Serializable {
return this;
}
public Builder withMetadata(String key, String value) {
public Builder addMetadata(String key, String value) {
this.metadata.put(key, value);
return this;
}
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;
}
......@@ -182,6 +191,11 @@ public class Application implements Serializable {
public Map<String, String> getMetadata() {
return metadata;
}
public Info getInfo() {
return info;
}
@Override
public String toString() {
return "Application [id=" + id + ", name=" + name + ", managementUrl="
......@@ -284,7 +298,7 @@ public class Application implements Serializable {
Iterator<Entry<String, JsonNode>> it = node.get("metadata").fields();
while (it.hasNext()) {
Entry<String, JsonNode> entry = it.next();
builder.withMetadata(entry.getKey(), entry.getValue().asText());
builder.addMetadata(entry.getKey(), entry.getValue().asText());
}
}
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;
import java.io.Serializable;
......@@ -5,6 +20,8 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonIgnore;
/**
* Represents a certain status a certain time.
*
......@@ -30,6 +47,10 @@ public class StatusInfo implements Serializable {
return new StatusInfo(statusCode, System.currentTimeMillis(), details);
}
public static StatusInfo valueOf(String statusCode) {
return valueOf(statusCode, null);
}
public static StatusInfo ofUnknown() {
return valueOf("UNKNOWN", null);
}
......@@ -70,6 +91,26 @@ public class StatusInfo implements Serializable {
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
public int hashCode() {
final int prime = 31;
......@@ -100,4 +141,9 @@ public class StatusInfo implements Serializable {
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;
import de.codecentric.boot.admin.event.ClientApplicationDeregisteredEvent;
import de.codecentric.boot.admin.event.ClientApplicationRegisteredEvent;
import de.codecentric.boot.admin.model.Application;
import de.codecentric.boot.admin.model.StatusInfo;
import de.codecentric.boot.admin.registry.store.ApplicationStore;
/**
......@@ -70,13 +69,15 @@ public class ApplicationRegistry implements ApplicationEventPublisherAware {
String applicationId = generator.generateId(application);
Assert.notNull(applicationId, "ID must not be null");
StatusInfo existingStatusInfo = getExistingStatusInfo(applicationId);
Application registering = Application.copyOf(application).withId(applicationId)
.withStatusInfo(existingStatusInfo).build();
Application.Builder builder = Application.copyOf(application).withId(applicationId);
Application existing = getApplication(applicationId);
if (existing != null) {
// Copy Status and Info from existing registration.
builder.withStatusInfo(existing.getStatusInfo()).withInfo(existing.getInfo());
}
Application registering = builder.build();
Application replaced = store.save(registering);
if (replaced == null) {
LOGGER.info("New Application {} registered ", registering);
publisher.publishEvent(new ClientApplicationRegisteredEvent(registering));
......@@ -90,13 +91,6 @@ public class ApplicationRegistry implements ApplicationEventPublisherAware {
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.
......
......@@ -15,8 +15,6 @@
*/
package de.codecentric.boot.admin.registry;
import static java.util.Arrays.asList;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
......@@ -25,18 +23,14 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationEventPublisher;
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.web.client.RestTemplate;
import de.codecentric.boot.admin.event.ClientApplicationStatusChangedEvent;
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.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
......@@ -48,16 +42,13 @@ public class StatusUpdater implements ApplicationEventPublisherAware {
private static final Logger LOGGER = LoggerFactory.getLogger(StatusUpdater.class);
private final ApplicationStore store;
private final RestTemplate restTemplate;
private final HttpHeadersProvider httpHeadersProvider;
private final ApplicationOperations applicationOps;
private ApplicationEventPublisher publisher;
private long statusLifetime = 10_000L;
public StatusUpdater(RestTemplate restTemplate, ApplicationStore store,
HttpHeadersProvider httpHeadersProvider) {
this.restTemplate = restTemplate;
public StatusUpdater(ApplicationStore store, ApplicationOperations applicationOps) {
this.store = store;
this.httpHeadersProvider = httpHeadersProvider;
this.applicationOps = applicationOps;
}
public void updateStatusForAllApplications() {
......@@ -72,22 +63,45 @@ public class StatusUpdater implements ApplicationEventPublisherAware {
public void updateStatus(Application application) {
StatusInfo oldStatus = application.getStatusInfo();
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);
if (!newStatus.equals(oldStatus)) {
if (statusChanged) {
publisher.publishEvent(
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) {
LOGGER.trace("Updating status for {}", application);
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) {
return StatusInfo.valueOf((String) response.getBody().get("status"),
response.getBody());
......@@ -100,28 +114,12 @@ public class StatusUpdater implements ApplicationEventPublisherAware {
if ("OFFLINE".equals(application.getStatusInfo().getStatus())) {
LOGGER.debug("Couldn't retrieve status for {}", application, ex);
} else {
LOGGER.warn("Couldn't retrieve status for {}", application, ex);
LOGGER.info("Couldn't retrieve status for {}", application, 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) {
Map<String, Serializable> details = new HashMap<>();
details.put("message", ex.getMessage());
......@@ -141,5 +139,4 @@ public class StatusUpdater implements ApplicationEventPublisherAware {
public void setApplicationEventPublisher(ApplicationEventPublisher 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 {
@Test
public void test_sanitize_metadata() throws JsonProcessingException {
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);
assertThat(json, not(containsString("qwertz123")));
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 {
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 @@
*/
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.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import java.util.Collection;
......@@ -27,12 +30,15 @@ import org.mockito.Mockito;
import org.springframework.context.ApplicationEventPublisher;
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;
public class ApplicationRegistryTest {
private ApplicationRegistry registry = new ApplicationRegistry(new SimpleApplicationStore(),
new HashingApplicationUrlIdGenerator());
private ApplicationStore store = new SimpleApplicationStore();
private ApplicationIdGenerator idGenerator = new HashingApplicationUrlIdGenerator();
private ApplicationRegistry registry = new ApplicationRegistry(store, idGenerator);
public ApplicationRegistryTest() {
registry.setApplicationEventPublisher(Mockito.mock(ApplicationEventPublisher.class));
......@@ -81,6 +87,25 @@ public class ApplicationRegistryTest {
}
@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 {
Application app = registry.register(Application.create("abc")
.withHealthUrl("http://localhost/health")
......
......@@ -15,6 +15,7 @@
*/
package de.codecentric.boot.admin.registry;
import static org.hamcrest.Matchers.hasEntry;
import static org.junit.Assert.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.argThat;
......@@ -25,6 +26,7 @@ import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.io.Serializable;
import java.util.Collections;
import java.util.Map;
......@@ -32,44 +34,37 @@ import org.hamcrest.CoreMatchers;
import org.junit.Before;
import org.junit.Test;
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.web.client.ResourceAccessException;
import org.springframework.web.client.RestTemplate;
import de.codecentric.boot.admin.event.ClientApplicationStatusChangedEvent;
import de.codecentric.boot.admin.model.Application;
import de.codecentric.boot.admin.model.StatusInfo;
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 {
private ApplicationOperations applicationOps;
private StatusUpdater updater;
private SimpleApplicationStore store;
private RestTemplate template;
private ApplicationEventPublisher publisher;
@Before
public void setup() {
HttpHeadersProvider httpHeadersProvider = mock(HttpHeadersProvider.class);
when(httpHeadersProvider.getHeaders(any(Application.class))).thenReturn(new HttpHeaders());
store = new SimpleApplicationStore();
template = mock(RestTemplate.class);
updater = new StatusUpdater(template, store, httpHeadersProvider);
applicationOps = mock(ApplicationOperations.class);
updater = new StatusUpdater(store, applicationOps);
publisher = mock(ApplicationEventPublisher.class);
updater.setApplicationEventPublisher(publisher);
}
@Test
@SuppressWarnings("rawtypes")
public void test_update_statusChanged() {
when(template.exchange(eq("health"), eq(HttpMethod.GET), isA(HttpEntity.class),
eq(Map.class))).thenReturn(
ResponseEntity.ok().body((Map) Collections.singletonMap("status", "UP")));
when(applicationOps.getHealth(isA(Application.class))).thenReturn(ResponseEntity.ok()
.body(Collections.<String, Serializable>singletonMap("status", "UP")));
when(applicationOps.getInfo(isA(Application.class))).thenReturn(ResponseEntity.ok()
.body(Collections.<String, Serializable>singletonMap("foo", "bar")));
updater.updateStatus(
Application.create("foo").withId("id").withHealthUrl("health").build());
......@@ -77,30 +72,30 @@ public class StatusUpdaterTest {
Application app = store.find("id");
assertThat(app.getStatusInfo().getStatus(), CoreMatchers.is("UP"));
assertThat((Map<String, ? extends Serializable>) app.getInfo().getValues(),
hasEntry("foo", (Serializable) "bar"));
verify(publisher)
.publishEvent(argThat(CoreMatchers.isA(ClientApplicationStatusChangedEvent.class)));
}
@Test
@SuppressWarnings("rawtypes")
public void test_update_statusUnchanged() {
when(template.exchange(eq("health"), eq(HttpMethod.GET), isA(HttpEntity.class),
eq(Map.class))).thenReturn(
ResponseEntity.ok((Map) Collections.singletonMap("status", "UNKNOWN")));
when(applicationOps.getHealth(any(Application.class))).thenReturn(ResponseEntity
.ok(Collections.<String, Serializable>singletonMap("status", "UNKNOWN")));
updater.updateStatus(
Application.create("foo").withId("id").withHealthUrl("health").build());
verify(publisher, never())
.publishEvent(argThat(CoreMatchers.isA(ClientApplicationStatusChangedEvent.class)));
verify(applicationOps, never()).getInfo(isA(Application.class));
}
@Test
@SuppressWarnings("rawtypes")
public void test_update_noBody() {
// HTTP 200 - UP
when(template.exchange(eq("health"), eq(HttpMethod.GET), isA(HttpEntity.class),
eq(Map.class))).thenReturn(ResponseEntity.ok((Map) null));
when(applicationOps.getHealth(any(Application.class)))
.thenReturn(ResponseEntity.ok((Map<String, Serializable>) null));
updater.updateStatus(
Application.create("foo").withId("id").withHealthUrl("health").build());
......@@ -108,8 +103,8 @@ public class StatusUpdaterTest {
assertThat(store.find("id").getStatusInfo().getStatus(), CoreMatchers.is("UP"));
// HTTP != 200 - DOWN
when(template.exchange(eq("health"), eq(HttpMethod.GET), isA(HttpEntity.class),
eq(Map.class))).thenReturn(ResponseEntity.status(503).body((Map) null));
when(applicationOps.getHealth(any(Application.class)))
.thenReturn(ResponseEntity.status(503).body((Map<String, Serializable>) null));
updater.updateStatus(
Application.create("foo").withId("id").withHealthUrl("health").build());
......@@ -119,8 +114,8 @@ public class StatusUpdaterTest {
@Test
public void test_update_offline() {
when(template.exchange(eq("health"), eq(HttpMethod.GET), isA(HttpEntity.class),
eq(Map.class))).thenThrow(new ResourceAccessException("error"));
when(applicationOps.getHealth(any(Application.class)))
.thenThrow(new ResourceAccessException("error"));
Application app = Application.create("foo").withId("id").withHealthUrl("health")
.withStatusInfo(StatusInfo.ofUp()).build();
......@@ -130,21 +125,23 @@ public class StatusUpdaterTest {
}
@Test
@SuppressWarnings("rawtypes")
public void test_updateStatusForApplications() throws InterruptedException {
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
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),
eq(Map.class))).thenReturn(ResponseEntity.ok((Map) null));
when(applicationOps.getHealth(eq(app1)))
.thenReturn(ResponseEntity.ok((Map<String, Serializable>) null));
updater.updateStatusForAllApplications();
assertThat(store.find("id-1").getStatusInfo().getStatus(), CoreMatchers.is("UP"));
verify(template, never()).exchange(eq("health-2"), eq(HttpMethod.GET),
isA(HttpEntity.class), eq(Map.class));
verify(applicationOps, never()).getHealth(eq(app2));
}
}
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 {
@Test
public void test_auth_header() {
Application app = Application.create("test").withHealthUrl("/health")
.withMetadata("user.name", "test").withMetadata("user.password", "drowssap")
.addMetadata("user.name", "test").addMetadata("user.password", "drowssap")
.build();
assertThat(headersProvider.getHeaders(app).get(HttpHeaders.AUTHORIZATION).get(0),
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