diff --git a/README.md b/README.md index 006eea5d..9d974727 100644 --- a/README.md +++ b/README.md @@ -9,18 +9,18 @@ # Statup - Status Page & Monitoring Server An easy to use Status Page for your websites and applications. Statup will automatically fetch the application and render a beautiful status page with tons of features for you to build an even better status page. This Status Page generator allows you to use MySQL, Postgres, or SQLite on multiple operating systems. -[![Gitter](https://img.shields.io/gitter/room/nwjs/nw.js.svg)](https://gitter.im/statup-app/general) +[![GoDoc](https://godoc.org/github.com/golang/gddo?status.svg)](https://godoc.org/github.com/hunterlong/statup) [![Gitter](https://img.shields.io/gitter/room/nwjs/nw.js.svg)](https://gitter.im/statup-app/general) ## A Future-Proof Status Page -Statup strives to remain future-proof and remain intact if a failure is created. Your Statup service should not be running on the same instance you're trying to monitor. If your server crashes your Status Page should still remaining online to notify your users of downtime. +Statup strives to remain future-proof and remain intact if a failure is created. Your Statup service should not be running on the same instance you're trying to monitor. If your server crashes your Status Page should still remaining online to notify your users of downtime.

## Lightweight and Fast -Statup is a very lightweight application and is available for Linux, Mac, and Windows. The Docker image is only ~16Mb so you know that this application won't be filling up your hard drive space. -The Status binary for all other OS's is ~17Mb at most. +Statup is a very lightweight application and is available for Linux, Mac, and Windows. The Docker image is only ~16Mb so you know that this application won't be filling up your hard drive space. +The Status binary for all other OS's is ~17Mb at most. ## No Requirements Statup is built in Go Language so all you need is the precompile binary based on your operating system. You won't need to install anything extra once you have the Statup binary installed. You can even run Statup on a Raspberry Pi. @@ -33,32 +33,32 @@ Statup will allow you to completely customize your Status Page using SASS stylin

## Mobile View is Gorgeous -Your status page will be optimized for mobile and desktop viewers. Statup has a full width edge to edge view, which you can also edit to meet your requirements. +Your status page will be optimized for mobile and desktop viewers. Statup has a full width edge to edge view, which you can also edit to meet your requirements. ## Run on Any Server -Whether you're a Docker fan-boy or a [AWS EC2](https://github.com/hunterlong/statup/wiki/AWS-EC2) master, Statup gives you multiple options to simply get running. Our Amazon AMI image is only 8Gb and will automatically update to the most stable version of Statup. +Whether you're a Docker fan-boy or a [AWS EC2](https://github.com/hunterlong/statup/wiki/AWS-EC2) master, Statup gives you multiple options to simply get running. Our Amazon AMI image is only 8Gb and will automatically update to the most stable version of Statup. 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, Email, Twilio and more -Statup includes email notification via SMTP and Slack integration using [Incoming Webhook](https://api.slack.com/incoming-webhooks). Insert the webhook URL into the Settings page in Statup and enable the Slack integration. Anytime a service fails, you're channel that you specified on Slack will receive a message. +Statup includes email notification via SMTP and Slack integration using [Incoming Webhook](https://api.slack.com/incoming-webhooks). Insert the webhook URL into the Settings page in Statup and enable the Slack integration. Anytime a service fails, you're channel that you specified on Slack will receive a message. ## User Created Plugins and Notifiers View the [Plugin Wiki](https://github.com/hunterlong/statup/wiki/Statup-Plugins) to see detailed information about Golang Plugins. Statup isn't just another Status Page for your applications, it's a framework that allows you to create your own plugins to interact with every element of your status page. [Notifier's](https://github.com/hunterlong/statup/wiki/Notifiers) can also be create with only 1 golang file. -Plugin are created in Golang using the [statup/plugin](https://github.com/hunterlong/statup/tree/master/plugin) golang package. The plugin package has a list of interfaces/events to accept into your own plugin application. +Plugin are created in Golang using the [statup/plugin](https://github.com/hunterlong/statup/tree/master/plugin) golang package. The plugin package has a list of interfaces/events to accept into your own plugin application.

