core initial setup fixes - Slack integration - template updates

pull/10/head
Hunter Long 2018-07-01 03:24:35 -07:00
parent cdbdcc1dbf
commit 9e9c29df31
21 changed files with 198 additions and 73 deletions

View File

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

View File

@ -1,6 +1,6 @@
FROM alpine:latest
ENV VERSION=v0.28.4
ENV VERSION=v0.28.5
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

@ -48,6 +48,10 @@ Whether you're a Docker fan-boy or a AWS EC2 master, Statup gives you multiple o
Running on an EC2 server might be the most cost effective way to host your own Statup Status Page. The server runs on the smallest EC2 instance (t2.nano) AWS has to offer, which only costs around $4.60 USD a month for your dedicated Status Page.
Want to run it on your own Docker server? Awesome! Statup has multiple docker-compose.yml files to work with. Statup can automatically create a SSL Certification for your status page.
## Slack Integration
Everyone uses Slack and Statup should also. You can create an [Incoming Webhook](https://api.slack.com/incoming-webhooks) in Slack and insert the URL into the Settings page in Statup. Anytime a service fails, you're channel that you specified on Slack will receive a message.
This is a brand new feature, right now it is sending basic text. With Slack Messaging format, I plan on creating a more detailed message for a cleaner look.
## Email Nofitications
Statup includes email notification via SMTP if your services go offline.

View File

@ -10,12 +10,19 @@ import (
)
func CopyToPublic(box *rice.Box, folder, file string) {
utils.Log(1, fmt.Sprintf("Copying %v to %v...", file, folder))
assetFolder := fmt.Sprintf("assets/%v/%v", folder, file)
if folder == "" {
assetFolder = fmt.Sprintf("assets/%v", file)
}
utils.Log(1, fmt.Sprintf("Copying %v to %v", file, assetFolder))
base, err := box.String(file)
if err != nil {
utils.Log(3, fmt.Sprintf("Failed to copy %v to %v, %v.", file, folder, err))
utils.Log(3, fmt.Sprintf("Failed to copy %v to %v, %v.", file, assetFolder, err))
}
err = ioutil.WriteFile(assetFolder, []byte(base), 0644)
if err != nil {
utils.Log(3, fmt.Sprintf("Failed to write file %v to %v, %v.", file, assetFolder, err))
}
ioutil.WriteFile("assets/"+folder+"/"+file, []byte(base), 0644)
}
func MakePublicFolder(folder string) {

View File

@ -1,6 +1,7 @@
package core
import (
"bytes"
"fmt"
"github.com/hunterlong/statup/types"
"github.com/hunterlong/statup/utils"
@ -61,7 +62,13 @@ func (s *Service) Check() *Service {
client := http.Client{
Timeout: 30 * time.Second,
}
response, err := client.Get(s.Domain)
var response *http.Response
if s.Method == "POST" {
response, err = client.Post(s.Domain, "application/json", bytes.NewBuffer([]byte(s.PostData)))
} else {
response, err = client.Get(s.Domain)
}
if err != nil {
s.Failure(fmt.Sprintf("HTTP Error %v", err))
return s
@ -74,9 +81,15 @@ func (s *Service) Check() *Service {
return s
}
defer response.Body.Close()
contents, _ := ioutil.ReadAll(response.Body)
contents, err := ioutil.ReadAll(response.Body)
if err != nil {
utils.Log(2, err)
}
if s.Expected != "" {
match, _ := regexp.MatchString(s.Expected, string(contents))
match, err := regexp.MatchString(s.Expected, string(contents))
if err != nil {
utils.Log(2, err)
}
if !match {
s.LastResponse = string(contents)
s.LastStatusCode = response.StatusCode
@ -119,5 +132,5 @@ func (s *Service) Failure(issue string) {
utils.Log(2, fmt.Sprintf("Service %v Failing: %v", s.Name, issue))
s.CreateFailure(data)
//SendFailureEmail(s)
OnFailure(s)
OnFailure(s, data)
}

View File

@ -1,8 +1,11 @@
package core
import (
"bytes"
"fmt"
"github.com/hunterlong/statup/types"
"github.com/hunterlong/statup/utils"
"net/http"
"time"
)
@ -83,3 +86,21 @@ func SelectCommunication(id int64) *Communication {
}
return nil
}
func SendSlackMessage(msg string) error {
fullMessage := fmt.Sprintf("{\"text\":\"%v\"}", msg)
utils.Log(1, fmt.Sprintf("Sending JSON to Slack Webhook: %v", fullMessage))
slack := SelectCommunication(2)
if slack == nil {
utils.Log(3, fmt.Sprintf("Slack communication database entry was not found."))
return nil
}
client := http.Client{
Timeout: 15 * time.Second,
}
_, err := client.Post(slack.Host, "application/json", bytes.NewBuffer([]byte(fullMessage)))
if err != nil {
utils.Log(3, err)
}
return err
}

View File

@ -70,6 +70,13 @@ func (c Core) BaseSASS() string {
return OpenAsset("scss/base.scss")
}
func (c Core) MobileSASS() string {
if !UsingAssets {
return ""
}
return OpenAsset("scss/mobile.scss")
}
func (c Core) AllOnline() bool {
for _, s := range CoreApp.Services {
if !s.Online {

View File

@ -92,12 +92,12 @@ func (c *DbConfig) Save() error {
var err error
config, err := os.Create("config.yml")
if err != nil {
utils.Log(2, err)
utils.Log(4, err)
return err
}
data, err := yaml.Marshal(c)
if err != nil {
utils.Log(2, err)
utils.Log(3, err)
return err
}
config.WriteString(string(data))
@ -105,12 +105,12 @@ func (c *DbConfig) Save() error {
Configs, err = LoadConfig()
if err != nil {
utils.Log(2, err)
utils.Log(3, err)
return err
}
err = DbConnection(Configs.Connection)
if err != nil {
utils.Log(2, err)
utils.Log(4, err)
return err
}
DropDatabase()
@ -124,9 +124,11 @@ func (c *DbConfig) Save() error {
ApiSecret: utils.NewSHA1Hash(16),
Domain: c.Domain,
}
col := DbSession.Collection("core")
_, err = col.Insert(newCore)
if err == nil {
CoreApp = newCore
}
return err
}

View File

@ -1,6 +1,7 @@
package core
import (
"fmt"
"github.com/fatih/structs"
"github.com/hunterlong/statup/plugin"
"upper.io/db.v3/lib/sqlbuilder"
@ -18,10 +19,18 @@ func OnSuccess(s *Service) {
}
}
func OnFailure(s *Service) {
func OnFailure(s *Service, f FailureData) {
for _, p := range AllPlugins {
p.OnFailure(structs.Map(s))
}
slack := SelectCommunication(2)
if slack == nil {
return
}
if slack.Enabled {
msg := fmt.Sprintf("Service %v is currently offline! Issue: %v", s.Name, f.Issue)
SendSlackMessage(msg)
}
}
func OnSettingsSaved(c *Core) {

View File

@ -195,13 +195,16 @@ func (u *Service) Delete() error {
return err
}
func (u *Service) Update(s *Service) {
func (u *Service) Update(s *Service) *Service {
s.CreatedAt = time.Now()
res := serviceCol().Find("id", u.Id)
err := res.Update(s)
if err != nil {
utils.Log(3, fmt.Sprintf("Failed to update service %v. %v", u.Name, err))
}
*u = *s
OnUpdateService(u)
return u
}
func (u *Service) Create() (int64, error) {
@ -213,7 +216,7 @@ func (u *Service) Create() (int64, error) {
}
u.Id = uuid.(int64)
CoreApp.Services = append(CoreApp.Services, u)
go u.CheckQueue()
//go u.CheckQueue()
OnNewService(u)
return uuid.(int64), err
}

View File

@ -1,7 +1,6 @@
package core
import (
"fmt"
"github.com/hunterlong/statup/utils"
"os"
)
@ -13,6 +12,12 @@ func InsertDefaultComms() {
Enabled: false,
}
Create(emailer)
slack := &Communication{
Method: "slack",
Removable: false,
Enabled: false,
}
Create(slack)
}
func DeleteConfig() {
@ -27,41 +32,43 @@ type ErrorResponse struct {
}
func LoadSampleData() error {
fmt.Println("Inserting Sample Data...")
utils.Log(1, "Inserting Sample Data...")
s1 := &Service{
Name: "Google",
Domain: "https://google.com",
ExpectedStatus: 200,
Interval: 10,
Port: 0,
Type: "https",
Type: "http",
Method: "GET",
}
s2 := &Service{
Name: "Statup.io",
Domain: "https://statup.io",
Name: "Statup Github",
Domain: "https://github.com/hunterlong/statup",
ExpectedStatus: 200,
Interval: 15,
Interval: 30,
Port: 0,
Type: "https",
Type: "http",
Method: "GET",
}
s3 := &Service{
Name: "Statup.io SSL Check",
Domain: "https://statup.io",
Name: "JSON Users Test",
Domain: "https://jsonplaceholder.typicode.com/users",
ExpectedStatus: 200,
Interval: 15,
Interval: 60,
Port: 443,
Type: "tcp",
Type: "http",
Method: "GET",
}
s4 := &Service{
Name: "Github Failing Check",
Domain: "https://github.com/thisisnotausernamemaybeitis",
ExpectedStatus: 200,
Interval: 15,
Port: 0,
Type: "https",
Method: "GET",
Name: "JSON API Tester",
Domain: "https://jsonplaceholder.typicode.com/posts",
ExpectedStatus: 201,
Expected: `(title)": "((\\"|[statup])*)"`,
Interval: 30,
Type: "http",
Method: "POST",
PostData: `{ "title": "statup", "body": "bar", "userId": 19999 }`,
}
s1.Create()
s2.Create()

View File

@ -49,8 +49,7 @@ func LogoutHandler(w http.ResponseWriter, r *http.Request) {
}
func HelpHandler(w http.ResponseWriter, r *http.Request) {
auth := IsAuthenticated(r)
if !auth {
if !IsAuthenticated(r) {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}

View File

@ -33,6 +33,7 @@ func Router() *mux.Router {
r.Handle("/settings/css", http.HandlerFunc(SaveSASSHandler)).Methods("POST")
r.Handle("/settings/build", http.HandlerFunc(SaveAssetsHandler)).Methods("GET")
r.Handle("/settings/email", http.HandlerFunc(SaveEmailSettingsHandler)).Methods("POST")
r.Handle("/settings/slack", http.HandlerFunc(SaveSlackSettingsHandler)).Methods("POST")
r.Handle("/plugins/download/{name}", http.HandlerFunc(PluginsDownloadHandler))
r.Handle("/plugins/{name}/save", http.HandlerFunc(PluginSavedHandler)).Methods("POST")
r.Handle("/help", http.HandlerFunc(HelpHandler))

View File

@ -10,8 +10,7 @@ import (
)
func ServicesHandler(w http.ResponseWriter, r *http.Request) {
auth := IsAuthenticated(r)
if !auth {
if !IsAuthenticated(r) {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
@ -19,8 +18,7 @@ func ServicesHandler(w http.ResponseWriter, r *http.Request) {
}
func CreateServiceHandler(w http.ResponseWriter, r *http.Request) {
auth := IsAuthenticated(r)
if !auth {
if !IsAuthenticated(r) {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
@ -34,6 +32,7 @@ func CreateServiceHandler(w http.ResponseWriter, r *http.Request) {
interval, _ := strconv.Atoi(r.PostForm.Get("interval"))
port, _ := strconv.Atoi(r.PostForm.Get("port"))
checkType := r.PostForm.Get("check_type")
postData := r.PostForm.Get("post_data")
service := &core.Service{
Name: name,
@ -44,17 +43,18 @@ func CreateServiceHandler(w http.ResponseWriter, r *http.Request) {
Interval: interval,
Type: checkType,
Port: port,
PostData: postData,
}
_, err := service.Create()
if err != nil {
go service.CheckQueue()
utils.Log(3, fmt.Sprintf("Error starting %v check routine. %v", service.Name, err))
}
go service.CheckQueue()
http.Redirect(w, r, "/services", http.StatusSeeOther)
}
func ServicesDeleteHandler(w http.ResponseWriter, r *http.Request) {
auth := IsAuthenticated(r)
if !auth {
if !IsAuthenticated(r) {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
@ -89,8 +89,7 @@ func ServicesBadgeHandler(w http.ResponseWriter, r *http.Request) {
}
func ServicesUpdateHandler(w http.ResponseWriter, r *http.Request) {
auth := IsAuthenticated(r)
if !auth {
if !IsAuthenticated(r) {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
@ -105,7 +104,9 @@ func ServicesUpdateHandler(w http.ResponseWriter, r *http.Request) {
interval, _ := strconv.Atoi(r.PostForm.Get("interval"))
port, _ := strconv.Atoi(r.PostForm.Get("port"))
checkType := r.PostForm.Get("check_type")
postData := r.PostForm.Get("post_data")
serviceUpdate := &core.Service{
Id: service.Id,
Name: name,
Domain: domain,
Method: method,
@ -114,14 +115,14 @@ func ServicesUpdateHandler(w http.ResponseWriter, r *http.Request) {
Interval: interval,
Type: checkType,
Port: port,
PostData: postData,
}
service.Update(serviceUpdate)
service = service.Update(serviceUpdate)
ExecuteResponse(w, r, "service.html", service)
}
func ServicesDeleteFailuresHandler(w http.ResponseWriter, r *http.Request) {
auth := IsAuthenticated(r)
if !auth {
if !IsAuthenticated(r) {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
@ -134,8 +135,7 @@ func ServicesDeleteFailuresHandler(w http.ResponseWriter, r *http.Request) {
}
func CheckinCreateUpdateHandler(w http.ResponseWriter, r *http.Request) {
auth := IsAuthenticated(r)
if !auth {
if !IsAuthenticated(r) {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}

View File

@ -113,3 +113,21 @@ func SaveEmailSettingsHandler(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/settings", http.StatusSeeOther)
}
func SaveSlackSettingsHandler(w http.ResponseWriter, r *http.Request) {
auth := IsAuthenticated(r)
if !auth {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
slack := core.SelectCommunication(2)
r.ParseForm()
slack.Host = r.PostForm.Get("host")
slack.Enabled = true
if slack.Host == "" {
slack.Enabled = false
}
core.Update(slack)
core.SendSlackMessage("This is a test from Statup!")
http.Redirect(w, r, "/settings", http.StatusSeeOther)
}

View File

@ -7,7 +7,6 @@ import (
"net/http"
"os"
"strconv"
"time"
)
func SetupHandler(w http.ResponseWriter, r *http.Request) {
@ -75,7 +74,11 @@ func ProcessSetupHandler(w http.ResponseWriter, r *http.Request) {
}
err := config.Save()
if err != nil {
utils.Log(2, err)
utils.Log(4, err)
}
if err != nil {
utils.Log(3, err)
config.Error = err
SetupResponseError(w, r, config)
return
@ -83,7 +86,7 @@ func ProcessSetupHandler(w http.ResponseWriter, r *http.Request) {
core.Configs, err = core.LoadConfig()
if err != nil {
utils.Log(2, err)
utils.Log(3, err)
config.Error = err
SetupResponseError(w, r, config)
return
@ -91,7 +94,7 @@ func ProcessSetupHandler(w http.ResponseWriter, r *http.Request) {
err = core.DbConnection(core.Configs.Connection)
if err != nil {
utils.Log(2, err)
utils.Log(3, err)
core.DeleteConfig()
config.Error = err
SetupResponseError(w, r, config)
@ -112,8 +115,8 @@ func ProcessSetupHandler(w http.ResponseWriter, r *http.Request) {
go core.LoadSampleData()
}
core.SelectCore()
http.Redirect(w, r, "/", http.StatusSeeOther)
time.Sleep(2 * time.Second)
//mainProcess()
}

View File

@ -29,7 +29,7 @@ func main() {
CatchCLI(os.Args)
os.Exit(0)
}
utils.Log(1, fmt.Sprintf("Starting Statup v%v\n", VERSION))
utils.Log(1, fmt.Sprintf("Starting Statup v%v", VERSION))
RenderBoxes()
core.HasAssets()

View File

@ -196,11 +196,11 @@ func TestService_Create(t *testing.T) {
}
func TestService_Check(t *testing.T) {
service := core.SelectService(2)
service := core.SelectService(1)
assert.NotNil(t, service)
assert.Equal(t, "Statup.io", service.Name)
assert.Equal(t, "Google", service.Name)
out := service.Check()
assert.Equal(t, false, out.Online)
assert.Equal(t, true, out.Online)
}
func TestService_AvgTime(t *testing.T) {
@ -226,12 +226,12 @@ func TestService_GraphData(t *testing.T) {
func TestBadService_Create(t *testing.T) {
service := &core.Service{
Name: "bad service",
Name: "Bad Service",
Domain: "https://9839f83h72gey2g29278hd2od2d.com",
ExpectedStatus: 200,
Interval: 10,
Port: 0,
Type: "https",
Type: "http",
Method: "GET",
}
id, err := service.Create()
@ -242,7 +242,7 @@ func TestBadService_Create(t *testing.T) {
func TestBadService_Check(t *testing.T) {
service := core.SelectService(4)
assert.NotNil(t, service)
assert.Equal(t, "Github Failing Check", service.Name)
assert.Equal(t, "JSON API Tester", service.Name)
}
func TestService_Hits(t *testing.T) {

View File

@ -101,11 +101,17 @@
<label for="service_check_type" class="col-sm-4 col-form-label">Service Check Type</label>
<div class="col-sm-8">
<select name="method" class="form-control" id="service_check_type" value="{{.Method}}">
<option value="GET" selected>GET</option>
<option value="POST">POST</option>
<option value="GET" {{if eq .Method "GET"}}selected{{end}}>GET</option>
<option value="POST" {{if eq .Method "POST"}}selected{{end}}>POST</option>
</select>
</div>
</div>
<div class="form-group row">
<label for="post_data" class="col-sm-4 col-form-label">Post Data (JSON)</label>
<div class="col-sm-8">
<textarea name="post_data" class="form-control" id="post_data" rows="3">{{.PostData}}</textarea>
</div>
</div>
<div class="form-group row">
<label for="service_response" class="col-sm-4 col-form-label">Expected Response (Regex)</label>
<div class="col-sm-8">

View File

@ -77,6 +77,12 @@
</select>
</div>
</div>
<div class="form-group row">
<label for="post_data" class="col-sm-4 col-form-label">Post Data (JSON)</label>
<div class="col-sm-8">
<textarea name="post_data" class="form-control" id="post_data" rows="3"></textarea>
</div>
</div>
<div class="form-group row">
<label for="service_response" class="col-sm-4 col-form-label">Expected Response (Regex)</label>
<div class="col-sm-8">

View File

@ -28,6 +28,7 @@
<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>
<a class="nav-link" id="v-pills-style-tab" data-toggle="pill" href="#v-pills-style" role="tab" aria-controls="v-pills-style" aria-selected="false">Theme Editor</a>
<a class="nav-link" id="v-pills-email-tab" data-toggle="pill" href="#v-pills-email" role="tab" aria-controls="v-pills-email" aria-selected="true">Email Settings</a>
<a class="nav-link" id="v-pills-slack-tab" data-toggle="pill" href="#v-pills-slack" role="tab" aria-controls="v-pills-slack" aria-selected="true">Slack Updates</a>
{{ range .Communications }}
{{ end }}
@ -107,34 +108,34 @@
{{end}}
</div>
{{ range .Communications }}
<div class="tab-pane fade" id="v-pills-{{ .Method }}" role="tabpanel" aria-labelledby="v-pills-{{ .Method }}-tab">
{{ with $c := index .Communications 0 }}
<div class="tab-pane fade" id="v-pills-{{ $c.Method }}" role="tabpanel" aria-labelledby="v-pills-{{ $c.Method }}-tab">
<form method="POST" action="/settings/{{ .Method }}">
<form method="POST" action="/settings/{{ $c.Method }}">
<div class="form-group">
<label for="host">SMTP Host</label>
<input type="text" name="host" class="form-control" value="{{ .Host }}" id="host" placeholder="Great Uptime">
<input type="text" name="host" class="form-control" value="{{ $c.Host }}" id="host" placeholder="Great Uptime">
</div>
<div class="form-group">
<label for="username">SMTP Username</label>
<input type="text" name="username" class="form-control" value="{{ .Username }}" id="username" placeholder="Great Uptime">
<input type="text" name="username" class="form-control" value="{{ $c.Username }}" id="username" placeholder="Great Uptime">
</div>
<div class="form-group">
<label for="password">SMTP Password</label>
<input type="password" name="password" class="form-control" value="{{ .Password }}" id="password">
<input type="password" name="password" class="form-control" value="{{ $c.Password }}" id="password">
</div>
<div class="form-group">
<label for="port">SMTP Port</label>
<input type="number" name="port" class="form-control" value="{{ .Port }}" id="port" placeholder="587">
<input type="number" name="port" class="form-control" value="{{ $c.Port }}" id="port" placeholder="587">
</div>
<div class="form-group">
<label for="address">Outgoing Email Address</label>
<input type="text" name="address" class="form-control" value="{{ .Var1 }}" id="address" placeholder="noreply@domain.com">
<input type="text" name="address" class="form-control" value="{{ $c.Var1 }}" id="address" placeholder="noreply@domain.com">
</div>
<div class="form-group">
@ -149,6 +150,24 @@
</div>
{{ end }}
{{ with $c := index .Communications 1 }}
<div class="tab-pane fade" id="v-pills-{{ $c.Method }}" role="tabpanel" aria-labelledby="v-pills-{{ $c.Method }}-tab">
<form method="POST" action="/settings/{{ $c.Method }}">
<div class="form-group">
<label for="host">Slack Webhook URL</label>
<input type="text" name="host" class="form-control" value="{{ $c.Host }}" id="host" placeholder="https://hooks.slack.com/services/TJIIDSJIFJ/729FJSDF/hua463asda9af79">
</div>
<button type="submit" class="btn btn-primary btn-block">Save Slack Settings</button>
</form>
</div>
{{ end }}
<div class="tab-pane fade" id="v-pills-browse" role="tabpanel" aria-labelledby="v-pills-browse-tab">
{{ range .Repos }}
<div class="card col-6" style="width: 18rem;">