pull/429/head
Hunter Long 2020-03-10 21:37:52 -07:00
parent b0ea8b3621
commit bbf8073315
23 changed files with 571 additions and 205 deletions

View File

@ -131,10 +131,6 @@ func main() {
exit(err) exit(err)
} }
if err = confgs.VerifyMigration(); err != nil {
exit(err)
}
exists := confgs.Db.HasTable("core") exists := confgs.Db.HasTable("core")
if !exists { if !exists {
var srvs int64 var srvs int64
@ -161,6 +157,10 @@ func main() {
} }
if err = confgs.DatabaseChanges(); err != nil {
exit(err)
}
if err := confgs.MigrateDatabase(); err != nil { if err := confgs.MigrateDatabase(); err != nil {
exit(err) exit(err)
} }

View File

@ -1,6 +1,7 @@
package database package database
import ( import (
"errors"
"fmt" "fmt"
"github.com/statping/statping/types" "github.com/statping/statping/types"
"github.com/statping/statping/utils" "github.com/statping/statping/utils"
@ -144,7 +145,7 @@ type isObject interface {
Db() Database Db() Database
} }
func ParseQueries(r *http.Request, o isObject) *GroupQuery { func ParseQueries(r *http.Request, o isObject) (*GroupQuery, error) {
fields := parseGet(r) fields := parseGet(r)
grouping := fields.Get("group") grouping := fields.Get("group")
startField := utils.ToInt(fields.Get("start")) startField := utils.ToInt(fields.Get("start"))
@ -179,11 +180,15 @@ func ParseQueries(r *http.Request, o isObject) *GroupQuery {
db: q, db: q,
} }
if query.Start.After(query.End) {
return nil, errors.New("start time is after ending time")
}
if startField == 0 { if startField == 0 {
query.Start = time.Now().Add(-7 * types.Day).UTC() query.Start = utils.Now().Add(-7 * types.Day)
} }
if endField == 0 { if endField == 0 {
query.End = time.Now().UTC() query.End = utils.Now()
} }
if query.End.After(utils.Now()) { if query.End.After(utils.Now()) {
query.End = utils.Now() query.End = utils.Now()
@ -203,7 +208,7 @@ func ParseQueries(r *http.Request, o isObject) *GroupQuery {
} }
query.db = q query.db = q
return query return query, nil
} }
func parseForm(r *http.Request) url.Values { func parseForm(r *http.Request) url.Values {

View File

@ -1,6 +1,6 @@
<template> <template>
<div id="app"> <div id="app">
<router-view :loaded="loaded"/> <router-view :app="app" :loaded="loaded"/>
<Footer :logged_in="logged_in" :version="version" v-if="$route.path !== '/setup'"/> <Footer :logged_in="logged_in" :version="version" v-if="$route.path !== '/setup'"/>
</div> </div>
</template> </template>
@ -19,10 +19,13 @@
loaded: false, loaded: false,
version: "", version: "",
logged_in: false, logged_in: false,
app: null
} }
}, },
async created() { async created() {
await this.$store.dispatch('loadRequired') this.app = await this.$store.dispatch('loadRequired')
this.app = {...this.$store.state}
if (this.$store.getters.core.logged_in) { if (this.$store.getters.core.logged_in) {
await this.$store.dispatch('loadAdmin') await this.$store.dispatch('loadAdmin')

View File

@ -6,7 +6,34 @@ HTML,BODY {
} }
.index-chart { .index-chart {
height: 490px; height: $service-card-height;
}
.sub-service-card {
border: 1px solid #dcdcdc87;
padding-top: 7px;
padding-bottom: 10px;
height: 155px;
}
.expanded-service {
height: 89vh;
/*Animation*/
-webkit-transition: height 2s ease;
-moz-transition: height 2s ease;
-o-transition: height 2s ease;
-ms-transition: height 2s ease;
transition: height 2s ease;
.stats_area {
display: none;
}
}
.service-chart {
bottom: -15px;
position: relative;
} }
.chartmarker { .chartmarker {
@ -167,7 +194,19 @@ HTML,BODY {
} }
.font-5 { .font-5 {
font-size: 20pt; font-size: 17pt;
}
.font-6 {
font-size: 24pt;
}
.font-7 {
font-size: 31pt;
}
.font-8 {
font-size: 38pt;
} }
.badge { .badge {
@ -225,6 +264,7 @@ HTML,BODY {
.card-body H4 A { .card-body H4 A {
color: $service-title; color: $service-title;
font-size: $service-title-size;
text-decoration: none; text-decoration: none;
} }
@ -248,7 +288,7 @@ HTML,BODY {
.service-chart-heatmap { .service-chart-heatmap {
position: relative; position: relative;
height: 300px; height: 180px;
width: 100%; width: 100%;
} }

View File

@ -7,7 +7,7 @@
} }
.index-chart { .index-chart {
height: 33vh; height: 380px;
} }
.sm-container { .sm-container {

View File

@ -8,9 +8,11 @@ $description-color: #939393;
$service-background: #ffffff; $service-background: #ffffff;
$service-border: 1px solid rgba(0,0,0,.125); $service-border: 1px solid rgba(0,0,0,.125);
$service-title: #444444; $service-title: #444444;
$service-title-size: 1.8rem;
$service-stats-color: #4f4f4f; $service-stats-color: #4f4f4f;
$service-description-color: #fff; $service-description-color: #fff;
$service-stats-size: 2.3rem; $service-stats-size: 2.3rem;
$service-card-height: 490px;
/* Button Colors */ /* Button Colors */
$success-color: #47d337; $success-color: #47d337;
@ -25,7 +27,6 @@ $footer-display: block;
/* Global Settings */ /* Global Settings */
$global-border-radius: 0.2rem; $global-border-radius: 0.2rem;
/* Mobile Settings */ /* Mobile Settings */
$sm-background-color: #fcfcfc; $sm-background-color: #fcfcfc;
$sm-border-radius: 0rem; $sm-border-radius: 0rem;

View File

@ -5,7 +5,7 @@
<a v-for="(service, index) in $store.getters.servicesInGroup(group.id)" v-bind:key="index" class="service_li list-group-item list-group-item-action"> <a v-for="(service, index) in $store.getters.servicesInGroup(group.id)" v-bind:key="index" class="service_li list-group-item list-group-item-action">
<router-link class="no-decoration" :to="serviceLink(service)">{{service.name}}</router-link> <router-link class="no-decoration" :to="serviceLink(service)">{{service.name}}</router-link>
<span class="badge bg-success float-right">{{service.online ? "ONLINE" : "OFFLINE"}}</span> <span class="badge float-right" :class="{'bg-success': service.online, 'bg-danger': !service.online }">{{service.online ? "ONLINE" : "OFFLINE"}}</span>
<GroupServiceFailures :service="service"/> <GroupServiceFailures :service="service"/>
</a> </a>

View File

@ -1,6 +1,6 @@
<template> <template>
<div> <div>
<h1 class="col-12 text-center mb-4 mt-sm-3 header-title">{{$store.getters.core.name}}</h1> <h1 class="col-12 text-center pt-4 mt-4 header-title">{{$store.getters.core.name}}</h1>
<h5 class="col-12 text-center mb-5 header-desc">{{$store.getters.core.description}}</h5> <h5 class="col-12 text-center mb-5 header-desc">{{$store.getters.core.description}}</h5>
</div> </div>
</template> </template>

View File

@ -1,13 +1,13 @@
<template> <template>
<div class="alert alert-primary" role="alert"> <div class="alert alert-primary pb-4 pt-3 mt-5 mb-5" role="alert">
<h3>{{message.title}}</h3> <h3 class="mb-3">{{message.title}}</h3>
<span class="mb-3">{{message.description}}</span> <span class="mb-3">{{message.description}}</span>
<div class="d-block mt-2 mb-4"> <div class="row d-block mt-3">
<span class="float-left small"> <span class="col-12 col-md-6 text-left small">
Started {{toLocal(message.start_on)}} ({{duration(new Date(), message.start_on)}} ago) Started {{niceDate(message.start_on)}} ({{ago(parseISO(message.start_on))}} ago)
</span> </span>
<span class="float-right small"> <span class="col-12 col-md-6 text-right float-right small">
Ends on {{toLocal(message.end_on)}} (in {{duration(message.end_on, new Date())}})</span> Ends on {{niceDate(message.end_on)}} (in {{ago(parseISO(message.end_on))}})</span>
</div> </div>
</div> </div>
</template> </template>

View File

@ -0,0 +1,74 @@
<template>
<div class="col-6 mt-4">
<div class="col-12 sub-service-card">
<div class="col-8 float-left p-0 mt-1 mb-3">
<span class="font-5 d-block">{{title}}</span>
<span class="text-muted font-3 d-block font-weight-bold">{{subtitle}}</span>
</div>
<div class="col-4 float-right text-right mt-2 p-0">
<span class="text-success font-5 font-weight-bold">{{value}}</span>
</div>
<MiniSparkLine :series="[{name: 'okokokok', data:[{x: '2019-01-01', y: 120},{x: '2019-01-02', y: 160},{x: '2019-01-03', y: 240},{x: '2019-01-04', y: 45}]}]"/>
</div>
</div>
</template>
<script>
import Api from "../../API";
import MiniSparkLine from './MiniSparkLine';
import ServiceSparkLine from './ServiceSparkLine';
export default {
name: 'Analytics',
components: { MiniSparkLine, ServiceSparkLine },
props: {
title: {
type: String,
required: true
},
subtitle: {
type: String,
required: true
},
value: {
type: Number,
required: true
},
level: {
type: Number,
required: false
}
},
data() {
return {
}
},
async mounted() {
await this.latencyYesterday();
},
async latencyYesterday() {
const todayTime = await Api.service_hits(this.service.id, this.toUnix(this.nowSubtract(86400)), this.toUnix(new Date()), this.group, false)
const fetched = await Api.service_hits(this.service.id, this.start, this.end, this.group, false)
let todayAmount = this.addAmounts(todayTime)
let yesterday = this.addAmounts(fetched)
window.console.log(todayAmount)
window.console.log(yesterday)
},
addAmounts(data) {
let total = 0
data.forEach((f) => {
total += parseInt(f.amount)
});
return total
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

View File

@ -0,0 +1,85 @@
<template v-if="series.length">
<apexchart width="100%" height="70" type="bar" :options="chartOpts" :series="series"></apexchart>
</template>
<script>
const timeoptions = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' };
export default {
name: 'MiniSparkLine',
props: {
series: {
type: Array,
default: []
},
title: {
type: String,
},
subtitle: {
type: String,
}
},
watch: {
title () {
},
subtitle () {
}
},
data() {
return {
chartOpts: {
chart: {
type: 'bar',
height: 180,
sparkline: {
enabled: true
},
},
stroke: {
curve: 'straight'
},
fill: {
opacity: 0.3,
},
yaxis: {
min: 0
},
colors: ['#b3bdc3'],
tooltip: {
theme: false,
enabled: false,
x: {
show: false,
},
y: {
formatter: (value) => { return value + "%" },
},
},
title: {
text: this.title,
offsetX: 0,
style: {
fontSize: '28px',
cssClass: 'apexcharts-yaxis-title'
}
},
subtitle: {
text: this.subtitle,
offsetX: 0,
style: {
fontSize: '14px',
cssClass: 'apexcharts-yaxis-title'
}
}
}
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

View File

@ -1,26 +1,35 @@
<template> <template>
<div class="mb-4"> <div class="mb-md-4 mb-5">
<div class="card index-chart"> <div class="card index-chart" :class="{'expanded-service': expanded}">
<div class="card-body"> <div class="card-body">
<div class="col-12"> <div class="col-12">
<h4 class="mt-3"> <h4 class="mt-3">
<router-link :to="serviceLink(service)" :in_service="service">{{service.name}}</router-link> <router-link :to="serviceLink(service)" class="d-inline-block text-truncate" style="max-width: 65vw;" :in_service="service">{{service.name}}</router-link>
<span class="badge float-right" :class="{'bg-success': service.online, 'bg-danger': !service.online}">{{service.online ? "ONLINE" : "OFFLINE"}}</span> <span class="badge float-right" :class="{'bg-success': service.online, 'bg-danger': !service.online}">{{service.online ? "ONLINE" : "OFFLINE"}}</span>
</h4> </h4>
<ServiceTopStats :service="service"/> <ServiceTopStats :service="service"/>
<div v-if="expanded" class="row">
<Analytics title="Last Failure" level="100" value="35%" subtitle="417 Days ago"/>
<Analytics title="Total Failures" level="100" value="35%" subtitle="417 Days ago"/>
<Analytics title="Highest Latency" level="100" value="450ms" subtitle="417 Days ago"/>
<Analytics title="Lowest Latency" level="100" value="120ms" subtitle="417 Days ago"/>
<Analytics title="Total Uptime" level="100" value="35%" subtitle="850ms"/>
<Analytics title="Total Downtime" level="100" value="35%" subtitle="32ms"/>
</div>
</div> </div>
</div> </div>
<div v-observe-visibility="visibleChart" class="chart-container"> <div v-if="!expanded" v-observe-visibility="visibleChart" class="chart-container">
<ServiceChart :service="service" :visible="visible"/> <ServiceChart :service="service" :visible="visible"/>
</div> </div>
<div class="row lower_canvas full-col-12 text-white" :class="{'bg-success': service.online, 'bg-danger': !service.online}"> <div class="row lower_canvas full-col-12 text-white" :class="{'bg-success': service.online, 'bg-danger': !service.online}">
<div class="col-md-8 col-6"> <div class="col-md-8 col-6">
<div class="dropup" :class="{show: dropDownMenu}"> <div class="dropup" :class="{show: dropDownMenu}">
<button style="font-size: 10pt;" @focusout="dropDownMenu = false" @click="dropDownMenu = !dropDownMenu" type="button" class="col-4 float-left btn btn-sm float-right btn-block text-white dropdown-toggle service_scale" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <button style="font-size: 10pt;" @focusout="dropDownMenu = false" @click="dropDownMenu = !dropDownMenu" type="button" class="d-none col-4 float-left btn btn-sm float-right btn-block text-white dropdown-toggle service_scale" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
24 Hours 24 Hours
</button> </button>
<div class="dropdown-menu" :class="{show: dropDownMenu}"> <div class="dropdown-menu" :class="{show: dropDownMenu}">
@ -34,8 +43,13 @@
</div> </div>
<div class="col-md-4 col-6 float-right"> <div class="col-md-4 col-6 float-right">
<router-link :to="serviceLink(service)" class="btn btn-sm float-right dyn-dark btn-block text-white" :class="{'bg-success': service.online, 'bg-danger': !service.online}"> <router-link :to="serviceLink(service)" class="d-none btn btn-sm float-right dyn-dark btn-block text-white" :class="{'bg-success': service.online, 'bg-danger': !service.online}">
View Service</router-link> View Service</router-link>
<button @click="expanded = !expanded" class="btn btn-sm float-right dyn-dark btn-block text-white" :class="{'bg-success': service.online, 'bg-danger': !service.online}">View Service</button>
</div>
<div v-if="expanded" class="row">
<Analytics title="Last Failure" value="417 Days ago"/>
</div> </div>
</div> </div>
@ -44,12 +58,13 @@
</template> </template>
<script> <script>
import Analytics from './Analytics';
import ServiceChart from "./ServiceChart"; import ServiceChart from "./ServiceChart";
import ServiceTopStats from "@/components/Service/ServiceTopStats"; import ServiceTopStats from "@/components/Service/ServiceTopStats";
export default { export default {
name: 'ServiceBlock', name: 'ServiceBlock',
components: {ServiceTopStats, ServiceChart}, components: { Analytics, ServiceTopStats, ServiceChart},
props: { props: {
service: { service: {
type: Object, type: Object,
@ -58,6 +73,7 @@ export default {
}, },
data() { data() {
return { return {
expanded: false,
visible: false, visible: false,
dropDownMenu: false, dropDownMenu: false,
timeframes: [ timeframes: [

View File

@ -1,5 +1,5 @@
<template v-show="showing"> <template v-show="showing">
<apexchart v-if="ready" width="100%" height="235" type="area" :options="chartOptions" :series="series"/> <apexchart v-if="ready" class="service-chart" width="100%" height="100%" type="area" :options="chartOptions" :series="series"/>
</template> </template>
<script> <script>
@ -49,7 +49,7 @@
text: 'Loading...' text: 'Loading...'
}, },
chart: { chart: {
height: 210, height: "100%",
width: "100%", width: "100%",
type: "area", type: "area",
animations: { animations: {
@ -163,14 +163,14 @@
visible: function(newVal, oldVal) { visible: function(newVal, oldVal) {
if (newVal && !this.showing) { if (newVal && !this.showing) {
this.showing = true this.showing = true
this.chartHits("2h") this.chartHits("1h")
} }
} }
}, },
methods: { methods: {
async chartHits(group) { async chartHits(group) {
window.console.log(this.service.created_at) const start = this.nowSubtract(84600 * 3)
this.data = await Api.service_hits(this.service.id, this.toUnix(this.service.created_at), this.toUnix(new Date()), group, false) this.data = await Api.service_hits(this.service.id, this.toUnix(start), this.toUnix(new Date()), group, false)
if (this.data.length === 0 && group !== "1h") { if (this.data.length === 0 && group !== "1h") {
await this.chartHits("1h") await this.chartHits("1h")

View File

@ -1,5 +1,5 @@
<template> <template>
<apexchart v-if="ready" width="100%" height="300" type="heatmap" :options="chartOptions" :series="series"></apexchart> <apexchart v-if="ready" width="100%" height="180" type="heatmap" :options="plotOptions" :series="series"></apexchart>
</template> </template>
<script> <script>
@ -20,19 +20,32 @@
return { return {
ready: false, ready: false,
data: [], data: [],
chartOptions: { plotOptions: {
heatmap: { chart: {
selection: {
enabled: false
},
zoom: {
enabled: false
},
toolbar: {
show: false
},
},
colors: [ "#cb3d36" ],
enableShades: true,
shadeIntensity: 0.5,
colorScale: { colorScale: {
ranges: [{ ranges: [ {
from: 0, from: 0,
to: 1, to: 0,
color: 'rgba(235,63,48,0.69)', color: '#bababa',
name: 'low', name: 'none',
}, },
{ {
from: 2, from: 2,
to: 10, to: 10,
color: 'rgba(245,43,43,0.58)', color: '#cb3d36',
name: 'medium', name: 'medium',
}, },
{ {
@ -42,76 +55,66 @@
name: 'high', name: 'high',
} }
] ]
}
}, },
chart: { xaxis: {
height: "100%", tickAmount: '1',
width: "100%", tickPlacement: 'between',
type: 'heatmap', min: 1,
toolbar: { max: 31,
show: false type: "numeric",
}
},
dataLabels: {
enabled: false,
},
enableShades: true,
shadeIntensity: 0.5,
colors: ["#d53a3b"],
series: [{data: [{}]}],
yaxis: {
labels: { labels: {
formatter: (value) => { show: true
return value
},
},
}, },
tooltip: { tooltip: {
enabled: true, enabled: false
x: { }
show: false,
},
y: {
formatter: function(val, opts) { return val+" Failures" },
title: {
formatter: (seriesName) => seriesName,
}, },
yaxis: {
labels: {
show: true
}, },
} }
}, },
series: [{ series: [ {
data: [] data: []
}] } ]
} }
}, },
methods: { methods: {
async chartHeatmap() { async chartHeatmap() {
let start = new Date(new Date().getUTCFullYear(), new Date().getUTCMonth()-3, 1); let start = new Date(new Date().getUTCFullYear(), new Date().getUTCMonth()-2, 1);
let monthData = []; let monthData = [];
let monthNum = start.getUTCMonth()
for (i=0; i<=3; i++) { for (let i=1; i<=3; i++) {
let end = this.lastDayOfMonth(start.getUTCMonth()+start) let end = this.lastDayOfMonth(monthNum)
const inputdata = this.heatmapData(start,end)
window.console.log("getting: ",start, end)
const inputdata = await this.heatmapData(start, end)
monthData.push(inputdata) monthData.push(inputdata)
start = new Date(start.getUTCFullYear(), start.getUTCMonth()+1, 1);
monthNum += 1
} }
this.series = monthData window.console.log(monthData)
this.series = monthData.reverse()
this.ready = true this.ready = true
}, },
async heatmapData(start, end) { async heatmapData(start, end) {
console.log(start, end)
window.console.log("start: ", start)
window.console.log("end: ", end)
const data = await Api.service_failures_data(this.service.id, this.toUnix(start), this.toUnix(end), "24h", true) const data = await Api.service_failures_data(this.service.id, this.toUnix(start), this.toUnix(end), "24h", true)
let dataArr = [] let dataArr = []
data.forEach(function(d) { data.forEach((d) => {
dataArr.push({x: d.timeframe, y: d.amount}); dataArr.push({x: this.parseTime(d.timeframe).getUTCDate(), y: d.amount});
}); });
let date = new Date(dataArr[0].x); let date = new Date(dataArr[0].x);
const output = [{name: date.toLocaleString('en-us', { month: 'long'}), data: dataArr}] return {name: start.toLocaleString('en-us', { month: 'long'}), data: dataArr}
} }
} }
} }

View File

@ -1,5 +1,8 @@
import Vue from "vue"; import Vue from "vue";
const { zonedTimeToUtc, utcToZonedTime, lastDayOfMonth, subSeconds, parse, parseISO, getUnixTime, fromUnixTime, format, differenceInSeconds, formatDistanceToNow, formatDistance } = require('date-fns') const { zonedTimeToUtc, utcToZonedTime, lastDayOfMonth, subSeconds, parse, getUnixTime, fromUnixTime, differenceInSeconds, formatDistance } = require('date-fns')
import formatDistanceToNow from 'date-fns/formatDistanceToNow'
import format from 'date-fns/format'
import parseISO from 'date-fns/parseISO'
export default Vue.mixin({ export default Vue.mixin({
methods: { methods: {
@ -7,10 +10,10 @@ export default Vue.mixin({
return new Date() return new Date()
}, },
current() { current() {
return parse(new Date()) return parseISO(new Date())
}, },
utc(val) { utc(val) {
return fromUnixTime(this.toUnix(val) + val.getTimezoneOffset() * 60 * 1000) return new Date.UTC(val)
}, },
ago(t1) { ago(t1) {
return formatDistanceToNow(t1) return formatDistanceToNow(t1)
@ -25,11 +28,14 @@ export default Vue.mixin({
return formatDistance(t1, t2) return formatDistance(t1, t2)
}, },
niceDate(val) { niceDate(val) {
return this.parseTime(val).format('LLLL') return format(parseISO(val), "EEEE, MMM do h:mma")
}, },
parseTime(val) { parseTime(val) {
return parseISO(val) return parseISO(val)
}, },
parseISO(v) {
return parseISO(v)
},
toLocal(val, suf = 'at') { toLocal(val, suf = 'at') {
const t = this.parseTime(val) const t = this.parseTime(val)
return format(t, `EEEE, MMM do h:mma`) return format(t, `EEEE, MMM do h:mma`)
@ -41,7 +47,7 @@ export default Vue.mixin({
return fromUnixTime(val) return fromUnixTime(val)
}, },
isBetween(t1, t2) { isBetween(t1, t2) {
return differenceInSeconds(parseISO(t1), parseISO(t2)) > 0 return differenceInSeconds(t1, t2) >= 0
}, },
hour() { hour() {
return 3600 return 3600
@ -111,10 +117,10 @@ export default Vue.mixin({
return {data: newSet} return {data: newSet}
}, },
lastDayOfMonth(month) { lastDayOfMonth(month) {
return new Date(new Date().getUTCFullYear(), month + 1, 0); return new Date(Date.UTC(new Date().getUTCFullYear(), month + 1, 0))
}, },
firstDayOfMonth(month) { firstDayOfMonth(month) {
return new Date(new Date().getUTCFullYear(), month, 1).getUTCDate(); return new Date(Date.UTC(new Date().getUTCFullYear(), month, 1)).getUTCDate()
} }
} }
}); });

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="container col-md-7 col-sm-12 mt-4 sm-container index_container pt-5"> <div class="container col-md-7 col-sm-12 sm-container index_container">
<Header/> <Header/>

View File

@ -1,5 +1,5 @@
<template> <template>
<div v-if="ready" class="container col-md-7 col-sm-12 mt-md-5 bg-light"> <div v-if="service" class="container col-md-7 col-sm-12 mt-md-5 bg-light">
<div class="col-12 mb-4"> <div class="col-12 mb-4">
@ -8,7 +8,7 @@
</span> </span>
<h4 class="mt-2"> <h4 class="mt-2">
<router-link to="/">{{$store.getters.core.name}}</router-link> - {{service.name}} <router-link to="/" class="text-black-50 text-decoration-none">{{$store.getters.core.name}}</router-link> - <span class="text-muted">{{service.name}}</span>
<span class="badge float-right d-none d-md-block" :class="{'bg-success': service.online, 'bg-danger': !service.online}"> <span class="badge float-right d-none d-md-block" :class="{'bg-success': service.online, 'bg-danger': !service.online}">
{{service.online ? "ONLINE" : "OFFLINE"}} {{service.online ? "ONLINE" : "OFFLINE"}}
</span> </span>
@ -16,7 +16,7 @@
<ServiceTopStats :service="service"/> <ServiceTopStats :service="service"/>
<div v-for="(message, index) in messages" v-if="messageInRange(message)"> <div v-for="(message, index) in $store.getters.serviceMessages(service.id)" v-if="messageInRange(message)">
<MessageBlock :message="message"/> <MessageBlock :message="message"/>
</div> </div>
@ -33,13 +33,13 @@
<apexchart width="100%" height="420" type="area" :options="chartOptions" :series="series"></apexchart> <apexchart width="100%" height="420" type="area" :options="chartOptions" :series="series"></apexchart>
</div> </div>
<div class="service-chart-heatmap mt-3 mb-4"> <div class="service-chart-heatmap mt-5 mb-4">
<ServiceHeatmap :service="service"/> <ServiceHeatmap :service="service"/>
</div> </div>
<div v-if="series" class="service-chart-container"> <!-- <div v-if="series" class="service-chart-container">-->
<apexchart width="100%" height="300" type="range" :options="dailyRangeOpts" :series="series"></apexchart> <!-- <apexchart width="100%" height="300" type="range" :options="dailyRangeOpts" :series="series"></apexchart>-->
</div> <!-- </div>-->
<nav v-if="service.failures" class="nav nav-pills flex-column flex-sm-row mt-3" id="service_tabs"> <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='failures'" class="flex-sm-fill text-sm-center nav-link active">Failures</a>
@ -95,8 +95,10 @@
import Checkin from "../forms/Checkin"; import Checkin from "../forms/Checkin";
import ServiceHeatmap from "@/components/Service/ServiceHeatmap"; import ServiceHeatmap from "@/components/Service/ServiceHeatmap";
import ServiceTopStats from "@/components/Service/ServiceTopStats"; import ServiceTopStats from "@/components/Service/ServiceTopStats";
import store from '../store'
import flatPickr from 'vue-flatpickr-component'; import flatPickr from 'vue-flatpickr-component';
import 'flatpickr/dist/flatpickr.css'; import 'flatpickr/dist/flatpickr.css';
const timeoptions = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' };
const axisOptions = { const axisOptions = {
labels: { labels: {
@ -134,16 +136,15 @@ export default {
}, },
data() { data() {
return { return {
id: null, id: this.$route.params.id,
tab: "failures", tab: "failures",
service: {},
authenticated: false, authenticated: false,
ready: false, ready: true,
data: null, data: null,
messages: [], messages: [],
failures: [], failures: [],
start_time: "", start_time: this.nowSubtract(84600 * 30),
end_time: "", end_time: new Date(),
dailyRangeOpts: { dailyRangeOpts: {
chart: { chart: {
height: 500, height: 500,
@ -153,6 +154,19 @@ export default {
}, },
chartOptions: { chartOptions: {
chart: { chart: {
events: {
beforeZoom: async (chartContext, { xaxis }) => {
const start = (xaxis.min / 1000).toFixed(0)
const end = (xaxis.max / 1000).toFixed(0)
await this.chartHits(start, end, "10m")
return {
xaxis: {
min: this.fromUnix(start),
max: this.fromUnix(end)
}
}
},
},
height: 500, height: 500,
width: "100%", width: "100%",
type: "area", type: "area",
@ -163,39 +177,65 @@ export default {
} }
}, },
selection: { selection: {
enabled: false enabled: true
}, },
zoom: { zoom: {
enabled: false enabled: true
}, },
toolbar: { toolbar: {
show: false show: true
}, },
stroke: {
show: false,
curve: 'smooth',
lineCap: 'butt',
}, },
grid: {
show: true,
padding: {
top: 0,
right: 0,
bottom: 0,
left: 0,
}
}, },
xaxis: { xaxis: {
type: "datetime", type: "datetime",
...axisOptions labels: {
}, show: true
yaxis: {
...axisOptions
}, },
tooltip: { tooltip: {
enabled: false, enabled: true
marker: { }
show: false, },
yaxis: {
labels: {
show: true
},
},
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>Average Response Time: </span><span class="font-3">${val}</span><span>${dt}</span></div>`
},
fixed: {
enabled: true,
position: 'topRight',
offsetX: -30,
offsetY: 40,
}, },
x: { x: {
show: false, show: false,
} format: 'dd MMM',
formatter: undefined,
},
y: {
formatter: undefined,
title: {
formatter: (seriesName) => seriesName,
},
},
}, },
legend: { legend: {
show: false, show: false,
@ -205,7 +245,7 @@ export default {
}, },
floating: true, floating: true,
axisTicks: { axisTicks: {
show: false show: true
}, },
axisBorder: { axisBorder: {
show: false show: false
@ -231,23 +271,32 @@ export default {
}, },
} }
}, },
async mounted() { computed: {
const id = this.$attrs.id service () {
this.id = id return this.$store.getters.serviceByAll(this.id)
let service;
if (this.isInt(id)) {
service = this.$store.getters.serviceById(id)
} else {
service = this.$store.getters.serviceByPermalink(id)
} }
this.service = service },
this.getService(service) watch: {
this.messages = this.$store.getters.serviceMessages(service.id) service: function(n, o) {
this.chartHits()
}
},
created() {
},
mounted() {
}, },
methods: { methods: {
async get() {
const s = store.getters.serviceByAll(this.id)
window.console.log("service: ", s)
this.getService(this.service)
this.messages = this.$store.getters.serviceMessages(this.service.id)
},
messageInRange(message) { messageInRange(message) {
const start = this.isBetween(new Date(), message.start_on) const start = this.isBetween(this.now(), this.parseTime(message.start_on))
const end = this.isBetween(message.end_on, new Date()) const end = this.isBetween(this.parseTime(message.end_on), this.now())
return start && end return start && end
}, },
async getService(s) { async getService(s) {
@ -255,11 +304,19 @@ export default {
await this.serviceFailures() await this.serviceFailures()
}, },
async serviceFailures() { async serviceFailures() {
this.failures = await Api.service_failures(this.service.id, this.now() - 3600, this.now(), 15) let tt = this.startEndTimes()
this.failures = await Api.service_failures(this.service.id, tt.start, tt.end)
}, },
async chartHits() { async chartHits(start=0, end=99999999999, group="30m") {
this.end_time = new Date() let tt = {};
this.data = await Api.service_hits(this.service.id, this.toUnix(this.service.created_at), this.toUnix(new Date()), "30m", false) 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") { if (this.data.length === 0 && group !== "1h") {
await this.chartHits("1h") await this.chartHits("1h")
} }
@ -268,11 +325,15 @@ export default {
...this.convertToChartData(this.data) ...this.convertToChartData(this.data)
}] }]
this.ready = true this.ready = true
},
startEndTimes() {
const start = this.toUnix(this.parseTime(this.service.stats.first_hit))
const end = this.toUnix(new Date())
return {start, end}
} }
} }
} }
</script> </script>
<!-- Add "scoped" attribute to limit CSS to this component only --> <!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped> <style scoped>
</style> </style>

View File

@ -27,7 +27,6 @@ const routes = [
}, },
{ {
path: '/dashboard', path: '/dashboard',
name: 'Dashboard',
component: Dashboard, component: Dashboard,
meta: { meta: {
requiresAuth: true requiresAuth: true

View File

@ -25,8 +25,7 @@ export default new Vuex.Store({
groups: [], groups: [],
messages: [], messages: [],
users: [], users: [],
notifiers: [], notifiers: []
integrations: []
}, },
getters: { getters: {
hasAllData: state => state.hasAllData, hasAllData: state => state.hasAllData,
@ -38,13 +37,19 @@ export default new Vuex.Store({
messages: state => state.messages, messages: state => state.messages,
users: state => state.users, users: state => state.users,
notifiers: state => state.notifiers, notifiers: state => state.notifiers,
integrations: state => state.integrations,
servicesInOrder: state => state.services.sort((a, b) => a.order_id - b.order_id), servicesInOrder: state => state.services.sort((a, b) => a.order_id - b.order_id),
groupsInOrder: state => state.groups.sort((a, b) => a.order_id - b.order_id), groupsInOrder: state => state.groups.sort((a, b) => a.order_id - b.order_id),
groupsClean: state => state.groups.filter(g => g.name !== '').sort((a, b) => a.order_id - b.order_id), groupsClean: state => state.groups.filter(g => g.name !== '').sort((a, b) => a.order_id - b.order_id),
groupsCleanInOrder: state => state.groups.filter(g => g.name !== '').sort((a, b) => a.order_id - b.order_id).sort((a, b) => a.order_id - b.order_id), groupsCleanInOrder: state => state.groups.filter(g => g.name !== '').sort((a, b) => a.order_id - b.order_id).sort((a, b) => a.order_id - b.order_id),
serviceByAll: (state) => (element) => {
if (element % 1 === 0) {
return state.services.find(s => s.id == element)
} else {
return state.services.find(s => s.permalink === element)
}
},
serviceById: (state) => (id) => { serviceById: (state) => (id) => {
return state.services.find(s => s.id == id) return state.services.find(s => s.id == id)
}, },
@ -100,12 +105,13 @@ export default new Vuex.Store({
}, },
setNotifiers (state, notifiers) { setNotifiers (state, notifiers) {
state.notifiers = notifiers state.notifiers = notifiers
},
setIntegrations (state, integrations) {
state.integrations = integrations
} }
}, },
actions: { actions: {
async getAllServices(context) {
const services = await Api.services()
context.commit("setServices", services);
},
async loadRequired(context) { async loadRequired(context) {
const core = await Api.core() const core = await Api.core()
context.commit("setCore", core); context.commit("setCore", core);
@ -140,8 +146,6 @@ export default new Vuex.Store({
context.commit("setNotifiers", notifiers); context.commit("setNotifiers", notifiers);
const users = await Api.users() const users = await Api.users()
context.commit("setUsers", users); context.commit("setUsers", users);
const integrations = await Api.integrations()
context.commit("setIntegrations", integrations);
} }
} }
}); });

View File

@ -128,7 +128,11 @@ func apiServiceDataHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
groupQuery := database.ParseQueries(r, service.AllHits()) groupQuery, err := database.ParseQueries(r, service.AllHits())
if err != nil {
sendErrorJson(err, w, r)
return
}
objs, err := groupQuery.GraphData(database.ByAverage("latency", 1000)) objs, err := groupQuery.GraphData(database.ByAverage("latency", 1000))
if err != nil { if err != nil {
@ -146,7 +150,11 @@ func apiServiceFailureDataHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
groupQuery := database.ParseQueries(r, service.AllFailures()) groupQuery, err := database.ParseQueries(r, service.AllFailures())
if err != nil {
sendErrorJson(err, w, r)
return
}
objs, err := groupQuery.GraphData(database.ByCount) objs, err := groupQuery.GraphData(database.ByCount)
if err != nil { if err != nil {
@ -164,7 +172,11 @@ func apiServicePingDataHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
groupQuery := database.ParseQueries(r, service.AllHits()) groupQuery, err := database.ParseQueries(r, service.AllHits())
if err != nil {
sendErrorJson(err, w, r)
return
}
objs, err := groupQuery.GraphData(database.ByAverage("ping_time", 1000)) objs, err := groupQuery.GraphData(database.ByAverage("ping_time", 1000))
if err != nil { if err != nil {
@ -216,7 +228,11 @@ func apiServiceFailuresHandler(r *http.Request) interface{} {
} }
var fails []*failures.Failure var fails []*failures.Failure
database.ParseQueries(r, service.AllFailures()).Find(&fails) query, err := database.ParseQueries(r, service.AllFailures())
if err != nil {
return err
}
query.Find(&fails)
return fails return fails
} }
@ -228,6 +244,10 @@ func apiServiceHitsHandler(r *http.Request) interface{} {
} }
var hts []*hits.Hit var hts []*hits.Hit
database.ParseQueries(r, service.AllHits()).Find(&hts) query, err := database.ParseQueries(r, service.AllHits())
if err != nil {
return err
}
query.Find(&hts)
return hts return hts
} }

View File

@ -0,0 +1,64 @@
package configs
import (
"fmt"
"github.com/statping/statping/utils"
"os"
)
const latestMigration = 1583860000
func init() {
os.Setenv("MIGRATION_ID", utils.ToString(latestMigration))
}
func (c *DbConfig) genericMigration(alterStr string) error {
if err := c.Db.Exec(fmt.Sprintf("ALTER TABLE hits %s COLUMN latency TYPE BIGINT;", alterStr)).Error(); err != nil {
return err
}
if err := c.Db.Exec(fmt.Sprintf("ALTER TABLE hits %s COLUMN ping_time TYPE BIGINT;", alterStr)).Error(); err != nil {
return err
}
if err := c.Db.Exec(fmt.Sprintf("ALTER TABLE failures %s COLUMN latency TYPE BIGINT;", alterStr)).Error(); err != nil {
return err
}
if err := c.Db.Exec("UPDATE hits SET latency = CAST(latency * 1000000 AS bigint);").Error(); err != nil {
return err
}
if err := c.Db.Exec("UPDATE hits SET ping_time = CAST(ping_time * 1000000 AS bigint);").Error(); err != nil {
return err
}
if err := c.Db.Exec("UPDATE failures SET ping_time = CAST(ping_time * 1000000 AS bigint);").Error(); err != nil {
return err
}
return nil
}
func (c *DbConfig) sqliteMigration() error {
if err := c.Db.Exec(`ALTER TABLE hits RENAME TO hits_backup;`).Error(); err != nil {
return err
}
if err := c.Db.Exec(`CREATE TABLE hits (id INTEGER PRIMARY KEY AUTOINCREMENT, service bigint, latency bigint, ping_time bigint, created_at datetime);`).Error(); err != nil {
return err
}
if err := c.Db.Exec(`INSERT INTO hits (id, service, latency, ping_time, created_at) SELECT id, service, CAST(latency * 1000000 AS bigint), CAST(ping_time * 1000000 AS bigint), created_at FROM hits_backup;`).Error(); err != nil {
return err
}
// failures table
if err := c.Db.Exec(`ALTER TABLE failures RENAME TO failures_backup;`).Error(); err != nil {
return err
}
if err := c.Db.Exec(`CREATE TABLE failures (id INTEGER PRIMARY KEY AUTOINCREMENT, issue varchar(255), method varchar(255), method_id bigint, service bigint, ping_time bigint, checkin bigint, error_code bigint, created_at datetime);`).Error(); err != nil {
return err
}
if err := c.Db.Exec(`INSERT INTO failures (id, issue, method, method_id, service, ping_time, checkin, created_at) SELECT id, issue, method, method_id, service, CAST(ping_time * 1000000 AS bigint), checkin, created_at FROM failures_backup;`).Error(); err != nil {
return err
}
if err := c.Db.Exec(`DROP TABLE hits_backup;`).Error(); err != nil {
return err
}
if err := c.Db.Exec(`DROP TABLE failures_backup;`).Error(); err != nil {
return err
}
return nil
}

View File

@ -2,6 +2,9 @@ package configs
import ( import (
"fmt" "fmt"
_ "github.com/jinzhu/gorm/dialects/mysql"
_ "github.com/jinzhu/gorm/dialects/postgres"
_ "github.com/jinzhu/gorm/dialects/sqlite"
"github.com/statping/statping/types/checkins" "github.com/statping/statping/types/checkins"
"github.com/statping/statping/types/core" "github.com/statping/statping/types/core"
"github.com/statping/statping/types/failures" "github.com/statping/statping/types/failures"
@ -12,53 +15,36 @@ import (
"github.com/statping/statping/types/notifications" "github.com/statping/statping/types/notifications"
"github.com/statping/statping/types/services" "github.com/statping/statping/types/services"
"github.com/statping/statping/types/users" "github.com/statping/statping/types/users"
_ "github.com/jinzhu/gorm/dialects/mysql"
_ "github.com/jinzhu/gorm/dialects/postgres"
_ "github.com/jinzhu/gorm/dialects/sqlite"
) )
// InsertNotifierDB inject the Statping database instance to the Notifier package func (c *DbConfig) DatabaseChanges() error {
//func (c *DbConfig) InsertNotifierDB() error { var cr core.Core
// if !database.Available() { c.Db.Model(&core.Core{}).Find(&cr)
// err := c.Connect()
// if err != nil {
// return errors.New("database connection has not been created")
// }
// }
// notifiers.SetDB(database.DB())
// return nil
//}
// InsertIntegratorDB inject the Statping database instance to the Integrations package if latestMigration > cr.MigrationId {
//func (c *DbConfig) InsertIntegratorDB() error { log.Infof("Statping database is out of date, migrating to: %d", latestMigration)
// if !database.Available() {
// err := c.Connect()
// if err != nil {
// return errors.Wrap(err,"database connection has not been created")
// }
// }
// integrations.SetDB(database.DB())
// return nil
//}
func (c *DbConfig) VerifyMigration() error { switch c.Db.DbType() {
case "mysql":
if err := c.genericMigration("MODIFY"); err != nil {
return err
}
case "postgres":
if err := c.genericMigration("ALTER"); err != nil {
return err
}
default:
if err := c.sqliteMigration(); err != nil {
return err
}
}
query := ` if err := c.Db.Exec(fmt.Sprintf("UPDATE core SET migration_id = %d", latestMigration)).Error(); err != nil {
BEGIN TRANSACTION; return err
ALTER TABLE hits ALTER COLUMN latency BIGINT; }
ALTER TABLE hits ALTER COLUMN ping_time BIGINT;
ALTER TABLE failures ALTER COLUMN ping_time BIGINT;
UPDATE hits SET latency = CAST(latency * 10000 AS BIGINT);
UPDATE hits SET ping_time = CAST(ping_time * 100000 AS BIGINT);
UPDATE failures SET ping_time = CAST(ping_time * 100000 AS BIGINT);
COMMIT;`
fmt.Println(c.Db.DbType()) }
return nil
q := c.Db.Raw(query).Debug()
return q.Error()
} }
//MigrateDatabase will migrate the database structure to current version. //MigrateDatabase will migrate the database structure to current version.

View File

@ -3,12 +3,11 @@ package core
import ( import (
"github.com/statping/statping/types/null" "github.com/statping/statping/types/null"
"github.com/statping/statping/utils" "github.com/statping/statping/utils"
"time"
) )
func Samples() error { func Samples() error {
apiKey := utils.Getenv("API_KEY", "samplekey") apiKey := utils.Getenv("API_KEY", utils.RandomString(16))
apiSecret := utils.Getenv("API_SECRET", "samplesecret") apiSecret := utils.Getenv("API_SECRET", utils.RandomString(16))
core := &Core{ core := &Core{
Name: "Statping Sample Data", Name: "Statping Sample Data",
@ -16,10 +15,10 @@ func Samples() error {
ApiKey: apiKey.(string), ApiKey: apiKey.(string),
ApiSecret: apiSecret.(string), ApiSecret: apiSecret.(string),
Domain: "http://localhost:8080", Domain: "http://localhost:8080",
Version: "test", CreatedAt: utils.Now(),
CreatedAt: time.Now().UTC(),
UseCdn: null.NewNullBool(false), UseCdn: null.NewNullBool(false),
Footer: null.NewNullString(""), Footer: null.NewNullString(""),
MigrationId: utils.Now().Unix(),
} }
return core.Create() return core.Create()