Commit 01c0d1b4 by Johannes Edmeier

add journal

parent a688e0a2
......@@ -25,6 +25,10 @@
color: $grey-light;
}
.is-selectable {
cursor: pointer;
}
.card .table tr:last-of-type > td,
.card .table tr:last-of-type > th {
border-bottom: none;
......
......@@ -70,7 +70,7 @@ exports[`application-status should match the snapshot with status UNKNOWN 1`] =
>
<font-awesome-icon
class="application-status__icon application-status__icon--UNKNOWN"
icon="querstion-circle"
icon="question-circle"
/>
<!---->
......
......@@ -33,7 +33,7 @@
'OUT_OF_SERVICE': 'ban',
'DOWN': 'times-circle',
'OFFLINE': 'minus-circle',
'UNKNOWN': 'querstion-circle'
'UNKNOWN': 'question-circle'
};
export default {
......
......@@ -38,6 +38,7 @@ import sbaInstancesLoggers from './views/instances/loggers';
import sbaInstancesShell from './views/instances/shell';
import sbaInstancesThreaddump from './views/instances/threaddump';
import sbaInstancesTrace from './views/instances/trace';
import sbaJournal from './views/journal';
import sbaShell from './views/shell'
fontawesome.library.add(faGithub, faStackOverflow, faGitter, faTrash, faDownload, faStepForward, faStepBackward, faCheck, faQuestionCircle, faBan, faTimesCircle, faMinusCircle, faExclamation,
......@@ -80,7 +81,7 @@ views.register({
order: 0,
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: '/instances/:instanceId', component: sbaInstancesShell, props: true,
......
......@@ -15,22 +15,7 @@
*/
import waitForPolyfill from '@/utils/eventsource-polyfill';
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 {Observable} from '@/utils/rxjs';
import axios from 'axios';
import * as _ from 'lodash';
import Instance from './instance';
......
......@@ -19,7 +19,7 @@ import logtail from '@/utils/logtail';
import {Observable} from '@/utils/rxjs'
import axios from 'axios';
import _ from 'lodash';
import moment from 'moment/moment';
import moment from 'moment';
const actuatorMimeTypes = ['application/vnd.spring-boot.actuator.v2+json',
'application/vnd.spring-boot.actuator.v1+json',
......@@ -38,12 +38,6 @@ class Instance {
return this.registration.source === 'http-api';
}
static async get(id) {
return await axios.get(`instances/${id}`, {
transformResponse: Instance._toInstance
});
}
async unregister() {
return axios.delete(`instances/${this.id}`);
}
......@@ -125,14 +119,18 @@ class Instance {
.concatMap(response => Observable.of(response.data.threads));
}
async getStream() {
static async fetchEvents() {
return await axios.get(`instances/events`);
}
static async getEventStream() {
await waitForPolyfill();
return Observable.create(observer => {
const eventSource = new EventSource(`instances/${this.id}`);
const eventSource = new EventSource('instances/events');
eventSource.onmessage = message => observer.next({
...message,
data: Instance._toInstance(message.data)
data: JSON.parse(message.data)
});
eventSource.onerror = err => observer.error(err);
return () => {
......@@ -141,6 +139,12 @@ class Instance {
});
}
static async get(id) {
return await axios.get(`instances/${id}`, {
transformResponse: Instance._toInstance
});
}
static _toInstance(data) {
const instance = JSON.parse(data);
return Object.assign(new Instance(instance.id), instance);
......
......@@ -47,7 +47,7 @@
</div>
<div v-if="statusGroups.length === 0"
class="content">
<p>No applications registered.</p>
<p class="is-muted">No applications registered.</p>
</div>
</div>
</section>
......@@ -62,17 +62,17 @@
components: {
applicationsList,
},
data: () => ({
applications: [],
_applications: [],
errors: [],
subscription: null
}),
computed: {
applications() {
return this.$data._applications.filter(application => application.instances.length > 0);
},
statusGroups() {
const nonEmpty = this.applications.filter(application => application.instances.length > 0);
const byStatus = _.groupBy(nonEmpty, application => application.status);
const byStatus = _.groupBy(this.applications, application => application.status);
const list = _.transform(byStatus, (result, value, key) => {
result.push({status: key, applications: value})
}, []);
......@@ -90,27 +90,25 @@
}, 0);
}
},
async created() {
try {
this.applications = (await Application.list()).data;
this.$data._applications = (await Application.list()).data;
} catch (e) {
this.errors.push(e);
}
this.subscription = (await Application.getStream()).subscribe({
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) {
this.applications.splice(idx, 1, message.data);
this.$data._applications.splice(idx, 1, message.data);
} else {
this.applications.push(message.data);
this.$data._applications.push(message.data);
}
},
error: err => this.errors.push(err)
});
},
destroyed() {
if (this.subscription !== null) {
this.subscription.unsubscribe();
......
......@@ -149,6 +149,7 @@
display: inline-flex;
flex-direction: column;
justify-content: space-between;
margin-right: 0.5rem;
}
}
}
......
......@@ -29,7 +29,7 @@
</thead>
<tbody>
<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)">
<td class="threads__thread-name">
<thread-tag :thread-state="thread.threadState"></thread-tag>
......
......@@ -28,7 +28,8 @@
</tr>
</thead>
<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)"
:key="trace.key">
<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