pull/10/head
Hunter Long 2018-06-21 21:02:57 -07:00
parent 7705087345
commit b4fe30c2cc
26 changed files with 472 additions and 177 deletions

4
.gitignore vendored
View File

@ -3,4 +3,6 @@ rice-box.go
config.yml config.yml
statup.db statup.db
plugins/*.so plugins/*.so
data data
build
vendor

View File

@ -45,6 +45,9 @@ notifications:
before_install: before_install:
- if [[ "$TRAVIS_BRANCH" == "master" ]]; then travis_wait 30 docker pull karalabe/xgo-latest; fi - if [[ "$TRAVIS_BRANCH" == "master" ]]; then travis_wait 30 docker pull karalabe/xgo-latest; fi
after_success:
- curl -s -X POST $DOCKER > /dev/null
before_script: before_script:
- mysql -e 'CREATE DATABASE IF NOT EXISTS test;' - mysql -e 'CREATE DATABASE IF NOT EXISTS test;'
- psql -c 'create database test;' -U postgres - psql -c 'create database test;' -U postgres

139
Gopkg.lock generated Normal file
View File

@ -0,0 +1,139 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
branch = "master"
name = "github.com/GeertJohan/go.rice"
packages = [
".",
"embedded"
]
revision = "c02ca9a983da5807ddf7d796784928f5be4afd09"
[[projects]]
branch = "master"
name = "github.com/daaku/go.zipexe"
packages = ["."]
revision = "a5fe2436ffcb3236e175e5149162b41cd28bd27d"
[[projects]]
name = "github.com/davecgh/go-spew"
packages = ["spew"]
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
version = "v1.1.0"
[[projects]]
name = "github.com/fatih/structs"
packages = ["."]
revision = "a720dfa8df582c51dee1b36feabb906bde1588bd"
version = "v1.0"
[[projects]]
name = "github.com/go-sql-driver/mysql"
packages = ["."]
revision = "d523deb1b23d913de5bdada721a6071e71283618"
version = "v1.4.0"
[[projects]]
name = "github.com/go-yaml/yaml"
packages = ["."]
revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183"
version = "v2.2.1"
[[projects]]
name = "github.com/gorilla/context"
packages = ["."]
revision = "08b5f424b9271eedf6f9f0ce86cb9396ed337a42"
version = "v1.1.1"
[[projects]]
name = "github.com/gorilla/mux"
packages = ["."]
revision = "e3702bed27f0d39777b0b37b664b6280e8ef8fbf"
version = "v1.6.2"
[[projects]]
name = "github.com/gorilla/securecookie"
packages = ["."]
revision = "e59506cc896acb7f7bf732d4fdf5e25f7ccd8983"
version = "v1.1.1"
[[projects]]
name = "github.com/gorilla/sessions"
packages = ["."]
revision = "03b6f63cc43ef9c7240a635a5e22b13180e822b8"
version = "v1.1.1"
[[projects]]
branch = "master"
name = "github.com/kardianos/osext"
packages = ["."]
revision = "ae77be60afb1dcacde03767a8c37337fad28ac14"
[[projects]]
branch = "master"
name = "github.com/lib/pq"
packages = [
".",
"oid"
]
revision = "90697d60dd844d5ef6ff15135d0203f65d2f53b8"
[[projects]]
name = "github.com/mattn/go-sqlite3"
packages = ["."]
revision = "25ecb14adfc7543176f7d85291ec7dba82c6f7e4"
version = "v1.9.0"
[[projects]]
name = "github.com/pmezard/go-difflib"
packages = ["difflib"]
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
version = "v1.0.0"
[[projects]]
name = "github.com/stretchr/testify"
packages = ["assert"]
revision = "f35b8ab0b5a2cef36673838d662e249dd9c94686"
version = "v1.2.2"
[[projects]]
branch = "master"
name = "golang.org/x/crypto"
packages = [
"bcrypt",
"blowfish"
]
revision = "a49355c7e3f8fe157a85be2f77e6e269a0f89602"
[[projects]]
name = "google.golang.org/appengine"
packages = ["cloudsql"]
revision = "b1f26356af11148e710935ed1ac8a7f5702c7612"
version = "v1.1.0"
[[projects]]
name = "upper.io/db.v3"
packages = [
".",
"internal/cache",
"internal/cache/hashstructure",
"internal/immutable",
"internal/sqladapter",
"internal/sqladapter/compat",
"internal/sqladapter/exql",
"lib/reflectx",
"lib/sqlbuilder",
"mysql",
"postgresql",
"sqlite"
]
revision = "d90922beee6de3f39c93ed677f6da82565d07154"
version = "v3.5.3"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "d8ed5645d6f4e746646932baa40deb382ea017a84594ec46fda94538f4d00db2"
solver-name = "gps-cdcl"
solver-version = 1

62
Gopkg.toml Normal file
View File

@ -0,0 +1,62 @@
# Gopkg.toml example
#
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
# for detailed Gopkg.toml documentation.
#
# required = ["github.com/user/thing/cmd/thing"]
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
#
# [[constraint]]
# name = "github.com/user/project"
# version = "1.0.0"
#
# [[constraint]]
# name = "github.com/user/project2"
# branch = "dev"
# source = "github.com/myfork/project2"
#
# [[override]]
# name = "github.com/x/y"
# version = "2.4.0"
#
# [prune]
# non-go = false
# go-tests = true
# unused-packages = true
[[constraint]]
branch = "master"
name = "github.com/GeertJohan/go.rice"
[[constraint]]
name = "github.com/fatih/structs"
version = "1.0.0"
[[constraint]]
name = "github.com/go-yaml/yaml"
version = "2.2.1"
[[constraint]]
name = "github.com/gorilla/mux"
version = "1.6.2"
[[constraint]]
name = "github.com/gorilla/sessions"
version = "1.1.1"
[[constraint]]
name = "github.com/stretchr/testify"
version = "1.2.2"
[[constraint]]
branch = "master"
name = "golang.org/x/crypto"
[[constraint]]
name = "upper.io/db.v3"
version = "3.5.3"
[prune]
go-tests = true
unused-packages = true

View File

@ -26,6 +26,15 @@ func (c *Core) Update() (*Core, error) {
return c, nil return c, nil
} }
func (c Core) AllOnline() bool {
for _, s := range services {
if !s.Online {
return false
}
}
return true
}
func SelectCore() (*Core, error) { func SelectCore() (*Core, error) {
var core Core var core Core
err := dbSession.Collection("core").Find().One(&core) err := dbSession.Collection("core").Find().One(&core)

View File

@ -8,11 +8,15 @@ import (
func (f *Failure) ParseError() string { func (f *Failure) ParseError() string {
err := strings.Contains(f.Issue, "operation timed out") err := strings.Contains(f.Issue, "operation timed out")
if err { if err {
return fmt.Sprintf("HTTP Request timed out after x seconds") return fmt.Sprintf("HTTP Request Timed Out")
} }
err = strings.Contains(f.Issue, "x509: certificate is valid") err = strings.Contains(f.Issue, "x509: certificate is valid")
if err { if err {
return fmt.Sprintf("SSL Certificate invalid") return fmt.Sprintf("SSL Certificate invalid")
} }
err = strings.Contains(f.Issue, "no such host")
if err {
return fmt.Sprintf("Domain is offline or not found")
}
return f.Issue return f.Issue
} }

View File

@ -1,6 +1,7 @@
package main package main
import ( import (
"github.com/ararog/timeago"
"time" "time"
) )
@ -9,7 +10,6 @@ type Failure struct {
Issue string `db:"issue"` Issue string `db:"issue"`
Service int64 `db:"service"` Service int64 `db:"service"`
CreatedAt time.Time `db:"created_at"` CreatedAt time.Time `db:"created_at"`
Ago string
} }
func (s *Service) CreateFailure(data FailureData) (int64, error) { func (s *Service) CreateFailure(data FailureData) (int64, error) {
@ -29,18 +29,32 @@ func (s *Service) CreateFailure(data FailureData) (int64, error) {
func (s *Service) SelectAllFailures() ([]*Failure, error) { func (s *Service) SelectAllFailures() ([]*Failure, error) {
var fails []*Failure var fails []*Failure
col := dbSession.Collection("failures").Find("service", s.Id) col := dbSession.Collection("failures").Find("service", s.Id).OrderBy("-id")
err := col.All(&fails) err := col.All(&fails)
return fails, err return fails, err
} }
func (u *Service) DeleteFailures() {
var fails []*Failure
col := dbSession.Collection("failures")
col.Find("service", u.Id).All(&fails)
for _, fail := range fails {
fail.Delete()
}
}
func (s *Service) LimitedFailures() []*Failure { func (s *Service) LimitedFailures() []*Failure {
var fails []*Failure var fails []*Failure
col := dbSession.Collection("failures").Find("service", s.Id).Limit(10) col := dbSession.Collection("failures").Find("service", s.Id)
col.All(&fails) col.OrderBy("-id").Limit(10).All(&fails)
return fails return fails
} }
func (f *Failure) Ago() string {
got, _ := timeago.TimeAgoWithTime(time.Now(), f.CreatedAt)
return got
}
func (f *Failure) Delete() error { func (f *Failure) Delete() error {
col := dbSession.Collection("failures").Find("id", f.Id) col := dbSession.Collection("failures").Find("id", f.Id)
return col.Delete() return col.Delete()

12
go.mod
View File

@ -1,12 +0,0 @@
module github.com/hunterlong/statup
require (
github.com/GeertJohan/go.rice v0.0.0-20170420135705-c02ca9a983da
github.com/daaku/go.zipexe v0.0.0-20150329023125-a5fe2436ffcb
github.com/go-yaml/yaml v0.0.0-20180328195020-5420a8b6744d
github.com/gorilla/sessions v1.1.1
github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1
github.com/lib/pq v0.0.0-20180523175426-90697d60dd84
golang.org/x/crypto v0.0.0-20180608092829-8ac0e0d97ce4
gopkg.in/check.v1 v1.0.0-20161208181325-20d25e280405
)

25
hits.go
View File

@ -1,6 +1,9 @@
package main package main
import "time" import (
"time"
"upper.io/db.v3"
)
type Hit struct { type Hit struct {
Id int `db:"id,omitempty"` Id int `db:"id,omitempty"`
@ -9,14 +12,17 @@ type Hit struct {
CreatedAt time.Time `db:"created_at"` CreatedAt time.Time `db:"created_at"`
} }
func hitCol() db.Collection {
return dbSession.Collection("hits")
}
func (s *Service) CreateHit(d HitData) (int64, error) { func (s *Service) CreateHit(d HitData) (int64, error) {
h := Hit{ h := Hit{
Service: s.Id, Service: s.Id,
Latency: d.Latency, Latency: d.Latency,
CreatedAt: time.Now(), CreatedAt: time.Now(),
} }
col := dbSession.Collection("hits") uuid, err := hitCol().Insert(h)
uuid, err := col.Insert(h)
if uuid == nil { if uuid == nil {
return 0, err return 0, err
} }
@ -25,20 +31,27 @@ func (s *Service) CreateHit(d HitData) (int64, error) {
func (s *Service) Hits() ([]Hit, error) { func (s *Service) Hits() ([]Hit, error) {
var hits []Hit var hits []Hit
col := dbSession.Collection("hits").Find("service", s.Id) col := hitCol().Find("service", s.Id).OrderBy("-id")
err := col.All(&hits)
return hits, err
}
func (s *Service) LimitedHits() ([]Hit, error) {
var hits []Hit
col := hitCol().Find("service", s.Id).Limit(256).OrderBy("-id")
err := col.All(&hits) err := col.All(&hits)
return hits, err return hits, err
} }
func (s *Service) SelectHitsGroupBy(group string) ([]Hit, error) { func (s *Service) SelectHitsGroupBy(group string) ([]Hit, error) {
var hits []Hit var hits []Hit
col := dbSession.Collection("hits").Find("service", s.Id) col := hitCol().Find("service", s.Id)
err := col.All(&hits) err := col.All(&hits)
return hits, err return hits, err
} }
func (s *Service) TotalHits() (uint64, error) { func (s *Service) TotalHits() (uint64, error) {
col := dbSession.Collection("hits").Find("service", s.Id) col := hitCol().Find("service", s.Id)
amount, err := col.Count() amount, err := col.Count()
return amount, err return amount, err
} }

View File

@ -4,16 +4,12 @@ HTML,BODY {
} }
.container { .container {
max-width: 790px; padding-top: 20px;
background-color: white; padding-bottom: 20px;
padding: 50px;
border-radius: 7px;
} }
.navbar { .navbar {
margin-left: -50px;
margin-top: -50px; margin-top: -50px;
width: 790px;
margin-bottom: 30px; margin-bottom: 30px;
} }
@ -54,7 +50,7 @@ HTML,BODY {
} }
.online_list { .online_list {
font-size: 1.5rem;
} }
.footer { .footer {
@ -90,17 +86,13 @@ HTML,BODY {
overflow: hidden; overflow: hidden;
} }
.card-body H3 A { .card-body H4 A {
color: #424242; color: #239e07;
text-decoration: none;
} }
@media (max-width: 767px) { @media (max-width: 767px) {
HTML,BODY {
background-color: #efefef;
margin: 0px 0;
}
.container { .container {
padding: 0px; padding: 0px;
} }
@ -117,7 +109,7 @@ HTML,BODY {
} }
.lg_number { .lg_number {
font-size: 22pt; font-size: 1.2rem;
} }

View File

@ -12,10 +12,12 @@
<body> <body>
<div class="container"> <div class="container col-md-7 col-sm-12 mt-2 bg-light">
{{template "nav"}} {{template "nav"}}
<div class="col-12">
<div class="row stats_area mb-4"> <div class="row stats_area mb-4">
<div class="col-4"> <div class="col-4">
@ -57,10 +59,12 @@
</div> </div>
</div>
</div> </div>
</div> </div>
{{template "footer"}}
<script src="/js/jquery-3.3.1.slim.min.js"></script> <script src="/js/jquery-3.3.1.slim.min.js"></script>
<script src="/js/bootstrap.min.js"></script> <script src="/js/bootstrap.min.js"></script>

8
html/tmpl/footer.html Normal file
View File

@ -0,0 +1,8 @@
{{ define "footer"}}
<div class="footer text-center">
{{ if .Core.Footer }}
{{ safe .Core.Footer }}
{{ end }}
<a href="https://statup.io" target="_blank">Statup made with ❤️</a> | <a href="/dashboard">Dashboard</a>
</div>
{{ end }}

View File

@ -11,15 +11,12 @@
</head> </head>
<body> <body>
<div class="container col-md-7 col-sm-12 mt-2 bg-light">
<div class="container">
{{if Auth}} {{if Auth}}
{{template "nav"}} {{template "nav"}}
{{end}} {{end}}
<div class="row">
<div class="col-12"> <div class="col-12">
<h2>Statup v{{ VERSION }} Help</h2> <h2>Statup v{{ VERSION }} Help</h2>
@ -51,8 +48,6 @@
</div> </div>
</div>
</div> </div>
<script src="/js/jquery-3.3.1.slim.min.js"></script> <script src="/js/jquery-3.3.1.slim.min.js"></script>

View File

@ -13,32 +13,42 @@
<body> <body>
<div class="container col-md-7 col-sm-12 mt-2">
<h1 class="text-center mb-4 mt-sm-3">{{.Core.Name}}</h1> <h1 class="text-center mb-4 mt-sm-3">{{.Core.Name}}</h1>
{{ if .Core.Description }} {{ if .Core.Description }}
<h5 class="text-center mb-1 mt-sm-2">{{ .Core.Description }}</h5> <h5 class="text-center mb-5 text-muted">{{ .Core.Description }}</h5>
{{ end }} {{ end }}
<div class="container"> <div class="col-12 mb-5">
<div class="col-12 mb-5"> {{ if .Core.AllOnline }}
<div class="alert alert-success mt-2 mb-2" role="alert">
<div class="list-group online_list"> All services are online and operational!
{{ range .Services }}
<a href="#" class="list-group-item list-group-item-action {{if .Online}}{{ end }}">
{{ .Name }}
{{if .Online}}
<span class="badge online_badge float-right">ONLINE</span>
{{ else }}
<span class="badge offline_badge float-right">OFFLINE</span>
{{end}}
</a>
{{ end }}
</div> </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 }}">
{{ .Name }}
{{if .Online}}
<span class="badge online_badge float-right">ONLINE</span>
{{ else }}
<span class="badge offline_badge float-right">OFFLINE</span>
{{end}}
</a>
{{ end }}
</div> </div>
</div>
<div class="col-12"> <div class="col-12">
{{ range .Services }} {{ range .Services }}
@ -50,26 +60,26 @@
<div class="progress-bar {{if .Online24Hours}} bg-success {{else}} bg-danger {{end}}" role="progressbar" style="width: {{.Online24Hours}}%" aria-valuenow="25" aria-valuemin="0" aria-valuemax="100"></div> <div class="progress-bar {{if .Online24Hours}} bg-success {{else}} bg-danger {{end}}" role="progressbar" style="width: {{.Online24Hours}}%" aria-valuenow="25" aria-valuemin="0" aria-valuemax="100"></div>
</div> </div>
<h3 class="mt-4"><a href="/service/{{.Id}}">{{ .Name }}</a> <h4 class="mt-4"><a href="/service/{{.Id}}">{{ .Name }}</a>
{{if .Online}} {{if .Online}}
<span class="badge online_badge float-right">ONLINE</span> <span class="badge online_badge float-right">ONLINE</span>
{{ else }} {{ else }}
<span class="badge offline_badge float-right">OFFLINE</span> <span class="badge offline_badge float-right">OFFLINE</span>
{{end}}</h3> {{end}}</h4>
<div class="row stats_area mt-5 mb-5"> <div class="row stats_area mt-5 mb-5">
<div class="col-4"> <div class="col-md-4 col-sm-12">
<span class="lg_number">{{.Online24}}%</span> <span class="lg_number">{{.Online24}}%</span>
Online last 24 Hours Online last 24 Hours
</div> </div>
<div class="col-4"> <div class="col-md-4 col-sm-6">
<span class="lg_number">{{.AvgTime}}ms</span> <span class="lg_number">{{.AvgTime}}ms</span>
Average Response Average Response
</div> </div>
<div class="col-4"> <div class="col-md-4 col-sm-6">
<span class="lg_number">{{.AvgUptime}}%</span> <span class="lg_number">{{.AvgUptime}}%</span>
Total Uptime Total Uptime
</div> </div>
@ -77,12 +87,19 @@
<canvas id="service_{{ .Id }}" width="400" height="120"></canvas> <canvas id="service_{{ .Id }}" width="400" height="120"></canvas>
{{ if .LimitedFailures }}
<div class="list-group mt-5">
{{ range .LimitedFailures }} {{ range .LimitedFailures }}
<blockquote class="blockquote text-right mt-3"> <a href="#" class="list-group-item list-group-item-action flex-column align-items-start">
<p class="mb-0">{{.ParseError}}</p> <div class="d-flex w-100 justify-content-between">
<footer class="blockquote-footer">Reported <cite title="Source Title">{{.Ago}}</cite></footer> <h5 class="mb-1">{{.ParseError}}</h5>
</blockquote> <small>Reported {{.Ago}}</small>
{{ end }} </div>
<p class="mb-1">{{.Issue}}</p>
</a>
{{ end }}
</div>
{{ end }}
</div> </div>
</div> </div>
@ -93,15 +110,11 @@
</div> </div>
<div class="footer text-center"> {{template "footer"}}
{{ if .Core.Footer }}
{{ safe .Core.Footer }}
{{ end }}
<a href="https://statup.io" target="_blank">Statup made with ❤️</a> | <a href="/dashboard">Dashboard</a>
</div>
<script> <script>
{{ range .Services }} {{ range .Services }}
{{if .GraphData}}
var ctx = document.getElementById("service_{{.Id}}").getContext('2d'); var ctx = document.getElementById("service_{{.Id}}").getContext('2d');
var chartdata = new Chart(ctx, { var chartdata = new Chart(ctx, {
@ -158,6 +171,7 @@ var chartdata = new Chart(ctx, {
} }
}); });
{{ end }} {{ end }}
{{ end }}
</script> </script>

View File

@ -12,12 +12,16 @@
<body> <body>
<div class="container"> <div class="container col-md-7 col-sm-12 mt-2 bg-light">
<div class="row">
<div class="col-12"> <div class="col-12">
{{ if not . }}
<div class="alert alert-danger" role="alert">
Incorrect login information submitted, try again.
</div>
{{ end }}
<form action="/dashboard" method="POST"> <form action="/dashboard" method="POST">
<div class="form-group row"> <div class="form-group row">
<label for="inputEmail3" class="col-sm-2 col-form-label">Username</label> <label for="inputEmail3" class="col-sm-2 col-form-label">Username</label>
@ -51,11 +55,10 @@
</div> </div>
</div>
</div> </div>
{{template "footer"}}
<script src="/js/jquery-3.3.1.slim.min.js"></script> <script src="/js/jquery-3.3.1.slim.min.js"></script>
<script src="/js/bootstrap.min.js"></script> <script src="/js/bootstrap.min.js"></script>
</body> </body>

View File

@ -12,7 +12,7 @@
<body> <body>
<div class="container"> <div class="container col-md-7 col-sm-12 mt-2 bg-light">
{{template "nav"}} {{template "nav"}}
@ -105,6 +105,8 @@
</div> </div>
{{template "footer"}}
<script src="/js/jquery-3.3.1.slim.min.js"></script> <script src="/js/jquery-3.3.1.slim.min.js"></script>
<script src="/js/bootstrap.min.js"></script> <script src="/js/bootstrap.min.js"></script>

View File

@ -12,14 +12,13 @@
</head> </head>
<body> <body>
<div class="container"> <div class="container col-md-7 col-sm-12 mt-2 bg-light">
{{if Auth}} {{if Auth}}
{{template "nav"}} {{template "nav"}}
{{end}} {{end}}
<div class="row">
<div class="col-12"> <div class="col-12">
<div class="col-12 mb-4"> <div class="col-12 mb-4">
@ -57,9 +56,8 @@
</blockquote> </blockquote>
{{ end }} {{ end }}
</div> </div>
</div>
{{if Auth}} {{if Auth}}
@ -69,44 +67,61 @@
<form action="/service/{{.Id}}" method="POST"> <form action="/service/{{.Id}}" method="POST">
<div class="form-group row"> <div class="form-group row">
<label for="inputEmail3" class="col-sm-4 col-form-label">Service Name</label> <label for="service_name" class="col-sm-4 col-form-label">Service Name</label>
<div class="col-sm-8"> <div class="col-sm-8">
<input type="text" name="name" class="form-control" value="{{.Name}}" id="inputEmail3"> <input type="text" name="name" class="form-control" id="service_name" value="{{.Name}}" placeholder="Name">
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label for="inputPassword3" class="col-sm-4 col-form-label">Application Endpoint (URL)</label> <label for="service_type" class="col-sm-4 col-form-label">Service Check Type</label>
<div class="col-sm-8"> <div class="col-sm-8">
<input type="text" name="domain" class="form-control" value="{{.Domain}}" id="inputPassword3"> <select name="check_type" class="form-control" id="service_type" value="{{.Type}}">
<option value="http" selected>HTTP Service</option>
<option value="tcp">TCP Service</option>
</select>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label for="inputPassword3" class="col-sm-4 col-form-label">Expected Response (Regex)</label> <label for="service_url" class="col-sm-4 col-form-label">Application Endpoint (URL)</label>
<div class="col-sm-8"> <div class="col-8">
<input type="text" name="expected" class="form-control" value="{{.Expected}}" id="inputPassword3"> <input type="text" name="domain" class="form-control" id="service_url" value="{{.Domain}}" placeholder="https://google.com">
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label for="inputPassword3" class="col-sm-4 col-form-label">Expected Status Code</label> <label for="service_check_type" class="col-sm-4 col-form-label">Service Check Type</label>
<div class="col-sm-8"> <div class="col-sm-8">
<input type="number" name="expected_status" class="form-control" value="{{.ExpectedStatus}}" id="inputPassword3"> <select name="method" class="form-control" id="service_check_type" value="{{.Method}}">
<option value="GET" selected>GET</option>
<option value="POST">POST</option>
</select>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label for="inputPassword3" class="col-sm-4 col-form-label">HTTP Method</label> <label for="service_response" class="col-sm-4 col-form-label">Expected Response (Regex)</label>
<div class="col-sm-8"> <div class="col-sm-8">
<input type="text" name="method" class="form-control" value="{{.Method}}" id="inputPassword3"> <textarea name="expected" class="form-control" id="service_response" rows="3">{{.Expected}}</textarea>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label for="inputPassword3" class="col-sm-4 col-form-label">Check Interval (Seconds)</label> <label for="service_response_code" class="col-sm-4 col-form-label">Expected Status Code</label>
<div class="col-sm-8"> <div class="col-sm-8">
<input type="number" name="interval" class="form-control" value="{{.Interval}}" id="inputPassword3"> <input type="number" name="expected_status" class="form-control" value="{{.ExpectedStatus}}" id="service_response_code" value="200">
</div>
</div>
<div class="form-group row">
<label for="service_port" class="col-sm-4 col-form-label">TCP Port</label>
<div class="col-sm-8">
<input type="number" name="port" class="form-control" value="{{.Port}}" id="service_port" placeholder="8080">
</div>
</div>
<div class="form-group row">
<label for="service_interval" class="col-sm-4 col-form-label">Check Interval (Seconds)</label>
<div class="col-sm-8">
<input type="number" name="interval" class="form-control" value="{{.Interval}}" id="service_interval" placeholder="10">
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<div class="col-sm-10"> <div class="col-sm-10">
<a class="btn btn-primary">Test</a>
<button type="submit" class="btn btn-success">Update Service</button> <button type="submit" class="btn btn-success">Update Service</button>
<a href="/service/{{ .Id }}/delete_failures" class="btn btn-danger">Delete All Failures</a> <a href="/service/{{ .Id }}/delete_failures" class="btn btn-danger">Delete All Failures</a>
</div> </div>
@ -127,6 +142,7 @@
</div> </div>
</div> </div>
{{template "footer"}}
<script> <script>
var ctx = document.getElementById("service").getContext('2d'); var ctx = document.getElementById("service").getContext('2d');

View File

@ -12,13 +12,11 @@
<body> <body>
<div class="container"> <div class="container col-md-7 col-sm-12 mt-2 bg-light">
{{template "nav"}} {{template "nav"}}
<div class="row"> <div class="col-12">
<div class="col-12">
<h3>Services</h3> <h3>Services</h3>
@ -48,53 +46,66 @@
</tbody> </tbody>
</table> </table>
</div>
<div class="col-12">
<h3>Create Service</h3> <h3>Create Service</h3>
<form action="/services" method="POST"> <form action="/services" method="POST">
<div class="form-group row"> <div class="form-group row">
<label for="inputEmail3" class="col-sm-4 col-form-label">Service Name</label> <label for="service_name" class="col-sm-4 col-form-label">Service Name</label>
<div class="col-sm-8"> <div class="col-sm-8">
<input type="text" name="name" class="form-control" id="inputEmail3" placeholder="Name"> <input type="text" name="name" class="form-control" id="service_name" placeholder="Name">
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label for="inputPassword3" class="col-sm-4 col-form-label">Application Endpoint (URL)</label> <label for="service_type" class="col-sm-4 col-form-label">Service Check Type</label>
<div class="col-sm-8"> <div class="col-sm-8">
<input type="text" name="domain" class="form-control" id="inputPassword3" placeholder="https://google.com"> <select name="check_type" class="form-control" id="service_type">
<option value="http" selected>HTTP Service</option>
<option value="tcp">TCP Service</option>
</select>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label for="inputPassword3" class="col-sm-4 col-form-label">Expected Response (Regex)</label> <label for="service_url" class="col-sm-4 col-form-label">Application Endpoint (URL)</label>
<div class="col-sm-8"> <div class="col-8">
<input type="text" name="expected" class="form-control" id="inputPassword3" placeholder="string"> <input type="text" name="domain" class="form-control" id="service_url" placeholder="https://google.com">
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label for="inputPassword3" class="col-sm-4 col-form-label">Expected Status Code</label> <label for="service_check_type" class="col-sm-4 col-form-label">Service Check Type</label>
<div class="col-sm-8"> <div class="col-sm-8">
<input type="number" name="expected_status" class="form-control" id="inputPassword3" placeholder="200"> <select name="method" class="form-control" id="service_check_type">
<option value="GET" selected>GET</option>
<option value="POST">POST</option>
</select>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label for="inputPassword3" class="col-sm-4 col-form-label">HTTP Method</label> <label for="service_response" class="col-sm-4 col-form-label">Expected Response (Regex)</label>
<div class="col-sm-8"> <div class="col-sm-8">
<input type="text" name="method" class="form-control" id="inputPassword3" placeholder="GET"> <textarea name="expected" class="form-control" id="service_response" rows="3"></textarea>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label for="inputPassword3" class="col-sm-4 col-form-label">Check Interval (Seconds)</label> <label for="service_response_code" class="col-sm-4 col-form-label">Expected Status Code</label>
<div class="col-sm-8"> <div class="col-sm-8">
<input type="number" name="interval" class="form-control" id="inputPassword3" placeholder="10"> <input type="number" name="expected_status" class="form-control" id="service_response_code" value="200">
</div>
</div>
<div class="form-group row">
<label for="service_port" class="col-sm-4 col-form-label">TCP Port</label>
<div class="col-sm-8">
<input type="number" name="port" class="form-control" id="service_port" placeholder="8080">
</div>
</div>
<div class="form-group row">
<label for="service_interval" class="col-sm-4 col-form-label">Check Interval (Seconds)</label>
<div class="col-sm-8">
<input type="number" name="interval" class="form-control" id="service_interval" placeholder="10">
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<div class="col-sm-10"> <div class="col-sm-10">
<a class="btn btn-primary">Test</a>
<button type="submit" class="btn btn-success">Create Service</button> <button type="submit" class="btn btn-success">Create Service</button>
</div> </div>
</div> </div>
@ -104,8 +115,7 @@
</div> </div>
{{template "footer"}}
</div>
<script src="/js/jquery-3.3.1.slim.min.js"></script> <script src="/js/jquery-3.3.1.slim.min.js"></script>
<script src="/js/bootstrap.min.js"></script> <script src="/js/bootstrap.min.js"></script>

View File

@ -13,7 +13,7 @@
<body> <body>
<div class="container"> <div class="container col-md-7 col-sm-12 mt-2 bg-light">
{{ if .Error }} {{ if .Error }}
<div class="alert alert-danger" role="alert"> <div class="alert alert-danger" role="alert">
@ -102,6 +102,8 @@
</div> </div>
{{template "footer"}}
<script src="/js/jquery-3.3.1.slim.min.js"></script> <script src="/js/jquery-3.3.1.slim.min.js"></script>
<script src="/js/bootstrap.min.js"></script> <script src="/js/bootstrap.min.js"></script>
<script src="/js/setup.js"></script> <script src="/js/setup.js"></script>

View File

@ -12,12 +12,10 @@
<body> <body>
<div class="container"> <div class="container col-md-7 col-sm-12 mt-2 bg-light">
{{template "nav"}} {{template "nav"}}
<div class="row">
<div class="col-12"> <div class="col-12">
<h3>Users</h3> <h3>Users</h3>
@ -74,10 +72,10 @@
</div> </div>
</div>
</div> </div>
{{template "footer"}}
<script src="/js/jquery-3.3.1.slim.min.js"></script> <script src="/js/jquery-3.3.1.slim.min.js"></script>
<script src="/js/bootstrap.min.js"></script> <script src="/js/bootstrap.min.js"></script>
</body> </body>

View File

@ -168,14 +168,6 @@ func TestService_Check(t *testing.T) {
assert.Equal(t, true, out.Online) assert.Equal(t, true, out.Online)
} }
func TestService_Hits(t *testing.T) {
service, err := SelectService(1)
assert.Nil(t, err)
hits, err := service.Hits()
assert.Nil(t, err)
assert.Equal(t, 0, len(hits))
}
func TestService_AvgTime(t *testing.T) { func TestService_AvgTime(t *testing.T) {
service, err := SelectService(1) service, err := SelectService(1)
assert.Nil(t, err) assert.Nil(t, err)
@ -193,7 +185,6 @@ func TestService_Online24(t *testing.T) {
} }
func TestService_GraphData(t *testing.T) { func TestService_GraphData(t *testing.T) {
t.SkipNow()
service, err := SelectService(1) service, err := SelectService(1)
assert.Nil(t, err) assert.Nil(t, err)
data := service.GraphData() data := service.GraphData()
@ -221,6 +212,22 @@ func TestBadService_Check(t *testing.T) {
assert.Equal(t, "Github Failing Check", service.Name) assert.Equal(t, "Github Failing Check", service.Name)
} }
func TestService_Hits(t *testing.T) {
service, err := SelectService(1)
assert.Nil(t, err)
hits, err := service.Hits()
assert.Nil(t, err)
assert.Equal(t, 5, len(hits))
}
func TestService_LimitedHits(t *testing.T) {
service, err := SelectService(1)
assert.Nil(t, err)
hits, err := service.LimitedHits()
assert.Nil(t, err)
assert.Equal(t, 5, len(hits))
}
func Test(t *testing.T) { func Test(t *testing.T) {
var err error var err error
configs, err = LoadConfig() configs, err = LoadConfig()

View File

@ -69,5 +69,5 @@ type Routing struct {
type Info struct { type Info struct {
Name string Name string
Description string Description string
Form string Form string
} }

View File

@ -7,6 +7,7 @@ import (
"math/rand" "math/rand"
"strconv" "strconv"
"time" "time"
"upper.io/db.v3"
) )
var ( var (
@ -32,17 +33,20 @@ type Service struct {
Failures []*Failure `json:"failures"` Failures []*Failure `json:"failures"`
} }
func serviceCol() db.Collection {
return dbSession.Collection("services")
}
func SelectService(id int64) (*Service, error) { func SelectService(id int64) (*Service, error) {
var service *Service var service *Service
col := dbSession.Collection("services") res := serviceCol().Find("id", id)
res := col.Find("id", id)
err := res.One(&service) err := res.One(&service)
return service, err return service, err
} }
func SelectAllServices() ([]*Service, error) { func SelectAllServices() ([]*Service, error) {
var services []*Service var services []*Service
col := dbSession.Collection("services").Find() col := serviceCol().Find()
err := col.All(&services) err := col.All(&services)
return services, err return services, err
} }
@ -95,7 +99,7 @@ type GraphJson struct {
func (s *Service) GraphData() string { func (s *Service) GraphData() string {
var d []GraphJson var d []GraphJson
hits, _ := s.Hits() hits, _ := s.LimitedHits()
for _, h := range hits { for _, h := range hits {
val := h.CreatedAt val := h.CreatedAt
o := GraphJson{ o := GraphJson{
@ -112,7 +116,7 @@ func (s *Service) AvgUptime() string {
failed, _ := s.TotalFailures() failed, _ := s.TotalFailures()
total, _ := s.TotalHits() total, _ := s.TotalHits()
if failed == 0 { if failed == 0 {
s.TotalUptime = "100.00" s.TotalUptime = "100"
return s.TotalUptime return s.TotalUptime
} }
if total == 0 { if total == 0 {
@ -125,35 +129,26 @@ func (s *Service) AvgUptime() string {
percent = 0 percent = 0
} }
s.TotalUptime = fmt.Sprintf("%0.2f", percent) s.TotalUptime = fmt.Sprintf("%0.2f", percent)
if s.TotalUptime == "100.00" {
s.TotalUptime = "100"
}
return s.TotalUptime return s.TotalUptime
} }
func (u *Service) Delete() error { func (u *Service) Delete() error {
col := dbSession.Collection("services") res := serviceCol().Find("id", u.Id)
res := col.Find("id", u.Id)
err := res.Delete() err := res.Delete()
OnDeletedService(u) OnDeletedService(u)
return err return err
} }
func (u *Service) DeleteFailures() {
var fails []*Failure
col := dbSession.Collection("failures")
col.Find("service", u.Id).All(&fails)
for _, fail := range fails {
fail.Delete()
}
}
func (u *Service) Update() { func (u *Service) Update() {
OnUpdateService(u) OnUpdateService(u)
} }
func (u *Service) Create() (int64, error) { func (u *Service) Create() (int64, error) {
u.CreatedAt = time.Now() u.CreatedAt = time.Now()
col := dbSession.Collection("services") uuid, err := serviceCol().Insert(u)
uuid, err := col.Insert(u)
services, _ = SelectAllServices() services, _ = SelectAllServices()
if uuid == nil { if uuid == nil {
return 0, err return 0, err

View File

@ -102,6 +102,15 @@ func SetupResponseError(w http.ResponseWriter, r *http.Request, a interface{}) {
ExecuteResponse(w, r, "setup.html", a) ExecuteResponse(w, r, "setup.html", a)
} }
func (c *DbConfig) Clean() *DbConfig {
if os.Getenv("DB_PORT") != "" {
if c.DbConn == "postgres" {
c.DbHost = c.DbHost + ":" + os.Getenv("DB_PORT")
}
}
return c
}
func (c *DbConfig) Save() error { func (c *DbConfig) Save() error {
var err error var err error
config, err := os.Create("config.yml") config, err := os.Create("config.yml")

View File

@ -32,6 +32,13 @@ CREATE TABLE services (
created_at TIMESTAMP 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 ( CREATE TABLE hits (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
service INTEGER NOT NULL REFERENCES services(id) ON DELETE CASCADE ON UPDATE CASCADE, service INTEGER NOT NULL REFERENCES services(id) ON DELETE CASCADE ON UPDATE CASCADE,
@ -42,6 +49,7 @@ CREATE TABLE hits (
CREATE TABLE failures ( CREATE TABLE failures (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
issue text, issue text,
method text,
service INTEGER NOT NULL REFERENCES services(id) ON DELETE CASCADE ON UPDATE CASCADE, service INTEGER NOT NULL REFERENCES services(id) ON DELETE CASCADE ON UPDATE CASCADE,
created_at TIMESTAMP WITHOUT TIME zone created_at TIMESTAMP WITHOUT TIME zone
); );

24
web.go
View File

@ -7,10 +7,10 @@ import (
"github.com/gorilla/sessions" "github.com/gorilla/sessions"
"html/template" "html/template"
"net/http" "net/http"
"regexp"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"regexp"
) )
var ( var (
@ -98,9 +98,7 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) {
session.Save(r, w) session.Save(r, w)
http.Redirect(w, r, "/dashboard", http.StatusSeeOther) http.Redirect(w, r, "/dashboard", http.StatusSeeOther)
} else { } else {
w.WriteHeader(502) ExecuteResponse(w, r, "login.html", auth)
w.Header().Set("Content-Type", "plain/text")
fmt.Fprintln(w, "bad")
} }
} }
@ -129,8 +127,8 @@ func CreateServiceHandler(w http.ResponseWriter, r *http.Request) {
expected := r.PostForm.Get("expected") expected := r.PostForm.Get("expected")
status, _ := strconv.Atoi(r.PostForm.Get("expected_status")) status, _ := strconv.Atoi(r.PostForm.Get("expected_status"))
interval, _ := strconv.Atoi(r.PostForm.Get("interval")) interval, _ := strconv.Atoi(r.PostForm.Get("interval"))
port, _ := strconv.Atoi(r.PostForm.Get("port"))
fmt.Println(r.PostForm) checkType := r.PostForm.Get("check_type")
service := Service{ service := Service{
Name: name, Name: name,
@ -139,10 +137,10 @@ func CreateServiceHandler(w http.ResponseWriter, r *http.Request) {
Expected: expected, Expected: expected,
ExpectedStatus: status, ExpectedStatus: status,
Interval: interval, Interval: interval,
Type: checkType,
Port: port,
} }
fmt.Println(service)
_, err := service.Create() _, err := service.Create()
if err != nil { if err != nil {
go service.CheckQueue() go service.CheckQueue()
@ -285,7 +283,7 @@ func PluginsHandler(w http.ResponseWriter, r *http.Request) {
for _, p := range allPlugins { for _, p := range allPlugins {
fields := structs.Map(p.GetInfo()) fields := structs.Map(p.GetInfo())
pluginFields = append(pluginFields, PluginSelect{p.GetInfo().Name, p.GetForm(),fields}) pluginFields = append(pluginFields, PluginSelect{p.GetInfo().Name, p.GetForm(), fields})
} }
core.PluginFields = pluginFields core.PluginFields = pluginFields
@ -294,7 +292,7 @@ func PluginsHandler(w http.ResponseWriter, r *http.Request) {
type PluginSelect struct { type PluginSelect struct {
Plugin string Plugin string
Form string Form string
Params map[string]interface{} Params map[string]interface{}
} }
@ -375,6 +373,7 @@ func ServicesViewHandler(w http.ResponseWriter, r *http.Request) {
func ExecuteResponse(w http.ResponseWriter, r *http.Request, file string, data interface{}) { func ExecuteResponse(w http.ResponseWriter, r *http.Request, file string, data interface{}) {
nav, _ := tmplBox.String("nav.html") nav, _ := tmplBox.String("nav.html")
footer, _ := tmplBox.String("footer.html")
render, err := tmplBox.String(file) render, err := tmplBox.String(file)
if err != nil { if err != nil {
panic(err) panic(err)
@ -398,6 +397,7 @@ func ExecuteResponse(w http.ResponseWriter, r *http.Request, file string, data i
}, },
}) })
t, _ = t.Parse(nav) t, _ = t.Parse(nav)
t, _ = t.Parse(footer)
t.Parse(render) t.Parse(render)
t.Execute(w, data) t.Execute(w, data)
} }
@ -431,8 +431,6 @@ func UsersDeleteHandler(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/users", http.StatusSeeOther) http.Redirect(w, r, "/users", http.StatusSeeOther)
} }
func UnderScoreString(str string) string { func UnderScoreString(str string) string {
// convert every letter to lower case // convert every letter to lower case
@ -454,4 +452,4 @@ func UnderScoreString(str string) string {
newStr = strings.TrimSuffix(newStr, "_") newStr = strings.TrimSuffix(newStr, "_")
return newStr return newStr
} }