174 lines
4 KiB
Go
174 lines
4 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"text/tabwriter"
|
|
"time"
|
|
)
|
|
|
|
type Target struct {
|
|
Name string `json:"name"`
|
|
URL string `json:"url"`
|
|
}
|
|
|
|
type Result struct {
|
|
Target Target
|
|
Up bool
|
|
StatusCode int
|
|
Latency time.Duration
|
|
CheckedAt time.Time
|
|
Err string
|
|
}
|
|
|
|
type Config struct {
|
|
Targets []Target `json:"targets"`
|
|
Interval int `json:"interval_seconds"`
|
|
DiscordWebhook string `json:"discord_webhook"`
|
|
LogFile string `json:"log_file"`
|
|
}
|
|
|
|
func check(t Target, timeout time.Duration) Result {
|
|
r := Result{Target: t, CheckedAt: time.Now()}
|
|
client := &http.Client{Timeout: timeout}
|
|
start := time.Now()
|
|
resp, err := client.Get(t.URL)
|
|
r.Latency = time.Since(start)
|
|
if err != nil {
|
|
r.Err = err.Error()
|
|
return r
|
|
}
|
|
defer resp.Body.Close()
|
|
r.StatusCode = resp.StatusCode
|
|
r.Up = resp.StatusCode >= 200 && resp.StatusCode < 400
|
|
return r
|
|
}
|
|
|
|
func sendDiscordAlert(webhookURL, message string) {
|
|
payload, _ := json.Marshal(map[string]string{"content": message})
|
|
resp, err := http.Post(webhookURL, "application/json", bytes.NewReader(payload))
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "discord alert failed: %v\n", err)
|
|
return
|
|
}
|
|
resp.Body.Close()
|
|
}
|
|
|
|
func printResults(w io.Writer, results []Result) {
|
|
tw := tabwriter.NewWriter(w, 0, 0, 2, ' ', 0)
|
|
for _, r := range results {
|
|
status := "DOWN"
|
|
if r.Up {
|
|
status = "UP"
|
|
}
|
|
code := "-"
|
|
if r.StatusCode != 0 {
|
|
code = fmt.Sprintf("%d", r.StatusCode)
|
|
}
|
|
fmt.Fprintf(tw, "%s\t%s\t%s\t%s\t%s\n",
|
|
r.CheckedAt.Format("2006-01-02 15:04:05"),
|
|
r.Target.Name,
|
|
status,
|
|
code,
|
|
r.Latency.Round(time.Millisecond),
|
|
)
|
|
}
|
|
tw.Flush()
|
|
}
|
|
|
|
func loadConfig(path string) (Config, error) {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
defer f.Close()
|
|
var cfg Config
|
|
if err := json.NewDecoder(f).Decode(&cfg); err != nil {
|
|
return Config{}, err
|
|
}
|
|
if cfg.Interval <= 0 {
|
|
cfg.Interval = 60
|
|
}
|
|
return cfg, nil
|
|
}
|
|
|
|
func runOnce(cfg Config, prevState map[string]bool, out io.Writer) {
|
|
results := make([]Result, len(cfg.Targets))
|
|
done := make(chan struct{}, len(cfg.Targets))
|
|
for i, t := range cfg.Targets {
|
|
go func(i int, t Target) {
|
|
results[i] = check(t, 10*time.Second)
|
|
done <- struct{}{}
|
|
}(i, t)
|
|
}
|
|
for range cfg.Targets {
|
|
<-done
|
|
}
|
|
|
|
printResults(out, results)
|
|
|
|
if cfg.DiscordWebhook != "" {
|
|
for _, r := range results {
|
|
wasUp, seen := prevState[r.Target.URL]
|
|
if !r.Up && (!seen || wasUp) {
|
|
msg := fmt.Sprintf("@here **%s** is DOWN (`%s`)", r.Target.Name, r.Target.URL)
|
|
if r.Err != "" {
|
|
msg += fmt.Sprintf("\nError: %s", r.Err)
|
|
} else {
|
|
msg += fmt.Sprintf("\nHTTP %d", r.StatusCode)
|
|
}
|
|
fmt.Fprintf(out, "%s\talert sent: %s is DOWN\n",
|
|
r.CheckedAt.Format("2006-01-02 15:04:05"), r.Target.Name)
|
|
sendDiscordAlert(cfg.DiscordWebhook, msg)
|
|
} else if r.Up && seen && !wasUp {
|
|
msg := fmt.Sprintf("**%s** is back UP (`%s`)", r.Target.Name, r.Target.URL)
|
|
fmt.Fprintf(out, "%s\talert sent: %s is back UP\n",
|
|
r.CheckedAt.Format("2006-01-02 15:04:05"), r.Target.Name)
|
|
sendDiscordAlert(cfg.DiscordWebhook, msg)
|
|
}
|
|
prevState[r.Target.URL] = r.Up
|
|
}
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
cfgPath := "config.json"
|
|
if len(os.Args) > 1 {
|
|
cfgPath = os.Args[1]
|
|
}
|
|
|
|
cfg, err := loadConfig(cfgPath)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "error loading config: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
logPath := cfg.LogFile
|
|
if logPath == "" {
|
|
logPath = "uptimetracker.log"
|
|
}
|
|
lf, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "error opening log file: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
defer lf.Close()
|
|
|
|
out := io.MultiWriter(os.Stdout, lf)
|
|
|
|
fmt.Fprintf(out, "%s\tstarted monitoring %d target(s) every %ds\n",
|
|
time.Now().Format("2006-01-02 15:04:05"), len(cfg.Targets), cfg.Interval)
|
|
|
|
prevState := make(map[string]bool)
|
|
runOnce(cfg, prevState, out)
|
|
|
|
ticker := time.NewTicker(time.Duration(cfg.Interval) * time.Second)
|
|
defer ticker.Stop()
|
|
for range ticker.C {
|
|
runOnce(cfg, prevState, out)
|
|
}
|
|
}
|