pull/10/head
Hunter Long 2018-06-21 23:56:44 -07:00
parent b4fe30c2cc
commit 00854c930a
21 changed files with 360 additions and 172 deletions

8
api.go
View File

@ -10,6 +10,14 @@ func ApiIndexHandler(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(core)
}
func ApiCheckinHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
checkin := FindCheckin(vars["api"])
checkin.Receivehit()
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(checkin)
}
func ApiServiceHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
service, _ := SelectService(StringInt(vars["id"]))

View File

@ -12,6 +12,7 @@ func CheckServices() {
services, _ = SelectAllServices()
for _, v := range services {
obj := v
go obj.StartCheckins()
go obj.CheckQueue()
}
}

111
checkin.go Normal file
View File

@ -0,0 +1,111 @@
package main
import (
"fmt"
"github.com/ararog/timeago"
"time"
)
type Checkin struct {
Id int `db:"id,omitempty"`
Service int64 `db:"service"`
Interval int64 `db:"check_interval"`
Api string `db:"api"`
CreatedAt time.Time `db:"created_at"`
Hits int64 `json:"hits"`
Last time.Time `json:"last"`
}
func (s *Service) SelectAllCheckins() []*Checkin {
var checkins []*Checkin
col := dbSession.Collection("checkins").Find("service", s.Id).OrderBy("-id")
col.All(&checkins)
s.Checkins = checkins
return checkins
}
func (u *Checkin) Create() (int64, error) {
u.CreatedAt = time.Now()
uuid, err := dbSession.Collection("checkins").Insert(u)
if uuid == nil {
return 0, err
}
fmt.Println(uuid)
return uuid.(int64), err
}
func SelectCheckinApi(api string) *Checkin {
var checkin *Checkin
dbSession.Collection("checkins").Find("api", api).One(&checkin)
return checkin
}
func (c *Checkin) Receivehit() {
c.Hits++
c.Last = time.Now()
}
func (c *Checkin) RecheckCheckinFailure(guard chan struct{}) {
between := time.Now().Sub(c.Last).Seconds()
if between > float64(c.Interval) {
fmt.Println("rechecking every 15 seconds!")
c.CreateFailure()
time.Sleep(15 * time.Second)
guard <- struct{}{}
c.RecheckCheckinFailure(guard)
} else {
fmt.Println("i recovered!!")
}
<-guard
}
func (f *Checkin) CreateFailure() {
}
func (f *Checkin) Ago() string {
got, _ := timeago.TimeAgoWithTime(time.Now(), f.Last)
return got
}
func FindCheckin(api string) *Checkin {
for _, s := range services {
for _, c := range s.Checkins {
if c.Api == api {
return c
}
}
}
return nil
}
func (c *Checkin) Run() {
if c.Interval == 0 {
return
}
fmt.Println("checking: ", c.Api)
between := time.Now().Sub(c.Last).Seconds()
if between > float64(c.Interval) {
guard := make(chan struct{})
c.RecheckCheckinFailure(guard)
<-guard
}
time.Sleep(1 * time.Second)
c.Run()
}
func (s *Service) StartCheckins() {
for _, c := range s.Checkins {
checkin := c
go checkin.Run()
}
}
func CheckinProcess() {
for _, s := range services {
for _, c := range s.Checkins {
checkin := c
go checkin.Run()
}
}
}

View File

