Commit a6e13ac3 by Johannes Edmeier

Add view for auditevents

parent dce53990
......@@ -10,12 +10,12 @@
"test:watch": "jest --watch"
},
"dependencies": {
"@fortawesome/fontawesome": "^1.1.1",
"@fortawesome/fontawesome-free-brands": "^5.0.3",
"@fortawesome/fontawesome-free-solid": "^5.0.3",
"@fortawesome/fontawesome": "^1.1.2",
"@fortawesome/fontawesome-free-brands": "^5.0.4",
"@fortawesome/fontawesome-free-solid": "^5.0.4",
"@fortawesome/vue-fontawesome": "0.0.22",
"axios": "^0.17.1",
"bulma": "^0.6.1",
"bulma": "^0.6.2",
"d3-array": "^1.2.1",
"d3-axis": "^1.0.8",
"d3-brush": "^1.0.4",
......@@ -36,10 +36,11 @@
"yamljs": "^0.3.0"
},
"devDependencies": {
"autoprefixer": "^7.2.4",
"@vue/test-utils": "^1.0.0-beta.10",
"autoprefixer": "^7.2.5",
"babel-core": "^6.25.0",
"babel-eslint": "^8.2.1",
"babel-jest": "^22.0.4",
"babel-jest": "^22.0.6",
"babel-loader": "^7.1.1",
"babel-plugin-lodash": "^3.3.2",
"babel-polyfill": "^6.26.0",
......@@ -59,7 +60,7 @@
"html-loader": "^0.5.4",
"html-webpack-plugin": "^2.30.0",
"identity-obj-proxy": "^3.0.0",
"jest": "^22.0.5",
"jest": "^22.0.6",
"jest-vue": "^0.8.2",
"lodash-webpack-plugin": "^0.11.4",
"node-sass": "^4.7.2",
......@@ -71,7 +72,6 @@
"vue-loader": "^13.7.0",
"vue-svg-loader": "^0.4.0",
"vue-template-compiler": "^2.5.13",
"vue-test-utils": "^1.0.0-beta.9",
"webpack": "^3.10.0",
"webpack-bundle-analyzer": "^2.9.2",
"webpack-dev-server": "^2.10.1"
......
......@@ -14,8 +14,8 @@
* limitations under the License.
*/
import {mount} from '@vue/test-utils';
import moment from 'moment';
import {mount} from 'vue-test-utils';
import sbaStatus from './sba-status';
moment.now = () => +new Date(1318781879406);
......
......@@ -14,8 +14,8 @@
* limitations under the License.
*/
import {shallow} from '@vue/test-utils';
import moment from 'moment';
import {shallow} from 'vue-test-utils';
import sbaTimeAgo from './sba-time-ago.js';
moment.now = () => +new Date(1318781879406);
......
......@@ -23,6 +23,7 @@ import sbaComponents from './components'
import FontAwesomeIcon from './utils/fontawesome';
import sbaAbout from './views/about';
import sbaApplications from './views/applications';
import sbaInstancesAuditevents from './views/instances/auditevents';
import sbaInstancesDetails from './views/instances/details';
import sbaInstancesEnv from './views/instances/env';
import sbaInstancesFlyway from './views/instances/flyway';
......@@ -85,6 +86,8 @@ views.register({
}, {
path: 'trace', component: sbaInstancesTrace, props: true, name: 'instance/trace',
}, {
path: 'auditevents', component: sbaInstancesAuditevents, props: true, name: 'instance/auditevents',
}, {
path: 'liquibase', component: sbaInstancesLiquibase, props: true, name: 'instance/liquibase',
}, {
path: 'flyway', component: sbaInstancesFlyway, props: true, name: 'instance/flyway',
......@@ -127,22 +130,27 @@ views.register({
order: 500,
});
views.register({
name: 'instance/auditevents',
template: 'Audit Log',
order: 600,
});
views.register({
name: 'instance/heapdump',
href: params => `instances/${params.instanceId}/actuator/heapdump`,
template: 'Heapdump',
order: 600,
order: 700,
isActive: ({instance}) => instance.hasEndpoint('heapdump')
});
views.register({
name: 'instance/liquibase',
template: 'Liquibase',
order: 700,
order: 800,
isActive: ({instance}) => instance.hasEndpoint('liquibase')
});
views.register({
name: 'instance/flyway',
template: 'Flyway',
order: 700,
order: 800,
isActive: ({instance}) => instance.hasEndpoint('flyway')
});
......
......@@ -19,7 +19,6 @@ import logtail from '@/utils/logtail';
import {Observable} from '@/utils/rxjs'
import axios from 'axios';
import _ from 'lodash';
import moment from 'moment';
const actuatorMimeTypes = ['application/vnd.spring-boot.actuator.v2+json',
'application/vnd.spring-boot.actuator.v1+json',
......@@ -97,34 +96,29 @@ class Instance {
});
}
streamLogfile(interval) {
return logtail(`instances/${this.id}/actuator/logfile`, interval);
async fetchTrace() {
return await axios.get(`instances/${this.id}/actuator/trace`, {
headers: {'Accept': actuatorMimeTypes}
});
}
streamTrace(interval) {
let lastTimestamp = moment(0);
return Observable.timer(0, interval)
.concatMap(() => axios.get(`instances/${this.id}/actuator/trace`, {
headers: {'Accept': actuatorMimeTypes}
}))
.concatMap(response => {
const traces = response.data.traces.filter(
trace => moment(trace.timestamp).isAfter(lastTimestamp)
);
if (traces.length > 0) {
lastTimestamp = traces[0].timestamp;
}
return Observable.of(traces);
});
async fetchThreaddump() {
return await axios.get(`instances/${this.id}/actuator/threaddump`, {
headers: {'Accept': actuatorMimeTypes}
});
}
streamThreaddump(interval) {
return Observable.timer(0, interval)
.concatMap(() => axios.get(`instances/${this.id}/actuator/threaddump`, {
headers: {'Accept': actuatorMimeTypes}
}))
.concatMap(response => Observable.of(response.data.threads));
async fetchAuditevents(after) {
return await axios.get(`instances/${this.id}/actuator/auditevents`, {
headers: {'Accept': actuatorMimeTypes},
params: {
after: after.toISOString()
}
});
}
streamLogfile(interval) {
return logtail(`instances/${this.id}/actuator/logfile`, interval);
}
static async fetchEvents() {
......
<!--
- 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>
<table class="table is-fullwidth is-hoverable">
<thead>
<tr>
<th>Timestamp</th>
<th>Event</th>
<th>Principal</th>
<th>Remote address</th>
<th>Session id</th>
</tr>
</thead>
<template v-for="event in events">
<tr class="is-selectable"
:class="{ 'event--is-detailed' : showDetails[event.key] }"
@click="showDetails[event.key] ? $delete(showDetails, event.key) : $set(showDetails, event.key, true)"
:key="event.key">
<td v-text="event.timestamp.format('L HH:mm:ss.SSS')"></td>
<td>
<span v-text="event.type" class="tag"
:class="{ 'is-success' : event.isSuccess(), 'is-danger' : event.isFailure() }"></span>
</td>
<td v-text="event.principal"></td>
<td v-text="event.remoteAddress"></td>
<td v-text="event.sessionId"></td>
</tr>
<tr class="event__detail" :key="`${event.key}-detail`" v-if="showDetails[event.key]">
<td colspan="5">
<pre v-text="toJson(event.data)"></pre>
</td>
</tr>
</template>
</table>
</template>
<script>
import prettyBytes from 'pretty-bytes';
export default {
props: ['events'],
data: () => ({
showDetails: {}
}),
methods: {
prettyBytes,
toJson(obj) {
return JSON.stringify(obj, null, 4);
}
}
}
</script>
<style lang="scss">
@import "~@/assets/css/utilities";
.event--is-detailed td {
border: none !important;
}
.event__detail td {
overflow-x: auto;
max-width: 1024px;
}
</style>
\ No newline at end of file
<!--
- 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>
<section class="section" :class="{ 'is-loading' : !events}">
<div class="container" v-if="events">
<div class="content" v-if="events.length > 0">
<auditevents-list :events="events"></auditevents-list>
</div>
<div class="content" v-else>
<p class="is-muted">No audit events recorded.</p>
</div>
</div>
</section>
</template>
<script>
import subscribing from '@/mixins/subscribing';
import {Observable} from '@/utils/rxjs';
import AuditeventsList from '@/views/instances/auditevents/auditevents-list';
import moment from 'moment';
class Auditevent {
constructor({timestamp, type, principal, data}) {
this.timestamp = moment(timestamp);
this.type = type;
this.principal = principal;
this.data = data;
}
get key() {
return `${this.timestamp}-${this.type}-${this.principal}`;
}
get remoteAddress() {
return this.data && this.data.details && this.data.details.remoteAddress || null;
}
get sessionId() {
return this.data && this.data.details && this.data.details.sessionId || null;
}
isSuccess() {
return this.type.toLowerCase().indexOf('success') >= 0;
}
isFailure() {
return this.type.toLowerCase().indexOf('failure') >= 0;
}
}
export default {
props: ['instance'],
mixins: [subscribing],
components: {AuditeventsList},
data: () => ({
events: null,
}),
computed: {},
watch: {
instance() {
this.subscribe();
},
},
methods: {
async fetchAuditevents() {
const response = await this.instance.fetchAuditevents(this.lastTimestamp);
const converted = response.data.events.map(event => new Auditevent(event));
converted.reverse();
if (converted.length > 0) {
this.lastTimestamp = converted[0].timestamp;
}
return converted;
},
createSubscription() {
const vm = this;
vm.lastTimestamp = moment(0);
if (this.instance) {
return Observable.timer(0, 5000)
.concatMap(this.fetchAuditevents)
.subscribe({
next: events => {
vm.events = vm.events ? events.concat(vm.events) : events;
},
errors: err => {
vm.unsubscribe();
}
});
}
}
}
}
</script>
\ No newline at end of file
......@@ -17,7 +17,7 @@
<template>
<section class="section">
<div class="container">
<div class="columns">
<div class="columns is-desktop">
<div class="column">
<details-info :instance="instance"></details-info>
</div>
......@@ -25,7 +25,7 @@
<details-health :instance="instance"></details-health>
</div>
</div>
<div class="columns">
<div class="columns is-desktop">
<div class="column">
<details-process :instance="instance"></details-process>
<details-gc :instance="instance"></details-gc>
......@@ -34,7 +34,7 @@
<details-threads :instance="instance"></details-threads>
</div>
</div>
<div class="columns">
<div class="columns is-desktop">
<div class="column">
<details-memory :instance="instance" type="heap"></details-memory>
</div>
......@@ -42,7 +42,7 @@
<details-memory :instance="instance" type="nonheap"></details-memory>
</div>
</div>
<div class="columns">
<div class="columns is-desktop">
<div class="column">
<details-datasources :instance="instance"></details-datasources>
</div>
......
......@@ -15,7 +15,7 @@
-->
<template>
<div class="section logfile-view" :class="{ 'is-loading' : !subscription }">
<div class="section logfile-view" :class="{ 'is-loading' : skippedBytes === null }">
<div class="logfile-view-actions">
<div class="logfile-view-actions__navigation">
<sba-icon-button :disabled="atTop" @click.native="scrollToTop">
......
......@@ -26,6 +26,7 @@
<script>
import subscribing from '@/mixins/subscribing';
import {Observable} from '@/utils/rxjs';
import _ from 'lodash';
import moment from 'moment-shortformat';
import threadsList from './threads-list';
......@@ -92,10 +93,15 @@
entry.timeline[entry.timeline.length - 1].end = now;
});
},
async fetchThreaddump() {
const response = await this.instance.fetchThreaddump();
return response.data.threads;
},
createSubscription() {
const vm = this;
if (this.instance) {
return this.instance.streamThreaddump(1000)
return Observable.timer(0, 1000)
.concatMap(vm.fetchThreaddump)
.subscribe({
next: threads => {
vm.updateTimelines(threads);
......
......@@ -78,6 +78,7 @@
<script>
import subscribing from '@/mixins/subscribing';
import {Observable} from '@/utils/rxjs';
import moment from 'moment';
import sbaTracesChart from './traces-chart';
import sbaTracesList from './traces-list';
......@@ -88,7 +89,7 @@
: (val, key) => oldFilter(val, key) && addedFilter(val, key);
class Trace {
constructor(timestamp, info) {
constructor({timestamp, info}) {
this.info = info;
this.timestamp = moment(timestamp);
}
......@@ -187,13 +188,25 @@
},
},
methods: {
async fetchTrace() {
const response = await this.instance.fetchTrace();
const traces = response.data.traces.filter(
trace => moment(trace.timestamp).isAfter(this.lastTimestamp)
);
if (traces.length > 0) {
this.lastTimestamp = traces[0].timestamp;
}
return traces;
},
createSubscription() {
const vm = this;
vm.lastTimestamp = moment(0);
if (this.instance) {
return this.instance.streamTrace(5 * 1000)
return Observable.timer(0, 5000)
.concatMap(vm.fetchTrace)
.subscribe({
next: rawTraces => {
const traces = rawTraces.map(trace => new Trace(trace.timestamp, trace.info));
const traces = rawTraces.map(trace => new Trace(trace));
vm.traces = vm.traces ? traces.concat(vm.traces) : traces;
},
errors: err => {
......
......@@ -33,5 +33,5 @@ public class UiController {
public String login() {
return "login";
}
}
......@@ -43,9 +43,7 @@ public class InstancesReactiveProxyController extends AbstractInstancesProxyCont
ServerHttpRequest request,
ServerHttpResponse response) {
String endpointLocalPath = getEndpointLocalPath(request.getPath().pathWithinApplication().value());
URI uri = UriComponentsBuilder.fromPath(endpointLocalPath)
.queryParams(request.getQueryParams())
.build()
URI uri = UriComponentsBuilder.fromPath(endpointLocalPath).query(request.getURI().getRawQuery()).build(true)
.toUri();
return super.forward(instanceId, uri, request.getMethod(), request.getHeaders(),
......
......@@ -32,10 +32,8 @@ import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.reactive.function.BodyExtractors;
import org.springframework.web.reactive.function.BodyInserters;
......@@ -57,7 +55,6 @@ public class InstancesServletProxyController extends AbstractInstancesProxyContr
@RequestMapping(path = REQUEST_MAPPING_PATH)
@ResponseBody
public Mono<Void> endpointProxy(@PathVariable("instanceId") String instanceId,
@RequestParam MultiValueMap<String, String> queryParams,
HttpServletRequest servletRequest,
HttpServletResponse servletResponse) {
ServerHttpRequest request = new ServletServerHttpRequest(servletRequest);
......@@ -66,7 +63,11 @@ public class InstancesServletProxyController extends AbstractInstancesProxyContr
String pathWithinApplication = servletRequest.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE)
.toString();
String endpointLocalPath = getEndpointLocalPath(pathWithinApplication);
URI uri = UriComponentsBuilder.fromPath(endpointLocalPath).queryParams(queryParams).build().toUri();
URI uri = UriComponentsBuilder.fromPath(endpointLocalPath)
.query(request.getURI().getRawQuery())
.build(true)
.toUri();
return super.forward(instanceId, uri, request.getMethod(), request.getHeaders(), () -> {
try {
......
/*
* Copyright 2014-2017 the original author or authors.
* 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.
......@@ -127,9 +127,7 @@ public final class InstanceFilterFunctions {
.subList(1, oldUrl.getPathSegments().size())
.toArray(new String[]{});
return UriComponentsBuilder.fromUriString(targetUrl)
.pathSegment(newPathSegments)
.queryParams(oldUrl.getQueryParams())
.build()
.pathSegment(newPathSegments).query(oldUrl.getQuery()).build(true)
.toUri();
}
......
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