Commit c02875e4 by Johannes Stelzer

Added event journal containing all client application events.

Added a simple event journal, jorunaling all ClientApplicationEvents using a simple, non-persistent store. So all information is lost after restart. Added corresponding view to UI.
parent 0a931384
......@@ -30,6 +30,7 @@
</div>
<ul class="nav pull-right">
<li class="navbar-link" ng-class="{active: $state.includes('apps') || $state.includes('overview') }"><a ui-sref="overview">Applications</a></li>
<li class="navbar-link" ng-class="{active: $state.includes('journal')}"><a ui-sref="journal">Journal</a></li>
<li class="navbar-link" ng-class="{active: $state.includes('about')}"><a ui-sref="about">About</a></li>
</ul>
</div>
......
......@@ -112,6 +112,11 @@ springBootAdmin.config(function ($stateProvider, $urlRouterProvider) {
url: '/trace',
templateUrl: 'views/apps/trace.html',
controller: 'traceCtrl'
})
.state('journal', {
url: '/journal',
templateUrl: 'views/journal.html',
controller: 'journalCtrl'
});
});
springBootAdmin.run(function ($rootScope, $state) {
......
......@@ -14,3 +14,4 @@ springBootAdmin.controller('loggingCtrl', require('./apps/loggingCtrl'));
springBootAdmin.controller('jmxCtrl', require('./apps/jmxCtrl'));
springBootAdmin.controller('threadsCtrl', require('./apps/threadsCtrl'));
springBootAdmin.controller('traceCtrl', require('./apps/traceCtrl'));
springBootAdmin.controller('journalCtrl', require('./journalCtrl'));
/*
* Copyright 2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
module.exports = function ($scope, $http) {
$http.get('/api/journal').success(function(journal) {
$scope.journal = journal;
}).error(function(error) {
$scope.error = error;
});
};
<div class="container content">
<h2 >About Spring-Boot Admin</h2>
<p>
This is an administration GUI for Spring-Boot applications. All applications have to register themselves at this application.
This is done by including <a href="">spring-boot-starters-admin-client</a> as dependency. This will
......
<div class="container content">
<h2 >Journal</h2>
<div class="alert alert-error" ng-if="error">
<b>Error:</b> {{ error }}
</div>
<div class="row">
<table class="table table-striped">
<thead>
<tr>
<th>Time</th>
<th>Application</th>
<th>Event</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="event in journal | orderBy:'timestamp':true" >
<td>{{ event.timestamp | date:'yyyy-MM-dd-HH.mm.ss.sss' }}</td>
<td>{{ event.application.name }} ({{ event.application.id }})<br/>
<span class="muted">{{ event.application.serviceUrl || event.application.managementUrl || event.application.healthUrl }}</span>
</td>
<td>{{ event.type }}<br/>
<span ng-if="event.type == 'STATUS_CHANGE'">
<span class="status-{{event.data.from}}">{{ event.data.from }}</span>
-&gt; <span class="status-{{event.data.to}}">{{ event.data.to }}</span>
</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
......@@ -35,8 +35,12 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter
import com.fasterxml.jackson.databind.ObjectMapper;
import de.codecentric.boot.admin.controller.JournalController;
import de.codecentric.boot.admin.controller.RegistryController;
import de.codecentric.boot.admin.event.ClientApplicationRegisteredEvent;
import de.codecentric.boot.admin.journal.ApplicationEventJournal;
import de.codecentric.boot.admin.journal.store.JournaledEventStore;
import de.codecentric.boot.admin.journal.store.SimpleJournaledEventStore;
import de.codecentric.boot.admin.registry.ApplicationIdGenerator;
import de.codecentric.boot.admin.registry.ApplicationRegistry;
import de.codecentric.boot.admin.registry.HashingApplicationUrlIdGenerator;
......@@ -141,4 +145,24 @@ public class AdminServerWebConfiguration extends WebMvcConfigurerAdapter impleme
return registrar;
}
@Bean
@ConditionalOnMissingBean
public ApplicationEventJournal applicationEventJournal(
JournaledEventStore store) {
return new ApplicationEventJournal(store);
}
@Bean
@ConditionalOnMissingBean
public JournaledEventStore journaledEventStore() {
return new SimpleJournaledEventStore();
}
@Bean
@ConditionalOnMissingBean
public JournalController journalController(
ApplicationEventJournal eventJournal) {
return new JournalController(eventJournal);
}
}
/*
* Copyright 2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.codecentric.boot.admin.controller;
import java.util.Collection;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import de.codecentric.boot.admin.journal.ApplicationEventJournal;
import de.codecentric.boot.admin.journal.JournaledEvent;
/**
* REST-Controller for querying all client application events.
*
* @author Johannes Stelzer
*
*/
@RestController
@RequestMapping("/api/journal")
public class JournalController {
private ApplicationEventJournal eventJournal;
public JournalController(ApplicationEventJournal eventJournal) {
this.eventJournal = eventJournal;
}
@RequestMapping
public Collection<JournaledEvent> getJournal() {
return eventJournal.getEvents();
}
}
......@@ -18,7 +18,8 @@ package de.codecentric.boot.admin.event;
import de.codecentric.boot.admin.model.Application;
/**
* * This event gets emitted when an application is registered.
* This event gets emitted when an application is registered.
*
* @author Johannes Stelzer
*/
public class ClientApplicationRegisteredEvent extends ClientApplicationEvent {
......
/*
* Copyright 2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.codecentric.boot.admin.journal;
import java.util.Collection;
import org.springframework.context.ApplicationListener;
import de.codecentric.boot.admin.event.ClientApplicationEvent;
import de.codecentric.boot.admin.journal.store.JournaledEventStore;
/**
* Listens for all ClientApplicationEvents and stores them as JournaledEvents in
* a store.
*
* @author Johannes Stelzer
*
*/
public class ApplicationEventJournal implements
ApplicationListener<ClientApplicationEvent> {
private final JournaledEventStore store;
public ApplicationEventJournal(JournaledEventStore store) {
this.store = store;
}
@Override
public void onApplicationEvent(ClientApplicationEvent event) {
store.store(JournaledEvent.fromEvent(event));
}
public Collection<JournaledEvent> getEvents() {
return store.findAll();
}
}
/*
* Copyright 2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.codecentric.boot.admin.journal;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import de.codecentric.boot.admin.event.ClientApplicationDeregisteredEvent;
import de.codecentric.boot.admin.event.ClientApplicationEvent;
import de.codecentric.boot.admin.event.ClientApplicationRegisteredEvent;
import de.codecentric.boot.admin.event.ClientApplicationStatusChangedEvent;
import de.codecentric.boot.admin.model.Application;
/**
*
* Journal-representation of ClientApplicationEvents. In opposite to the
* ClientApplicationEvent.class this Class has no reference to the event-source.
*
* @author Johannes Stelzer
*
*/
@JsonInclude(Include.NON_EMPTY)
public class JournaledEvent {
public enum Type {
REGISTRATION, DEREGISTRATION, STATUS_CHANGE
}
private final Type type;
private final long timestamp;
private final Application application;
private final Map<String, Object> data;
private JournaledEvent(Type type, long timestamp, Application application,
Map<String, Object> data) {
this.type = type;
this.timestamp = timestamp;
this.application = application;
if (data != null) {
this.data = Collections.unmodifiableMap(new HashMap<String, Object>(
data));
} else {
this.data = Collections.emptyMap();
}
}
public static JournaledEvent fromEvent(ClientApplicationEvent event) {
long timestamp = event.getTimestamp();
Application application = event.getApplication();
if (event instanceof ClientApplicationRegisteredEvent) {
return new JournaledEvent(Type.REGISTRATION, timestamp, application,
null);
} else if (event instanceof ClientApplicationDeregisteredEvent) {
return new JournaledEvent(Type.DEREGISTRATION, timestamp,
application, null);
} else if (event instanceof ClientApplicationStatusChangedEvent) {
ClientApplicationStatusChangedEvent changeEvent = (ClientApplicationStatusChangedEvent) event;
Map<String, Object> data = new HashMap<String, Object>();
data.put("from", changeEvent.getFrom().getStatus());
data.put("to", changeEvent.getTo().getStatus());
return new JournaledEvent(Type.STATUS_CHANGE, timestamp, application,
data);
}
throw new IllegalArgumentException(
"Couldn't convert event to JournaledEvent: " + event.toString());
}
public Application getApplication() {
return application;
}
public Map<String, Object> getData() {
return data;
}
public long getTimestamp() {
return timestamp;
}
public Type getType() {
return type;
}
@Override
public String toString() {
return "JournaledEvent [type=" + type + ", timestamp=" + timestamp
+ ", application=" + application + ", data=" + data + "]";
}
}
\ No newline at end of file
/*
* Copyright 2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.codecentric.boot.admin.journal.store;
import java.util.Collection;
import de.codecentric.boot.admin.journal.JournaledEvent;
/**
* Interface for storing JournaledEvent
*
* @author Johannes Stelzer
*
*/
public interface JournaledEventStore {
Collection<JournaledEvent> findAll();
void store(JournaledEvent event);
}
/*
* Copyright 2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.codecentric.boot.admin.journal.store;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import de.codecentric.boot.admin.journal.JournaledEvent;
/**
* Simple, non-persistent Store for JournaledEvent
*
* @author Johannes Stelzer
*
*/
public class SimpleJournaledEventStore implements JournaledEventStore {
private final List<JournaledEvent> store = Collections
.synchronizedList(new ArrayList<JournaledEvent>(1000));
@Override
public Collection<JournaledEvent> findAll() {
ArrayList<JournaledEvent> list = new ArrayList<JournaledEvent>(
store);
Collections.reverse(list);
return list;
}
@Override
public void store(JournaledEvent event) {
store.add(event);
}
}
/*
* Copyright 2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.codecentric.boot.admin.controller;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import java.util.Collection;
import org.junit.Test;
import de.codecentric.boot.admin.event.ClientApplicationRegisteredEvent;
import de.codecentric.boot.admin.journal.ApplicationEventJournal;
import de.codecentric.boot.admin.journal.JournaledEvent;
import de.codecentric.boot.admin.journal.store.SimpleJournaledEventStore;
import de.codecentric.boot.admin.model.Application;
public class JournalControllerTest {
private ApplicationEventJournal journal = new ApplicationEventJournal(
new SimpleJournaledEventStore());
private JournalController controller = new JournalController(journal);
@Test
public void test_getJournal() {
journal.onApplicationEvent(new ClientApplicationRegisteredEvent(
new Object(), Application.create("foo").withId("bar").build()));
Collection<JournaledEvent> history = controller.getJournal();
assertThat(history.size(), is(1));
JournaledEvent event = history.iterator().next();
assertThat(event.getType(), is(JournaledEvent.Type.REGISTRATION));
}
}
/*
* Copyright 2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.codecentric.boot.admin.journal;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import java.util.Collection;
import org.junit.Test;
import de.codecentric.boot.admin.event.ClientApplicationRegisteredEvent;
import de.codecentric.boot.admin.journal.ApplicationEventJournal;
import de.codecentric.boot.admin.journal.JournaledEvent;
import de.codecentric.boot.admin.journal.store.SimpleJournaledEventStore;
import de.codecentric.boot.admin.model.Application;
public class ApplicationEventJournalTest {
private ApplicationEventJournal journal = new ApplicationEventJournal(
new SimpleJournaledEventStore());
@Test
public void test_registration() {
journal.onApplicationEvent(new ClientApplicationRegisteredEvent(
new Object(), Application.create("foo").withId("bar").build()));
Collection<JournaledEvent> events = journal.getEvents();
assertThat(events.size(), is(1));
JournaledEvent event = events.iterator().next();
assertThat(event.getType(), is(JournaledEvent.Type.REGISTRATION));
}
}
/*
* Copyright 2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.codecentric.boot.admin.journal;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import java.util.Collections;
import org.junit.Test;
import de.codecentric.boot.admin.event.ClientApplicationDeregisteredEvent;
import de.codecentric.boot.admin.event.ClientApplicationRegisteredEvent;
import de.codecentric.boot.admin.event.ClientApplicationStatusChangedEvent;
import de.codecentric.boot.admin.journal.JournaledEvent;
import de.codecentric.boot.admin.model.Application;
import de.codecentric.boot.admin.model.StatusInfo;
public class JournaledEventTest {
@Test
public void test_from_event_registration() {
JournaledEvent event = JournaledEvent
.fromEvent(new ClientApplicationRegisteredEvent(new Object(),
Application.create("foo").withId("bar").build()));
assertThat(event.getApplication(),
is(Application.create("foo").withId("bar").build()));
assertThat(event.getData(), is(Collections.<String, Object> emptyMap()));
assertThat(event.getType(), is(JournaledEvent.Type.REGISTRATION));
assertTrue(event.getTimestamp() > 0);
}
@Test
public void test_from_event_deregistration() {
JournaledEvent event = JournaledEvent
.fromEvent(new ClientApplicationDeregisteredEvent(new Object(),
Application.create("foo").withId("bar").build()));
assertThat(event.getApplication(),
is(Application.create("foo").withId("bar").build()));
assertThat(event.getData(), is(Collections.<String, Object> emptyMap()));
assertThat(event.getType(), is(JournaledEvent.Type.DEREGISTRATION));
assertTrue(event.getTimestamp() > 0);
}
@Test
public void test_from_event_status_change() {
JournaledEvent event = JournaledEvent
.fromEvent(new ClientApplicationStatusChangedEvent(
new Object(),
Application.create("foo").withId("bar").build(),
StatusInfo.ofUnknown(), StatusInfo.ofUp()));
assertThat(event.getApplication(),
is(Application.create("foo").withId("bar").build()));
assertThat(event.getData().get("from"), is((Object) StatusInfo
.ofUnknown()
.getStatus()));
assertThat(event.getData().get("to"), is((Object) StatusInfo.ofUp()
.getStatus()));
assertThat(event.getType(), is(JournaledEvent.Type.STATUS_CHANGE));
assertTrue(event.getTimestamp() > 0);
}
}
/*
* Copyright 2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.codecentric.boot.admin.journal.store;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import org.junit.Test;
import de.codecentric.boot.admin.event.ClientApplicationDeregisteredEvent;
import de.codecentric.boot.admin.event.ClientApplicationRegisteredEvent;
import de.codecentric.boot.admin.journal.JournaledEvent;
import de.codecentric.boot.admin.journal.store.SimpleJournaledEventStore;
import de.codecentric.boot.admin.model.Application;
public class SimpleJournaledEventStoreTest {
private SimpleJournaledEventStore store = new SimpleJournaledEventStore();
@Test
public void test_store() {
List<JournaledEvent> events = Arrays.asList(JournaledEvent
.fromEvent(new ClientApplicationRegisteredEvent(new Object(),
Application.create("foo").withId("bar").build())),
JournaledEvent
.fromEvent(new ClientApplicationDeregisteredEvent(
new Object(), Application.create("foo")
.withId("bar").build()))
);
for (JournaledEvent event : events) {
store.store(event);
}
// Items are stored in reverse order
List<JournaledEvent> reversed = new ArrayList<JournaledEvent>(
events);
Collections.reverse(reversed);
assertThat(store.findAll(),
is((Collection<JournaledEvent>) reversed));
}
}
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