core updates - sql migrations

pull/10/head
Hunter Long 2018-07-04 02:00:16 -07:00
parent b58326a453
commit cb46bf6496
19 changed files with 163 additions and 202 deletions

View File

@ -18,7 +18,7 @@ services:
env:
global:
- VERSION=0.29
- VERSION=0.29.1
- DB_HOST=localhost
- DB_USER=travis
- DB_PASS=

View File

@ -5,7 +5,7 @@ gem install sass
sass source/scss/base.scss source/css/base.css
# MIGRATION SQL FILE FOR CURRENT VERSION
printf "UPDATE core SET version='$VERSION';\n" >> source/sql/upgrade.sql
#printf "UPDATE core SET version='$VERSION';\n" >> source/sql/upgrade.sql
# COMPILE SRC INTO BIN
rice embed-go

View File

@ -1,6 +1,6 @@
FROM alpine:latest
ENV VERSION=v0.29
ENV VERSION=v0.29.1
RUN apk --no-cache add libstdc++ ca-certificates
RUN wget -q https://github.com/hunterlong/statup/releases/download/$VERSION/statup-linux-alpine.tar.gz && \

View File

@ -13,12 +13,13 @@ var (
)
func LoadConfig() (*Config, error) {
var config Config
var config *Config
file, err := ioutil.ReadFile("config.yml")
if err != nil {
return nil, err
}
err = yaml.Unmarshal(file, &config)
Configs = &config
return &config, err
Configs = config
CoreApp.DbConnection = config.Connection
return config, err
}

View File

