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

View File

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

View File

@ -1,6 +1,6 @@
<template>
<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'"/>
</div>
</template>
@ -19,10 +19,13 @@
loaded: false,
version: "",
logged_in: false,
app: null
}
},
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) {
await this.$store.dispatch('loadAdmin')

View File

@ -6,7 +6,34 @@ HTML,BODY {
}
.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 {
@ -167,7 +194,19 @@ HTML,BODY {
}
.font-5 {
font-size: 20pt;
font-size: 17pt;
}
.font-6 {
font-size: 24pt;
}
.font-7 {
font-size: 31pt;
}
.font-8 {
font-size: 38pt;
}
.badge {
@ -225,6 +264,7 @@ HTML,BODY {
.card-body H4 A {
color: $service-title;
font-size: $service-title-size;
text-decoration: none;
}
@ -248,7 +288,7 @@ HTML,BODY {
.service-chart-heatmap {
position: relative;
height: 300px;
height: 180px;
width: 100%;
}

View File

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

View File

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

View File

@ -1,6 +1,6 @@
<template>
<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>
</div>
</template>

View File

@ -1,13 +1,13 @@
<template>
<div class="alert alert-primary" role="alert">
<h3>{{message.title}}</h3>
<div class="alert alert-primary pb-4 pt-3 mt-5 mb-5" role="alert">
<h3 class="mb-3">{{message.title}}</h3>
<span class="mb-3">{{message.description}}</span>
<div class="d-block mt-2 mb-4">
<span class="float-left small">
Started {{toLocal(message.start_on)}} ({{duration(new Date(), message.start_on)}} ago)
<div class="row d-block mt-3">
<span class="col-12 col-md-6 text-left small">
Started {{niceDate(message.start_on)}} ({{ago(parseISO(message.start_on))}} ago)
</span>
<span class="float-right small">
Ends on {{toLocal(message.end_on)}} (in {{duration(message.end_on, new Date())}})</span>
<span class="col-12 col-md-6 text-right float-right small">
Ends on {{niceDate(message.end_on)}} (in {{ago(parseISO(message.end_on))}})</span>
</div>
</div>
</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>
<div class="mb-4">
<div class="card index-chart">
<div class="mb-md-4 mb-5">
<div class="card index-chart" :class="{'expanded-service': expanded}">
<div class="card-body">
<div class="col-12">
<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>
</h4>
<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 v-observe-visibility="visibleChart" class="chart-container">
<div v-if="!expanded" v-observe-visibility="visibleChart" class="chart-container">
<ServiceChart :service="service" :visible="visible"/>
</div>
<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="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
</button>
<div class="dropdown-menu" :class="{show: dropDownMenu}">
@ -34,8 +43,13 @@
</div>
<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>
<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>
@ -44,12 +58,13 @@
</template>
<script>
import Analytics from './Analytics';
import ServiceChart from "./ServiceChart";
import ServiceTopStats from "@/components/Service/ServiceTopStats";
export default {
name: 'ServiceBlock',
components: {ServiceTopStats, ServiceChart},
components: { Analytics, ServiceTopStats, ServiceChart},
props: {
service: {
type: Object,
@ -58,6 +73,7 @@ export default {
},
data() {
return {
expanded: false,
visible: false,
dropDownMenu: false,
timeframes: [

View File

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

View File

@ -1,5 +1,5 @@
<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>
<script>
@ -20,19 +20,32 @@
return {
ready: false,
data: [],
chartOptions: {
heatmap: {
plotOptions: {
chart: {
selection: {
enabled: false
},
zoom: {
enabled: false
},
toolbar: {
show: false
},
},
colors: [ "#cb3d36" ],
enableShades: true,
shadeIntensity: 0.5,
colorScale: {
ranges: [{
ranges: [ {
from: 0,
to: 1,
color: 'rgba(235,63,48,0.69)',
name: 'low',
to: 0,
color: '#bababa',
name: 'none',
},
{
from: 2,
to: 10,
color: 'rgba(245,43,43,0.58)',
color: '#cb3d36',
name: 'medium',
},
{
@ -42,76 +55,66 @@
name: 'high',
}
]
},
xaxis: {
tickAmount: '1',
tickPlacement: 'between',
min: 1,
max: 31,
type: "numeric",
labels: {
show: true
},
tooltip: {
enabled: false
}
},
chart: {
height: "100%",
width: "100%",
type: 'heatmap',
toolbar: {
show: false
}
},
dataLabels: {
enabled: false,
},
enableShades: true,
shadeIntensity: 0.5,
colors: ["#d53a3b"],
series: [{data: [{}]}],
yaxis: {
labels: {
formatter: (value) => {
return value
},
},
},
tooltip: {
enabled: true,
x: {
show: false,
},
y: {
formatter: function(val, opts) { return val+" Failures" },
title: {
formatter: (seriesName) => seriesName,
},
show: true
},
}
},
series: [{
},
series: [ {
data: []
}]
} ]
}
},
methods: {
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 monthNum = start.getUTCMonth()
for (i=0; i<=3; i++) {
let end = this.lastDayOfMonth(start.getUTCMonth()+start)
const inputdata = this.heatmapData(start,end)
for (let i=1; i<=3; i++) {
let end = this.lastDayOfMonth(monthNum)
window.console.log("getting: ",start, end)
const inputdata = await this.heatmapData(start, end)
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
},
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)
let dataArr = []
data.forEach(function(d) {
dataArr.push({x: d.timeframe, y: d.amount});
data.forEach((d) => {
dataArr.push({x: this.parseTime(d.timeframe).getUTCDate(), y: d.amount});
});
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";
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({
methods: {
@ -7,10 +10,10 @@ export default Vue.mixin({
return new Date()
},
current() {
return parse(new Date())
return parseISO(new Date())
},
utc(val) {
return fromUnixTime(this.toUnix(val) + val.getTimezoneOffset() * 60 * 1000)
return new Date.UTC(val)
},
ago(t1) {
return formatDistanceToNow(t1)
@ -25,11 +28,14 @@ export default Vue.mixin({
return formatDistance(t1, t2)
},
niceDate(val) {
return this.parseTime(val).format('LLLL')
return format(parseISO(val), "EEEE, MMM do h:mma")
},
parseTime(val) {
return parseISO(val)
},
parseISO(v) {
return parseISO(v)
},
toLocal(val, suf = 'at') {
const t = this.parseTime(val)
return format(t, `EEEE, MMM do h:mma`)
@ -41,7 +47,7 @@ export default Vue.mixin({
return fromUnixTime(val)
},
isBetween(t1, t2) {
return differenceInSeconds(parseISO(t1), parseISO(t2)) > 0
return differenceInSeconds(t1, t2) >= 0
},
hour() {
return 3600
@ -111,10 +117,10 @@ export default Vue.mixin({
return {data: newSet}
},
lastDayOfMonth(month) {
return new Date(new Date().getUTCFullYear(), month + 1, 0);
return new Date(Date.UTC(new Date().getUTCFullYear(), month + 1, 0))
},
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>
<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/>

View File

@ -1,5 +1,5 @@
<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">
@ -8,7 +8,7 @@
</span>
<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}">
{{service.online ? "ONLINE" : "OFFLINE"}}
</span>
@ -16,7 +16,7 @@
<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"/>
</div>
@ -33,13 +33,13 @@
<apexchart width="100%" height="420" type="area" :options="chartOptions" :series="series"></apexchart>
</div>
<div class="service-chart-heatmap mt-3 mb-4">
<div class="service-chart-heatmap mt-5 mb-4">
<ServiceHeatmap :service="service"/>
</div>
<div v-if="series" class="service-chart-container">
<apexchart width="100%" height="300" type="range" :options="dailyRangeOpts" :series="series"></apexchart>
</div>
<!-- <div v-if="series" class="service-chart-container">-->
<!-- <apexchart width="100%" height="300" type="range" :options="dailyRangeOpts" :series="series"></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>
@ -95,8 +95,10 @@
import Checkin from "../forms/Checkin";
import ServiceHeatmap from "@/components/Service/ServiceHeatmap";
import ServiceTopStats from "@/components/Service/ServiceTopStats";
import store from '../store'
import flatPickr from 'vue-flatpickr-component';
import 'flatpickr/dist/flatpickr.css';
const timeoptions = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' };
const axisOptions = {
labels: {
@ -134,16 +136,15 @@ export default {
},
data() {
return {
id: null,
id: this.$route.params.id,
tab: "failures",
service: {},
authenticated: false,
ready: false,
ready: true,
data: null,
messages: [],
failures: [],
start_time: "",
end_time: "",
start_time: this.nowSubtract(84600 * 30),
end_time: new Date(),
dailyRangeOpts: {
chart: {
height: 500,
@ -153,6 +154,19 @@ export default {
},
chartOptions: {
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,
width: "100%",
type: "area",
@ -163,39 +177,65 @@ export default {
}
},
selection: {
enabled: false
enabled: true
},
zoom: {
enabled: false
enabled: true
},
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: {
type: "datetime",
...axisOptions
labels: {
show: true
},
tooltip: {
enabled: true
}
},
yaxis: {
...axisOptions
labels: {
show: true
},
},
tooltip: {
enabled: false,
marker: {
show: false,
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: {
show: false,
}
format: 'dd MMM',
formatter: undefined,
},
y: {
formatter: undefined,
title: {
formatter: (seriesName) => seriesName,
},
},
},
legend: {
show: false,
@ -205,7 +245,7 @@ export default {
},
floating: true,
axisTicks: {
show: false
show: true
},
axisBorder: {
show: false
@ -231,23 +271,32 @@ export default {
},
}
},
async mounted() {
const id = this.$attrs.id
this.id = id
let service;
if (this.isInt(id)) {
service = this.$store.getters.serviceById(id)
} else {
service = this.$store.getters.serviceByPermalink(id)
computed: {
service () {
return this.$store.getters.serviceByAll(this.id)
}
this.service = service
this.getService(service)
this.messages = this.$store.getters.serviceMessages(service.id)
},
watch: {
service: function(n, o) {
this.chartHits()
}
},
created() {
},
mounted() {
},
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) {
const start = this.isBetween(new Date(), message.start_on)
const end = this.isBetween(message.end_on, new Date())
const start = this.isBetween(this.now(), this.parseTime(message.start_on))
const end = this.isBetween(this.parseTime(message.end_on), this.now())
return start && end
},
async getService(s) {
@ -255,11 +304,19 @@ export default {
await this.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() {
this.end_time = new Date()
this.data = await Api.service_hits(this.service.id, this.toUnix(this.service.created_at), this.toUnix(new Date()), "30m", false)
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") {
await this.chartHits("1h")
}
@ -268,11 +325,15 @@ export default {
...this.convertToChartData(this.data)
}]
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>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

View File

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

View File

@ -25,8 +25,7 @@ export default new Vuex.Store({
groups: [],
messages: [],
users: [],
notifiers: [],
integrations: []
notifiers: []
},
getters: {
hasAllData: state => state.hasAllData,
@ -38,13 +37,19 @@ export default new Vuex.Store({
messages: state => state.messages,
users: state => state.users,
notifiers: state => state.notifiers,
integrations: state => state.integrations,
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),
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),
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) => {
return state.services.find(s => s.id == id)
},
@ -100,12 +105,13 @@ export default new Vuex.Store({
},
setNotifiers (state, notifiers) {
state.notifiers = notifiers
},
setIntegrations (state, integrations) {
state.integrations = integrations
}
},
actions: {
async getAllServices(context) {
const services = await Api.services()
context.commit("setServices", services);
},
async loadRequired(context) {
const core = await Api.core()
context.commit("setCore", core);
@ -140,8 +146,6 @@ export default new Vuex.Store({
context.commit("setNotifiers", notifiers);
const users = await Api.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
}
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))
if err != nil {
@ -146,7 +150,11 @@ func apiServiceFailureDataHandler(w http.ResponseWriter, r *http.Request) {
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)
if err != nil {
@ -164,7 +172,11 @@ func apiServicePingDataHandler(w http.ResponseWriter, r *http.Request) {
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))
if err != nil {
@ -216,7 +228,11 @@ func apiServiceFailuresHandler(r *http.Request) interface{} {
}
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
}
@ -228,6 +244,10 @@ func apiServiceHitsHandler(r *http.Request) interface{} {
}
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
}

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 (
"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/core"
"github.com/statping/statping/types/failures"
@ -12,53 +15,36 @@ import (
"github.com/statping/statping/types/notifications"
"github.com/statping/statping/types/services"
"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) InsertNotifierDB() error {
// if !database.Available() {
// err := c.Connect()
// if err != nil {
// return errors.New("database connection has not been created")
// }
// }
// notifiers.SetDB(database.DB())
// return nil
//}
func (c *DbConfig) DatabaseChanges() error {
var cr core.Core
c.Db.Model(&core.Core{}).Find(&cr)
// InsertIntegratorDB inject the Statping database instance to the Integrations package
//func (c *DbConfig) InsertIntegratorDB() error {
// 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
//}
if latestMigration > cr.MigrationId {
log.Infof("Statping database is out of date, migrating to: %d", latestMigration)
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 := `
BEGIN TRANSACTION;
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;`
if err := c.Db.Exec(fmt.Sprintf("UPDATE core SET migration_id = %d", latestMigration)).Error(); err != nil {
return err
}
fmt.Println(c.Db.DbType())
q := c.Db.Raw(query).Debug()
return q.Error()
}
return nil
}
//MigrateDatabase will migrate the database structure to current version.

View File

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