diff --git a/README.md b/README.md index f8ad0b2..8ae3511 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,85 @@ Can work with email, text (sms), push notifications, etc. # Install -Git: +## Downloads + +### MacOS + +MacOS (darwin): [64-bit Download ](https://rootprojects.org/watchdog/dist/darwin/amd64/watchdog) + +``` +curl https://rootprojects.org/watchdog/dist/darwin/amd64/watchdog -o watchdog +``` + +### Windows + +
+See download options +Windows 10: [64-bit Download](https://rootprojects.org/watchdog/dist/windows/amd64/watchdog.exe) + +``` +powershell.exe $ProgressPreference = 'SilentlyContinue'; Invoke-WebRequest https://rootprojects.org/watchdog/dist/windows/amd64/watchdog.exe -OutFile watchdog.exe +``` + +Windows 7: [32-bit Download](https://rootprojects.org/watchdog/dist/windows/386/watchdog.exe) + +``` +powershell.exe "(New-Object Net.WebClient).DownloadFile('https://rootprojects.org/watchdog/dist/windows/386/watchdog.exe', 'watchdog.exe')" +``` + +
+ +### Linux + +
+See download options + +Linux (64-bit): [Download](https://rootprojects.org/watchdog/dist/linux/amd64/watchdog) + +``` +curl https://rootprojects.org/watchdog/dist/linux/amd64/watchdog -o watchdog +``` + +Linux (32-bit): [Download](https://rootprojects.org/watchdog/dist/linux/386/watchdog) + +``` +curl https://rootprojects.org/watchdog/dist/linux/386/watchdog -o watchdog +``` + +
+ +### Raspberry Pi (Linux ARM) + +
+See download options + +RPi 4 (64-bit armv8): [Download](https://rootprojects.org/watchdog/dist/linux/armv8/watchdog) + +``` +curl https://rootprojects.org/watchdog/dist/linux/armv8/watchdog -o watchdog` +``` + +RPi 3 (armv7): [Download](https://rootprojects.org/watchdog/dist/linux/armv7/watchdog) + +``` +curl https://rootprojects.org/watchdog/dist/linux/armv7/watchdog -o watchdog +``` + +ARMv6: [Download](https://rootprojects.org/watchdog/dist/linux/armv6/watchdog) + +``` +curl https://rootprojects.org/watchdog/dist/linux/armv6/watchdog -o watchdog +``` + +RPi Zero (armv5): [Download](https://rootprojects.org/watchdog/dist/linux/armv5/watchdog) + +``` +curl https://rootprojects.org/watchdog/dist/linux/armv5/watchdog -o watchdog +``` + +
+ +## Git: ```bash git clone https://git.coolaj86.com/coolaj86/watchdog.go.git @@ -21,19 +99,6 @@ pushd cmd/watchdog go build -mod=vendor ``` -Zip: - -- Linux - - [watchdog-v1.1.0-linux-amd64.zip](https://git.rootprojects.org/root/watchdog.go/releases/download/v1.1.0-linux/watchdog-v1.1.0-linux-amd64.zip) - - [watchdog-v1.1.0-linux-386.zip](https://git.rootprojects.org/root/watchdog.go/releases/download/v1.1.0-linux/watchdog-v1.1.0-linux-386.zip) - - [watchdog-v1.1.0-linux-armv7.zip](https://git.rootprojects.org/root/watchdog.go/releases/download/v1.1.0-linux/watchdog-v1.1.0-linux-armv7.zip) - - [watchdog-v1.1.0-linux-armv5.zip](https://git.rootprojects.org/root/watchdog.go/releases/download/v1.1.0-linux/watchdog-v1.1.0-linux-armv5.zip) -- MacOS - - [watchdog-v1.1.0-darwin-amd64.zip](https://git.rootprojects.org/root/watchdog.go/releases/download/v1.1.0-macos/watchdog-v1.1.0-darwin-amd64.zip) -- Windows - - [watchdog-v1.1.0-windows-amd64.zip](https://git.rootprojects.org/root/watchdog.go/releases/download/v1.1.0-windows/watchdog-v1.1.0-windows-amd64.zip) - - [watchdog-v1.1.0-windows-386.zip](https://git.rootprojects.org/root/watchdog.go/releases/download/v1.1.0-windows/watchdog-v1.1.0-windows-386.zip) - # Usage Mac, Linux: @@ -48,6 +113,15 @@ Windows: watchdog.exe -c config.json ``` +# Changelog + +- v1.2.0 + - report when sites come back up + - and more template vars + - and localization for status +- v1.1.0 support `json` request bodies (for Pushbullet) +- v1.0.0 support Twilio and Mailgun + # Getting Started
@@ -78,6 +152,23 @@ Be careful of "smart quotes" and HTML entities: - `We’re Open!` is not `We're Open!` - Neither is `We're Open!` nor `We're Open!` +Leave empty for No Content pages, such as redirects. + +### `badwords` + +The opposite of `keywords`. + +If a literal, exact match of badwords exists as part of the response, the site is considered to be down. + +Ignored if empty. + +### `localizations` + +Normally `{{ .Status }}` will be `"up"` or `"down"` and `{{ .Message }}` will be `"is down"` or `"came back up"`. +Localizations allow you to swap that out for something else. + +I added this so that I could use "πŸ”₯πŸ”₯πŸ”₯" and "πŸ‘" for myself without imposing upon others. + ### `webhooks` This references the arbitrary `name` of a webhook in the `webhooks` array. @@ -114,7 +205,10 @@ command="systemctl restart foo.service",no-port-forwarding,no-x11-forwarding,no-
{{ .Name }} and other template variables -`{{ .Name }}` is the only template variable right now. +`{{ .Name }}` is the name of your site. +`{{ .Message }}` is either `went down` or `came back up`. +`{{ .Status }}` is either `up` or `down`. +`{{ .Watchdog }}` is the name of your watchdog (useful if you have multiple). It refers to the name of the watch, which is "Example Site" in the sample config below. @@ -234,11 +328,13 @@ The examples below are shown with Mailgun, Pushbullet, and Twilio, as taken from ```json { + "watchdog": "Monitor A", "watches": [ { "name": "Example Site", "url": "https://example.com/", "keywords": "My Site", + "badwords": "Could not connect to database.", "webhooks": ["my_mailgun", "my_pushbullet", "my_twilio"], "recover_script": "systemctl restart example-site" } @@ -258,8 +354,8 @@ The examples below are shown with Mailgun, Pushbullet, and Twilio, as taken from "form": { "from": "Watchdog ", "to": "jon.doe@gmail.com", - "subject": "{{ .Name }} is down.", - "text": "The system is down. Check up on {{ .Name }} ASAP." + "subject": "[{{ .Watchdog }}] {{ .Name }} {{ .Message }}.", + "text": "{{ .Name }} {{ .Message }}. Reported by {{ .Watchdog }}." } }, { @@ -271,8 +367,8 @@ The examples below are shown with Mailgun, Pushbullet, and Twilio, as taken from "User-Agent": "Watchdog/1.0" }, "json": { - "body": "The system is down. Check up on {{ .Name }} ASAP.", - "title": "{{ .Name }} is down.", + "body": "The system {{ .Message }}. Check up on {{ .Name }} ASAP.", + "title": "{{ .Name }} {{ .Message }}.", "type": "note" } }, @@ -293,7 +389,11 @@ The examples below are shown with Mailgun, Pushbullet, and Twilio, as taken from "Body": "[{{ .Name }}] The system is down. The system is down." } } - ] + ], + "localizations": { + "up": "πŸ‘", + "down": "πŸ”₯πŸ”₯πŸ”₯" + } } ``` diff --git a/build-all.sh b/build-all.sh new file mode 100644 index 0000000..8bac14f --- /dev/null +++ b/build-all.sh @@ -0,0 +1,45 @@ +#GOOS=windows GOARCH=amd64 go install +#go tool dist list + +# TODO move this into tools/build.go + +export CGO_ENABLED=0 +exe=watchdog +gocmd=. + +echo "" +go generate -mod=vendor ./... + +echo "" +echo "Windows amd64" +#GOOS=windows GOARCH=amd64 go build -mod=vendor -o dist/windows/amd64/${exe}.exe -ldflags "-H=windowsgui" $gocmd +#GOOS=windows GOARCH=amd64 go build -mod=vendor -o dist/windows/amd64/${exe}.debug.exe +GOOS=windows GOARCH=amd64 go build -mod=vendor -o dist/windows/amd64/${exe}.exe +echo "Windows 386" +#GOOS=windows GOARCH=386 go build -mod=vendor -o dist/windows/386/${exe}.exe -ldflags "-H=windowsgui" $gocmd +#GOOS=windows GOARCH=386 go build -mod=vendor -o dist/windows/386/${exe}.debug.exe +GOOS=windows GOARCH=386 go build -mod=vendor -o dist/windows/386/${exe}.exe + +echo "" +echo "Darwin (macOS) amd64" +GOOS=darwin GOARCH=amd64 go build -mod=vendor -o dist/darwin/amd64/${exe} $gocmd + +echo "" +echo "Linux amd64" +GOOS=linux GOARCH=amd64 go build -mod=vendor -o dist/linux/amd64/${exe} $gocmd +echo "Linux 386" +GOOS=linux GOARCH=386 go build -mod=vendor -o dist/linux/386/${exe} $gocmd + +echo "" +echo "RPi 4 (64-bit) ARMv8" +GOOS=linux GOARCH=arm64 go build -mod=vendor -o dist/linux/armv8/${exe} $gocmd +echo "RPi 3 B+ ARMv7" +GOOS=linux GOARCH=arm GOARM=7 go build -mod=vendor -o dist/linux/armv7/${exe} $gocmd +echo "ARMv6" +GOOS=linux GOARCH=arm GOARM=6 go build -mod=vendor -o dist/linux/armv6/${exe} $gocmd +echo "RPi Zero ARMv5" +GOOS=linux GOARCH=arm GOARM=5 go build -mod=vendor -o dist/linux/armv5/${exe} $gocmd + +echo "" +rsync -av ./dist/ ubuntu@rootprojects.org:/srv/www/rootprojects.org/$exe/dist/ +# https://rootprojects.org/serviceman/dist/windows/amd64/serviceman.exe diff --git a/build.sh b/build.sh deleted file mode 100644 index 2a64f37..0000000 --- a/build.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env bash - -export CGO_ENABLED=0 -#GOOS=windows GOARCH=amd64 go install -go tool dist list - -gocmd=watchdog.go -golib="" -echo "" - -echo "" -echo "Windows amd64" -GOOS=windows GOARCH=amd64 go build -o dist/windows-amd64/watchdog.exe $gocmd $golib -echo "Windows 386" -GOOS=windows GOARCH=386 go build -o dist/windows-386/watchdog.exe $gocmd $golib - -echo "" -echo "Darwin (macOS) amd64" -GOOS=darwin GOARCH=amd64 go build -o dist/darwin-amd64/watchdog $gocmd $golib - -echo "" -echo "Linux amd64" -GOOS=linux GOARCH=amd64 go build -o dist/linux-amd64/watchdog $gocmd $golib -echo "Linux 386" - -echo "" -GOOS=linux GOARCH=386 go build -o dist/linux-386/watchdog $gocmd $golib -echo "RPi 3 B+ ARMv7" -GOOS=linux GOARCH=arm GOARM=7 go build -o dist/linux-armv7/watchdog $gocmd $golib -echo "RPi Zero ARMv5" -GOOS=linux GOARCH=arm GOARM=5 go build -o dist/linux-armv5/watchdog $gocmd $golib - -my_ver=$(git describe --tags) -pushd dist - ls -d *-* | while read my_dist - do - if [ -d "$my_dist" ]; then - #tar -czvf watchdog-$my_ver-$my_dist.tar.gz $my_dist - zip -r watchdog-$my_ver-$my_dist.zip $my_dist - fi - done -popd - -echo "" -echo "" diff --git a/cmd/watchdog/watchdog.go b/cmd/watchdog/watchdog.go index db98cf7..e23b910 100644 --- a/cmd/watchdog/watchdog.go +++ b/cmd/watchdog/watchdog.go @@ -11,7 +11,7 @@ import ( "os" "strings" - watchdog "git.rootprojects.org/root/watchdog.go" + watchdog "git.rootprojects.org/root/go-watchdog" ) var GitRev, GitVersion, GitTimestamp string @@ -83,13 +83,16 @@ func main() { logQueue <- fmt.Sprintf("Watching '%s'", c.Name) go func(c watchdog.ConfigWatch) { d := watchdog.New(&watchdog.Dog{ - Name: c.Name, - CheckURL: c.URL, - Keywords: c.Keywords, - Recover: c.RecoverScript, - Webhooks: c.Webhooks, - AllWebhooks: allWebhooks, - Logger: logQueue, + Watchdog: config.Watchdog, + Name: c.Name, + CheckURL: c.URL, + Keywords: c.Keywords, + Badwords: c.Badwords, + Localizations: config.Localizations, + Recover: c.RecoverScript, + Webhooks: c.Webhooks, + AllWebhooks: allWebhooks, + Logger: logQueue, }) d.Watch() }(config.Watches[i]) diff --git a/doc.go b/doc.go index 8b12f6b..d3ef69e 100644 --- a/doc.go +++ b/doc.go @@ -5,5 +5,5 @@ // The git tag version describes the state of the binary, // not the state of the library. The API is not yet stable. // -// See https://git.rootproject.org/root/watchdog.go for pre-built binaries. +// See https://git.rootproject.org/root/go-watchdog for pre-built binaries. package watchdog diff --git a/go.mod b/go.mod index 258910d..962e514 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module git.rootprojects.org/root/watchdog.go +module git.rootprojects.org/root/go-watchdog go 1.12 diff --git a/watchdog.go b/watchdog.go index 4493674..755acae 100644 --- a/watchdog.go +++ b/watchdog.go @@ -14,24 +14,50 @@ import ( "time" ) +type Status int + +const ( + StatusDown Status = iota + StatusUp +) + +func (s Status) String() string { + // ... just wishing Go had enums like Rust... + switch s { + case StatusUp: + return "up" + case StatusDown: + return "down" + default: + return "[[internal error]]" + } +} + type Dog struct { - Name string - CheckURL string - Keywords string - Recover string - Webhooks []string - AllWebhooks map[string]Webhook - Logger chan string - error error - failures int - passes int - lastFailed time.Time - lastPassed time.Time - lastNotified time.Time + Watchdog string + Name string + CheckURL string + Keywords string + Badwords string + Localizations map[string]string + Recover string + Webhooks []string + AllWebhooks map[string]Webhook + Logger chan string + status Status + changed bool + error error + failures int + passes int + lastFailed time.Time + lastPassed time.Time + lastNotified time.Time } func New(d *Dog) *Dog { d.lastPassed = time.Now().Add(-5 * time.Minute) + d.status = StatusUp + d.changed = false return d } @@ -49,10 +75,14 @@ func (d *Dog) watch() { err := d.check() if nil == err { + if d.changed { + d.notify("came back up") + } return } - time.Sleep(time.Duration(2) * time.Second) + time.Sleep(time.Duration(5) * time.Second) + err2 := d.check() if nil != err2 { d.Logger <- fmt.Sprintf("Down: '%s': %s", d.Name, err2) @@ -61,7 +91,6 @@ func (d *Dog) watch() { return } - failure := false t := 10 for { d.recover() @@ -71,34 +100,55 @@ func (d *Dog) watch() { err := d.check() if nil != err { d.Logger <- fmt.Sprintf("Unrecoverable: '%s': %s", d.Name, err) - failure = true - } else { - failure = false } // We should notify if - // * We've had success since the last notification - // * It's been at least 5 minutes since the last notification - fiveMinutesAgo := time.Now().Add(-5 * time.Minute) - if d.lastPassed.After(d.lastNotified) && d.lastNotified.Before(fiveMinutesAgo) { - d.notify(failure) - } - if !failure || d.failures >= 5 { + // * The status has changed + // + // TODO what if the server is flip-flopping rapidly? + // how to rate limit? + // "{{ .Server }} is on cooldown for 30 minutes" + if d.changed { + d.notify("went down") + if StatusUp == d.status { + break + } + + // * We've had success since the last notification + // * It's been at least 5 minutes since the last notification + //fiveMinutesAgo := time.Now().Add(-5 * time.Minute) + //if d.lastPassed.After(d.lastNotified) && d.lastNotified.Before(fiveMinutesAgo) { + //} + //if !failure || d.failures >= 5 { // go back to the main 5-minute loop - break + // break + //} } } } func (d *Dog) check() error { + previousStatus := d.status + var err error defer func() { + // Are we up, or down? if nil != err { + d.status = StatusDown d.failures += 1 d.lastFailed = time.Now() } else { + d.status = StatusUp d.lastPassed = time.Now() d.passes += 1 + d.Logger <- fmt.Sprintf("Up: '%s'", d.Name) + } + + // Has that changed? + if previousStatus != d.status { + d.changed = true + } else { + d.changed = false } }() @@ -115,13 +165,21 @@ func (d *Dog) check() error { return err } + // Note: empty matches empty as true, so this works for checking redirects if !bytes.Contains(b, []byte(d.Keywords)) { err = fmt.Errorf("Down: '%s' Not Found for '%s'", d.Keywords, d.Name) d.Logger <- fmt.Sprintf("%s", err) d.error = err return err - } else { - d.Logger <- fmt.Sprintf("Up: '%s'", d.Name) + } + + if "" != d.Badwords { + if !bytes.Contains(b, []byte(d.Badwords)) { + err = fmt.Errorf("Down: '%s' Found for '%s'", d.Badwords, d.Name) + d.Logger <- fmt.Sprintf("%s", err) + d.error = err + return err + } } return nil @@ -154,8 +212,8 @@ func (d *Dog) recover() { } } -func (d *Dog) notify(hardFail bool) { - d.Logger <- fmt.Sprintf("Notifying the authorities of %s's failure", d.Name) +func (d *Dog) notify(msg string) { + d.Logger <- fmt.Sprintf("Notifying the authorities of %s's status change", d.Name) d.lastNotified = time.Now() for i := range d.Webhooks { @@ -172,11 +230,11 @@ func (d *Dog) notify(hardFail bool) { continue } - d.notifyOne(h, hardFail) + d.notifyOne(h, msg) } } -func (d *Dog) notifyOne(h Webhook, hardFail bool) { +func (d *Dog) notifyOne(h Webhook, msg string) { // TODO do this in main on config init if "" == h.Method { h.Method = "POST" @@ -191,7 +249,10 @@ func (d *Dog) notifyOne(h Webhook, hardFail bool) { v := h.Form[k] // because `{{` gets urlencoded //k = strings.Replace(k, "{{ .Name }}", d.Name, -1) + v = strings.Replace(v, "{{ .Watchdog }}", d.Watchdog, -1) v = strings.Replace(v, "{{ .Name }}", d.Name, -1) + v = strings.Replace(v, "{{ .Status }}", d.localize(d.status.String()), -1) + v = strings.Replace(v, "{{ .Message }}", d.localize(msg), -1) d.Logger <- fmt.Sprintf("[HEADER] %s: %s", k, v) form.Set(k, v) } @@ -203,7 +264,12 @@ func (d *Dog) notifyOne(h Webhook, hardFail bool) { return } // `{{` should be left alone - body = strings.NewReader(strings.Replace(string(bodyBuf), "{{ .Name }}", d.Name, -1)) + v := string(bodyBuf) + v = strings.Replace(v, "{{ .Watchdog }}", d.Watchdog, -1) + v = strings.Replace(v, "{{ .Name }}", d.Name, -1) + v = strings.Replace(v, "{{ .Status }}", d.localize(d.status.String()), -1) + v = strings.Replace(v, "{{ .Message }}", d.localize(msg), -1) + body = strings.NewReader(v) } client := NewHTTPClient() @@ -260,16 +326,27 @@ func (d *Dog) notifyOne(h Webhook, hardFail bool) { // TODO some sort of way to determine if data is successful (keywords) d.Logger <- fmt.Sprintf("[Notify] Success? %#v", data) } +func (d *Dog) localize(msg string) string { + for k := range d.Localizations { + if k == msg { + return d.Localizations[k] + } + } + return msg +} type Config struct { - Watches []ConfigWatch `json:"watches"` - Webhooks []Webhook `json:"webhooks"` + Watchdog string `json:"watchdog"` + Watches []ConfigWatch `json:"watches"` + Webhooks []Webhook `json:"webhooks"` + Localizations map[string]string `json:"localizations"` } type ConfigWatch struct { Name string `json:"name"` URL string `json:"url"` Keywords string `json:"keywords"` + Badwords string `json:"badwords"` Webhooks []string `json:"webhooks"` RecoverScript string `json:"recover_script"` }