## Easy to use Dashboard -Having a straight forward dashboard makes Statup that much better. Monitor your websites and applications with a basic HTTP GET request, or add a POST request with your own JSON to post to the endpoint. +Having a straight forward dashboard makes Statup that much better. Monitor your websites and applications with a basic HTTP GET request, or add a POST request with your own JSON to post to the endpoint.

## Exporting Static HTML -If you want to use Statup as a CLI application without running a server, you can export your status page to a static HTML. +If you want to use Statup as a CLI application without running a server, you can export your status page to a static HTML. This export tool is very useful for people who want to export their HTML and upload/commit it to Github Pages or an FTP server. ```dash statup export @@ -73,7 +73,7 @@ docker run -it -p 8080:8080 hunterlong/statup There are multiple ways to startup a Statup server. You want to make sure Statup is on it's own instance that is not on the same server as the applications you wish to monitor. It doesn't look good when your Status Page goes down, I recommend a small EC2 instance so you can set it, and forget it. ## Docker Compose -In this folder there is a standard docker-compose file that include nginx, postgres, and Statup. +In this folder there is a standard docker-compose file that include nginx, postgres, and Statup. ```bash docker-compose up -d ``` @@ -91,7 +91,7 @@ Once your instance has started, it will take a moment to get your SSL certificat Running Statup on the smallest EC2 server is very quick using the AWS AMI Image. Checkout the [AWS Wiki](https://github.com/hunterlong/statup/wiki/AWS-EC2) to see a step by step guide on how to get your EC2 Statup service online. ##### Create Security Groups -Create the AWS Security Groups with the commands below, Statup will expose port 80 and 443. +Create the AWS Security Groups with the commands below, Statup will expose port 80 and 443. ```bash aws ec2 create-security-group --group-name StatupPublicHTTP \ --description "Statup HTTP Server on port 80 and 443" @@ -100,7 +100,7 @@ aws ec2 create-security-group --group-name StatupPublicHTTP \ aws ec2 authorize-security-group-ingress \ --group-id sg-7e8b830f --protocol tcp \ --port 80 --cidr 0.0.0.0/0 - + aws ec2 authorize-security-group-ingress \ --group-id sg-7e8b830f --protocol tcp \ --port 443 --cidr 0.0.0.0/0 @@ -139,10 +139,10 @@ scrape_configs: ``` ## Contributing -Statup accepts Push Requests! Feel free to add your own features and notifiers. You probably want to checkout the [Notifier Wiki](https://github.com/hunterlong/statup/wiki/Notifiers) to get a better understanding on how to create your own notification methods for failing/successful services. Testing on Statup will test each function on MySQL, Postgres, and SQLite. I recommend you run a MySQL and a Postgres Docker image for testing. +Statup accepts Push Requests! Feel free to add your own features and notifiers. You probably want to checkout the [Notifier Wiki](https://github.com/hunterlong/statup/wiki/Notifiers) to get a better understanding on how to create your own notification methods for failing/successful services. Testing on Statup will test each function on MySQL, Postgres, and SQLite. I recommend you run a MySQL and a Postgres Docker image for testing. [![Go Report Card](https://goreportcard.com/badge/github.com/hunterlong/statup)](https://goreportcard.com/report/github.com/hunterlong/statup) -[![Build Status](https://travis-ci.org/hunterlong/statup.svg?branch=master)](https://travis-ci.org/hunterlong/statup) [![Cypress.io tests](https://img.shields.io/badge/cypress.io-tests-green.svg?style=flat-square)](https://dashboard.cypress.io/#/projects/bi8mhr/runs) +[![Build Status](https://travis-ci.org/hunterlong/statup.svg?branch=master)](https://travis-ci.org/hunterlong/statup) [![Cypress.io tests](https://img.shields.io/badge/cypress.io-tests-green.svg?style=flat-square)](https://dashboard.cypress.io/#/projects/bi8mhr/runs) [![Docker Pulls](https://img.shields.io/docker/pulls/hunterlong/statup.svg)](https://hub.docker.com/r/hunterlong/statup/builds/) [![Godoc](https://godoc.org/github.com/hunterlong/statup?status.svg)](https://godoc.org/github.com/hunterlong/statup)[![Coverage Status](https://coveralls.io/repos/github/hunterlong/statup/badge.svg?branch=master)](https://coveralls.io/github/hunterlong/statup?branch=master) diff --git a/core/checkin.go b/core/checkin.go index 5e033ccc..4afee07b 100644 --- a/core/checkin.go +++ b/core/checkin.go @@ -20,7 +20,6 @@ import ( "github.com/ararog/timeago" "github.com/hunterlong/statup/types" "github.com/hunterlong/statup/utils" - "github.com/jinzhu/gorm" "time" ) @@ -80,17 +79,18 @@ func (u *Checkin) Hits() []CheckinHit { return checkins } -func (u *Checkin) Update() (int64, error) { - fmt.Println("updating: ", u.Id, u.ApiKey) - exists := checkinDB().Find(&u).RecordNotFound() - var row *gorm.DB - if !exists { - row = checkinDB().Update(&u) - } else { - u.ApiKey = utils.RandomString(7) - row = checkinDB().Create(&u) +func (u *Checkin) Create() (int64, error) { + u.ApiKey = utils.RandomString(7) + row := checkinDB().Create(&u) + if row.Error != nil { + utils.Log(2, row.Error) + return 0, row.Error } - fmt.Println("found: ", exists, u.Id, u.Service, u.Interval, u.GracePeriod, u.ApiKey) + return u.Id, row.Error +} + +func (u *Checkin) Update() (int64, error) { + row := checkinDB().Update(&u) if row.Error != nil { utils.Log(2, row.Error) return 0, row.Error diff --git a/core/notifier/example_test.go b/core/notifier/example_test.go index 92719e05..f3848530 100644 --- a/core/notifier/example_test.go +++ b/core/notifier/example_test.go @@ -33,7 +33,7 @@ var example = &ExampleNotifier{&Notification{ Description: "Example Notifier", Author: "Hunter Long", AuthorUrl: "https://github.com/hunterlong", - Delay: time.Duration(200 * time.Millisecond), + Delay: time.Duration(3 * time.Second), Limits: 7, Form: []NotificationForm{{ Type: "text", @@ -182,7 +182,7 @@ func (n *ExampleNotifier) OnUpdatedNotifier(s *Notification) { } // Create a new notifier that includes a form for the end user to insert their own values -func Example() { +func ExampleNotification() { // Create a new variable for your Notifier example = &ExampleNotifier{&Notification{ Method: "Example", @@ -198,29 +198,91 @@ func Example() { Placeholder: "Insert your Host here.", DbField: "host", SmallText: "you can also use SmallText to insert some helpful hints under this input", + }, { + Type: "text", + Title: "API Key", + Placeholder: "Include some type of API key here", + DbField: "api_key", }}, }} - // AddNotifier accepts a notifier to load into the Statup Notification system - AddNotifier(example) + // AddNotifier accepts a Notifier to load into the Statup Notification system + err := AddNotifier(example) + fmt.Println(err) + // Output: } -// Add any type of interface to the AddQueue function when a service is successful -func Example_onSuccess() { +// Add a Notifier to the AddQueue function to insert it into the system +func ExampleAddNotifier() { + err := AddNotifier(example) + fmt.Println(err) + // Output: +} + +// OnSuccess will be triggered everytime a service is online +func ExampleNotification_OnSuccess() { msg := fmt.Sprintf("this is a successful message as a string passing into AddQueue function") example.AddQueue(msg) + fmt.Println(len(example.Queue)) + // Output: 1 } -// Add any type of interface to the AddQueue function when a service is successful -func Example_onFailure() { +// Add any type of interface to the AddQueue function to be ran in the queue +func ExampleNotification_AddQueue() { msg := fmt.Sprintf("this is a failing message as a string passing into AddQueue function") example.AddQueue(msg) + queue := example.Queue + fmt.Printf("Example has %v items in the queue", len(queue)) + // Output: Example has 2 items in the queue } // The Send method will run the main functionality of your notifier -func Example_send() { - // example.Send(msg interface{}) - for i := 0; i <= 10; i++ { - fmt.Printf("do something awesome rather than a loop %v\n", i) - } +func ExampleNotification_Send() { + msg := "this can be any type of interface" + example.Send(msg) + queue := example.Queue + fmt.Printf("Example has %v items in the queue", len(queue)) + // Output: + // i received this string: this can be any type of interface + // Example has 2 items in the queue +} + +// LastSent will return the time.Duration of the last sent message +func ExampleNotification_LastSent() { + last := example.LastSent() + fmt.Printf("Last message was sent %v seconds ago", last.Seconds()) + // Output: Last message was sent 0 seconds ago +} + +// Logs will return a slice of previously sent items from your notifier +func ExampleNotification_Logs() { + logs := example.Logs() + fmt.Printf("Example has %v items in the log", len(logs)) + // Output: Example has 0 items in the log +} + +// SentLastMinute will return he amount of notifications sent in last 1 minute +func ExampleNotification_SentLastMinute() { + lastMinute := example.SentLastMinute() + fmt.Printf("%v notifications sent in the last minute", lastMinute) + // Output: 0 notifications sent in the last minute +} + +// SentLastHour will return he amount of notifications sent in last 1 hour +func ExampleNotification_SentLastHour() { + lastHour := example.SentLastHour() + fmt.Printf("%v notifications sent in the last hour", lastHour) + // Output: 0 notifications sent in the last hour +} + +// SentLastHour will return he amount of notifications sent in last 1 hour +func ExampleNotification_WithinLimits() { + ok, err := example.WithinLimits() + if err != nil { + panic(err) + } + if ok { + fmt.Printf("Example notifier is still within its sending limits") + } + // Output: Example notifier is still within its sending limits } diff --git a/core/notifier/notifiers.go b/core/notifier/notifiers.go index 50d3ebe7..7da5fd32 100644 --- a/core/notifier/notifiers.go +++ b/core/notifier/notifiers.go @@ -94,18 +94,14 @@ func SetDB(d *gorm.DB) { db = d } -func asNotifier(n interface{}) Notifier { - return n.(Notifier) -} - -func asNotification(n interface{}) *Notification { - return n.(Notifier).Select() +func asNotification(n Notifier) *Notification { + return n.Select() } // AddNotifier accept a Notifier interface to be added into the array -func AddNotifier(n interface{}) error { +func AddNotifier(n Notifier) error { if isType(n, new(Notifier)) { - err := checkNotifierForm(asNotifier(n)) + err := checkNotifierForm(n) if err != nil { return err } diff --git a/core/notifier/notifiers_test.go b/core/notifier/notifiers_test.go index ef77caa5..5706554c 100644 --- a/core/notifier/notifiers_test.go +++ b/core/notifier/notifiers_test.go @@ -231,10 +231,9 @@ func TestRunAllQueueAndStop(t *testing.T) { assert.True(t, example.IsRunning()) assert.Equal(t, 16, len(example.Queue)) go Queue(example) - assert.Equal(t, 16, len(example.Queue)) - time.Sleep(12 * time.Second) - assert.Equal(t, 6, len(example.Queue)) + time.Sleep(13 * time.Second) + assert.True(t, len(example.Queue) >= 10) example.close() assert.False(t, example.IsRunning()) - assert.Equal(t, 6, len(example.Queue)) + assert.True(t, len(example.Queue) >= 10) } diff --git a/core/services.go b/core/services.go index 0f9b8536..39bbf642 100644 --- a/core/services.go +++ b/core/services.go @@ -49,10 +49,10 @@ func SelectService(id int64) *Service { return nil } -func (s *Service) Checkin() *Checkin { - var checkin types.Checkin - checkinDB().Find(&checkin, "service = ?", s.Id) - return &Checkin{&checkin} +func (s *Service) Checkins() []*Checkin { + var checkin []*Checkin + checkinDB().Where("service = ?", s.Id).Find(&checkin) + return checkin } // SelectAllServices returns a slice of *core.Service to be store on []*core.Services, should only be called once on startup. @@ -66,7 +66,7 @@ func (c *Core) SelectAllServices() ([]*Service, error) { CoreApp.Services = nil for _, service := range services { service.Start() - service.Checkin() + service.Checkins() service.AllFailures() CoreApp.Services = append(CoreApp.Services, service) } diff --git a/core/services_test.go b/core/services_test.go index b7775940..a222f67e 100644 --- a/core/services_test.go +++ b/core/services_test.go @@ -17,6 +17,7 @@ package core import ( "github.com/hunterlong/statup/types" + "github.com/hunterlong/statup/utils" "github.com/stretchr/testify/assert" "testing" "time" @@ -224,7 +225,7 @@ func TestCreateFailingHTTPService(t *testing.T) { func TestServiceFailedCheck(t *testing.T) { service := SelectService(17) assert.Equal(t, "Bad URL", service.Name) - service.Check(true) + service.Check(false) assert.Equal(t, "Bad URL", service.Name) assert.False(t, service.Online) } @@ -249,7 +250,7 @@ func TestCreateFailingTCPService(t *testing.T) { func TestServiceFailedTCPCheck(t *testing.T) { service := SelectService(newServiceId) - service.Check(true) + service.Check(false) assert.Equal(t, "Bad TCP", service.Name) assert.False(t, service.Online) } @@ -342,3 +343,57 @@ func TestDNScheckService(t *testing.T) { assert.Nil(t, err) assert.NotZero(t, amount) } + +func TestCreateCheckin(t *testing.T) { + checkin := ReturnCheckin(&types.Checkin{ + Service: 1, + Interval: 10, + GracePeriod: 5, + ApiKey: utils.RandomString(7), + }) + id, err := checkin.Create() + assert.Nil(t, err) + assert.NotZero(t, id) +} + +func TestSelectCheckin(t *testing.T) { + service := SelectService(1) + checkins := service.Checkins() + assert.NotNil(t, checkins) + assert.Equal(t, 1, len(checkins)) + first := checkins[0] + assert.Equal(t, int64(10), first.Interval) + assert.Equal(t, 7, len(first.ApiKey)) + assert.Equal(t, int64(5), first.GracePeriod) +} + +func TestCreateCheckinHits(t *testing.T) { + service := SelectService(1) + checkins := service.Checkins() + first := checkins[0] + created := time.Now().Add(-2 * time.Hour) + for i := 0; i <= 20; i++ { + hit := ReturnCheckinHit(&types.CheckinHit{ + Checkin: first.Id, + From: "192.168.1.1", + CreatedAt: created, + }) + hit.Create() + created = created.Add(10 * time.Second) + } + hits := first.Hits() + assert.Equal(t, 21, len(hits)) +} + +func TestSelectCheckinMethods(t *testing.T) { + time.Sleep(5 * time.Second) + service := SelectService(1) + checkins := service.Checkins() + assert.NotNil(t, checkins) + first := checkins[0] + lastHit := first.Last() + assert.Equal(t, float64(10), first.Period().Seconds()) + assert.Equal(t, float64(5), first.Grace().Seconds()) + assert.Equal(t, time.Now().Day(), lastHit.CreatedAt.Day()) + assert.Equal(t, "Just now", lastHit.Ago()) +} diff --git a/handlers/services.go b/handlers/services.go index ac42709f..887369ce 100644 --- a/handlers/services.go +++ b/handlers/services.go @@ -261,7 +261,9 @@ func checkinCreateUpdateHandler(w http.ResponseWriter, r *http.Request) { } vars := mux.Vars(r) service := core.SelectService(utils.StringInt(vars["id"])) - checkin := service.Checkin() + api := r.PostForm.Get("api") + checkin := core.SelectCheckin(api) + interval := utils.StringInt(r.PostForm.Get("interval")) grace := utils.StringInt(r.PostForm.Get("grace")) checkin.Interval = interval diff --git a/source/tmpl/service.html b/source/tmpl/service.html index ef11eacd..ef81135b 100644 --- a/source/tmpl/service.html +++ b/source/tmpl/service.html @@ -105,16 +105,18 @@ - {{$ch := $s.Checkin}} + {{range $s.Checkins}} + {{ $ch := . }} {{CoreApp.Domain}}/checkin/{{$ch.ApiKey}} every {{Duration $ch.Period}}
after {{Duration $ch.Grace}} {{Ago $ch.Last.CreatedAt}} {{ if lt $ch.Expected 0}}{{Duration $ch.Expected}} ago{{else}}in {{Duration $ch.Expected}}{{end}} + {{template "form_checkin" $ch}} + {{end}} - {{template "form_checkin" $ch}}