Commit 69a964e3 by Johannes Edmeier

Add charts for memory and datasources

Reduce bundlesize by picking parts of fontawesome and d3 Add mixings for subscribing components
parent b3137edd
spring.resources.cachePeriod=3600
#
# 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.
#
server.port=8081
info.stage=test
logging.file=/tmp/log.log
......
......@@ -10,3 +10,6 @@ management:
endpoints:
web:
expose: "*"
endpoint:
health:
show-details: true
\ No newline at end of file
......@@ -10,6 +10,9 @@ management:
endpoints:
web:
expose: "*"
endpoint:
health:
show-details: true
spring:
application:
......
......@@ -16,7 +16,14 @@
"@fortawesome/vue-fontawesome": "0.0.22",
"axios": "^0.17.1",
"bulma": "^0.6.1",
"d3": "^4.12.2",
"d3-array": "^1.2.1",
"d3-axis": "^1.0.8",
"d3-brush": "^1.0.4",
"d3-scale": "^1.0.7",
"d3-selection": "^1.2.0",
"d3-shape": "^1.2.0",
"d3-time": "^1.0.8",
"d3-time-format": "^2.1.1",
"event-source-polyfill": "0.0.12",
"linkifyjs": "^2.1.5",
"lodash": "^4.17.4",
......@@ -42,7 +49,7 @@
"clean-webpack-plugin": "^0.1.17",
"cross-env": "^5.1.3",
"css-hot-loader": "^1.3.5",
"css-loader": "^0.28.4",
"css-loader": "^0.28.8",
"css-mqpacker": "^6.0.1",
"eslint": "^4.14.0",
"eslint-plugin-html": "^4.0.1",
......@@ -58,7 +65,7 @@
"lodash-webpack-plugin": "^0.11.4",
"node-sass": "^4.7.2",
"optimize-css-assets-webpack-plugin": "^3.2.0",
"postcss-loader": "^2.0.9",
"postcss-loader": "^2.0.10",
"sass-loader": "^6.0.6",
"style-loader": "^0.19.1",
"url-loader": "^0.6.2",
......
......@@ -50,3 +50,34 @@
white-space: nowrap;
}
}
//D3 Charts
.tick {
& line {
stroke: currentColor;
}
& text {
fill: currentColor;
font-size: $size-7;
}
}
path.domain {
stroke: currentColor;
}
.has-bullet::before {
background: currentColor;
content: '';
width: 0.75em;
height: 0.75em;
display: inline-block;
margin-right: 0.25em;
}
@each $name, $pair in $colors {
.has-bullet.has-bullet-#{$name}::before {
$color: nth($pair, 1);
background-color: $color;
}
}
\ No newline at end of file
......@@ -14,18 +14,27 @@
* limitations under the License.
*/
import fontawesome from '@fortawesome/fontawesome'
import {faGithub, faGitter, faStackOverflow} from '@fortawesome/fontawesome-free-brands'
import {
faBan, faBook, faCheck, faDownload, faExclamation, faMinusCircle, faQuestionCircle, faStepBackward,
faStepForward, faTimesCircle, faTrash
} from '@fortawesome/fontawesome-free-solid'
import FontAwesomeIcon from '@fortawesome/vue-fontawesome'
import fontawesome from '@fortawesome/fontawesome';
import faGithub from '@fortawesome/fontawesome-free-brands/faGithub';
import faGitter from '@fortawesome/fontawesome-free-brands/faGitter';
import faStackOverflow from '@fortawesome/fontawesome-free-brands/faStackOverflow';
import faBan from '@fortawesome/fontawesome-free-solid/faBan';
import faBook from '@fortawesome/fontawesome-free-solid/faBook';
import faCheck from '@fortawesome/fontawesome-free-solid/faCheck';
import faDownload from '@fortawesome/fontawesome-free-solid/faDownload';
import faExclamation from '@fortawesome/fontawesome-free-solid/faExclamation';
import faMinusCircle from '@fortawesome/fontawesome-free-solid/faMinusCircle';
import faQuestionCircle from '@fortawesome/fontawesome-free-solid/faQuestionCircle';
import faStepBackward from '@fortawesome/fontawesome-free-solid/faStepBackward';
import faStepForward from '@fortawesome/fontawesome-free-solid/faStepForward';
import faTimesCircle from '@fortawesome/fontawesome-free-solid/faTimesCircle';
import faTrash from '@fortawesome/fontawesome-free-solid/faTrash';
import FontAwesomeIcon from '@fortawesome/vue-fontawesome';
import moment from 'moment';
import Vue from 'vue'
import VueRouter from 'vue-router'
import './assets/css/base.scss'
import './assets/img/favicon.png'
import Vue from 'vue';
import VueRouter from 'vue-router';
import './assets/css/base.scss';
import './assets/img/favicon.png';
import sbaComponents from './components'
import sbaAbout from './views/about';
import sbaApplications from './views/applications';
......
/*
* 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.
*/
export default {
created() {
this.subscribe();
},
beforeDestroy() {
this.unsubscribe();
},
methods: {
async subscribe() {
if (!this.subscription) {
this.subscription = await this.createSubscription();
}
},
unsubscribe() {
if (this.subscription) {
try {
this.subscription.unsubscribe();
} finally {
this.subscription = null;
}
}
}
}
}
\ No newline at end of file
......@@ -49,7 +49,7 @@ class Instance {
}
async fetchMetric(metric, tags) {
const params = tags ? {tags: _.entries(tags).map(([name, value]) => `${name}:${value}`)} : {};
const params = tags ? {tag: _.entries(tags).map(([name, value]) => `${name}:${value}`).join(',')} : {};
return axios.get(`instances/${this.id}/actuator/metrics/${metric}`, {
headers: {'Accept': actuatorMimeTypes},
params
......@@ -62,8 +62,8 @@ class Instance {
});
}
async fetchEnv() {
return await axios.get(`instances/${this.id}/actuator/env`, {
async fetchEnv(name) {
return await axios.get(`instances/${this.id}/actuator/env/${name || '' }`, {
headers: {'Accept': actuatorMimeTypes}
});
}
......
/*
* 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.
*/
import * as array from 'd3-array';
import * as axis from 'd3-axis';
import * as brush from 'd3-brush';
import * as scale from 'd3-scale';
import * as selection from 'd3-selection';
import * as shape from 'd3-shape';
import * as time from 'd3-time';
import * as timeFormat from 'd3-time-format';
export default {
...shape,
...scale,
...axis,
...array,
...brush,
...time,
...timeFormat,
...selection
}
\ No newline at end of file
......@@ -54,18 +54,19 @@
</template>
<script>
import subscribing from '@/mixins/subscribing';
import Application from '@/services/application'
import * as _ from 'lodash';
import applicationsList from './applications-list.vue'
export default {
mixins: [subscribing],
components: {
applicationsList,
},
data: () => ({
_applications: [],
errors: [],
subscription: null
errors: []
}),
computed: {
applications() {
......@@ -90,28 +91,25 @@
}, 0);
}
},
async created() {
try {
this.$data._applications = (await Application.list()).data;
} catch (e) {
this.errors.push(e);
}
methods: {
async createSubscription() {
try {
this.$data._applications = (await Application.list()).data;
} catch (e) {
this.errors.push(e);
}
this.subscription = (await Application.getStream()).subscribe({
next: message => {
const idx = this.$data._applications.findIndex(application => application.name === message.data.name);
if (idx >= 0) {
this.$data._applications.splice(idx, 1, message.data);
} else {
this.$data._applications.push(message.data);
}
},
error: err => this.errors.push(err)
});
},
destroyed() {
if (this.subscription !== null) {
this.subscription.unsubscribe();
return (await Application.getStream()).subscribe({
next: message => {
const idx = this.$data._applications.findIndex(application => application.name === message.data.name);
if (idx >= 0) {
this.$data._applications.splice(idx, 1, message.data);
} else {
this.$data._applications.push(message.data);
}
},
error: err => this.errors.push(err)
});
}
}
}
......
<!--
- 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="datasource-chart">
<svg class="datasource-chart__svg"></svg>
</div>
</template>
<script>
import d3 from '@/utils/d3';
import moment from 'moment';
export default {
props: ['data'],
methods: {
drawChart(_data) {
const vm = this;
const data = _data.length === 1 ? _data.concat([{..._data[0], timestamp: _data[0].timestamp + 1}]) : _data;
///setup x and y scale
const extent = d3.extent(data, d => d.timestamp);
const x = d3.scaleTime()
.range([0, vm.width])
.domain(extent);
const y = d3.scaleLinear()
.range([vm.height, 0])
.domain([0, d3.max(data, d => d.active) * 1.1]);
//draw max
const max = vm.areas.selectAll('.datasource-chart__line--max')
.data([data]);
max.enter().append('path')
.merge(max)
.attr('class', 'datasource-chart__line--max')
.attr('d', d3.line()
.x(d => x(d.timestamp))
.y(d => y(d.max)));
max.exit().remove();
//draw areas
const active = vm.areas.selectAll('.mem-chart__area--active')
.data([data]);
active.enter().append('path')
.merge(active)
.attr('class', 'mem-chart__area--active')
.attr('d', d3.area()
.x(d => x(d.timestamp))
.y0(y(0))
.y1(d => y(d.active)));
active.exit().remove();
//draw axis
vm.xAxis.call(d3.axisBottom(x)
.ticks(5)
.tickFormat(d => moment(d).format("HH:mm:ss"))
);
vm.yAxis.call(d3.axisLeft(y)
.ticks(5)
);
},
},
mounted() {
const margin = {
top: 5,
right: 5,
bottom: 30,
left: 20,
};
this.width = this.$el.getBoundingClientRect().width - margin.left - margin.right;
this.height = this.$el.getBoundingClientRect().height - margin.top - margin.bottom;
this.chartLayer = d3.select(this.$el.querySelector('.datasource-chart__svg'))
.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);
this.xAxis = this.chartLayer.append('g')
.attr('class', 'datasource-chart__axis-x')
.attr('transform', `translate(0,${this.height})`);
this.yAxis = this.chartLayer.append('g')
.attr('class', 'datasource-chart__axis-y')
.attr('stroke', null);
this.areas = this.chartLayer.append('g');
this.drawChart(this.data);
},
watch: {
data(newVal) {
this.drawChart(newVal);
}
}
}
</script>
<style lang="scss">
@import "~@/assets/css/utilities";
.datasource-chart {
&__svg {
height: 125px;
width: 100%;
}
&__area {
&--active {
fill: $info;
opacity: 0.8;
}
}
&__line--max {
stroke: $grey;
}
}
</style>
\ No newline at end of file
......@@ -15,77 +15,95 @@
-->
<template>
<sba-panel title="DataSources" v-if="hasDataSources">
<div class="content" slot="text">
<table class="table data-table">
<template v-for="dataSource in dataSources">
<tr>
<td :rowspan="3" v-text="dataSource.name"></td>
<td colspan="2">
<span class="is-pulled-left">active connections</span>
<span v-text="dataSource.active"></span>
<progress v-if="dataSource.max >= 0"
class="progress is-small" :value="dataSource.active"
:max="dataSource.max"></progress>
</td>
</tr>
<tr>
<td>min connections</td>
<td class="has-text-right" v-text="dataSource.min"></td>
</tr>
<tr>
<td>max connections</td>
<td class="has-text-right" v-text="dataSource.max" v-if="dataSource.max >= 0"></td>
<td class="has-text-right" v-else>unlimited</td>
</tr>
</template>
</table>
<sba-panel :title="`Datasource: ${upperFirst(dataSource)}`" v-if="current">
<div slot="text">
<div class="level datasource-current">
<div class="level-item has-text-centered">
<div>
<p class="heading has-bullet has-bullet-info">Active connections</p>
<p v-text="current.active"></p>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="heading">Min connections</p>
<p v-text="current.min"></p>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="heading">Max connections</p>
<p v-if="current.max >= 0" v-text="current.max"></p>
<p v-else>unlimited</p>
</div>
</div>
</div>
<datasource-chart :data="chartData"></datasource-chart>
</div>
</sba-panel>
</template>
<script>
import subscribing from '@/mixins/subscribing';
import {Observable} from '@/utils/rxjs';
import _ from 'lodash';
import moment from 'moment';
import datasourceChart from './datasource-chart';
export default {
props: ['instance'],
props: ['instance', 'dataSource'],
mixins: [subscribing],
components: {
datasourceChart
},
data: () => ({
hasDataSources: false,
dataSources: [],
current: null,
chartData: [],
}),
created() {
this.fetchMetrics();
},
watch: {
instance(newVal, oldVal) {
if (newVal !== oldVal) {
this.fetchMetrics();
}
instance() {
this.subscribe()
},
dataSource() {
this.current = null;
this.chartData = [];
}
},
methods: {
upperFirst: _.upperFirst,
async fetchMetrics() {
if (this.instance) {
try {
const response = await this.instance.fetchMetric('data.source.active.connections');
this.hasDataSources = true;
const dataSourcesNames = response.data.availableTags.filter(tag => tag.tag === 'name')[0].values;
dataSourcesNames.forEach(this.fetchDataSourceMetrics);
} catch (error) {
this.hasDataSources = false;
}
}
},
async fetchDataSourceMetrics(name) {
const responseActive = this.instance.fetchMetric('data.source.active.connections', {name});
const responseMin = this.instance.fetchMetric('data.source.min.connections', {name});
const responseMax = this.instance.fetchMetric('data.source.max.connections', {name});
const responseActive = this.instance.fetchMetric('data.source.active.connections', {name: this.dataSource});
const responseMin = this.instance.fetchMetric('data.source.min.connections', {name: this.dataSource});
const responseMax = this.instance.fetchMetric('data.source.max.connections', {name: this.dataSource});
this.dataSources.push({
name,
return {
active: (await responseActive).data.measurements[0].value,
min: (await responseMin).data.measurements[0].value,
max: (await responseMax).data.measurements[0].value
});
};
},
createSubscription() {
const vm = this;
if (this.instance) {
return Observable.timer(0, 2500)
.concatMap(this.fetchMetrics)
.subscribe({
next: data => {
vm.current = data;
vm.chartData.push({...data, timestamp: moment.now().valueOf()});
},
errors: err => {
vm.unsubscribe();
}
});
}
}
}
}
</script>
\ No newline at end of file
</script>
<style lang="scss">
.datasource-current {
margin-bottom: 0 !important;
}
</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>
<div>
<details-datasource v-for="dataSource in dataSources" :key="dataSource"
:instance="instance" :data-source="dataSource"></details-datasource>
</div>
</template>
<script>
import subscribing from '@/mixins/subscribing';
import {Observable} from '@/utils/rxjs';
import detailsDatasource from './details-datasource';
export default {
props: ['instance', 'type'],
mixins: [subscribing],
components: {
detailsDatasource
},
data: () => ({
dataSources: [],
}),
watch: {
instance() {
this.subscribe();
}
},
methods: {
async fetchDataSources() {
if (this.instance) {
try {
const response = await this.instance.fetchMetric('data.source.active.connections');
return response.data.availableTags.filter(tag => tag.tag === 'name')[0].values;
} catch (error) {
return [];
}
}
},
createSubscription() {
const vm = this;
if (this.instance) {
return Observable.timer(0, 2500)
.concatMap(this.fetchDataSources)
.subscribe({
next: names => {
vm.dataSources = names
},
errors: err => {
vm.unsubscribe();
}
});
}
}
}
}
</script>
<style lang="scss">
</style>
\ No newline at end of file
......@@ -37,10 +37,8 @@
this.fetchHealth();
},
watch: {
instance(newVal, oldVal) {
if (newVal !== oldVal) {
this.fetchHealth();
}
instance() {
this.fetchHealth();
}
},
methods: {
......
......@@ -40,10 +40,8 @@
this.fetchInfo();
},
watch: {
instance(newVal, oldVal) {
if (newVal !== oldVal) {
this.fetchInfo();
}
instance() {
this.fetchInfo();
}
},
methods: {
......
<!--
- 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>
<sba-panel :title="`Memory: ${name}`" v-if="current">
<div slot="text">
<div class="level memory-current">
<div class="level-item has-text-centered" v-if="current.metaspace">
<div>
<p class="heading has-bullet has-bullet-primary">Metaspace</p>
<p v-text="prettyBytes(current.metaspace)"></p>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="heading has-bullet has-bullet-info">Used</p>
<p v-text="prettyBytes(current.used)"></p>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="heading has-bullet has-bullet-warning">Size</p>
<p v-text="prettyBytes(current.committed)"></p>
</div>
</div>
<div class="level-item has-text-centered" v-if="current.max >= 0">
<div>
<p class="heading">Max</p>
<p v-text="prettyBytes(current.max)"></p>
</div>
</div>
</div>
<mem-chart :data="chartData"></mem-chart>
</div>
</sba-panel>
</template>
<script>
import subscribing from '@/mixins/subscribing';
import {Observable} from '@/utils/rxjs';
import moment from 'moment';
import prettyBytes from 'pretty-bytes';
import memChart from './mem-chart';
export default {
props: ['instance', 'type'],
mixins: [subscribing],
components: {
memChart
},
data: () => ({
current: null,
chartData: [],
}),
computed: {
name() {
switch (this.type) {
case 'heap':
return 'Heap';
case 'nonheap':
return 'Non heap';
default:
return this.type;
}
}
},
watch: {
instance() {
this.subscribe();
}
},
methods: {
prettyBytes,
async fetchMetrics() {
if (this.instance) {
const responseMax = this.instance.fetchMetric('jvm.memory.max', {area: this.type});
const responseUsed = this.instance.fetchMetric('jvm.memory.used', {area: this.type});
const responeMetaspace = this.type === 'nonheap'
? this.instance.fetchMetric('jvm.memory.used', {area: this.type, id: 'Metaspace'})
: null;
const responseCommitted = this.instance.fetchMetric('jvm.memory.committed', {area: this.type});
return {
max: (await responseMax).data.measurements[0].value,
used: (await responseUsed).data.measurements[0].value,
metaspace: responeMetaspace ? (await responeMetaspace).data.measurements[0].value : null,
committed: (await responseCommitted).data.measurements[0].value
};
}
},
createSubscription() {
const vm = this;
if (this.instance) {
return Observable.timer(0, 2500)
.concatMap(this.fetchMetrics)
.subscribe({
next: data => {
vm.current = data;
vm.chartData.push({...data, timestamp: moment.now().valueOf()});
},
errors: err => {
vm.unsubscribe();
}
});
}
}
}
}
</script>
<style lang="scss">
@import "~@/assets/css/utilities";
.memory-current {
margin-bottom: 0 !important;
}
</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>
<sba-panel title="Process" v-if="pid || metrics['process.uptime'] ">
<div class="level" slot="text">
<div class="level-item has-text-centered" v-if="pid">
<div>
<p class="heading">PID</p>
<p v-text="pid"></p>
</div>
</div>
<div class="level-item has-text-centered" v-if="metrics['process.uptime']">
<div>
<p class="heading">Uptime</p>
<p>
<process-uptime :value="metrics['process.uptime']"></process-uptime>
</p>
</div>
</div>
<div class="level-item has-text-centered" v-if="metrics['system.cpu.count']">
<div>
<p class="heading">CPUs</p>
<p v-text="metrics['system.cpu.count']"></p>
</div>
</div>
<div class="level-item has-text-centered" v-if="metrics['system.load.average.1m']">
<div>
<p class="heading">System Load (last 1m)</p>
<p v-text="metrics['system.load.average.1m'].toFixed(2)"></p>
</div>
</div>
</div>
</sba-panel>
</template>
<script>
import _ from 'lodash';
import processUptime from './process-uptime'
export default {
props: ['instance'],
components: {
processUptime
},
data: () => ({
pid: null,
metrics: {
'process.uptime': null,
'system.load.average.1m': null,
'system.cpu.count': null,
}
}),
created() {
this.fetchMetrics();
this.fetchPid();
},
watch: {
instance() {
this.fetchMetrics();
this.fetchPid();
}
},
methods: {
async fetchMetrics() {
if (this.instance) {
const vm = this;
_.entries(vm.metrics).forEach(async ([name]) => {
const response = await vm.instance.fetchMetric(name);
vm.metrics[name] = response.data.measurements[0].value;
}
)
}
},
async fetchPid() {
if (this.instance) {
const vm = this;
const response = await vm.instance.fetchEnv('PID');
vm.pid = response.data.property.value;
}
}
}
}
</script>
\ No newline at end of file
......@@ -15,56 +15,37 @@
-->
<template>
<sba-panel title="JVM" v-if="metrics['process.uptime']">
<div class="content" slot="text">
<table class="table data-table">
<tr>
<td>Uptime</td>
<td colspan="2">
<jvm-uptime :value="metrics['process.uptime']"></jvm-uptime>
</td>
</tr>
<tr v-if="metrics['system.load.average.1m']">
<td>System load (last 1m)</td>
<td colspan="2"
v-text="metrics['system.load.average.1m'].toFixed(2)"></td>
</tr>
<tr>
<td>Available CPUs</td>
<td colspan="2" v-text="metrics['system.cpu.count']"></td>
</tr>
<tr>
<td rowspan="3">Threads</td>
<td>live</td>
<td v-text="metrics['jvm.threads.live']"></td>
</tr>
<tr>
<td>peak</td>
<td v-text="metrics['jvm.threads.peak']"></td>
</tr>
<tr>
<td>daemon</td>
<td v-text="metrics['jvm.threads.daemon']"></td>
</tr>
</table>
<sba-panel title="Threads" v-if="metrics['jvm.threads.live']">
<div class="level" slot="text">
<div class="level-item has-text-centered">
<div>
<p class="heading">Live</p>
<p v-text="metrics['jvm.threads.live']"></p>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="heading">Live Peak</p>
<p v-text="metrics['jvm.threads.peak']"></p>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="heading">Daemon</p>
<p v-text="metrics['jvm.threads.daemon']"></p>
</div>
</div>
</div>
</sba-panel>
</template>
<script>
import _ from 'lodash';
import jvmUptime from './jvm-uptime'
export default {
props: ['instance'],
components: {
jvmUptime
},
data: () => ({
metrics: {
'process.uptime': null,
'system.load.average.1m': null,
'system.cpu.count': null,
'jvm.threads.live': null,
'jvm.threads.peak': null,
'jvm.threads.daemon': null
......@@ -74,10 +55,8 @@
this.fetchMetrics();
},
watch: {
instance(newVal, oldVal) {
if (newVal !== oldVal) {
this.fetchMetrics();
}
instance() {
this.fetchMetrics();
}
},
methods: {
......
......@@ -20,11 +20,26 @@
<div class="columns">
<div class="column">
<details-info :instance="instance"></details-info>
<details-jvm :instance="instance"></details-jvm>
</div>
<div class="column">
<details-health :instance="instance"></details-health>
<details-datasource :instance="instance"></details-datasource>
</div>
</div>
<div class="columns">
<div class="column">
<details-process :instance="instance"></details-process>
<details-memory :instance="instance" type="heap"></details-memory>
</div>
<div class="column">
<details-threads :instance="instance"></details-threads>
<details-memory :instance="instance" type="nonheap"></details-memory>
</div>
</div>
<div class="columns">
<div class="column">
<details-datasources :instance="instance"></details-datasources>
</div>
<div class="column">
</div>
</div>
</div>
......@@ -32,14 +47,16 @@
</template>
<script>
import detailsDatasource from './details-datasource';
import detailsHealth from './details-health.vue';
import detailsInfo from './details-info.vue';
import detailsJvm from './details-jvm.vue';
import detailsDatasources from './details-datasources';
import detailsHealth from './details-health';
import detailsInfo from './details-info';
import detailsMemory from './details-memory';
import detailsProcess from './details-process';
import detailsThreads from './details-threads';
export default {
components: {
detailsHealth, detailsInfo, detailsJvm, detailsDatasource
detailsHealth, detailsInfo, detailsProcess, detailsThreads, detailsDatasources, detailsMemory
},
props: ['instance']
}
......
<!--
- 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="mem-chart">
<svg class="mem-chart__svg"></svg>
</div>
</template>
<script>
import d3 from '@/utils/d3';
import moment from 'moment';
import prettyBytes from 'pretty-bytes';
export default {
props: ['data'],
data: () => ({}),
methods: {
drawChart(_data) {
const vm = this;
const data = _data.length === 1 ? _data.concat([{..._data[0], timestamp: _data[0].timestamp + 1}]) : _data;
///setup x and y scale
const extent = d3.extent(data, d => d.timestamp);
const x = d3.scaleTime()
.range([0, vm.width])
.domain(extent);
const y = d3.scaleLinear()
.range([vm.height, 0])
.domain([0, d3.max(data, d => d.committed) * 1.1]);
//draw max
const max = vm.areas.selectAll('.mem-chart__line--max')
.data([data]);
max.enter().append('path')
.merge(max)
.attr('class', 'mem-chart__line--max')
.attr('d', d3.line()
.x(d => x(d.timestamp))
.y(d => y(d.max)));
max.exit().remove();
//draw areas
const committed = vm.areas.selectAll('.mem-chart__area--committed')
.data([data]);
committed.enter().append('path')
.merge(committed)
.attr('class', 'mem-chart__area--committed')
.attr('d', d3.area()
.x(d => x(d.timestamp))
.y0(d => y(d.used))
.y1(d => y(d.committed)));
committed.exit().remove();
const used = vm.areas.selectAll('.mem-chart__area--used')
.data([data]);
used.enter().append('path')
.merge(used)
.attr('class', 'mem-chart__area--used')
.attr('d', d3.area()
.x(d => x(d.timestamp))
.y0(d => y(d.metaspace || 0))
.y1(d => y(d.used)));
used.exit().remove();
const metaspace = vm.areas.selectAll('.mem-chart__area--metaspace')
.data([data]);
metaspace.enter().append('path')
.merge(metaspace)
.attr('class', 'mem-chart__area--metaspace')
.attr('d', d3.area()
.x(d => x(d.timestamp))
.y0(y(0))
.y1(d => y(d.metaspace)));
metaspace.exit().remove();
//draw axis
vm.xAxis.call(d3.axisBottom(x)
.ticks(5)
.tickFormat(d => moment(d).format("HH:mm:ss"))
);
vm.yAxis.call(d3.axisLeft(y)
.ticks(5)
.tickFormat(prettyBytes)
);
},
},
mounted() {
const margin = {
top: 5,
right: 5,
bottom: 30,
left: 50,
};
this.width = this.$el.getBoundingClientRect().width - margin.left - margin.right;
this.height = this.$el.getBoundingClientRect().height - margin.top - margin.bottom;
this.chartLayer = d3.select(this.$el.querySelector('.mem-chart__svg'))
.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);
this.xAxis = this.chartLayer.append('g')
.attr('class', 'mem-chart__axis-x')
.attr('transform', `translate(0,${this.height})`);
this.yAxis = this.chartLayer.append('g')
.attr('class', 'mem-chart__axis-y')
.attr('stroke', null);
this.areas = this.chartLayer.append('g');
this.drawChart(this.data);
},
watch: {
data(newVal) {
this.drawChart(newVal);
}
}
}
</script>
<style lang="scss">
@import "~@/assets/css/utilities";
.mem-chart {
&__svg {
height: 125px;
width: 100%;
}
&__area {
&--committed {
fill: $warning;
opacity: 0.8;
}
&--used {
fill: $info;
opacity: 0.8;
}
&--metaspace {
fill: $primary;
opacity: 0.8;
}
}
&__line--max {
stroke: $grey;
}
}
</style>
\ No newline at end of file
......@@ -14,11 +14,13 @@
* limitations under the License.
*/
import {Observable} from '@/utils/rxjs'
import subscribing from '@/mixins/subscribing';
import {Observable} from '@/utils/rxjs';
import moment from 'moment';
export default {
props: ['value'],
mixins: [subscribing],
data: () => ({
startTs: null,
offset: null
......@@ -36,41 +38,21 @@ export default {
}
},
watch: {
value(newVal) {
this.stop();
if (newVal) {
this.start();
}
}
},
mounted() {
if (this.value && this.subscription == null) {
this.start();
value() {
this.subscribe();
}
},
beforeDestroy() {
this.stop();
},
methods: {
start() {
const vm = this;
this.startTs = moment.now();
this.offset = 0;
this.subscription = Observable.timer(0, 1000).subscribe({
next: () => {
vm.offset = moment.now().valueOf() - this.startTs.valueOf();
}
})
},
stop() {
this.startTs = null;
this.offset = null;
if (this.subscription) {
try {
this.subscription.unsubscribe();
} finally {
this.subscription = null;
}
createSubscription() {
if (this.value) {
const vm = this;
this.startTs = moment.now();
this.offset = 0;
return Observable.timer(0, 1000).subscribe({
next: () => {
vm.offset = moment.now().valueOf() - this.startTs.valueOf();
}
})
}
}
}
......
......@@ -93,10 +93,8 @@
this.fetchEnv();
},
watch: {
instance(newVal, oldVal) {
if (newVal !== oldVal) {
this.fetchEnv();
}
instance() {
this.fetchEnv();
}
},
methods: {
......
......@@ -67,10 +67,8 @@
this.fetchFlyway();
},
watch: {
instance(newVal, oldVal) {
if (newVal !== oldVal) {
this.fetchFlyway();
}
instance() {
this.fetchFlyway();
}
},
methods: {
......
......@@ -46,10 +46,8 @@
this.fetchLiquibase();
},
watch: {
instance(newVal, oldVal) {
if (newVal !== oldVal) {
this.fetchLiquibase();
}
instance() {
this.fetchLiquibase();
}
},
methods: {
......
......@@ -35,17 +35,18 @@
</template>
<script>
import subscribing from '@/mixins/subscribing';
import {animationFrame, Observable} from '@/utils/rxjs';
import _ from 'lodash';
import prettyBytes from 'pretty-bytes';
export default {
props: ['instance'],
mixins: [subscribing],
data: () => ({
subscription: null,
atBottom: true,
atTop: false,
skippedBytes: null,
skippedBytes: null
}),
computed: {
skippedHumanBytes() {
......@@ -53,16 +54,12 @@
}
},
watch: {
async instance(newVal, oldVal) {
if (newVal !== oldVal) {
this.stop();
this.start();
}
async instance() {
this.subscribe();
}
},
created() {
this.scrollParent = null;
this.start();
},
mounted() {
this.scrollParent = document.documentElement;
......@@ -73,10 +70,10 @@
window.removeEventListener('scroll', this.onScroll);
},
methods: {
start() {
createSubscription() {
const vm = this;
if (this.instance) {
this.subscription = this.instance.streamLogfile(1000)
return this.instance.streamLogfile(1000)
.do(chunk => vm.skippedBytes = vm.skippedBytes || chunk.skipped)
.concatMap(chunk => _.chunk(chunk.addendum.split(/\r?\n/), 250))
.map(lines => Observable.of(lines, animationFrame))
......@@ -94,20 +91,11 @@
}
},
errors: err => {
vm.stop();
vm.unsubscribe();
}
});
}
},
stop() {
if (this.subscription) {
try {
this.subscription.unsubscribe();
} finally {
this.subscription = null;
}
}
},
onScroll() {
this.atBottom = this.scrollParent.scrollTop >= this.scrollParent.scrollHeight - this.scrollParent.clientHeight;
this.atTop = this.scrollParent.scrollTop === 0;
......
......@@ -165,10 +165,8 @@
window.removeEventListener('scroll', this.onScroll);
},
watch: {
instance(newVal, oldVal) {
if (newVal !== oldVal) {
this.fetchLoggers();
}
instance() {
this.fetchLoggers();
}
}
}
......
......@@ -25,32 +25,24 @@
</template>
<script>
import subscribing from '@/mixins/subscribing';
import _ from 'lodash';
import moment from 'moment-shortformat';
import threadsList from './threads-list';
export default {
props: ['instance'],
mixins: [subscribing],
components: {
threadsList
},
data: () => ({
threads: null,
subscription: null
threads: null
}),
computed: {},
created() {
this.start();
},
beforeDestroy() {
this.stop();
},
watch: {
instance(newVal, oldVal) {
if (newVal !== oldVal) {
this.stop();
this.start()
}
instance() {
this.subscribe()
},
},
methods: {
......@@ -100,33 +92,20 @@
entry.timeline[entry.timeline.length - 1].end = now;
});
},
start() {
createSubscription() {
const vm = this;
if (this.instance) {
this.subscription = this.instance.streamThreaddump(1000)
return this.instance.streamThreaddump(1000)
.subscribe({
next: threads => {
vm.updateTimelines(threads);
},
errors: err => {
vm.stop();
vm.unsubscribe();
}
});
}
},
stop() {
if (this.subscription) {
try {
this.subscription.unsubscribe();
} finally {
this.subscription = null;
}
}
}
}
}
</script>
<style lang="scss">
</style>
\ No newline at end of file
</script>
\ No newline at end of file
......@@ -100,7 +100,7 @@
</div>
</template>
<script>
import * as d3 from 'd3';
import d3 from '@/utils/d3'
import _ from 'lodash';
import threadTag from './thread-tag';
......
......@@ -77,6 +77,7 @@
</template>
<script>
import subscribing from '@/mixins/subscribing';
import moment from 'moment';
import sbaTracesChart from './traces-chart';
import sbaTracesList from './traces-list';
......@@ -144,13 +145,13 @@
}
export default {
props: ['instance'],
mixins: [subscribing],
components: {
sbaTracesList, sbaTracesChart
},
props: ['instance'],
data: () => ({
traces: null,
subscription: null,
filter: null,
excludeActuator: true,
showSuccess: true,
......@@ -180,45 +181,27 @@
return this.filteredTraces.filter(trace => !trace.timestamp.isBefore(start) && !trace.timestamp.isAfter(end));
}
},
created() {
this.start();
},
beforeDestroy() {
this.stop();
},
watch: {
instance(newVal, oldVal) {
if (newVal !== oldVal) {
this.stop();
this.start();
}
instance() {
this.subscribe();
},
},
methods: {
start() {
createSubscription() {
const vm = this;
if (this.instance) {
this.subscription = this.instance.streamTrace(5 * 1000)
return this.instance.streamTrace(5 * 1000)
.subscribe({
next: rawTraces => {
const traces = rawTraces.map(trace => new Trace(trace.timestamp, trace.info));
vm.traces = vm.traces ? traces.concat(vm.traces) : traces;
},
errors: err => {
vm.stop();
vm.unsubscribe();
}
});
}
},
stop() {
if (this.subscription) {
try {
this.subscription.unsubscribe();
} finally {
this.subscription = null;
}
}
},
getFilterFn() {
let filterFn = null;
if (this.actuatorPath !== null && this.excludeActuator) {
......@@ -238,11 +221,7 @@
filterFn = addToFilter(filterFn, (trace) => !trace.isServerError());
}
return filterFn;
},
}
}
}
</script>
<style lang="scss">
@import "~@/assets/css/utilities";
</style>
\ No newline at end of file
</script>
\ No newline at end of file
......@@ -51,7 +51,8 @@
</template>
<script>
import * as d3 from 'd3';
import d3 from '@/utils/d3';
import {event as d3Event} from 'd3-selection'; //see https://github.com/d3/d3/issues/2733#issuecomment-190743489
import moment from 'moment-shortformat';
const interval = 1000;
......@@ -144,18 +145,18 @@
///setup x and y scale
const x = d3.scaleTime()
.range([0, vm.width])
.domain(d3.extent(data, (d) => d.timeStart));
.domain(d3.extent(data, d => d.timeStart));
this.x = x;
const y = d3.scaleLinear()
.range([vm.height, 0])
.domain([0, d3.max(data, (d) => d.totalCount)]);
.domain([0, d3.max(data, d => d.totalCount)]);
//setup areas
//draw areas
const area = d3.area()
.x((d) => x(d.data.timeStart))
.y0((d) => y(d[0]))
.y1((d) => y(d[1]));
.x(d => x(d.data.timeStart))
.y0(d => y(d[0]))
.y1(d => y(d[1]));
const stack = d3.stack()
.keys(['totalSuccess', 'totalClientErrors', 'totalServerErrors']);
......@@ -165,19 +166,19 @@
d.enter().append('path')
.merge(d)
.attr('class', (d) => `trace-chart__area trace-chart__area--${d.key}`)
.attr('class', d => `trace-chart__area trace-chart__area--${d.key}`)
.attr('d', area);
d.exit().remove();
//setup axis
//draw axis
vm.xAxis.call(d3.axisBottom(x)
.ticks(10)
.tickFormat(d => `-${moment(d).short(true)}`)
);
vm.yAxis.call(d3.axisRight(y)
.ticks(Math.min(5, d3.max(data, (d) => d.totalCount)))
.ticks(Math.min(5, d3.max(data, d => d.totalCount)))
.tickSize(this.width)
).call(
axis => axis.selectAll('.tick text')
......@@ -186,30 +187,30 @@
.attr('text-anchor', 'end')
);
//setup brush selection
//draw brush selection
const brush = d3.brushX()
.extent([[0, 0], [vm.width, vm.height]])
.on('start', () => {
if (d3.event.selection) {
if (d3Event.selection) {
vm.isBrushing = true;
vm.hovered = null;
}
})
.on('brush', function () {
if (d3.event.sourceEvent === null || d3.event.sourceEvent.type === 'brush') {
if (d3Event.sourceEvent === null || d3Event.sourceEvent.type === 'brush') {
return;
}
if (d3.event.selection) {
const floor = Math.floor(x.invert(d3.event.selection[0]) / interval) * interval;
const ceil = Math.ceil(x.invert(d3.event.selection[1]) / interval) * interval;
d3.select(this).call(d3.event.target.move, [floor, ceil].map(x));
if (d3Event.selection) {
const floor = Math.floor(x.invert(d3Event.selection[0]) / interval) * interval;
const ceil = Math.ceil(x.invert(d3Event.selection[1]) / interval) * interval;
d3.select(this).call(d3Event.target.move, [floor, ceil].map(x));
vm.brushSelection = [floor, ceil];
}
})
.on('end', () => {
vm.isBrushing = false;
if (!d3.event.selection) {
if (!d3Event.selection) {
vm.brushSelection = null;
}
});
......@@ -243,6 +244,14 @@
.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);
this.xAxis = this.chartLayer.append('g')
.attr('class', 'trace-chart__axis-x')
.attr('transform', `translate(0,${this.height})`);
this.yAxis = this.chartLayer.append('g')
.attr('class', 'trace-chart__axis-y')
.attr('stroke', null);
this.areas = this.chartLayer.append('g');
this.hover = this.chartLayer.append('path')
......@@ -252,14 +261,6 @@
this.brushGroup = this.chartLayer.append('g')
.attr('class', 'trace-chart__brush');
this.xAxis = this.chartLayer.append('g')
.attr('class', 'trace-chart__axis-x')
.attr('transform', `translate(0,${this.height})`);
this.yAxis = this.chartLayer.append('g')
.attr('class', 'trace-chart__axis-y')
.attr('stroke', null);
this.drawChart(this.chartData);
},
watch: {
......@@ -284,8 +285,6 @@
<style lang="scss">
@import "~@/assets/css/utilities";
$axis-color: $grey-darker;
.trace-chart {
&__svg {
height: 200px;
......@@ -331,33 +330,10 @@
fill-opacity: 1;
}
&__axis-x {
& .domain {
stroke: $axis-color;
}
& .tick {
& text {
fill: $axis-color;
font-size: $size-7;
}
& line {
stroke: $axis-color;
}
}
}
&__axis-y {
& .domain {
stroke: none;
}
& .tick {
& text {
fill: $axis-color;
font-size: $size-7;
}
& line {
stroke: $axis-color;
}
}
.tick:not(:first-of-type) {
& line {
......@@ -370,14 +346,17 @@
&__area {
&--totalSuccess {
fill: $success;
opacity: 0.8;
}
&--totalClientErrors {
fill: $warning;
opacity: 0.8;
}
&--totalServerErrors {
fill: $danger;
opacity: 0.8;
}
}
}
......
......@@ -29,13 +29,14 @@
</thead>
<template v-for="trace in traces">
<tr class="is-selectable"
:class="{ 'trace--is-detailed' : showDetails[trace.key], 'has-text-warning' : trace.isClientError(), 'has-text-danger' : trace.isServerError() }"
:class="{ 'trace--is-detailed' : showDetails[trace.key] }"
@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>
<td v-text="trace.method"></td>
<td v-text="trace.path"></td>
<td v-text="trace.status"></td>
<td v-text="trace.status"
:class="{ 'has-text-warning' : trace.isClientError(), 'has-text-danger' : trace.isServerError() }"></td>
<td v-text="trace.contentType"></td>
<td v-text="trace.contentLength ? prettyBytes(trace.contentLength) : ''"></td>
<td v-text="trace.timeTaken !== null && typeof trace.timeTaken !== 'undefined' ? `${trace.timeTaken} ms` : ''"></td>
......
......@@ -54,6 +54,7 @@
</template>
<script>
import subscribing from '@/mixins/subscribing';
import Instance from '@/services/instance';
import _ from 'lodash';
import moment from 'moment';
......@@ -73,48 +74,44 @@
}
export default {
mixins: [subscribing],
data: () => ({
_events: [],
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);
converted.reverse();
this.events = converted.concat(this.events);
const newInstanceNames = converted.filter(event => event.type === 'REGISTERED').reduce((names, event) => ({
...names,
[event.instance]: event.payload.registration.name
}), {});
_.assign(this.instanceNames, newInstanceNames);
},
async createSubscription() {
return (await Instance.getEventStream()).subscribe({
next: message => {
this.addEvents([message.data])
},
error: err => this.errors.push(err)
});
}
},
async created() {
try {
this.addEvents((await Instance.fetchEvents()).data);
this.events.sort((a, b) => b.timestamp - a.timestamp)
} 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
</script>
\ No newline at end of file
......@@ -160,6 +160,7 @@ const config = {
new BundleAnalyzerPlugin({
analyzerMode: 'static',
openAnalyzer: false,
reportFilename: '../report.html'
})
],
devtool: '#eval-source-map'
......
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