status/main.go

297 lines
6.5 KiB
Go
Raw Normal View History

2023-09-30 15:35:41 +00:00
package main
import (
2023-10-03 10:56:35 +00:00
"context"
2023-10-03 12:08:50 +00:00
"embed"
2023-10-07 08:24:49 +00:00
"fmt"
2023-10-01 15:40:22 +00:00
"io/fs"
2023-09-30 16:56:37 +00:00
"log"
2023-10-01 00:17:09 +00:00
"net"
2023-09-30 15:35:41 +00:00
"net/http"
2023-10-01 00:17:09 +00:00
"os"
2023-10-03 13:49:49 +00:00
os_signal "os/signal"
"strings"
2023-10-03 13:49:49 +00:00
"syscall"
2023-10-03 10:56:35 +00:00
"time"
2023-09-30 15:35:41 +00:00
2023-10-01 08:21:30 +00:00
"git.exozy.me/exozyme/status/core"
2023-10-01 15:40:22 +00:00
"git.exozy.me/exozyme/status/scanner"
2023-09-30 15:35:41 +00:00
"github.com/cbroglie/mustache"
2023-10-20 08:25:18 +00:00
"github.com/timewasted/go-accept-headers"
2023-09-30 15:35:41 +00:00
)
2023-10-01 15:40:22 +00:00
//go:embed all:public
var staticAssets embed.FS
//go:embed all:templates
var templateAssets embed.FS
func parseAndRenderTemplate(templateName string, data interface{}) (string, error) {
rawTemplate, err := templateAssets.ReadFile(templateName)
if err != nil {
return "", err
}
tmpl, err := mustache.ParseString(string(rawTemplate))
2023-09-30 15:35:41 +00:00
if err != nil {
log.Println("Failed to parse template:", err)
return "", err
}
res, err := tmpl.Render(data)
if err != nil {
log.Println("Failed to render template:", err)
return "", err
}
return res, nil
}
2023-10-20 08:25:18 +00:00
func index(w http.ResponseWriter, r *http.Request) {
2023-09-30 17:04:55 +00:00
if r.Method != "GET" {
return
}
2023-10-01 08:49:02 +00:00
config_new, err := core.LoadConfig()
if err != nil {
log.Println("[WARN] failed to load config:", err)
} else {
config = config_new
}
2023-09-30 16:15:11 +00:00
// get service status'
2023-10-03 10:56:35 +00:00
timeout := time.Second
2023-10-03 12:08:50 +00:00
ctx, cancel := context.WithTimeout(context.Background(), timeout)
2023-10-07 08:24:49 +00:00
defer cancel()
2023-10-03 12:48:42 +00:00
data := scanner.CheckServiceBatch(ctx, config.Service)
2023-10-07 08:24:49 +00:00
non_duplicates, err := dbm.StoreServiceStatusBatch(data, time.Now())
if err != nil {
http.Error(w, err.Error(), 500)
return
}
2024-01-21 18:54:20 +00:00
err_annouce := announce(non_duplicates)
if err_annouce != nil {
log.Println(err_annouce)
2023-10-07 08:24:49 +00:00
}
2023-09-30 17:04:55 +00:00
2023-10-03 10:40:41 +00:00
// don't uncomment this in production. verbose
// log.Printf("%v", data)
2023-09-30 17:04:55 +00:00
2023-10-20 08:25:18 +00:00
ua := r.UserAgent()
prefer_text := strings.HasPrefix(ua, "curl/")
header_accept := accept.Parse(r.Header.Get("Accept"))
accept_text := header_accept.Accepts("text/plain")
accept_html := header_accept.Accepts("text/html")
if (!accept_html || prefer_text) && accept_text {
indexForVT100(w, r, data)
} else if accept_html {
indexForBrowser(w, r, data)
} else { // when no valid content type is requested
// not to-spec. still sends plain text anyway
indexForVT100(w, r, data)
}
}
func indexForVT100(w http.ResponseWriter, r *http.Request, data scanner.ServiceStatusBatch) {
w.Header().Add("Content-Type", "text/plain; charset=utf-8")
parsedTemplate := formatStatusUpdateAsText(data)
2023-10-20 08:25:18 +00:00
w.Write([]byte(parsedTemplate))
}
func indexForBrowser(w http.ResponseWriter, r *http.Request, data scanner.ServiceStatusBatch) {
2023-09-30 17:57:42 +00:00
parsedTemplate, err := parseAndRenderTemplate("templates/index.html", data)
2023-09-30 15:35:41 +00:00
if err != nil {
http.Error(w, err.Error(), 500)
2023-10-07 08:24:49 +00:00
return
2023-09-30 15:35:41 +00:00
}
w.Write([]byte(parsedTemplate))
}
2023-10-01 08:21:30 +00:00
var config *core.Config
2023-10-03 13:23:57 +00:00
var dbm core.Database
2023-10-07 08:24:49 +00:00
var client *core.MatrixClient
2023-10-01 08:21:30 +00:00
2023-10-03 13:49:49 +00:00
func cleanup() {
log.Println("Cleaning up")
_ = dbm.StoreSelfStatus(core.EventSelfDown, time.Now())
2023-10-03 18:23:25 +00:00
_ = dbm.Persist()
2023-10-03 13:49:49 +00:00
}
2023-09-30 15:35:41 +00:00
func main() {
2023-10-01 08:32:35 +00:00
var err error
config, err = core.LoadConfig()
2023-10-01 08:21:30 +00:00
if err != nil {
2023-10-03 13:49:49 +00:00
log.Panic(err)
2023-10-01 08:21:30 +00:00
}
2023-10-03 12:44:55 +00:00
dbm, err = core.OpenDatabase(config.Database)
if err != nil {
2023-10-03 13:49:49 +00:00
log.Panic(err)
2023-10-03 12:44:55 +00:00
}
2023-10-07 08:24:49 +00:00
client, err = core.NewMatrixClient(config.Matrix)
if err != nil {
log.Panic(err)
}
2023-10-20 08:25:18 +00:00
http.HandleFunc("/", index)
2023-10-01 15:40:22 +00:00
// serve public folder from embedfs
htmlContent, err := fs.Sub(fs.FS(staticAssets), "public")
if err != nil {
2023-10-03 13:49:49 +00:00
log.Panic(err)
2023-10-01 15:40:22 +00:00
}
fs := http.FileServer(http.FS(htmlContent))
2023-09-30 17:57:42 +00:00
http.Handle("/public/", http.StripPrefix("/public/", fs))
2023-09-30 15:35:41 +00:00
2023-10-01 00:17:09 +00:00
protocol := "tcp"
addr := "localhost:3333"
2023-10-01 03:34:12 +00:00
var unix_socket_path string
var is_unix bool
if config.UnixSocket != "" {
// get unix socket path from the config else
unix_socket_path = config.UnixSocket
is_unix = true
} else {
// fallback
unix_socket_path, is_unix = os.LookupEnv("PORT")
}
2023-10-01 00:17:09 +00:00
if is_unix {
protocol = "unix"
addr = unix_socket_path
2023-10-01 03:34:12 +00:00
// VnPower: is this unsafe?
2023-10-01 00:17:09 +00:00
removeFile(addr) // ideally this should be run at program exit. sadly Go's defer doesn't care about signals or panic
}
srv := &http.Server{Addr: addr, Handler: nil}
ln, err := net.Listen(protocol, addr)
if err != nil {
2023-10-03 13:49:49 +00:00
log.Panic(err)
2023-10-01 00:17:09 +00:00
}
2023-10-01 00:24:30 +00:00
if is_unix {
err = os.Chmod(addr, 0660)
if err != nil {
2023-10-03 13:49:49 +00:00
log.Panic(err)
2023-10-01 00:24:30 +00:00
}
log.Printf("Listening on %v\n", addr)
} else {
log.Printf("Listening on http://%v/\n", addr)
}
2023-10-03 13:49:49 +00:00
// log startup and register cleanup (on SIGTERM or panic)
2023-10-07 08:24:49 +00:00
ch_sigterm := make(chan os.Signal, 1)
2023-10-03 18:23:25 +00:00
os_signal.Notify(ch_sigterm, syscall.SIGTERM, syscall.SIGINT)
2023-10-03 13:49:49 +00:00
log.Print("Starting up")
err = dbm.StoreSelfStatus(core.EventSelfUp, time.Now())
if err != nil {
log.Panic(err)
}
defer cleanup()
2023-10-03 15:11:46 +00:00
go func() {
<-ch_sigterm
2023-10-03 13:49:49 +00:00
err := srv.Shutdown(context.Background())
2023-10-03 18:23:25 +00:00
if err != nil {
log.Printf("Error when shutting down: %v", err)
}
2023-10-03 13:49:49 +00:00
}()
2023-10-03 15:11:46 +00:00
btime, err := core.GetBootTime()
2024-01-10 22:44:17 +00:00
if err != nil {
log.Printf("Error in core.GetBootTime: %v", err)
} else {
time_passed := time.Now().Sub(btime)
time.Sleep(core.WaitDurationAfterBoot - time_passed)
}
2023-10-03 15:11:46 +00:00
go func() { // timed update
2023-10-03 15:11:46 +00:00
for {
err := routineCheck(5*time.Minute)
2023-10-07 08:24:49 +00:00
if err != nil {
log.Printf("Error in routineCheck: %v", err)
2024-01-10 22:44:17 +00:00
// idea: maybe we should delay here
2023-10-07 08:24:49 +00:00
}
2023-10-03 15:11:46 +00:00
}
}()
err = srv.Serve(ln) // serve http
2023-10-03 13:49:49 +00:00
if err != http.ErrServerClosed {
log.Panic(err)
}
2023-09-30 15:35:41 +00:00
}
2023-10-03 18:25:50 +00:00
func routineCheck(timeout time.Duration) error {
2023-10-03 18:25:50 +00:00
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
2023-10-07 08:24:49 +00:00
2023-10-03 18:25:50 +00:00
data := scanner.CheckServiceBatch(ctx, config.Service)
2023-10-07 08:24:49 +00:00
non_duplicates, err := dbm.StoreServiceStatusBatch(data, time.Now())
if err != nil {
return err
}
err = announce(non_duplicates)
2023-10-07 13:51:03 +00:00
if err != nil {
return err
}
2023-10-03 18:25:50 +00:00
// log.Printf("Routine check: %v", data)
<-ctx.Done()
2023-10-07 08:24:49 +00:00
return nil
}
func formatStatusUpdateAsText(updates []scanner.ServiceStatus) string {
2023-10-20 08:25:18 +00:00
result := ""
2023-10-07 08:24:49 +00:00
for _, v := range updates {
2023-10-07 08:50:48 +00:00
var upOrDown string
2023-10-07 08:24:49 +00:00
if v.Ok {
2023-10-07 08:50:48 +00:00
upOrDown = "UP"
2023-10-07 08:24:49 +00:00
} else {
2023-10-07 08:50:48 +00:00
upOrDown = "DOWN"
2023-10-07 08:24:49 +00:00
}
2023-10-07 08:50:48 +00:00
serviceType := fmt.Sprintf("[%s]", v.Type)
2023-10-20 08:25:18 +00:00
result += fmt.Sprintf("%-4s | %-24s %-7s %s\n", upOrDown, v.Name, serviceType, v.Status)
}
return result
}
var first_time_to_annouce = true
func announce(updates []scanner.ServiceStatus) error {
messageBody := ""
if first_time_to_annouce { // only show downed services
tmp := []scanner.ServiceStatus{}
num_omitted := 0
for _, v := range updates {
if !v.Ok {
tmp = append(tmp, v)
} else {
num_omitted += 1
}
}
updates = tmp
messageBody += fmt.Sprintf("UP | (%d services omitted)\n", num_omitted)
}
2023-10-20 08:25:18 +00:00
if len(updates) == 0 {
return nil
2023-10-07 08:24:49 +00:00
}
if first_time_to_annouce {
first_time_to_annouce = false
}
messageBody += formatStatusUpdateAsText(updates)
_, err := client.SendNoticeHTML(
"```\n"+messageBody+"```",
"<pre><code>"+strings.TrimRight(messageBody, "\n")+"</code></pre>", &updates)
2023-10-07 08:50:48 +00:00
log.Printf("notify: err=%v\n%v", err, updates)
return err
2023-10-03 18:25:50 +00:00
}