@ -4,6 +4,7 @@ import (
"github.com/GeertJohan/go.rice"
"github.com/hunterlong/statup/plugin"
"github.com/hunterlong/statup/types"
"time"
)
type PluginJSON types.PluginJSON
@ -25,6 +26,8 @@ type Core struct {
AllPlugins []plugin.PluginActions
Communications []*types.Communication
OfflineAssets bool
DbConnection string
started time.Time
}
var (
@ -41,7 +44,13 @@ var (
)
func init() {
CoreApp = NewCore()
}
func NewCore() *Core {
CoreApp = new(Core)
CoreApp.started = time.Now()
return CoreApp
}
func InitApp() {
@ -56,9 +65,10 @@ func InitApp() {
func (c *Core) Update() (*Core, error) {
res := DbSession.Collection("core").Find().Limit(1)
res.Update(c)
err := res.Update(c)
CoreApp = c
return c, nil
CoreApp.Services, err = SelectAllServices()
return c, err
}
func (c Core) UsingAssets() bool {
@ -102,6 +112,10 @@ func SelectCore() (*Core, error) {
return nil, err
}
CoreApp = c
CoreApp.DbConnection = Configs.Connection
CoreApp.Version = VERSION
CoreApp.Services, _ = SelectAllServices()
CoreApp.Update()
//store = sessions.NewCookieStore([]byte(core.ApiSecret))
return CoreApp, err
}

View File

@ -16,7 +16,6 @@ import (
)
var (
dbServer string
sqliteSettings sqlite.ConnectionURL
postgresSettings postgresql.ConnectionURL
mysqlSettings mysql.ConnectionURL
@ -68,7 +67,6 @@ func DbConnection(dbType string) error {
}
}
//dbSession.SetLogging(true)
dbServer = dbType
return err
}
@ -130,20 +128,87 @@ func (c *DbConfig) Save() error {
if err == nil {
CoreApp = newCore
}
CoreApp, err = SelectCore()
CoreApp.DbConnection = c.DbConn
return err
}
func RunDatabaseUpgrades() {
utils.Log(1, "Running Database Upgrade from 'upgrade.sql'...")
upgrade, _ := SqlBox.String("upgrade.sql")
requests := strings.Split(upgrade, ";")
for _, request := range requests {
_, err := DbSession.Exec(db.Raw(request + ";"))
if err != nil {
utils.Log(2, err)
func versionSplit(v string) (int64, int64, int64) {
currSplit := strings.Split(v, ".")
if len(currSplit) < 2 {
return 9999, 9999, 9999
}
var major, mid, minor string
if len(currSplit) == 3 {
major = currSplit[0]
mid = currSplit[1]
minor = currSplit[2]
return utils.StringInt(major), utils.StringInt(mid), utils.StringInt(minor)
}
major = currSplit[0]
mid = currSplit[1]
return utils.StringInt(major), utils.StringInt(mid), 0
}
func versionHigher(migrate string) bool {
cM, cMi, cMn := versionSplit(CoreApp.Version)
mM, mMi, mMn := versionSplit(migrate)
if mM > cM {
return true
}
if mMi > cMi {
return true
}
if mMn > cMn {
return true
}
return false
}
func RunDatabaseUpgrades() error {
var err error
utils.Log(1, fmt.Sprintf("Checking Database Upgrades from v%v in '%v_upgrade.sql'...", CoreApp.Version, CoreApp.DbConnection))
upgrade, _ := SqlBox.String(CoreApp.DbConnection + "_upgrade.sql")
// parse db version and upgrade file
ups := strings.Split(upgrade, "=========================================== ")
var ran int
for _, v := range ups {
if len(v) == 0 {
continue
}
vers := strings.Split(v, "\n")
version := vers[0]
data := vers[1:]
//fmt.Printf("Checking Migration from v%v to v%v - %v\n", CoreApp.Version, version, versionHigher(version))
if !versionHigher(version) {
//fmt.Printf("Already up-to-date with v%v\n", version)
continue
}
fmt.Printf("Migration Database from v%v to v%v\n", CoreApp.Version, version)
for _, m := range data {
if m == "" {
continue
}
fmt.Printf("Running Migration: %v\n", m)
_, err := DbSession.Exec(db.Raw(m + ";"))
if err != nil {
utils.Log(2, err)
continue
}
ran++
CoreApp.Version = m
}
}
utils.Log(1, "Database Upgraded")
CoreApp.Update()
CoreApp, err = SelectCore()
if ran > 0 {
utils.Log(1, fmt.Sprintf("Database Upgraded, %v query ran", ran))
} else {
utils.Log(1, fmt.Sprintf("Database is already up-to-date, latest v%v", CoreApp.Version))
}
return err
}
func DropDatabase() {
@ -161,9 +226,9 @@ func DropDatabase() {
func CreateDatabase() {
fmt.Println("Creating Tables...")
sql := "postgres_up.sql"
if dbServer == "mysql" {
if CoreApp.DbConnection == "mysql" {
sql = "mysql_up.sql"
} else if dbServer == "sqlite" {
} else if CoreApp.DbConnection == "sqlite" {
sql = "sqlite_up.sql"
}
up, _ := SqlBox.String(sql)

View File

@ -124,17 +124,27 @@ func (s *Service) SmallText() string {
return fmt.Sprintf("No Failures in the last 24 hours! %v", hits[0])
}
func GroupDataBy(column string, id int64, tm time.Time, increment string) string {
var sql string
fmt.Println("gropu by", column, CoreApp.DbConnection)
switch CoreApp.DbConnection {
case "mysql":
sql = fmt.Sprintf("SELECT CONCAT(date_format(created_at, '%%Y-%%m-%%dT%%H:%%i:00Z')) AS created_at, AVG(latency)*1000 AS value FROM %v WHERE service=%v AND DATE_FORMAT(created_at, '%%Y-%%m-%%dT%%TZ') BETWEEN DATE_FORMAT('%v', '%%Y-%%m-%%dT%%TZ') AND DATE_FORMAT(NOW(), '%%Y-%%m-%%dT%%TZ') GROUP BY 1 ORDER BY created_at ASC;", column, id, tm.Format(time.RFC3339))
case "sqlite":
sql = fmt.Sprintf("SELECT strftime('%%Y-%%m-%%dT%%H:%%M:00Z', created_at), AVG(latency)*1000 as value FROM %v WHERE service=%v AND created_at >= '%v' GROUP BY strftime('%%M:00', created_at) ORDER BY created_at ASC;", column, id, tm.Format(time.RFC3339))
case "postgres":
sql = fmt.Sprintf("SELECT date_trunc('%v', created_at), AVG(latency)*1000 AS value FROM %v WHERE service=%v AND created_at >= '%v' GROUP BY 1 ORDER BY date_trunc ASC;", increment, column, id, tm.Format(time.RFC3339))
}
fmt.Println(sql)
return sql
}
func (s *Service) GraphData() string {
var d []*DateScan
increment := "minute"
since := time.Now().Add(time.Hour*-24 + time.Minute*0 + time.Second*0)
// group by interval sql query for postgres, mysql and sqlite
sql := fmt.Sprintf("SELECT date_trunc('%v', created_at), AVG(latency)*1000 AS value FROM hits WHERE service=%v AND created_at > '%v' GROUP BY 1 ORDER BY date_trunc ASC;", increment, s.Id, since.Format(time.RFC3339))
if dbServer == "mysql" {
sql = fmt.Sprintf("SELECT CONCAT(date_format(created_at, '%%Y-%%m-%%dT%%TZ')) AS created_at, AVG(latency)*1000 AS value FROM hits WHERE service=%v AND DATE_FORMAT(created_at, '%%Y-%%m-%%dT%%TZ') BETWEEN DATE_FORMAT(NOW() - INTERVAL 12 HOUR, '%%Y-%%m-%%dT%%TZ') AND DATE_FORMAT(NOW(), '%%Y-%%m-%%dT%%TZ') GROUP BY created_at ORDER BY created_at ASC;", s.Id)
} else if dbServer == "sqlite" {
sql = fmt.Sprintf("SELECT strftime('%%Y-%%m-%%dT%%H:%%M:%%SZ', created_at), AVG(latency)*1000 as value FROM hits WHERE service=%v AND created_at >= '%v' GROUP BY strftime('%%M', created_at) ORDER BY created_at ASC;", s.Id, since.Format(time.RFC3339))
}
sql := GroupDataBy("hits", s.Id, since, "minute")
dated, err := DbSession.Query(db.Raw(sql))
if err != nil {
utils.Log(2, err)

View File

@ -1,6 +1,7 @@
package handlers
import (
"fmt"
"github.com/gorilla/sessions"
"github.com/hunterlong/statup/core"
"github.com/hunterlong/statup/types"
@ -22,14 +23,14 @@ var (
func RunHTTPServer() {
utils.Log(1, "Statup HTTP Server running on http://localhost:8080")
r := Router()
//for _, p := range allPlugins {
// info := p.GetInfo()
// for _, route := range p.Routes() {
// path := fmt.Sprintf("/plugins/%v/%v", info.Name, route.URL)
// r.Handle(path, http.HandlerFunc(route.Handler)).Methods(route.Method)
// fmt.Printf("Added Route %v for plugin %v\n", path, info.Name)
// }
//}
for _, p := range core.CoreApp.AllPlugins {
info := p.GetInfo()
for _, route := range p.Routes() {
path := fmt.Sprintf("/plugins/%v/%v", info.Name, route.URL)
r.Handle(path, http.HandlerFunc(route.Handler)).Methods(route.Method)
fmt.Printf("Added Route %v for plugin %v\n", path, info.Name)
}
}
srv := &http.Server{
Addr: "0.0.0.0:8080",
WriteTimeout: time.Second * 15,

View File

@ -1,19 +1,19 @@
package handlers
import (
"fmt"
"github.com/gorilla/mux"
"github.com/gorilla/sessions"
"github.com/hunterlong/statup/core"
"github.com/tdewolff/minify"
"net/http"
"time"
)
func Router() *mux.Router {
r := mux.NewRouter()
r.Handle("/", http.HandlerFunc(IndexHandler))
LocalizedAssets(r)
m := minify.New()
r.Handle("/charts.js", m.Middleware(http.HandlerFunc(RenderServiceChartsHandler)))
r.Handle("/charts.js", http.HandlerFunc(RenderServiceChartsHandler))
r.Handle("/setup", http.HandlerFunc(SetupHandler)).Methods("GET")
r.Handle("/setup", http.HandlerFunc(ProcessSetupHandler)).Methods("POST")
r.Handle("/dashboard", http.HandlerFunc(DashboardHandler)).Methods("GET")
@ -49,7 +49,12 @@ func Router() *mux.Router {
r.Handle("/api/users/{id}", http.HandlerFunc(ApiUserHandler))
r.Handle("/metrics", http.HandlerFunc(PrometheusHandler))
r.NotFoundHandler = http.HandlerFunc(Error404Handler)
Store = sessions.NewCookieStore([]byte("secretinfo"))
if core.CoreApp != nil {
cookie := fmt.Sprintf("%v_%v", core.CoreApp.ApiSecret, time.Now().Nanosecond())
Store = sessions.NewCookieStore([]byte(cookie))
} else {
Store = sessions.NewCookieStore([]byte("secretinfo"))
}
return r
}

View File

@ -11,8 +11,8 @@ import (
func RenderServiceChartsHandler(w http.ResponseWriter, r *http.Request) {
services := core.CoreApp.Services
//w.Header().Set("Content-Type", "text/javascript")
//w.Header().Set("Cache-Control", "no-cache, private, max-age=0")
w.Header().Set("Content-Type", "text/javascript")
w.Header().Set("Cache-Control", "max-age=60")
ExecuteJSResponse(w, r, "charts.js", services)
}
@ -29,7 +29,6 @@ func CreateServiceHandler(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
fmt.Println("service adding")
r.ParseForm()
name := r.PostForm.Get("name")
domain := r.PostForm.Get("domain")

View File

@ -1,6 +1,7 @@
package handlers
import (
"github.com/gorilla/sessions"
"github.com/hunterlong/statup/core"
"github.com/hunterlong/statup/types"
"github.com/hunterlong/statup/utils"
@ -106,7 +107,7 @@ func ProcessSetupHandler(w http.ResponseWriter, r *http.Request) {
admin := &core.User{
Username: config.Username,
Password: config.Password,
Email: email,
Email: config.Email,
Admin: true,
}
admin.Create()
@ -116,6 +117,7 @@ func ProcessSetupHandler(w http.ResponseWriter, r *http.Request) {
}
core.InitApp()
Store = sessions.NewCookieStore([]byte(core.CoreApp.ApiSecret))
time.Sleep(2 * time.Second)
http.Redirect(w, r, "/", http.StatusSeeOther)
}

View File

@ -13,7 +13,6 @@ import (
"os"
"strings"
"testing"
"time"
)
var (
@ -28,6 +27,7 @@ func RunInit(t *testing.T) {
os.Remove("./index.html")
route = handlers.Router()
LoadDotEnvs()
core.CoreApp = core.NewCore()
}
var forceSequential chan bool = make(chan bool, 1)
@ -49,6 +49,9 @@ func TestRunAll(t *testing.T) {
t.Run(dbt+" Sample Data", func(t *testing.T) {
RunInsertMysqlSample(t)
})
t.Run(dbt+" Load Configs", func(t *testing.T) {
RunLoadConfig(t)
})
t.Run(dbt+" Select Core", func(t *testing.T) {
RunSelectCoreMYQL(t, dbt)
})
@ -213,11 +216,20 @@ func RunInsertMysqlSample(t *testing.T) {
assert.Nil(t, err)
}
func RunLoadConfig(t *testing.T) {
var err error
core.Configs, err = core.LoadConfig()
assert.Nil(t, err)
assert.NotNil(t, core.Configs)
}
func RunSelectCoreMYQL(t *testing.T, db string) {
var err error
core.CoreApp, err = core.SelectCore()
assert.Nil(t, err)
t.Log(core.CoreApp)
assert.Equal(t, "Testing "+db, core.CoreApp.Name)
assert.Equal(t, db, core.CoreApp.DbConnection)
assert.NotEmpty(t, core.CoreApp.ApiKey)
assert.NotEmpty(t, core.CoreApp.ApiSecret)
assert.Equal(t, VERSION, core.CoreApp.Version)
@ -363,7 +375,6 @@ func RunCreateService_Hits(t *testing.T) {
service := s.Check()
assert.NotNil(t, service)
}
time.Sleep(1 * time.Second)
}
}

View File

@ -1,7 +1,6 @@
package plugin
import (
"fmt"
"net/http"
"upper.io/db.v3/lib/sqlbuilder"
)
@ -26,10 +25,6 @@ func SetDatabase(database sqlbuilder.Database) {
DB = database
}
func Throw(err error) {
fmt.Println(err)
}
type PluginInfo struct {
Info Info
PluginActions

View File

@ -1,7 +1,7 @@
[{
"name": "slack",
"description": "slack bot that send a message in a channel when server is down.",
"repo": "https://github.com/hunterlong/statup_slack",
"name": "Example Plugin",
"description": "An example of a plugin for Statup",
"repo": "https://github.com/hunterlong/statup_plugin",
"author": "Hunter Long",
"namespace": "slack"
"namespace": "example"
}]

View File

@ -1,146 +1,6 @@
{{ range . }}{{ if .AvgTime }}var ctx = document.getElementById("service_{{.Id}}").getContext('2d');
var chartdata = new Chart(ctx, {
type: 'line',
data: {
datasets: [{
label: 'Response Time (Milliseconds)',
data: {{safe .GraphData}},
backgroundColor: [
'rgba(47, 206, 30, 0.92)'
],
borderColor: [
'rgb(47, 171, 34)'
],
borderWidth: 1
}]
},
options: {
maintainAspectRatio: false,
scaleShowValues: true,
layout: {
padding: {
left: 0,
right: 0,
top: 0,
bottom: -10
}
},
hover: {
animationDuration: 0,
},
responsiveAnimationDuration: 0,
animation: {
duration: 3500,
onComplete: function() {
var chartInstance = this.chart,
ctx = chartInstance.ctx;
var controller = this.chart.controller;
var xAxis = controller.scales['x-axis-0'];
var yAxis = controller.scales['y-axis-0'];
ctx.font = Chart.helpers.fontString(Chart.defaults.global.defaultFontSize, Chart.defaults.global.defaultFontStyle, Chart.defaults.global.defaultFontFamily);
ctx.textAlign = 'center';
ctx.textBaseline = 'bottom';
var numTicks = xAxis.ticks.length;
var yOffsetStart = xAxis.width / numTicks;
var halfBarWidth = (xAxis.width / (numTicks * 2));
xAxis.ticks.forEach(function(value, index) {
var xOffset = 20;
var yOffset = (yOffsetStart * index) + halfBarWidth;
ctx.fillStyle = '#e2e2e2';
ctx.fillText(value, yOffset, xOffset);
});
this.data.datasets.forEach(function(dataset, i) {
var meta = chartInstance.controller.getDatasetMeta(i);
var hxH = 0;
var hyH = 0;
var hxL = 0;
var hyL = 0;
var highestNum = 0;
var lowestnum = 999999999999;
meta.data.forEach(function(bar, index) {
var data = dataset.data[index];
if (data.y {{safe "<"}} lowestnum) {
lowestnum = data.y;
hxL = bar._model.x;
hyL = bar._model.y;
}
if (data.y > highestNum) {
highestNum = data.y;
hxH = bar._model.x;
hyH = bar._model.y;
}
});
if (hxH {{safe ">"}}= 820) {
hxH = 820;
} else if (hxH {{safe "<"}}= 50) {
hxH = 50;
}
if (hxL {{safe ">"}}= 820) {
hxL = 820;
} else if (hxL {{safe "<"}}= 70) {
hxL = 70;
}
ctx.fillStyle = '#ffa7a2';
ctx.fillText(highestNum+"ms", hxH - 40, hyH + 15);
ctx.fillStyle = '#45d642';
ctx.fillText(lowestnum+"ms", hxL, hyL + 10);
console.log("done service_id_{{.Id}}")
});
}
},
legend: {
display: false
},
tooltips: {
"enabled": false
},
scales: {
yAxes: [{
display: false,
ticks: {
fontSize: 20,
display: false,
beginAtZero: false
},
gridLines: {
display:false
}
}],
xAxes: [{
type: 'time',
distribution: 'series',
autoSkip: false,
gridLines: {
display:false
},
ticks: {
stepSize: 1,
min: 0,
fontColor: "white",
fontSize: 20,
display: false,
}
}]
},
elements: {
point: {
radius: 0
}
}
}
});
{{ range . }}{{ if .AvgTime }}var ctx_{{.Id}}=document.getElementById("service_{{.Id}}").getContext('2d');var chartdata=new Chart(ctx_{{.Id}},{type:'line',data:{datasets:[{label:'Response Time (Milliseconds)',data:{{safe .GraphData}},backgroundColor:['rgba(47, 206, 30, 0.92)'],borderColor:['rgb(47, 171, 34)'],borderWidth:1}]},options:{maintainAspectRatio:!1,scaleShowValues:!0,layout:{padding:{left:0,right:0,top:0,bottom:-10}},hover:{animationDuration:0,},responsiveAnimationDuration:0,animation:{duration:3500,onComplete:function(){var chartInstance=this.chart,ctx=chartInstance.ctx;var controller=this.chart.controller;var xAxis=controller.scales['x-axis-0'];var yAxis=controller.scales['y-axis-0'];ctx.font=Chart.helpers.fontString(Chart.defaults.global.defaultFontSize,Chart.defaults.global.defaultFontStyle,Chart.defaults.global.defaultFontFamily);ctx.textAlign='center';ctx.textBaseline='bottom';var numTicks=xAxis.ticks.length;var yOffsetStart=xAxis.width/numTicks;var halfBarWidth=(xAxis.width/(numTicks*2));xAxis.ticks.forEach(function(value,index){var xOffset=20;var yOffset=(yOffsetStart*index)+halfBarWidth;ctx.fillStyle='#e2e2e2';ctx.fillText(value,yOffset,xOffset)});this.data.datasets.forEach(function(dataset,i){var meta=chartInstance.controller.getDatasetMeta(i);var hxH=0;var hyH=0;var hxL=0;var hyL=0;var highestNum=0;var lowestnum=999999999999;meta.data.forEach(function(bar,index){var data=dataset.data[index];if(lowestnum>data.y){lowestnum=data.y;hxL=bar._model.x;hyL=bar._model.y}
if(data.y>highestNum){highestNum=data.y;hxH=bar._model.x;hyH=bar._model.y}});if(hxH>=820){hxH=820}else if(50>=hxH){hxH=50}
if(hxL>=820){hxL=820}else if(70>=hxL){hxL=70}
ctx.fillStyle='#ffa7a2';ctx.fillText(highestNum+"ms",hxH-40,hyH+15);ctx.fillStyle='#45d642';ctx.fillText(lowestnum+"ms",hxL,hyL+10);console.log("done service_id_{{.Id}}")})}},legend:{display:!1},tooltips:{"enabled":!1},scales:{yAxes:[{display:!1,ticks:{fontSize:20,display:!1,beginAtZero:!1},gridLines:{display:!1}}],xAxes:[{type:'time',distribution:'series',autoSkip:!1,gridLines:{display:!1},ticks:{stepSize:1,min:0,fontColor:"white",fontSize:20,display:!1,}}]},elements:{point:{radius:0}}}})
{{ end }}
{{ end }}

View File

View File

View File

@ -45,9 +45,7 @@
<div class="mt-4" id="service_id_{{.Id}}">
<div class="card">
<div class="card-body">
<div class="col-12">
<h4 class="mt-3"><a href="/service/{{.Id}}"{{if not .Online}} class="text-danger"{{end}}>{{ .Name }}</a>
{{if .Online}}
<span class="badge bg-success float-right">ONLINE</span>