UI, and more issue fixes

pull/496/head
hunterlong 2020-04-14 09:08:50 -07:00
parent 6257ecb5cb
commit 3b7df28c47
25 changed files with 739 additions and 328 deletions

1
.github/FUNDING.yml vendored
View File

@ -1,2 +1,3 @@
github: hunterlong
patreon: statping
custom: ['https://www.buymeacoffee.com/hunterlong']

View File

@ -1,3 +1,9 @@
# 0.90.26 (04-13-2020)
- Fixed Delete Failures button/function
- Removed timezone field from Settings (core)
- Modified CDN asset URL
- Fixed single Service view, more complex charts
# 0.90.25
- Added string response on OnTest for Notifiers
- Modified UI to show user the response for a Notifier.

View File

@ -27,8 +27,8 @@ lite: clean
reup: down clean compose-build-full up
test: clean
go test -v -p=4 -ldflags="-X main.VERSION=testing" -coverprofile=coverage.out ./...
test: clean compile
go test -v -p=1 -ldflags="-X main.VERSION=testing" -coverprofile=coverage.out ./...
# build all arch's and release Statping
release: test-deps

View File

@ -146,6 +146,45 @@ type isObject interface {
Db() Database
}
func ParseRequest(r *http.Request) (*GroupQuery, error) {
fields := parseGet(r)
grouping := fields.Get("group")
startField := utils.ToInt(fields.Get("start"))
endField := utils.ToInt(fields.Get("end"))
limit := utils.ToInt(fields.Get("limit"))
offset := utils.ToInt(fields.Get("offset"))
fill, _ := strconv.ParseBool(fields.Get("fill"))
orderBy := fields.Get("order")
if limit == 0 {
limit = 10000
}
if grouping == "" {
grouping = "1h"
}
groupDur, err := time.ParseDuration(grouping)
if err != nil {
log.Errorln(err)
groupDur = 1 * time.Hour
}
query := &GroupQuery{
Start: time.Unix(startField, 0).UTC(),
End: time.Unix(endField, 0).UTC(),
Group: groupDur,
Order: orderBy,
Limit: int(limit),
Offset: int(offset),
FillEmpty: fill,
}
if query.Start.After(query.End) {
return nil, errors.New("start time is after ending time")
}
return query, nil
}
func ParseQueries(r *http.Request, o isObject) (*GroupQuery, error) {
fields := parseGet(r)
grouping := fields.Get("group")
@ -169,6 +208,9 @@ func ParseQueries(r *http.Request, o isObject) (*GroupQuery, error) {
log.Errorln(err)
groupDur = 1 * time.Hour
}
if endField == 0 {
endField = utils.Now().Unix()
}
query := &GroupQuery{
Start: time.Unix(startField, 0).UTC(),

View File

@ -10,9 +10,9 @@
<base href="{{BasePath}}">
{{if USE_CDN}}
<link rel="shortcut icon" type="image/x-icon" href="https://assets.statping.com/favicon.ico">
<link rel="stylesheet" href="https://assets.statping.com/css/bootstrap.min.css">
<link rel="stylesheet" href="https://assets.statping.com/css/base.css">
<link rel="stylesheet" href="https://assets.statping.com/font/all.css">
<link rel="stylesheet" href="https://assets.statping.com/vendor.css">
<link rel="stylesheet" href="https://assets.statping.com/style.css">
<link rel="stylesheet" href="https://assets.statping.com/main.css">
{{else}}
<link rel="shortcut icon" type="image/x-icon" href="favicon.ico">
{{if USING_ASSETS}}
@ -33,11 +33,11 @@
<div id="app" class="statping_container"></div>
{{if USE_CDN}}
<script src="https://assets.statping.com/js/bundle.js"></script>
<script src="https://assets.statping.com/js/vendor.chunk.js"></script>
<script src="https://assets.statping.com/js/polyfill.chunk.js"></script>
<script src="https://assets.statping.com/js/style.chunk.js"></script>
<script src="https://assets.statping.com/js/main.chunk.js"></script>
<script src="https://assets.statping.com/bundle.js"></script>
<script src="https://assets.statping.com/vendor.chunk.js"></script>
<script src="https://assets.statping.com/polyfill.chunk.js"></script>
<script src="https://assets.statping.com/style.chunk.js"></script>
<script src="https://assets.statping.com/main.chunk.js"></script>
{{else}}
<% _.each(htmlWebpackPlugin.tags.bodyTags, function(bodyTag) { %>
<%= bodyTag %> <% }) %>

View File

@ -56,8 +56,8 @@ class Api {
return axios.get('api/services/' + id + '/failure_data?start=' + start + '&end=' + end + '&group=' + group + '&fill=' + fill).then(response => (response.data))
}
async service_uptime(id) {
return axios.get('api/services/' + id + '/uptime_data').then(response => (response.data))
async service_uptime(id, start, end) {
return axios.get('api/services/' + id + '/uptime_data?start=' + start + '&end=' + end).then(response => (response.data))
}
async service_heatmap(id, start, end, group) {

View File

@ -41,7 +41,7 @@
</template>
<script>
import Api from "../API";
import Api from "../../API";
export default {
name: 'Checkins',

View File

@ -36,10 +36,12 @@
</td>
<td class="text-right">
<div v-if="$store.state.admin" class="btn-group">
<a @click.prevent="editGroup(group, edit)" href="#" class="btn btn-outline-secondary"><font-awesome-icon icon="chart-area" /> Edit</a>
<a @click.prevent="deleteGroup(group)" href="#" class="btn btn-danger">
<button @click.prevent="editGroup(group, edit)" href="#" class="btn btn-sm btn-outline-secondary">
<font-awesome-icon icon="edit" />
</button>
<button @click.prevent="deleteGroup(group)" href="#" class="btn btn-sm btn-danger">
<font-awesome-icon icon="times" />
</a>
</button>
</div>
</td>
</tr>

View File

@ -1,9 +1,13 @@
<template>
<div class="col-12">
<h2>{{service.name}} Failures
<span class="btn btn-outline-danger float-right">Delete All</span></h2>
<button v-if="failures.length>0" @click="deleteFailures" class="btn btn-outline-danger float-right">Delete All</button></h2>
<div class="list-group mt-3 mb-4">
<div class="alert alert-info" v-if="failures.length===0">
You don't have any failures for {{service.name}}. Way to go!
</div>
<div v-for="(failure, index) in failures" :key="index" class="mb-2 list-group-item list-group-item-action flex-column align-items-start">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">{{failure.issue}}</h5>
@ -39,7 +43,7 @@
</template>
<script>
import Api from "../API";
import Api from "../../API";
export default {
name: 'Failures',
@ -72,13 +76,21 @@ export default {
await this.gotoPage(1)
},
methods: {
async deleteFailures() {
const c = confirm('Are you sure you want to delete all failures?')
if (c) {
await Api.service_failures_delete(this.service)
this.service = await Api.service(this.service.id)
this.total = 0
await this.load()
}
},
async gotoPage(page) {
this.page = page;
this.offset = (page-1) * this.limit;
window.console.log('page', this.page, this.limit, this.offset);
await this.load()
},
async load() {
this.failures = await Api.service_failures(this.service.id, 0, 9999999999, this.limit, this.offset)
}
}

View File

@ -48,7 +48,7 @@
</template>
<script>
import Api from "../API";
import Api from "../../API";
import FormIncidentUpdates from "@/forms/IncidentUpdates";
export default {

View File

@ -2,7 +2,9 @@
<div class="row">
<div v-for="(incident, i) in incidents" class="col-12 mt-4 mb-3">
<span class="braker mt-1 mb-3"></span>
<h6>Incident: {{incident.title}}<span class="font-2 float-right">{{niceDate(incident.created_at)}}</span></h6>
<h6>Incident: {{incident.title}}
<span class="font-2 float-right">{{niceDate(incident.created_at)}}</span>
</h6>
<span class="font-2" v-html="incident.description"></span>
<UpdatesBlock :incident="incident"/>

View File

@ -1,9 +1,11 @@
<template>
<div class="row">
<div v-for="(update, i) in updates" v-bind:key="i" class="col-12 mt-3">
<span class="col-2 badge text-uppercase" :class="badgeClass(update.type)">{{update.type}}</span>
<span class="col-10">{{update.message}}</span>
<span class="col-12 font-1 float-right text-black-50">{{ago(update.created_at)}} ago</span>
<div class="col-md-2 col-12">
<span class="badge text-uppercase" :class="badgeClass(update.type)">{{update.type}}</span>
</div>
<div class="col-md-12 col-12 mt-2 font-3">{{update.message}}</div>
<div class="col-12 font-1 float-right text-black-50 mt-2">{{ago(update.created_at)}} ago</div>
</div>
</div>
</template>

View File

@ -0,0 +1,276 @@
<template>
<div>
<div class="service-chart-container">
<apexchart width="100%" height="420" type="area" :options="main_chart_options" :series="main_chart"></apexchart>
</div>
</div>
</template>
<script>
import Api from "../../API";
const timeoptions = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' };
export default {
name: 'AdvancedChart',
props: {
service: {
type: Object,
required: true
},
start: {
type: String,
required: true
},
end: {
type: String,
required: true
},
group: {
type: String,
required: true
},
updated: {
type: Function,
required: true
},
},
data() {
return {
loading: true,
main_data: null,
expanded_data: null,
main_chart_options: {
noData: {
text: "Loading...",
align: 'center',
verticalAlign: 'middle',
offsetX: 0,
offsetY: -20,
style: {
color: "#bababa",
fontSize: '27px'
}
},
chart: {
id: 'mainchart',
events: {
dataPointSelection: (event, chartContext, config) => {
window.console.log('slect')
window.console.log(event)
},
updated: (chartContext, config) => {
window.console.log('updated')
},
beforeZoom: (chartContext, { xaxis }) => {
const start = (xaxis.min / 1000).toFixed(0)
const end = (xaxis.max / 1000).toFixed(0)
window.console.log(start, end)
this.updated(this.fromUnix(start), this.fromUnix(end))
return {
xaxis: {
min: this.fromUnix(start),
max: this.fromUnix(end)
}
}
},
scrolled: (chartContext, { xaxis }) => {
window.console.log(xaxis)
},
},
height: 500,
width: "100%",
type: "area",
animations: {
enabled: false,
initialAnimation: {
enabled: true
}
},
selection: {
enabled: true
},
zoom: {
enabled: true
},
toolbar: {
show: true
},
stroke: {
show: false,
curve: 'stepline',
lineCap: 'butt',
},
},
xaxis: {
type: "datetime",
labels: {
show: true
},
tooltip: {
enabled: false
}
},
yaxis: {
labels: {
show: true
},
},
markers: {
size: 0,
strokeWidth: 0,
hover: {
size: undefined,
sizeOffset: 0
}
},
tooltip: {
theme: false,
enabled: true,
custom: function ({ series, seriesIndex, dataPointIndex, w }) {
let ts = w.globals.seriesX[seriesIndex][dataPointIndex];
const dt = new Date(ts).toLocaleDateString("en-us", timeoptions)
let val = series[seriesIndex][dataPointIndex];
if (val >= 1000) {
val = (val * 0.1).toFixed(0) + " milliseconds"
} else {
val = (val * 0.01).toFixed(0) + " microseconds"
}
return `<div class="chartmarker"><span>Response Time: </span><span class="font-3">${val}</span><span>${dt}</span></div>`
},
fixed: {
enabled: true,
position: 'topRight',
offsetX: -30,
offsetY: 40,
},
x: {
show: true,
},
y: {
formatter: undefined,
title: {
formatter: (seriesName) => seriesName,
},
},
},
legend: {
show: false,
},
dataLabels: {
enabled: false
},
floating: true,
axisTicks: {
show: true
},
axisBorder: {
show: false
},
fill: {
colors: ["#48d338"],
opacity: 1,
type: 'solid'
},
stroke: {
show: true,
curve: 'smooth',
lineCap: 'butt',
colors: ["#3aa82d"],
width: 2,
}
},
expanded_chart_options: {
chart: {
id: "chart1",
height: 130,
type: "bar",
foreColor: "#ccc",
brush: {
target: "chart2",
enabled: true
},
selection: {
enabled: true,
fill: {
color: "#fff",
opacity: 0.4
},
xaxis: {
min: new Date("27 Jul 2017 10:00:00").getTime(),
max: new Date("14 Aug 2999 10:00:00").getTime()
}
}
},
colors: ["#FF0080"],
stroke: {
width: 2
},
grid: {
borderColor: "#444"
},
markers: {
size: 0
},
xaxis: {
type: "datetime",
tooltip: {
enabled: false
}
},
yaxis: {
tickAmount: 2
}
}
}
},
async mounted() {
await this.update_data();
},
computed: {
main_chart () {
return [{
name: this.service.name,
...this.convertToChartData(this.main_data)
}]
},
expanded_chart () {
return this.toBarData(this.expanded_data)
},
params () {
return {start: this.toUnix(new Date(this.start)), end: this.toUnix(new Date(this.end))}
},
},
watch: {
start: function(n, o) {
this.update_data()
},
end: function(n, o) {
this.update_data()
},
group: function(n, o) {
this.update_data()
},
},
methods: {
async update_data() {
this.loading = true
await this.chartHits()
// await this.expanded_hits()
this.loading = false
},
async expanded_hits() {
this.expanded_data = await this.load_hits(0, 99999999999, "24h")
},
async chartHits() {
this.main_data = await this.load_hits()
},
async load_hits(start=this.params.start, end=this.params.end, group=this.group) {
return await Api.service_hits(this.service.id, start, end, group, false)
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

View File

@ -49,12 +49,9 @@
</div>
<div class="col-md-4 col-6 float-right">
<button v-if="!expanded" @click="showMoreStats" class="btn btn-sm float-right dyn-dark text-white" :class="{'bg-success': service.online, 'bg-danger': !service.online}">View Service</button>
<button v-if="expanded" @click="expanded = false" class="btn btn-sm float-right dyn-dark text-white" :class="{'btn-outline-success': service.online, 'bg-danger': !service.online}">Hide</button>
</div>
<div v-if="expanded" class="row">
<Analytics title="Last Failure" value="417 Days ago"/>
<button v-if="!expanded" @click="setService" class="btn btn-sm float-right dyn-dark text-white" :class="{'bg-success': service.online, 'bg-danger': !service.online}">
View Service
</button>
</div>
</div>
@ -133,6 +130,10 @@ export default {
this.track_service = this.in_service
},
methods: {
async setService() {
await this.$store.commit('setService', this.service)
this.$router.push('/service/'+this.service.id, {props: {in_service: this.service}})
},
async showMoreStats() {
this.expanded = !this.expanded;
@ -174,7 +175,6 @@ export default {
if (!this.timer_func) {
this.timer_func = setInterval(async () => {
this.track_service = await Api.service(this.service.id)
window.console.log(this.track_service.name)
}, this.track_service.check_interval * 1000)
}
}

View File

@ -67,23 +67,23 @@
<div class="card-footer">
<div class="row">
<div class="col-3">
<div class="col-12 col-md-3 mb-2 mb-md-0">
<router-link :to="{path: `/dashboard/service/${service.id}/incidents`, params: {id: service.id} }" class="btn btn-block btn-white incident">
Incidents
</router-link>
</div>
<div class="col-3">
<div class="col-12 col-md-3 mb-2 mb-md-0">
<router-link :to="{path: `/dashboard/service/${service.id}/checkins`, params: {id: service.id} }" class="btn btn-block btn-white checkins">
Checkins
</router-link>
</div>
<div class="col-3">
<div class="col-12 col-md-3 mb-2 mb-md-0">
<router-link :to="{path: `/dashboard/service/${service.id}/failures`, params: {id: service.id} }" class="btn btn-block btn-white failures">
Failures <span class="badge badge-danger float-right mt-1">{{service.stats.failures}}</span>
</router-link>
</div>
<div class="col-3 pt-2">
<span class="text-black-50 float-right">{{service.online_7_days}}% Uptime</span>
<div class="col-12 col-md-3 mb-2 mb-md-0 mt-0 mt-md-1">
<span class="text-black-50 float-md-right">{{service.online_7_days}}% Uptime</span>
</div>
</div>
@ -155,13 +155,6 @@
this.set2 = await this.getHits(24, "1h")
this.set2_name = this.calc(this.set2)
this.loaded = true
},
async deleteFailures() {
const c = confirm('Are you sure you want to delete all failures?')
if (c) {
await Api.service_failures_delete(this.service)
this.service = await Api.service(this.service.id)
}
},
Tab(name) {
if (this.openTab === name) {

View File

@ -42,6 +42,9 @@ export default Vue.mixin({
dur(t1, t2) {
return formatDistance(t1, t2)
},
format(val, type="EEEE, MMM do h:mma") {
return format(val, type)
},
niceDate(val) {
return format(parseISO(val), "EEEE, MMM do h:mma")
},
@ -119,6 +122,13 @@ export default Vue.mixin({
return "bars"
}
},
toBarData(data = []) {
let newSet = [];
data.forEach((f) => {
newSet.push([this.toUnix(this.parseISO(f.timeframe)), f.amount])
})
return newSet
},
convertToChartData(data = [], multiplier=1, asInt=false) {
if (!data) {
return {data: []}

View File

@ -21,69 +21,42 @@
</div>
<div class="row mt-5 mb-4">
<span class="col-6 font-2">
<flatPickr v-model="start_time" type="text" name="start_time" class="form-control form-control-plaintext" id="start_time" value="0001-01-01T00:00:00Z" required />
</span>
<span class="col-6 font-2">
<flatPickr v-model="end_time" type="text" name="end_time" class="form-control form-control-plaintext" id="end_time" value="0001-01-01T00:00:00Z" required />
</span>
<div class="col-12 col-md-5 font-2 mb-3 mb-md-0">
<flatPickr :disabled="loading" @on-change="onnn" v-model="start_time" :config="{ enableTime: true, altInput: true, altFormat: 'Y-m-d h:i K', maxDate: new Date() }" type="text" class="btn btn-white text-left" required />
<small class="d-block">From {{this.format(new Date(start_time))}}</small>
</div>
<div class="col-12 col-md-5 font-2 mb-3 mb-md-0">
<flatPickr :disabled="loading" @on-change="onnn" v-model="end_time" :config="{ enableTime: true, altInput: true, altFormat: 'Y-m-d h:i K', maxDate: new Date()}" type="text" class="btn btn-white text-left" required />
<small class="d-block">To {{this.format(new Date(end_time))}}</small>
</div>
<div class="col-12 col-md-2">
<select :disabled="loading" @change="chartHits" v-model="group" class="form-control">
<option value="1m">1 Minute</option>
<option value="5m">5 Minutes</option>
<option value="15m">15 Minute</option>
<option value="30m">30 Minutes</option>
<option value="1h">1 Hour</option>
<option value="3h">3 Hours</option>
<option value="6h">6 Hours</option>
<option value="12h">12 Hours</option>
<option value="24h">1 Day</option>
<option value="168h">7 Days</option>
<option value="360h">15 Days</option>
</select>
<small class="d-block d-md-none d-block">Increment Timeframe</small>
</div>
</div>
<div v-if="series" class="service-chart-container">
<apexchart width="100%" height="420" type="area" :options="chartOptions" :series="series"></apexchart>
<AdvancedChart :group="group" :updated="updated_chart" :start="start_time.toString()" :end="end_time.toString()" :service="service"/>
<div v-if="!loading" class="col-12">
<apexchart width="100%" height="120" type="rangeBar" :options="timeRangeOptions" :series="uptime_data"></apexchart>
</div>
<div class="service-chart-heatmap mt-5 mb-4">
<ServiceHeatmap :service="service"/>
</div>
<div v-if="load_timedata" class="col-12">
<apexchart width="100%" height="420" type="rangeBar" :options="timeRangeOptions" :series="rangeSeries"></apexchart>
</div>
<nav v-if="service.failures" class="nav nav-pills flex-column flex-sm-row mt-3" id="service_tabs">
<a @click="tab='failures'" class="flex-sm-fill text-sm-center nav-link active">Failures</a>
<a @click="tab='incidents'" class="flex-sm-fill text-sm-center nav-link">Incidents</a>
<a @click="tab='checkins'" v-if="$store.getters.token" class="flex-sm-fill text-sm-center nav-link">Checkins</a>
<a @click="tab='response'" v-if="$store.getters.token" class="flex-sm-fill text-sm-center nav-link">Response</a>
</nav>
<div v-if="service.failures" class="tab-content">
<div class="tab-pane fade active show">
<ServiceFailures :service="service"/>
</div>
<div class="tab-pane fade" :class="{active: tab === 'incidents'}" id="incidents">
</div>
<div class="tab-pane fade" :class="{show: tab === 'checkins'}" id="checkins">
<div class="card">
<div class="card-body">
<Checkin :service="service"/>
</div>
</div>
</div>
<div class="tab-pane fade" :class="{show: tab === 'response'}" id="response">
<div class="col-12 mt-4">
<h3>Last Response</h3>
<textarea rows="8" class="form-control" readonly>invalid route</textarea>
<div class="form-group row mt-2">
<label for="last_status_code" class="col-sm-3 col-form-label">HTTP Status Code</label>
<div class="col-sm-2">
<input type="text" id="last_status_code" class="form-control" value="200" readonly>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@ -99,6 +72,7 @@
import store from '../store'
import flatPickr from 'vue-flatpickr-component';
import 'flatpickr/dist/flatpickr.css';
import AdvancedChart from "@/components/Service/AdvancedChart";
const timeoptions = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' };
const axisOptions = {
@ -128,6 +102,7 @@
export default {
name: 'Service',
components: {
AdvancedChart,
ServiceTopStats,
ServiceHeatmap,
ServiceFailures,
@ -137,16 +112,18 @@ export default {
},
data() {
return {
id: 0,
tab: "failures",
authenticated: false,
ready: true,
group: "1h",
data: null,
uptime_data: null,
loading: true,
messages: [],
failures: [],
start_time: this.nowSubtract(84600 * 30),
end_time: new Date(),
timedata: [],
end_time: this.nowSubtract(0),
timedata: null,
load_timedata: false,
dailyRangeOpts: {
chart: {
@ -157,8 +134,21 @@ export default {
},
timeRangeOptions: {
chart: {
height: 200,
type: 'rangeBar'
id: 'uptime',
height: 120,
type: 'rangeBar',
toolbar: {
show: false
},
zoom: {
enabled: false
}
},
selection: {
enabled: true
},
zoom: {
enabled: true
},
plotOptions: {
bar: {
@ -170,16 +160,10 @@ export default {
}
},
dataLabels: {
enabled: true,
formatter: (val, opts) => {
var label = opts.w.globals.labels[opts.dataPointIndex]
var a = this.parseISO(val[0])
var b = this.parseISO(val[1])
return label
},
style: {
colors: ['#f3f4f5', '#fff']
}
enabled: false
},
tooltip: {
enabled: false,
},
xaxis: {
type: 'datetime'
@ -195,12 +179,32 @@ export default {
}
},
chartOptions: {
noData: {
text: "Loading...",
align: 'center',
verticalAlign: 'middle',
offsetX: 0,
offsetY: -20,
style: {
color: "#bababa",
fontSize: '27px'
}
},
chart: {
id: 'mainchart',
events: {
beforeZoom: async (chartContext, { xaxis }) => {
dataPointSelection: (event, chartContext, config) => {
window.console.log('slect')
window.console.log(event)
},
updated: (chartContext, config) => {
window.console.log('updated')
},
beforeZoom: (chartContext, { xaxis }) => {
const start = (xaxis.min / 1000).toFixed(0)
const end = (xaxis.max / 1000).toFixed(0)
await this.chartHits(start, end, "10m")
this.start_time = this.fromUnix(start)
this.end_time = this.fromUnix(end)
return {
xaxis: {
min: this.fromUnix(start),
@ -208,6 +212,9 @@ export default {
}
}
},
scrolled: (chartContext, { xaxis }) => {
window.console.log(xaxis)
},
},
height: 500,
width: "100%",
@ -233,20 +240,28 @@ export default {
lineCap: 'butt',
},
},
xaxis: {
type: "datetime",
labels: {
show: true
},
tooltip: {
enabled: true
}
xaxis: {
type: "datetime",
labels: {
show: true
},
yaxis: {
labels: {
show: true
},
tooltip: {
enabled: false
}
},
yaxis: {
labels: {
show: true
},
},
markers: {
size: 0,
strokeWidth: 0,
hover: {
size: undefined,
sizeOffset: 0
}
},
tooltip: {
theme: false,
enabled: true,
@ -259,7 +274,7 @@ export default {
} else {
val = (val * 0.01).toFixed(0) + " microseconds"
}
return `<div class="chartmarker"><span>Average Response Time: </span><span class="font-3">${val}</span><span>${dt}</span></div>`
return `<div class="chartmarker"><span>Response Time: </span><span class="font-3">${val}</span><span>${dt}</span></div>`
},
fixed: {
enabled: true,
@ -268,9 +283,8 @@ export default {
offsetY: 40,
},
x: {
show: false,
format: 'dd MMM',
formatter: undefined,
show: true,
},
y: {
formatter: undefined,
@ -320,63 +334,84 @@ export default {
core () {
return this.$store.getters.core
},
uptime_data() {
const data = this.timedata.series.filter(g => g.online)
const offData = this.timedata.series.filter(g => !g.online)
let arr = [];
data.forEach((d) => {
arr.push({
name: "Online", data: {
x: 'Online',
y: [
new Date(d.start).getTime(),
new Date(d.end).getTime()
],
fillColor: '#0db407'
}
})
})
offData.forEach((d) => {
arr.push({
name: "offline", data: {
x: 'Offline',
y: [
new Date(d.start).getTime(),
new Date(d.end).getTime()
],
fillColor: '#b40707'
}
})
})
return arr
params () {
return {start: this.toUnix(new Date(this.start_time)), end: this.toUnix(new Date(this.end_time))}
},
rangeSeries() {
return [{data: this.time_chart_data}]
id () {
return this.$route.params.id;
},
uptimeSeries () {
return this.timedata.series
},
mainChart () {
return [{
name: this.service.name,
...this.convertToChartData(this.data)
}]
}
},
watch: {
service: function(n, o) {
this.chartHits()
this.fetchUptime()
this.onnn()
},
load_timedata: function(n, o) {
this.chartHits()
this.onnn()
}
},
async created() {
this.id = this.$route.params.id;
},
async mounted() {
if (!this.$store.getters.service) {
const s = await Api.service(this.id)
this.$store.commit('setService', s)
}
},
methods: {
async updated_chart(start, end) {
this.start_time = start
this.end_time = end
this.loading = false
},
async onnn() {
this.loading = true
await this.chartHits()
await this.fetchUptime()
this.loading = false
},
async fetchUptime() {
this.timedata = await Api.service_uptime(this.id)
this.load_timedata = true
},
async get() {
const s = this.$store.getters.serviceByAll(this.id)
window.console.log("service: ", s)
this.getService(this.service)
this.messages = this.$store.getters.serviceMessages(this.service.id)
},
const uptime = await Api.service_uptime(this.id, this.params.start, this.params.end)
window.console.log(uptime)
this.uptime_data = this.parse_uptime(uptime)
},
parse_uptime(timedata) {
const data = timedata.series.filter((g) => g.online) || []
const offData = timedata.series.filter((g) => !g.online) || []
let arr = [];
window.console.log(data)
if (data) {
data.forEach((d) => {
arr.push({
x: 'Online',
y: [
new Date(d.start).getTime(),
new Date(d.end).getTime()
],
fillColor: '#0db407'
})
})
}
if (offData) {
offData.forEach((d) => {
arr.push({
x: 'Offline',
y: [
new Date(d.start).getTime(),
new Date(d.end).getTime()
],
fillColor: '#b40707'
})
})
}
return [{data: arr}]
},
messageInRange(message) {
const start = this.isBetween(new Date(), message.start_on)
const end = this.isBetween(message.end_on, new Date())
@ -387,31 +422,15 @@ export default {
await this.serviceFailures()
},
async serviceFailures() {
let tt = this.startEndTimes()
this.failures = await Api.service_failures(this.service.id, tt.start, tt.end)
this.failures = await Api.service_failures(this.service.id, this.params.start, this.params.end)
},
async chartHits(start=0, end=99999999999, group="30m") {
let tt = {};
if (start === 0) {
tt = this.startEndTimes()
} else {
tt = {start, end}
}
this.data = await Api.service_hits(this.service.id, tt.start, tt.end, group, false)
if (this.data.length === 0 && group !== "1h") {
async chartHits(start=0, end=99999999999) {
this.data = await Api.service_hits(this.service.id, this.params.start, this.params.end, this.group, false)
if (this.data.length === 0 && this.group !== "1h") {
this.group = "1h"
await this.chartHits("1h")
}
this.series = [{
name: this.service.name,
...this.convertToChartData(this.data)
}]
this.ready = true
},
startEndTimes() {
const start = this.toUnix(this.service.stats.first_hit)
const end = this.toUnix(new Date())
return {start, end}
}
}
}

View File

@ -13,9 +13,9 @@ import VueRouter from "vue-router";
import Setup from "./forms/Setup";
import Api from "./API";
import Incidents from "@/pages/Incidents";
import Checkins from "@/pages/Checkins";
import Failures from "@/pages/Failures";
import Incidents from "@/components/Dashboard/Incidents";
import Checkins from "@/components/Dashboard/Checkins";
import Failures from "@/components/Dashboard/Failures";
const routes = [
{

View File

@ -22,6 +22,7 @@ export default new Vuex.Store({
core: {},
token: null,
services: [],
service: null,
groups: [],
messages: [],
users: [],
@ -36,6 +37,7 @@ export default new Vuex.Store({
core: state => state.core,
token: state => state.token,
services: state => state.services,
service: state => state.service,
groups: state => state.groups,
messages: state => state.messages,
incidents: state => state.incidents,
@ -104,6 +106,9 @@ export default new Vuex.Store({
setToken (state, token) {
state.token = token
},
setService (state, service) {
state.service = service
},
setServices (state, services) {
state.services = services
},

View File

@ -76,9 +76,6 @@ func apiCoreHandler(w http.ResponseWriter, r *http.Request) {
if c.Domain != app.Domain {
app.Domain = c.Domain
}
if c.Timezone != app.Timezone {
app.Timezone = c.Timezone
}
app.OAuth = c.OAuth
app.UseCdn = null.NewNullBool(c.UseCdn.Bool)
app.AllowReports = null.NewNullBool(c.AllowReports.Bool)

View File

@ -88,8 +88,6 @@ func (s Storage) List() map[string]Item {
//Get a cached content by key
func (s Storage) Get(key string) []byte {
s.mu.Lock()
defer s.mu.Unlock()
item := s.items[key]
if item.Expired() {
CacheStorage.Delete(key)

View File

@ -9,8 +9,6 @@ import (
"github.com/statping/statping/types/services"
"github.com/statping/statping/utils"
"net/http"
"sort"
"time"
)
type serviceOrder struct {
@ -185,131 +183,37 @@ func apiServiceTimeDataHandler(w http.ResponseWriter, r *http.Request) {
return
}
allFailures := service.AllFailures()
allHits := service.AllHits()
tMap := make(map[time.Time]bool)
for _, v := range allHits.List() {
tMap[v.CreatedAt] = true
}
for _, v := range allFailures.List() {
tMap[v.CreatedAt] = false
groupHits, err := database.ParseQueries(r, service.AllHits())
if err != nil {
sendErrorJson(err, w, r)
return
}
var servs []ser
for t, v := range tMap {
s := ser{
Time: t,
Online: v,
}
servs = append(servs, s)
groupFailures, err := database.ParseQueries(r, service.AllFailures())
if err != nil {
sendErrorJson(err, w, r)
return
}
sort.Sort(ByTime(servs))
var allFailures []*failures.Failure
var allHits []*hits.Hit
var allTimes []series
online := servs[0].Online
thisTime := servs[0].Time
for i := 0; i < len(servs); i++ {
v := servs[i]
if v.Online != online {
s := series{
Start: thisTime,
End: v.Time,
Duration: v.Time.Sub(thisTime).Milliseconds(),
Online: online,
}
allTimes = append(allTimes, s)
thisTime = v.Time
online = v.Online
}
if err := groupHits.Find(&allHits); err != nil {
sendErrorJson(err, w, r)
return
}
first := servs[0].Time
last := servs[len(servs)-1].Time
if !service.Online {
s := series{
Start: allTimes[len(allTimes)-1].End,
End: utils.Now(),
Duration: utils.Now().Sub(last).Milliseconds(),
Online: service.Online,
}
allTimes = append(allTimes, s)
} else {
l := allTimes[len(allTimes)-1]
allTimes[len(allTimes)-1] = series{
Start: l.Start,
End: utils.Now(),
Duration: utils.Now().Sub(l.Start).Milliseconds(),
Online: true,
}
if err := groupFailures.Find(&allFailures); err != nil {
sendErrorJson(err, w, r)
return
}
jj := uptimeSeries{
Start: first,
End: last,
Uptime: addDurations(allTimes, true),
Downtime: addDurations(allTimes, false),
Series: allTimes,
uptimeData, err := service.UptimeData(allHits, allFailures)
if err != nil {
sendErrorJson(err, w, r)
return
}
returnJson(jj, w, r)
}
type ByTime []ser
func (a ByTime) Len() int { return len(a) }
func (a ByTime) Less(i, j int) bool { return a[i].Time.Before(a[j].Time) }
func (a ByTime) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func addDurations(s []series, on bool) int64 {
var dur int64
for _, v := range s {
if v.Online == on {
dur += v.Duration
}
}
return dur
}
type ser struct {
Time time.Time
Online bool
}
func findNextFailure(m map[time.Time]bool, after time.Time, online bool) time.Time {
for k, v := range m {
if k.After(after) && v == online {
return k
}
}
return time.Time{}
}
//func calculateDuration(m map[time.Time]bool, on bool) time.Duration {
// var t time.Duration
// for t, v := range m {
// if v == on {
// t.
// }
// }
//}
type uptimeSeries struct {
Start time.Time `json:"start"`
End time.Time `json:"end"`
Uptime int64 `json:"uptime"`
Downtime int64 `json:"downtime"`
Series []series `json:"series"`
}
type series struct {
Start time.Time `json:"start"`
End time.Time `json:"end"`
Duration int64 `json:"duration"`
Online bool `json:"online"`
returnJson(uptimeData, w, r)
}
func apiServiceDeleteHandler(w http.ResponseWriter, r *http.Request) {

View File

@ -32,7 +32,6 @@ type Core struct {
Setup bool `gorm:"-" json:"setup"`
MigrationId int64 `gorm:"column:migration_id" json:"migration_id,omitempty"`
UseCdn null.NullBool `gorm:"column:use_cdn;default:false" json:"using_cdn,omitempty"`
Timezone float32 `gorm:"column:timezone;default:-8.0" json:"timezone,omitempty"`
LoggedIn bool `gorm:"-" json:"logged_in"`
IsAdmin bool `gorm:"-" json:"admin"`
AllowReports null.NullBool `gorm:"column:allow_reports;default:false" json:"allow_reports"`

View File

@ -3,11 +3,15 @@ package services
import (
"crypto/sha1"
"encoding/hex"
"errors"
"fmt"
"github.com/statping/statping/types"
"github.com/statping/statping/types/failures"
"github.com/statping/statping/types/hits"
"github.com/statping/statping/types/null"
"github.com/statping/statping/utils"
"net/url"
"sort"
"strconv"
"strings"
"time"
@ -19,6 +23,145 @@ func (s *Service) Duration() time.Duration {
return time.Duration(s.Interval) * time.Second
}
// Start will create a channel for the service checking go routine
func (s *Service) UptimeData(hits []*hits.Hit, fails []*failures.Failure) (*UptimeSeries, error) {
if len(hits) == 0 {
return nil, errors.New("service does not have any successful hits")
}
// if theres no failures, then its been online 100%,
// return a series from created time, to current.
if len(fails) == 0 {
fistHit := hits[0]
duration := utils.Now().Sub(fistHit.CreatedAt).Milliseconds()
set := []series{
{
Start: fistHit.CreatedAt,
End: utils.Now(),
Duration: duration,
Online: true,
},
}
out := &UptimeSeries{
Start: fistHit.CreatedAt,
End: utils.Now(),
Uptime: duration,
Downtime: 0,
Series: set,
}
return out, nil
}
tMap := make(map[time.Time]bool)
for _, v := range hits {
tMap[v.CreatedAt] = true
}
for _, v := range fails {
tMap[v.CreatedAt] = false
}
var servs []ser
for t, v := range tMap {
s := ser{
Time: t,
Online: v,
}
servs = append(servs, s)
}
if len(servs) == 0 {
return nil, errors.New("error generating uptime data structure")
}
sort.Sort(ByTime(servs))
var allTimes []series
online := servs[0].Online
thisTime := servs[0].Time
for i := 0; i < len(servs); i++ {
v := servs[i]
if v.Online != online {
s := series{
Start: thisTime,
End: v.Time,
Duration: v.Time.Sub(thisTime).Milliseconds(),
Online: online,
}
allTimes = append(allTimes, s)
thisTime = v.Time
online = v.Online
}
}
if len(allTimes) == 0 {
return nil, errors.New("error generating uptime series structure")
}
first := servs[0].Time
last := servs[len(servs)-1].Time
if !s.Online {
s := series{
Start: allTimes[len(allTimes)-1].End,
End: utils.Now(),
Duration: utils.Now().Sub(last).Milliseconds(),
Online: s.Online,
}
allTimes = append(allTimes, s)
} else {
l := allTimes[len(allTimes)-1]
s := series{
Start: l.Start,
End: utils.Now(),
Duration: utils.Now().Sub(l.Start).Milliseconds(),
Online: true,
}
allTimes = append(allTimes, s)
}
response := &UptimeSeries{
Start: first,
End: last,
Uptime: addDurations(allTimes, true),
Downtime: addDurations(allTimes, false),
Series: allTimes,
}
return response, nil
}
func addDurations(s []series, on bool) int64 {
var dur int64
for _, v := range s {
if v.Online == on {
dur += v.Duration
}
}
return dur
}
type ser struct {
Time time.Time
Online bool
}
type UptimeSeries struct {
Start time.Time `json:"start"`
End time.Time `json:"end"`
Uptime int64 `json:"uptime"`
Downtime int64 `json:"downtime"`
Series []series `json:"series"`
}
type ByTime []ser
func (a ByTime) Len() int { return len(a) }
func (a ByTime) Less(i, j int) bool { return a[i].Time.Before(a[j].Time) }
func (a ByTime) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
type series struct {
Start time.Time `json:"start"`
End time.Time `json:"end"`
Duration int64 `json:"duration"`
Online bool `json:"online"`
}
// Start will create a channel for the service checking go routine
func (s *Service) Start() {
if s.IsRunning() {

View File

@ -1 +1 @@
0.90.25
0.90.26