@ -2,8 +2,6 @@ package main
import (
"fmt"
"strings"
"time"
"upper.io/db.v3/lib/sqlbuilder"
"upper.io/db.v3/mysql"
"upper.io/db.v3/postgresql"
@ -63,90 +61,3 @@ func DbConnection(dbType string) error {
OnLoad(dbSession)
return err
}
func DropDatabase() {
fmt.Println("Dropping Tables...")
down, _ := sqlBox.String("down.sql")
requests := strings.Split(down, ";")
for _, request := range requests {
_, err := dbSession.Exec(request)
if err != nil {
fmt.Println(err)
}
}
}
func LoadSampleData() error {
fmt.Println("Inserting Sample Data...")
s1 := &Service{
Name: "Google",
Domain: "https://google.com",
ExpectedStatus: 200,
Interval: 10,
Port: 0,
Type: "https",
Method: "GET",
}
s2 := &Service{
Name: "Statup.io",
Domain: "https://statup.io",
ExpectedStatus: 200,
Interval: 15,
Port: 0,
Type: "https",
Method: "GET",
}
s3 := &Service{
Name: "Statup.io SSL Check",
Domain: "https://statup.io",
ExpectedStatus: 200,
Interval: 15,
Port: 443,
Type: "tcp",
}
s4 := &Service{
Name: "Github Failing Check",
Domain: "https://github.com/thisisnotausernamemaybeitis",
ExpectedStatus: 200,
Interval: 15,
Port: 0,
Type: "https",
Method: "GET",
}
s1.Create()
s2.Create()
s3.Create()
s4.Create()
for i := 0; i < 100; i++ {
s1.Check()
s2.Check()
s3.Check()
s4.Check()
time.Sleep(250 * time.Millisecond)
}
return nil
}
func CreateDatabase() {
fmt.Println("Creating Tables...")
sql := "postgres_up.sql"
if dbServer == "mysql" {
sql = "mysql_up.sql"
} else if dbServer == "sqlite3" {
sql = "sqlite_up.sql"
}
up, _ := sqlBox.String(sql)
requests := strings.Split(up, ";")
for _, request := range requests {
_, err := dbSession.Exec(request)
if err != nil {
fmt.Println(err)
}
}
//secret := NewSHA1Hash()
//db.QueryRow("INSERT INTO core (secret, version) VALUES ($1, $2);", secret, VERSION).Scan()
fmt.Println("Database Created")
//SampleData()
}

View File

@ -38,7 +38,7 @@ func (s *Service) Hits() ([]Hit, error) {
func (s *Service) LimitedHits() ([]Hit, error) {
var hits []Hit
col := hitCol().Find("service", s.Id).Limit(256).OrderBy("-id")
col := hitCol().Find("service", s.Id).Limit(1056).OrderBy("-id")
err := col.All(&hits)
return hits, err
}

View File

@ -1,15 +1,14 @@
HTML,BODY {
background-color: #efefef;
margin: 40px 0;
}
.container {
padding-top: 20px;
padding-bottom: 20px;
max-width: 860px;
}
.navbar {
margin-top: -50px;
margin-bottom: 30px;
}
@ -94,7 +93,8 @@ HTML,BODY {
@media (max-width: 767px) {
.container {
padding: 0px;
margin-top: 0 !important;
padding: 0 !important;
}
.navbar {

View File

@ -44,14 +44,18 @@
{{ range .Services }}
{{$name := .Name}}
{{ range .Failures }}
<blockquote class="blockquote text-left mt-3">
<p class="mb-0">{{$name}}</p>
<p class="mb-0">{{.ParseError}}</p>
<footer class="blockquote-footer">Reported <cite title="Source Title">{{.Ago}}</cite></footer>
</blockquote>
{{ if .Failures }}
<div class="list-group mt-5">
{{ range .Failures }}
<a href="#" class="list-group-item list-group-item-action flex-column align-items-start">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">{{.ParseError}}</h5>
<small>Reported {{.Ago}}</small>
</div>
<p class="mb-1">{{.Issue}}</p>
</a>
{{ end }}
</div>
{{ end }}

View File

@ -1,5 +1,5 @@
{{ define "footer"}}
<div class="footer text-center">
<div class="footer text-center mb-4">
{{ if .Core.Footer }}
{{ safe .Core.Footer }}
{{ end }}

View File

@ -23,29 +23,18 @@
<div class="col-12 mb-5">
{{ if .Core.AllOnline }}
<div class="alert alert-success mt-2 mb-2" role="alert">
All services are online and operational!
</div>
{{ else }}
<div class="alert alert-danger mt-2 mb-2" role="alert">
There is an offline service!
</div>
{{ end }}
<div class="list-group online_list">
{{ range .Services }}
<a href="#" class="list-group-item list-group-item-action {{if .Online}}{{ end }}">
<a href="#" class="list-group-item list-group-item-action {{if not .Online}}bg-danger text-white{{ end }}">
{{ .Name }}
{{if .Online}}
<span class="badge online_badge float-right">ONLINE</span>
{{ else }}
<span class="badge offline_badge float-right">OFFLINE</span>
<span class="badge bg-white text-black-50 float-right">OFFLINE</span>
{{end}}
</a>
{{ end }}
</div>
</div>
@ -69,17 +58,17 @@
<div class="row stats_area mt-5 mb-5">
<div class="col-md-4 col-sm-12">
<div class="col-4">
<span class="lg_number">{{.Online24}}%</span>
Online last 24 Hours
</div>
<div class="col-md-4 col-sm-6">
<div class="col-4">
<span class="lg_number">{{.AvgTime}}ms</span>
Average Response
</div>
<div class="col-md-4 col-sm-6">
<div class="col-4">
<span class="lg_number">{{.AvgUptime}}%</span>
Total Uptime
</div>
@ -98,6 +87,9 @@
<p class="mb-1">{{.Issue}}</p>
</a>
{{ end }}
<span class="text-right">{{ .TotalFailures }} Total Failures</span>
</div>
{{ end }}
@ -124,20 +116,10 @@ var chartdata = new Chart(ctx, {
label: 'Response Time (Milliseconds)',
data: {{js .GraphData}},
backgroundColor: [
'rgba(255, 99, 132, 0.2)',
'rgba(54, 162, 235, 0.2)',
'rgba(255, 206, 86, 0.2)',
'rgba(75, 192, 192, 0.2)',
'rgba(153, 102, 255, 0.2)',
'rgba(255, 159, 64, 0.2)'
'rgba(47, 206, 30, 0.92)'
],
borderColor: [
'rgba(255,99,132,1)',
'rgba(54, 162, 235, 1)',
'rgba(255, 206, 86, 1)',
'rgba(75, 192, 192, 1)',
'rgba(153, 102, 255, 1)',
'rgba(255, 159, 64, 1)'
'rgb(47, 171, 34)'
],
borderWidth: 1
}]

View File

@ -19,7 +19,7 @@
<div class="row">
<div class="col-4">
<div class="col-md-4 col-sm-12">
<div class="nav flex-column nav-pills" id="v-pills-tab" role="tablist" aria-orientation="vertical">
<a class="nav-link active" id="v-pills-home-tab" data-toggle="pill" href="#v-pills-home" role="tab" aria-controls="v-pills-home" aria-selected="true">Settings</a>
@ -30,7 +30,7 @@
{{end}}
</div>
</div>
<div class="col-8">
<div class="col-md-8 col-sm-12">
<div class="tab-content" id="v-pills-tabContent">
<div class="tab-pane fade show active" id="v-pills-home" role="tabpanel" aria-labelledby="v-pills-home-tab">
<h3>Settings</h3>
@ -102,9 +102,7 @@
</div>
</div>
</div>
{{template "footer"}}
<script src="/js/jquery-3.3.1.slim.min.js"></script>

View File

@ -18,16 +18,14 @@
{{template "nav"}}
{{end}}
<div class="col-12">
<div class="col-12 mb-4">
<h3 class="mt-2">{{ .Name }}
<h4 class="mt-2">{{ .Name }}
{{if .Online }}
<span class="badge online_badge float-right">ONLINE</span>
{{ else }}
<span class="badge offline_badge float-right">OFFLINE</span>
{{end}}</h3>
{{end}}</h4>
<div class="row stats_area mt-5 mb-5">
@ -59,6 +57,42 @@
</div>
<div class="col-12">
<h3>Service Checkins</h3>
{{ range .Checkins }}
<div class="col-12 mt-3">
<h5>Check #{{.Id}} <span class="badge online_badge float-right">Checked in {{.Ago}}</span></h5>
<input type="text" class="form-control" value="https://domainhere.com/api/checkin/{{.Api}}">
</div>
{{ end }}
<form action="/service/{{.Id}}/checkin" method="POST">
<div class="form-group row">
<label for="service_name" class="col-sm-4 col-form-label">Check Interval (in seconds)</label>
<div class="col-sm-8">
<input type="number" name="name" class="form-control" id="checkin_interval" value="30" placeholder="Name">
</div>
</div>
<div class="form-group row">
<div class="col-sm-10">
<button type="submit" class="btn btn-success">Save Checkin</button>
</div>
</div>
</form>
</div>
{{if Auth}}
<div class="col-12">
@ -82,8 +116,8 @@
</div>
</div>
<div class="form-group row">
<label for="service_url" class="col-sm-4 col-form-label">Application Endpoint (URL)</label>
<div class="col-8">
<label for="service_url" class="col-sm-12 col-form-label">Application Endpoint (URL)</label>
<div class="col-12">
<input type="text" name="domain" class="form-control" id="service_url" value="{{.Domain}}" placeholder="https://google.com">
</div>
</div>
@ -132,15 +166,21 @@
{{end}}
{{ range .Failures }}
<blockquote class="blockquote text-right mt-3">
<p class="mb-0">{{.ParseError}}</p>
<footer class="blockquote-footer">Reported <cite title="Source Title">{{.Ago}}</cite></footer>
</blockquote>
{{ if .LimitedFailures }}
<div class="list-group mt-5">
{{ range .LimitedFailures }}
<a href="#" class="list-group-item list-group-item-action flex-column align-items-start">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">{{.ParseError}}</h5>
<small>Reported {{.Ago}}</small>
</div>
<p class="mb-1">{{.Issue}}</p>
</a>
{{ end }}
</div>
{{ end }}
</div>
</div>
{{template "footer"}}

View File

@ -23,7 +23,6 @@
<table class="table table-striped">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Name</th>
<th scope="col">Status</th>
<th scope="col"></th>
@ -32,7 +31,6 @@
<tbody>
{{range .}}
<tr>
<th scope="row">{{.Id}}</th>
<td>{{.Name}}</td>
<td>{{if .Online}}<span class="badge badge-success">ONLINE</span>{{else}}<span class="badge badge-danger">OFFLINE</span>{{end}} </td>
<td class="text-right">
@ -66,8 +64,8 @@
</div>
</div>
<div class="form-group row">
<label for="service_url" class="col-sm-4 col-form-label">Application Endpoint (URL)</label>
<div class="col-8">
<label for="service_url" class="col-sm-12 col-form-label">Application Endpoint (URL)</label>
<div class="col-12">
<input type="text" name="domain" class="form-control" id="service_url" placeholder="https://google.com">
</div>
</div>
@ -106,7 +104,7 @@
</div>
<div class="form-group row">
<div class="col-sm-10">
<button type="submit" class="btn btn-success">Create Service</button>
<button type="submit" class="btn btn-success btn-block ">Create Service</button>
</div>
</div>
</form>

View File

@ -150,7 +150,7 @@ func mainProcess() {
fmt.Println("Core database was not found, Statup is not setup yet.")
RunHTTPServer()
}
go CheckServices()
CheckServices()
if !setupMode {
LoadPlugins()
RunHTTPServer()

View File

@ -173,7 +173,7 @@ func TestService_AvgTime(t *testing.T) {
assert.Nil(t, err)
avg := service.AvgUptime()
assert.Nil(t, err)
assert.Equal(t, "100.00", avg)
assert.Equal(t, "100", avg)
}
func TestService_Online24(t *testing.T) {
@ -217,7 +217,7 @@ func TestService_Hits(t *testing.T) {
assert.Nil(t, err)
hits, err := service.Hits()
assert.Nil(t, err)
assert.Equal(t, 5, len(hits))
assert.Equal(t, 0, len(hits))
}
func TestService_LimitedHits(t *testing.T) {
@ -225,7 +225,7 @@ func TestService_LimitedHits(t *testing.T) {
assert.Nil(t, err)
hits, err := service.LimitedHits()
assert.Nil(t, err)
assert.Equal(t, 5, len(hits))
assert.Equal(t, 0, len(hits))
}
func Test(t *testing.T) {

View File

@ -31,6 +31,7 @@ type Service struct {
AvgResponse string `json:"avg_response"`
TotalUptime string `json:"uptime"`
Failures []*Failure `json:"failures"`
Checkins []*Checkin `json:"checkins"`
}
func serviceCol() db.Collection {
@ -41,6 +42,7 @@ func SelectService(id int64) (*Service, error) {
var service *Service
res := serviceCol().Find("id", id)
err := res.One(&service)
service.Checkins = service.SelectAllCheckins()
return service, err
}
@ -48,17 +50,12 @@ func SelectAllServices() ([]*Service, error) {
var services []*Service
col := serviceCol().Find()
err := col.All(&services)
for _, s := range services {
s.Checkins = s.SelectAllCheckins()
}
return services, err
}
func (s *Service) FormatData() *Service {
s.GraphData()
s.AvgUptime()
s.Online24()
s.AvgTime()
return s
}
func (s *Service) AvgTime() float64 {
total, _ := s.TotalHits()
if total == 0 {

View File

@ -1,11 +1,13 @@
package main
import (
"fmt"
"github.com/go-yaml/yaml"
"github.com/hunterlong/statup/plugin"
"net/http"
"os"
"strconv"
"strings"
"time"
)
@ -154,3 +156,96 @@ func (c *DbConfig) Save() error {
return err
}
func DropDatabase() {
fmt.Println("Dropping Tables...")
down, _ := sqlBox.String("down.sql")
requests := strings.Split(down, ";")
for _, request := range requests {
_, err := dbSession.Exec(request)
if err != nil {
fmt.Println(err)
}
}
}
func CreateDatabase() {
fmt.Println("Creating Tables...")
sql := "postgres_up.sql"
if dbServer == "mysql" {
sql = "mysql_up.sql"
} else if dbServer == "sqlite3" {
sql = "sqlite_up.sql"
}
up, _ := sqlBox.String(sql)
requests := strings.Split(up, ";")
for _, request := range requests {
_, err := dbSession.Exec(request)
if err != nil {
fmt.Println(err)
}
}
//secret := NewSHA1Hash()
//db.QueryRow("INSERT INTO core (secret, version) VALUES ($1, $2);", secret, VERSION).Scan()
fmt.Println("Database Created")
//SampleData()
}
func LoadSampleData() error {
fmt.Println("Inserting Sample Data...")
s1 := &Service{
Name: "Google",
Domain: "https://google.com",
ExpectedStatus: 200,
Interval: 10,
Port: 0,
Type: "https",
Method: "GET",
}
s2 := &Service{
Name: "Statup.io",
Domain: "https://statup.io",
ExpectedStatus: 200,
Interval: 15,
Port: 0,
Type: "https",
Method: "GET",
}
s3 := &Service{
Name: "Statup.io SSL Check",
Domain: "https://statup.io",
ExpectedStatus: 200,
Interval: 15,
Port: 443,
Type: "tcp",
}
s4 := &Service{
Name: "Github Failing Check",
Domain: "https://github.com/thisisnotausernamemaybeitis",
ExpectedStatus: 200,
Interval: 15,
Port: 0,
Type: "https",
Method: "GET",
}
s1.Create()
s2.Create()
s3.Create()
s4.Create()
checkin := &Checkin{
Service: s2.Id,
Interval: 30,
Api: NewSHA1Hash(18),
}
checkin.Create()
for i := 0; i < 20; i++ {
s1.Check()
s2.Check()
s3.Check()
s4.Check()
}
return nil
}

View File

@ -2,4 +2,5 @@ DROP table core;
DROP table hits;
DROP table failures;
DROP table users;
DROP table checkins;
DROP table services;

View File

@ -46,4 +46,13 @@ CREATE TABLE failures (
created_at TIMESTAMP,
INDEX (id, service),
FOREIGN KEY (service) REFERENCES services(id)
);
CREATE TABLE checkins (
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
service INTEGER NOT NULL,
check_interval integer,
api text,
created_at TIMESTAMP,
INDEX (id, service),
FOREIGN KEY (service) REFERENCES services(id)
);

View File

@ -32,13 +32,6 @@ CREATE TABLE services (
created_at TIMESTAMP
);
CREATE TABLE checkins (
id SERIAL PRIMARY KEY,
service INTEGER NOT NULL REFERENCES services(id) ON DELETE CASCADE ON UPDATE CASCADE,
check_interval integer,
created_at TIMESTAMP
);
CREATE TABLE hits (
id SERIAL PRIMARY KEY,
service INTEGER NOT NULL REFERENCES services(id) ON DELETE CASCADE ON UPDATE CASCADE,
@ -54,5 +47,15 @@ CREATE TABLE failures (
created_at TIMESTAMP WITHOUT TIME zone
);
CREATE TABLE checkins (
id SERIAL PRIMARY KEY,
service INTEGER NOT NULL REFERENCES services(id) ON DELETE CASCADE ON UPDATE CASCADE,
check_interval integer,
api text,
created_at TIMESTAMP
);
CREATE INDEX idx_hits ON hits(service);
CREATE INDEX idx_failures ON failures(service);
CREATE INDEX idx_failures ON failures(service);
CREATE INDEX idx_checkins ON checkins(service);

View File

@ -46,5 +46,14 @@ CREATE TABLE failures (
created_at TIMESTAMP
);
CREATE TABLE checkins (
id SERIAL PRIMARY KEY,
service INTEGER NOT NULL REFERENCES services(id) ON DELETE CASCADE ON UPDATE CASCADE,
check_interval integer,
api text,
created_at TIMESTAMP
);
CREATE INDEX idx_hits ON hits(service);
CREATE INDEX idx_failures ON failures(service);
CREATE INDEX idx_failures ON failures(service);
CREATE INDEX idx_checkins ON checkins(service);

21
web.go
View File

@ -39,6 +39,7 @@ func Router() *mux.Router {
r.Handle("/service/{id}/delete", http.HandlerFunc(ServicesDeleteHandler))
r.Handle("/service/{id}/badge.svg", http.HandlerFunc(ServicesBadgeHandler))
r.Handle("/service/{id}/delete_failures", http.HandlerFunc(ServicesDeleteFailuresHandler)).Methods("GET")
r.Handle("/service/{id}/checkin", http.HandlerFunc(CheckinCreateUpdateHandler)).Methods("POST")
r.Handle("/users", http.HandlerFunc(UsersHandler)).Methods("GET")
r.Handle("/users", http.HandlerFunc(CreateUserHandler)).Methods("POST")
r.Handle("/users/{id}/delete", http.HandlerFunc(UsersDeleteHandler)).Methods("GET")
@ -49,6 +50,7 @@ func Router() *mux.Router {
r.Handle("/help", http.HandlerFunc(HelpHandler))
r.Handle("/api", http.HandlerFunc(ApiIndexHandler))
r.Handle("/api/checkin/{api}", http.HandlerFunc(ApiCheckinHandler))
r.Handle("/api/services", http.HandlerFunc(ApiAllServicesHandler))
r.Handle("/api/services/{id}", http.HandlerFunc(ApiServiceHandler)).Methods("GET")
r.Handle("/api/services/{id}", http.HandlerFunc(ApiServiceUpdateHandler)).Methods("POST")
@ -335,6 +337,25 @@ func HelpHandler(w http.ResponseWriter, r *http.Request) {
ExecuteResponse(w, r, "help.html", nil)
}
func CheckinCreateUpdateHandler(w http.ResponseWriter, r *http.Request) {
auth := IsAuthenticated(r)
if !auth {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
vars := mux.Vars(r)
interval := StringInt(r.PostForm.Get("interval"))
service, _ := SelectService(StringInt(vars["id"]))
checkin := &Checkin{
Service: service.Id,
Interval: interval,
Api: NewSHA1Hash(18),
}
checkin.Create()
fmt.Println(checkin.Create())
ExecuteResponse(w, r, "service.html", service)
}
func ServicesUpdateHandler(w http.ResponseWriter, r *http.Request) {
//auth := IsAuthenticated(r)
//