Commit 7733bcf6 by Johannes Edmeier

Replace polling in the UI with server sent events

The polling in the UI is replaced with server side events. Status changes are now reported immediately from the admin server to the UI.
parent 26374475
......@@ -11,9 +11,6 @@ spring:
cloud:
config:
enabled: false
jackson:
serialization:
indent-output: true
endpoints:
health:
......
......@@ -14,9 +14,6 @@ spring:
cloud:
config:
enabled: false
jackson:
serialization:
indent-output: true
endpoints:
health:
......
This diff was suppressed by a .gitattributes entry.
......@@ -62,9 +62,8 @@ springBootAdmin.config(function ($stateProvider, $urlRouterProvider) {
resolve: {
application: function ($stateParams, Application) {
return Application.get({
id: $stateParams.id
})
.$promise;
id: $stateParams.id
}).$promise;
}
}
})
......@@ -119,6 +118,95 @@ springBootAdmin.config(function ($stateProvider, $urlRouterProvider) {
controller: 'journalCtrl'
});
});
springBootAdmin.run(function ($rootScope, $state) {
springBootAdmin.run(function ($rootScope, $state, $log, $filter, Notification, Application ) {
$rootScope.$state = $state;
$rootScope.applications = [];
$rootScope.indexOfApplication = function (id) {
for (var i = 0; i < $rootScope.applications.length; i++) {
if ($rootScope.applications[i].id === id) {
return i;
}
}
return -1;
};
var refresh = function(application) {
application.info = {};
application.refreshing = true;
application.getCapabilities();
application.getInfo().then(function(info) {
application.version = info.version;
application.infoDetails = null;
application.infoShort = '';
delete info.version;
var infoYml = $filter('yaml')(info);
if (infoYml !== '{}\n') {
application.infoShort = $filter('limitLines')(infoYml, 3);
if (application.infoShort !== infoYml) {
application.infoDetails = $filter('limitLines')(infoYml, 32000, 3);
}
}
}).finally(function(){
application.refreshing = false;
});
};
Application.query(function (applications) {
for (var i = 0; i < applications.length; i++) {
refresh(applications[i]);
}
$rootScope.applications = applications;
});
//setups up the sse-reciever
var journalEventSource = new EventSource('api/journal');
journalEventSource.onmessage = function(message) {
var event = JSON.parse(message.data);
Object.setPrototypeOf(event.application, Application.prototype);
var options = { tag: event.application.id,
body: 'Instance ' + event.application.id + '\n' + event.application.healthUrl,
icon: 'img/unknown.png',
timeout: 10000,
url: $state.href('apps.details', {id: event.application.id}) };
var title = event.application.name;
var index = $rootScope.indexOfApplication(event.application.id);
if (event.type === 'REGISTRATION') {
if (index === -1) {
$rootScope.applications.push(event.application);
}
title += ' instance registered.';
options.tag = event.application.id + '-REGISTRY';
} else if (event.type === 'DEREGISTRATION') {
if (index > -1) {
$rootScope.applications.splice(index, 1);
}
title += ' instance removed.';
options.tag = event.application.id + '-REGISTRY';
} else if (event.type === 'STATUS_CHANGE') {
refresh(event.application);
if (index > -1) {
$rootScope.applications[index] = event.application;
} else {
$rootScope.applications.push(event.application);
}
title += ' instance is ' + event.to.status;
options.tag = event.application.id + '-STATUS';
options.icon = event.to.status !== 'UP' ? 'img/error.png' : 'img/ok.png';
options.body = event.from.status + ' --> ' + event.to.status + '\n' + options.body;
}
$rootScope.$apply();
Notification.notify(title, options);
};
journalEventSource.onerror = function(event) {
$log.error('Could not read server sent event!', event);
};
});
......@@ -15,72 +15,11 @@
*/
'use strict';
module.exports = function ($scope, $location, $interval, $state, $filter, Application, Notification) {
var createNote = function(app) {
var title = app.name + (app.statusInfo.status === 'UP' ? ' is back ' : ' went ') + app.statusInfo.status;
var options = { tag: app.id,
body: 'Instance ' + app.id + '\n' + app.healthUrl,
icon: (app.statusInfo.status === 'UP' ? 'img/ok.png' : 'img/error.png'),
timeout: 15000,
url: $state.href('apps.details', {id: app.id}) };
Notification.notify(title, options);
};
var refresh = function(app) {
app.info = {};
app.needRefresh = true;
//find application in known applications and copy state --> less flickering
for (var j = 0; $scope.applications && j < $scope.applications.length; j++) {
if (app.id === $scope.applications[j].id) {
app.infoShort = $scope.applications[j].infoShort;
app.infoDetails = $scope.applications[j].infoDetails;
app.version = $scope.applications[j].version;
app.capabilities = $scope.applications[j].capabilities;
if (app.statusInfo.status !== $scope.applications[j].statusInfo.status) {
createNote(app); //issue notifiaction on state change
} else {
app.needRefresh = false; //if state hasn't change don't fetch info
}
break;
}
}
if (app.needRefresh) {
app.refreshing = true;
app.getCapabilities();
app.getInfo().then(function(info) {
app.version = info.version;
app.infoDetails = null;
app.infoShort = '';
delete info.version;
var infoYml = $filter('yaml')(info);
if (infoYml !== '{}\n') {
app.infoShort = $filter('limitLines')(infoYml, 3);
if (app.infoShort !== infoYml) {
app.infoDetails = $filter('limitLines')(infoYml, 32000, 3);
}
}
}).finally(function(){
app.refreshing = false;
});
}
};
$scope.loadData = function () {
Application.query(function (applications) {
for (var i = 0; i < applications.length; i++) {
refresh(applications[i]);
}
$scope.applications = applications;
});
};
module.exports = function ($rootScope, $scope) {
$scope.applications = $rootScope.applications;
$scope.remove = function (application) {
application.$remove(function () {
var index = $scope.applications.indexOf(application);
if (index > -1) {
$scope.applications.splice(index, 1);
}
});
application.$remove();
};
$scope.order = {
......@@ -105,13 +44,4 @@ module.exports = function ($scope, $location, $interval, $state, $filter, Applic
}
};
//initial load
$scope.loadData();
// reload site every 10 seconds
var intervalPromise = $interval(function () {
$scope.loadData();
}, 10000);
$scope.$on('$destroy', function () { $interval.cancel(intervalPromise); });
};
......@@ -15,10 +15,20 @@
*/
package de.codecentric.boot.admin.controller;
import static java.util.Collections.synchronizedCollection;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.event.EventListener;
import org.springframework.http.MediaType;
import org.springframework.util.MimeTypeUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import de.codecentric.boot.admin.event.ClientApplicationEvent;
import de.codecentric.boot.admin.journal.ApplicationEventJournal;
......@@ -30,16 +40,42 @@ import de.codecentric.boot.admin.journal.ApplicationEventJournal;
*/
@ResponseBody
public class JournalController {
private static Logger LOGGER = LoggerFactory.getLogger(JournalController.class);
private ApplicationEventJournal eventJournal;
private final Collection<SseEmitter> emitters = synchronizedCollection(
new LinkedList<SseEmitter>());
public JournalController(ApplicationEventJournal eventJournal) {
this.eventJournal = eventJournal;
}
@RequestMapping("/api/journal")
@RequestMapping(path = "/api/journal", produces = MimeTypeUtils.APPLICATION_JSON_VALUE)
public Collection<ClientApplicationEvent> getJournal() {
return eventJournal.getEvents();
}
@RequestMapping(path = "/api/journal", produces = "text/event-stream")
public SseEmitter getJournalEvents() {
final SseEmitter emitter = new SseEmitter();
emitter.onCompletion(new Runnable() {
@Override
public void run() {
emitters.remove(emitter);
}
});
emitters.add(emitter);
return emitter;
}
@EventListener
public void onClientApplicationEvent(ClientApplicationEvent event) {
for (SseEmitter emitter : new ArrayList<>(emitters)) {
try {
emitter.send(event, MediaType.APPLICATION_JSON);
} catch (Exception ex) {
LOGGER.debug("Error sending event to client ", ex);
}
}
}
}
......@@ -16,12 +16,14 @@
package de.codecentric.boot.admin.controller;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.CoreMatchers.sameInstance;
import static org.junit.Assert.assertThat;
import java.util.Collection;
import org.junit.Test;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import de.codecentric.boot.admin.event.ClientApplicationEvent;
import de.codecentric.boot.admin.event.ClientApplicationRegisteredEvent;
......@@ -37,8 +39,8 @@ public class JournalControllerTest {
@Test
public void test_getJournal() {
ClientApplicationEvent emittedEvent = new ClientApplicationRegisteredEvent(Application
.create("foo").withId("bar").build());
ClientApplicationEvent emittedEvent = new ClientApplicationRegisteredEvent(
Application.create("foo").withId("bar").build());
journal.onClientApplicationEvent(emittedEvent);
Collection<ClientApplicationEvent> history = controller.getJournal();
......@@ -48,4 +50,15 @@ public class JournalControllerTest {
ClientApplicationEvent event = history.iterator().next();
assertThat(event, sameInstance(emittedEvent));
}
@Test
public void test_getJournal_sse() {
ClientApplicationEvent emittedEvent = new ClientApplicationRegisteredEvent(
Application.create("foo").withId("bar").build());
SseEmitter emitter = controller.getJournalEvents();
journal.onClientApplicationEvent(emittedEvent);
assertThat(emitter, notNullValue());
}
}
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