Commit 01c0d1b4 by Johannes Edmeier

add journal

parent a688e0a2
...@@ -25,6 +25,10 @@ ...@@ -25,6 +25,10 @@
color: $grey-light; color: $grey-light;
} }
.is-selectable {
cursor: pointer;
}
.card .table tr:last-of-type > td, .card .table tr:last-of-type > td,
.card .table tr:last-of-type > th { .card .table tr:last-of-type > th {
border-bottom: none; border-bottom: none;
......
...@@ -70,7 +70,7 @@ exports[`application-status should match the snapshot with status UNKNOWN 1`] = ...@@ -70,7 +70,7 @@ exports[`application-status should match the snapshot with status UNKNOWN 1`] =
> >
<font-awesome-icon <font-awesome-icon
class="application-status__icon application-status__icon--UNKNOWN" class="application-status__icon application-status__icon--UNKNOWN"
icon="querstion-circle" icon="question-circle"
/> />
<!----> <!---->
......
...@@ -33,7 +33,7 @@ ...@@ -33,7 +33,7 @@
'OUT_OF_SERVICE': 'ban', 'OUT_OF_SERVICE': 'ban',
'DOWN': 'times-circle', 'DOWN': 'times-circle',
'OFFLINE': 'minus-circle', 'OFFLINE': 'minus-circle',
'UNKNOWN': 'querstion-circle' 'UNKNOWN': 'question-circle'
}; };
export default { export default {
......
...@@ -38,6 +38,7 @@ import sbaInstancesLoggers from './views/instances/loggers'; ...@@ -38,6 +38,7 @@ import sbaInstancesLoggers from './views/instances/loggers';
import sbaInstancesShell from './views/instances/shell'; import sbaInstancesShell from './views/instances/shell';
import sbaInstancesThreaddump from './views/instances/threaddump'; import sbaInstancesThreaddump from './views/instances/threaddump';
import sbaInstancesTrace from './views/instances/trace'; import sbaInstancesTrace from './views/instances/trace';
import sbaJournal from './views/journal';
import sbaShell from './views/shell' import sbaShell from './views/shell'
fontawesome.library.add(faGithub, faStackOverflow, faGitter, faTrash, faDownload, faStepForward, faStepBackward, faCheck, faQuestionCircle, faBan, faTimesCircle, faMinusCircle, faExclamation, fontawesome.library.add(faGithub, faStackOverflow, faGitter, faTrash, faDownload, faStepForward, faStepBackward, faCheck, faQuestionCircle, faBan, faTimesCircle, faMinusCircle, faExclamation,
...@@ -80,7 +81,7 @@ views.register({ ...@@ -80,7 +81,7 @@ views.register({
order: 0, order: 0,
component: sbaApplications component: sbaApplications
}); });
views.register({path: '/journal', name: 'journal', template: 'Journal', order: 100, component: sbaApplications}); views.register({path: '/journal', name: 'journal', template: 'Journal', order: 100, component: sbaJournal});
views.register({path: '/about', name: 'about', template: 'About', order: 200, component: sbaAbout}); views.register({path: '/about', name: 'about', template: 'About', order: 200, component: sbaAbout});
views.register({ views.register({
path: '/instances/:instanceId', component: sbaInstancesShell, props: true, path: '/instances/:instanceId', component: sbaInstancesShell, props: true,
......
...@@ -15,22 +15,7 @@ ...@@ -15,22 +15,7 @@
*/ */
import waitForPolyfill from '@/utils/eventsource-polyfill'; import waitForPolyfill from '@/utils/eventsource-polyfill';
import {Observable} from '@/utils/rxjs' import {Observable} from '@/utils/rxjs';
/*
* Copyright 2014-2017 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.
*/
import axios from 'axios'; import axios from 'axios';
import * as _ from 'lodash'; import * as _ from 'lodash';
import Instance from './instance'; import Instance from './instance';
......
...@@ -19,7 +19,7 @@ import logtail from '@/utils/logtail'; ...@@ -19,7 +19,7 @@ import logtail from '@/utils/logtail';
import {Observable} from '@/utils/rxjs' import {Observable} from '@/utils/rxjs'
import axios from 'axios'; import axios from 'axios';
import _ from 'lodash'; import _ from 'lodash';
import moment from 'moment/moment'; import moment from 'moment';
const actuatorMimeTypes = ['application/vnd.spring-boot.actuator.v2+json', const actuatorMimeTypes = ['application/vnd.spring-boot.actuator.v2+json',
'application/vnd.spring-boot.actuator.v1+json', 'application/vnd.spring-boot.actuator.v1+json',
...@@ -38,12 +38,6 @@ class Instance { ...@@ -38,12 +38,6 @@ class Instance {
return this.registration.source === 'http-api'; return this.registration.source === 'http-api';
} }
static async get(id) {
return await axios.get(`instances/${id}`, {
transformResponse: Instance._toInstance
});
}
async unregister() { async unregister() {
return axios.delete(`instances/${this.id}`); return axios.delete(`instances/${this.id}`);
} }
...@@ -125,14 +119,18 @@ class Instance { ...@@ -125,14 +119,18 @@ class Instance {
.concatMap(response => Observable.of(response.data.threads)); .concatMap(response => Observable.of(response.data.threads));
} }
async getStream() { static async fetchEvents() {
return await axios.get(`instances/events`);
}
static async getEventStream() {
await waitForPolyfill(); await waitForPolyfill();
return Observable.create(observer => { return Observable.create(observer => {
const eventSource = new EventSource(`instances/${this.id}`); const eventSource = new EventSource('instances/events');
eventSource.onmessage = message => observer.next({ eventSource.onmessage = message => observer.next({
...message, ...message,
data: Instance._toInstance(message.data) data: JSON.parse(message.data)
}); });
eventSource.onerror = err => observer.error(err); eventSource.onerror = err => observer.error(err);
return () => { return () => {
...@@ -141,6 +139,12 @@ class Instance { ...@@ -141,6 +139,12 @@ class Instance {
}); });
} }
static async get(id) {
return await axios.get(`instances/${id}`, {
transformResponse: Instance._toInstance
});
}
static _toInstance(data) { static _toInstance(data) {
const instance = JSON.parse(data); const instance = JSON.parse(data);
return Object.assign(new Instance(instance.id), instance); return Object.assign(new Instance(instance.id), instance);
......
...@@ -47,7 +47,7 @@ ...@@ -47,7 +47,7 @@
</div> </div>
<div v-if="statusGroups.length === 0" <div v-if="statusGroups.length === 0"
class="content"> class="content">
<p>No applications registered.</p> <p class="is-muted">No applications registered.</p>
</div> </div>
</div> </div>
</section> </section>
...@@ -62,17 +62,17 @@ ...@@ -62,17 +62,17 @@
components: { components: {
applicationsList, applicationsList,
}, },
data: () => ({ data: () => ({
applications: [], _applications: [],
errors: [], errors: [],
subscription: null subscription: null
}), }),
computed: { computed: {
applications() {
return this.$data._applications.filter(application => application.instances.length > 0);
},
statusGroups() { statusGroups() {
const nonEmpty = this.applications.filter(application => application.instances.length > 0); const byStatus = _.groupBy(this.applications, application => application.status);
const byStatus = _.groupBy(nonEmpty, application => application.status);
const list = _.transform(byStatus, (result, value, key) => { const list = _.transform(byStatus, (result, value, key) => {
result.push({status: key, applications: value}) result.push({status: key, applications: value})
}, []); }, []);
...@@ -90,27 +90,25 @@ ...@@ -90,27 +90,25 @@
}, 0); }, 0);
} }
}, },
async created() { async created() {
try { try {
this.applications = (await Application.list()).data; this.$data._applications = (await Application.list()).data;
} catch (e) { } catch (e) {
this.errors.push(e); this.errors.push(e);
} }
this.subscription = (await Application.getStream()).subscribe({ this.subscription = (await Application.getStream()).subscribe({
next: message => { next: message => {
const idx = this.applications.findIndex(application => application.name === message.data.name); const idx = this.$data._applications.findIndex(application => application.name === message.data.name);
if (idx >= 0) { if (idx >= 0) {
this.applications.splice(idx, 1, message.data); this.$data._applications.splice(idx, 1, message.data);
} else { } else {
this.applications.push(message.data); this.$data._applications.push(message.data);
} }
}, },
error: err => this.errors.push(err) error: err => this.errors.push(err)
}); });
}, },
destroyed() { destroyed() {
if (this.subscription !== null) { if (this.subscription !== null) {
this.subscription.unsubscribe(); this.subscription.unsubscribe();
......
...@@ -149,6 +149,7 @@ ...@@ -149,6 +149,7 @@
display: inline-flex; display: inline-flex;
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: space-between;
margin-right: 0.5rem;
} }
} }
} }
......
...@@ -29,7 +29,7 @@ ...@@ -29,7 +29,7 @@
</thead> </thead>
<tbody> <tbody>
<template v-for="thread in threadTimelines"> <template v-for="thread in threadTimelines">
<tr :key="thread.threadId" <tr class="is-selectable" :key="thread.threadId"
@click="showDetails[thread.threadId] ? $delete(showDetails, thread.threadId) : $set(showDetails, thread.threadId, true)"> @click="showDetails[thread.threadId] ? $delete(showDetails, thread.threadId) : $set(showDetails, thread.threadId, true)">
<td class="threads__thread-name"> <td class="threads__thread-name">
<thread-tag :thread-state="thread.threadState"></thread-tag> <thread-tag :thread-state="thread.threadState"></thread-tag>
......
...@@ -28,7 +28,8 @@ ...@@ -28,7 +28,8 @@
</tr> </tr>
</thead> </thead>
<template v-for="trace in traces"> <template v-for="trace in traces">
<tr :class="{ 'trace--is-detailed' : showDetails[trace.key], 'has-text-warning' : trace.isClientError(), 'has-text-danger' : trace.isServerError() }" <tr class="is-selectable"
:class="{ 'trace--is-detailed' : showDetails[trace.key], 'has-text-warning' : trace.isClientError(), 'has-text-danger' : trace.isServerError() }"
@click="showDetails[trace.key] ? $delete(showDetails, trace.key) : $set(showDetails, trace.key, true)" @click="showDetails[trace.key] ? $delete(showDetails, trace.key) : $set(showDetails, trace.key, true)"
:key="trace.key"> :key="trace.key">
<td v-text="trace.timestamp.format('L HH:mm:ss.SSS')"></td> <td v-text="trace.timestamp.format('L HH:mm:ss.SSS')"></td>
......
<!--
- Copyright 2014-2018 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.
-->
<template>
<div class="section">
<div class="container">
<h1 class="title">Event Journal</h1>
<div class="content">
<table class="table">
<thead>
<tr>
<th>Application</th>
<th>Instance</th>
<th>Time</th>
<th>Event</th>
</tr>
</thead>
<tbody>
<template v-for="event in events">
<tr class="is-selectable" :key="event.key"
@click="showPayload[event.key] ? $delete(showPayload, event.key) : $set(showPayload, event.key, true)">
<td v-text="instanceNames[event.instance] || '?'"></td>
<td v-text="event.instance"></td>
<td v-text="event.timestamp.format('L HH:mm:ss.SSS')"></td>
<td>
<span v-text="event.type"></span> <span v-if="event.type === 'STATUS_CHANGED'"
v-text="`(${event.payload.statusInfo.status})`"></span>
</td>
</tr>
<tr :key="`${event.key}-detail`" v-if="showPayload[event.key]">
<td colspan="4">
<pre v-text="toJson(event.payload)"></pre>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
</div>
</template>
<script>
import Instance from '@/services/instance';
import _ from 'lodash';
import moment from 'moment';
class Event {
constructor(event) {
this.instance = event.instance;
this.version = event.version;
this.type = event.type;
this.timestamp = moment(event.timestamp);
this.payload = _.omit(event, ['instance', 'version', 'timestamp', 'type']);
}
get key() {
return `${this.instance}-${this.version}`;
}
}
export default {
data: () => ({
_events: [],
showPayload: {},
instanceNames: {},
errors: []
}),
computed: {
events() {
return this.$data._events.reverse();
}
},
methods: {
toJson(obj) {
return JSON.stringify(obj, null, 4);
},
addEvents(data) {
const converted = data.map(event => new Event(event));
this.$data._events = this.$data._events.concat(converted);
const newInstanceNames = converted.filter(event => event.type === 'REGISTERED').reduce((names, event) => ({
...names,
[event.instance]: event.payload.registration.name
}), {});
_.assign(this.instanceNames, newInstanceNames);
}
},
async created() {
try {
this.addEvents((await Instance.fetchEvents()).data);
} catch (e) {
this.errors.push(e);
}
this.subscription = (await Instance.getEventStream()).subscribe({
next: message => {
this.addEvents([message.data])
},
error: err => this.errors.push(err)
});
},
}
</script>
<style lang="scss">
</style>
\ No newline at end of file
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