smon/main.go

691 lines
16 KiB
Go
Raw Normal View History

2024-04-29 08:36:13 +02:00
package main
import (
// External
ws "git.gibonuddevalla.se/go/webservice"
"git.gibonuddevalla.se/go/webservice/session"
2024-05-05 10:10:04 +02:00
werr "git.gibonuddevalla.se/go/wrappederror"
// Internal
"smon/notification"
2024-04-29 08:36:13 +02:00
// Standard
"embed"
"encoding/json"
"flag"
"fmt"
"html/template"
"io"
"io/fs"
"log/slog"
"net/http"
"os"
"path"
"sort"
"strconv"
2024-04-30 08:04:16 +02:00
"time"
2024-04-29 08:36:13 +02:00
)
2024-05-04 22:08:04 +02:00
const VERSION = "v6"
2024-04-29 08:36:13 +02:00
var (
2024-05-05 10:10:04 +02:00
logger *slog.Logger
flagConfigFile string
flagDev bool
service *ws.Service
logFile *os.File
parsedTemplates map[string]*template.Template
componentFilenames []string
notificationManager notification.Manager
2024-04-29 08:36:13 +02:00
//go:embed sql
sqlFS embed.FS
//go:embed views
viewFS embed.FS
//go:embed static
staticFS embed.FS
)
func init() { // {{{
opts := slog.HandlerOptions{}
logger = slog.New(slog.NewJSONHandler(os.Stdout, &opts))
confDir, err := os.UserConfigDir()
if err != nil {
2024-05-05 10:10:04 +02:00
logger.Error("application", "error", werr.Wrap(err))
2024-04-29 08:36:13 +02:00
}
cfgPath := path.Join(confDir, "smon.yaml")
flag.StringVar(&flagConfigFile, "config", cfgPath, "Path and filename of the YAML configuration file")
flag.BoolVar(&flagDev, "dev", false, "reload templates on each request")
flag.Parse()
componentFilenames, err = getComponentFilenames(viewFS)
if err != nil {
logger.Error("application", "error", err)
os.Exit(1)
}
parsedTemplates = make(map[string]*template.Template)
} // }}}
func main() { // {{{
var err error
2024-05-05 10:10:04 +02:00
werr.Init()
werr.SetLogCallback(logHandler)
2024-04-29 08:36:13 +02:00
service, err = ws.New(flagConfigFile, VERSION, logger)
if err != nil {
logger.Error("application", "error", err)
os.Exit(1)
}
smonConf := SmonConfiguration{}
j, _ := json.Marshal(service.Config.Application)
json.Unmarshal(j, &smonConf)
logFile, err = os.OpenFile(smonConf.LogFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
if err != nil {
logger.Error("application", "error", err)
return
}
service.SetDatabase(sqlProvider)
service.SetStaticFS(staticFS, "static")
service.SetStaticDirectory("static", flagDev)
2024-04-30 08:04:16 +02:00
service.InitDatabaseConnection()
err = service.Db.Connect()
if err != nil {
logger.Error("application", "error", err)
return
}
2024-05-05 10:10:04 +02:00
err = InitNotificationManager()
if err != nil {
err = werr.Wrap(err).Log()
logger.Error("notification", "error", err)
}
2024-04-29 08:36:13 +02:00
service.Register("/", false, false, staticHandler)
service.Register("/area/new/{name}", false, false, areaNew)
service.Register("/area/rename/{id}/{name}", false, false, areaRename)
service.Register("/section/new/{areaID}/{name}", false, false, sectionNew)
service.Register("/section/rename/{id}/{name}", false, false, sectionRename)
2024-04-29 08:36:13 +02:00
service.Register("/problems", false, false, pageProblems)
2024-05-01 20:01:43 +02:00
service.Register("/problem/acknowledge/{id}", false, false, pageProblemAcknowledge)
service.Register("/problem/unacknowledge/{id}", false, false, pageProblemUnacknowledge)
2024-05-01 10:02:33 +02:00
2024-04-30 08:04:16 +02:00
service.Register("/datapoints", false, false, pageDatapoints)
service.Register("/datapoint/edit/{id}", false, false, pageDatapointEdit)
service.Register("/datapoint/update/{id}", false, false, pageDatapointUpdate)
2024-05-02 08:59:55 +02:00
service.Register("/datapoint/delete/{id}", false, false, pageDatapointDelete)
2024-05-01 10:02:33 +02:00
2024-04-29 08:36:13 +02:00
service.Register("/triggers", false, false, pageTriggers)
service.Register("/trigger/edit/{id}", false, false, pageTriggerEdit)
2024-05-01 10:02:33 +02:00
service.Register("/trigger/edit/{id}/{sectionID}", false, false, pageTriggerEdit)
2024-04-29 08:36:13 +02:00
service.Register("/trigger/update/{id}", false, false, pageTriggerUpdate)
service.Register("/trigger/run/{id}", false, false, pageTriggerRun)
2024-05-01 10:02:33 +02:00
2024-04-29 08:36:13 +02:00
service.Register("/configuration", false, false, pageConfiguration)
service.Register("/entry/{datapoint}", false, false, entryDatapoint)
err = service.Start()
if err != nil {
2024-05-05 10:10:04 +02:00
logger.Error("webserver", "error", werr.Wrap(err))
2024-04-29 08:36:13 +02:00
os.Exit(1)
}
} // }}}
func sqlProvider(dbname string, version int) (sql []byte, found bool) { // {{{
var err error
sql, err = sqlFS.ReadFile(fmt.Sprintf("sql/%05d.sql", version))
if err != nil {
return
}
found = true
return
} // }}}
2024-05-05 10:10:04 +02:00
func logHandler(err werr.Error) { // {{{
2024-04-29 08:36:13 +02:00
j, _ := json.Marshal(err)
logFile.Write(j)
logFile.Write([]byte("\n"))
} // }}}
func httpError(w http.ResponseWriter, err error) { // {{{
resp := struct {
OK bool
Error string
}{
false,
err.Error(),
}
j, _ := json.Marshal(resp)
w.Write(j)
} // }}}
func staticHandler(w http.ResponseWriter, r *http.Request, sess *session.T) { // {{{
if flagDev && !reloadTemplates(w) {
return
}
if r.URL.Path == "/" {
pageIndex(w, r, sess)
return
}
service.StaticHandler(w, r, sess)
} // }}}
func entryDatapoint(w http.ResponseWriter, r *http.Request, sess *session.T) { // {{{
dpoint := r.PathValue("datapoint")
value, _ := io.ReadAll(r.Body)
err := DatapointAdd(dpoint, value)
if err != nil {
2024-05-05 10:10:04 +02:00
httpError(w, werr.Wrap(err).Log())
2024-04-29 08:36:13 +02:00
return
}
2024-04-30 20:10:05 +02:00
var triggers []Trigger
triggers, err = TriggersRetrieveByDatapoint(dpoint)
if err != nil {
logger.Error("entry", "error", err)
}
2024-04-30 20:10:05 +02:00
for _, trigger := range triggers {
var out any
out, err = trigger.Run()
if err != nil {
2024-05-05 10:10:04 +02:00
err = werr.Wrap(err).Log()
logger.Error("entry", "error", err)
2024-05-01 10:02:33 +02:00
}
logger.Debug("entry", "datapoint", dpoint, "value", value, "trigger", trigger, "result", out)
switch v := out.(type) {
case bool:
2024-05-01 10:02:33 +02:00
// Trigger returning true - a problem occurred
if v {
err = ProblemStart(trigger)
if err != nil {
2024-05-05 10:10:04 +02:00
err = werr.Wrap(err).Log()
logger.Error("entry", "error", err)
}
} else {
err = ProblemClose(trigger)
if err != nil {
2024-05-05 10:10:04 +02:00
err = werr.Wrap(err).Log()
logger.Error("entry", "error", err)
}
}
default:
err := fmt.Errorf(`Expression for trigger %s not returning bool (%T)`, trigger.Name, v)
logger.Info("entry", "error", err)
2024-05-05 10:10:04 +02:00
werr.Wrap(err).WithData(v).Log()
}
2024-04-30 20:10:05 +02:00
}
2024-04-29 08:36:13 +02:00
j, _ := json.Marshal(struct{ OK bool }{true})
w.Write(j)
} // }}}
func reloadTemplates(w http.ResponseWriter) bool { // {{{
parsedTemplates = make(map[string]*template.Template)
return true
} // }}}
func getComponentFilenames(efs fs.FS) (files []string, err error) { // {{{
files = []string{}
if err := fs.WalkDir(efs, "views/components", func(path string, d fs.DirEntry, err error) error {
if d == nil {
return nil
}
if d.IsDir() {
return nil
}
files = append(files, path)
return nil
}); err != nil {
return nil, err
}
return files, nil
} // }}}
func getPage(layout, page string) (tmpl *template.Template, err error) { // {{{
layoutFilename := fmt.Sprintf("views/layouts/%s.gotmpl", layout)
pageFilename := fmt.Sprintf("views/pages/%s.gotmpl", page)
if tmpl, found := parsedTemplates[page]; found {
return tmpl, nil
}
2024-04-30 08:04:16 +02:00
funcMap := template.FuncMap{
2024-05-01 20:01:43 +02:00
"format_time": func(t time.Time) template.HTML {
return template.HTML(
t.Local().Format(`<span class="date">2006-01-02</span> <span class="time">15:04<span class="seconds">:05</span></span>`),
)
2024-04-30 08:04:16 +02:00
},
}
2024-04-29 08:36:13 +02:00
filenames := []string{layoutFilename, pageFilename}
filenames = append(filenames, componentFilenames...)
logger.Info("template", "op", "parse", "layout", layout, "page", page, "filenames", filenames)
if flagDev {
2024-04-30 08:04:16 +02:00
tmpl, err = template.New("main.gotmpl").Funcs(funcMap).ParseFS(os.DirFS("."), filenames...)
2024-04-29 08:36:13 +02:00
} else {
2024-05-01 21:34:57 +02:00
tmpl, err = template.New("main.gotmpl").Funcs(funcMap).ParseFS(viewFS, filenames...)
2024-04-29 08:36:13 +02:00
}
if err != nil {
2024-05-05 10:10:04 +02:00
err = werr.Wrap(err).Log()
2024-04-29 08:36:13 +02:00
return
}
2024-04-30 08:04:16 +02:00
parsedTemplates[page] = tmpl
2024-04-29 08:36:13 +02:00
return
} // }}}
func pageIndex(w http.ResponseWriter, _ *http.Request, _ *session.T) { // {{{
page := Page{
LAYOUT: "main",
PAGE: "index",
}
page.Render(w)
} // }}}
2024-05-01 10:02:33 +02:00
func areaNew(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
name := r.PathValue("name")
err := AreaCreate(name)
if err != nil {
2024-05-05 10:10:04 +02:00
httpError(w, werr.Wrap(err).Log())
return
}
w.Header().Add("Location", "/configuration")
w.WriteHeader(302)
return
} // }}}
func areaRename(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
idStr := r.PathValue("id")
id, err := strconv.Atoi(idStr)
if err != nil {
2024-05-05 10:10:04 +02:00
httpError(w, werr.Wrap(err).Log())
return
}
name := r.PathValue("name")
err = AreaRename(id, name)
if err != nil {
2024-05-05 10:10:04 +02:00
httpError(w, werr.Wrap(err).Log())
return
}
w.Header().Add("Location", "/configuration")
w.WriteHeader(302)
return
} // }}}
func sectionNew(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
idStr := r.PathValue("areaID")
areaID, err := strconv.Atoi(idStr)
if err != nil {
2024-05-05 10:10:04 +02:00
httpError(w, werr.Wrap(err).Log())
return
}
name := r.PathValue("name")
err = SectionCreate(areaID, name)
if err != nil {
2024-05-05 10:10:04 +02:00
httpError(w, werr.Wrap(err).Log())
return
}
w.Header().Add("Location", "/configuration")
w.WriteHeader(302)
return
} // }}}
func sectionRename(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
idStr := r.PathValue("id")
id, err := strconv.Atoi(idStr)
if err != nil {
2024-05-05 10:10:04 +02:00
httpError(w, werr.Wrap(err).Log())
return
}
name := r.PathValue("name")
err = SectionRename(id, name)
if err != nil {
2024-05-05 10:10:04 +02:00
httpError(w, werr.Wrap(err).Log())
return
}
w.Header().Add("Location", "/configuration")
w.WriteHeader(302)
return
} // }}}
2024-04-29 08:36:13 +02:00
func pageProblems(w http.ResponseWriter, _ *http.Request, _ *session.T) { // {{{
page := Page{
LAYOUT: "main",
PAGE: "problems",
}
2024-05-01 20:01:43 +02:00
problems, err := ProblemsRetrieve()
if err != nil {
2024-05-05 10:10:04 +02:00
httpError(w, werr.Wrap(err).Log())
2024-05-01 20:01:43 +02:00
return
}
2024-04-29 08:36:13 +02:00
page.Data = map[string]any{
2024-05-01 20:01:43 +02:00
"Problems": problems,
2024-04-29 08:36:13 +02:00
}
page.Render(w)
return
} // }}}
2024-05-01 20:01:43 +02:00
func pageProblemAcknowledge(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
idStr := r.PathValue("id")
id, err := strconv.Atoi(idStr)
if err != nil {
2024-05-05 10:10:04 +02:00
httpError(w, werr.Wrap(err).Log())
2024-05-01 20:01:43 +02:00
return
}
err = ProblemAcknowledge(id, true)
if err != nil {
2024-05-05 10:10:04 +02:00
httpError(w, werr.Wrap(err).Log())
2024-05-01 20:01:43 +02:00
return
}
w.Header().Add("Location", "/problems")
w.WriteHeader(302)
return
} // }}}
func pageProblemUnacknowledge(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
idStr := r.PathValue("id")
id, err := strconv.Atoi(idStr)
if err != nil {
2024-05-05 10:10:04 +02:00
httpError(w, werr.Wrap(err).Log())
2024-05-01 20:01:43 +02:00
return
}
err = ProblemAcknowledge(id, false)
if err != nil {
2024-05-05 10:10:04 +02:00
httpError(w, werr.Wrap(err).Log())
2024-05-01 20:01:43 +02:00
return
}
w.Header().Add("Location", "/problems")
w.WriteHeader(302)
return
} // }}}
2024-05-01 10:02:33 +02:00
func pageDatapoints(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
2024-04-30 08:04:16 +02:00
page := Page{
LAYOUT: "main",
PAGE: "datapoints",
}
datapoints, err := DatapointsRetrieve()
if err != nil {
2024-05-05 10:10:04 +02:00
httpError(w, werr.Wrap(err).Log())
2024-04-30 08:04:16 +02:00
return
}
2024-05-01 10:02:33 +02:00
// The datapoint selector in trigger edit wants the raw data in JSON.
if r.URL.Query().Get("format") == "json" {
j, _ := json.Marshal(datapoints)
w.Write(j)
return
}
2024-04-30 08:04:16 +02:00
page.Data = map[string]any{
"Datapoints": datapoints,
}
page.Render(w)
return
} // }}}
func pageDatapointEdit(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
idStr := r.PathValue("id")
id, err := strconv.Atoi(idStr)
if err != nil {
2024-05-05 10:10:04 +02:00
httpError(w, werr.Wrap(err).Log())
2024-04-30 08:04:16 +02:00
return
}
var datapoint Datapoint
if id == 0 {
datapoint.Name = "new_datapoint"
datapoint.Datatype = "INT"
} else {
datapoint, err = DatapointRetrieve(id, "")
if err != nil {
2024-05-05 10:10:04 +02:00
httpError(w, werr.Wrap(err).Log())
2024-04-30 08:04:16 +02:00
return
}
}
page := Page{
LAYOUT: "main",
PAGE: "datapoint_edit",
MENU: "datapoints",
Icon: "datapoints",
Label: "Datapoint",
}
page.Data = map[string]any{
"Datapoint": datapoint,
}
page.Render(w)
return
} // }}}
func pageDatapointUpdate(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
idStr := r.PathValue("id")
id, err := strconv.Atoi(idStr)
if err != nil {
2024-05-05 10:10:04 +02:00
httpError(w, werr.Wrap(err).Log())
2024-04-30 08:04:16 +02:00
return
}
var dp Datapoint
dp.ID = id
dp.Name = r.FormValue("name")
dp.Datatype = DatapointType(r.FormValue("datatype"))
err = dp.Update()
if err != nil {
2024-05-05 10:10:04 +02:00
httpError(w, werr.Wrap(err).Log())
2024-04-30 08:04:16 +02:00
return
}
w.Header().Add("Location", "/datapoints")
w.WriteHeader(302)
} // }}}
2024-05-02 08:59:55 +02:00
func pageDatapointDelete(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
idStr := r.PathValue("id")
id, err := strconv.Atoi(idStr)
if err != nil {
2024-05-05 10:10:04 +02:00
httpError(w, werr.Wrap(err).Log())
2024-05-02 08:59:55 +02:00
return
}
err = DatapointDelete(id)
if err != nil {
2024-05-05 10:10:04 +02:00
httpError(w, werr.Wrap(err).Log())
2024-05-02 08:59:55 +02:00
return
}
w.Header().Add("Location", "/datapoints")
w.WriteHeader(302)
} // }}}
2024-05-01 10:02:33 +02:00
2024-04-29 08:36:13 +02:00
func pageTriggers(w http.ResponseWriter, _ *http.Request, _ *session.T) { // {{{
areas, err := TriggersRetrieve()
if err != nil {
2024-05-05 10:10:04 +02:00
httpError(w, werr.Wrap(err).Log())
2024-04-29 08:36:13 +02:00
return
}
sort.SliceStable(areas, func(i, j int) bool {
return areas[i].Name < areas[j].Name
})
page := Page{
LAYOUT: "main",
PAGE: "triggers",
Data: map[string]any{
"Areas": areas,
},
}
page.Render(w)
} // }}}
func pageTriggerEdit(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
idStr := r.PathValue("id")
id, err := strconv.Atoi(idStr)
if err != nil {
2024-05-05 10:10:04 +02:00
httpError(w, werr.Wrap(err).Log())
2024-04-29 08:36:13 +02:00
return
}
2024-05-01 10:02:33 +02:00
// Creating a new trigger uses the edit function.
// ID == 0 - create a new trigger.
// ID > 0 - edit existing trigger.
2024-04-29 08:36:13 +02:00
var trigger Trigger
2024-05-01 10:02:33 +02:00
if id > 0 {
trigger, err = TriggerRetrieve(id)
if err != nil {
2024-05-05 10:10:04 +02:00
httpError(w, werr.Wrap(err).Log())
2024-05-01 10:02:33 +02:00
return
}
} else {
// A new trigger needs to know which section it belongs to.
sectionIDStr := r.PathValue("sectionID")
if sectionIDStr != "" {
trigger.SectionID, err = strconv.Atoi(sectionIDStr)
if err != nil {
2024-05-05 10:10:04 +02:00
httpError(w, werr.Wrap(err).Log())
2024-05-01 10:02:33 +02:00
return
}
}
2024-04-29 08:36:13 +02:00
}
datapoints := make(map[string]Datapoint)
for _, dpname := range trigger.Datapoints {
2024-04-30 08:04:16 +02:00
dp, err := DatapointRetrieve(0, dpname)
2024-04-29 08:36:13 +02:00
if err != nil {
2024-05-05 10:10:04 +02:00
httpError(w, werr.Wrap(err).Log())
2024-04-29 08:36:13 +02:00
return
}
2024-05-01 10:02:33 +02:00
dp.LastDatapointValue.TemplateValue = dp.LastDatapointValue.Value()
2024-04-29 08:36:13 +02:00
datapoints[dpname] = dp
}
page := Page{
LAYOUT: "main",
PAGE: "trigger_edit",
MENU: "triggers",
Label: "Trigger",
Icon: "triggers",
Data: map[string]any{
"Trigger": trigger,
"Datapoints": datapoints,
},
}
page.Render(w)
} // }}}
func pageTriggerUpdate(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
idStr := r.PathValue("id")
id, err := strconv.Atoi(idStr)
if err != nil {
2024-05-05 10:10:04 +02:00
httpError(w, werr.Wrap(err).Log())
2024-04-29 08:36:13 +02:00
return
}
var trigger Trigger
2024-05-01 10:02:33 +02:00
if id > 0 {
trigger, err = TriggerRetrieve(id)
if err != nil {
2024-05-05 10:10:04 +02:00
httpError(w, werr.Wrap(err).Log())
2024-05-01 10:02:33 +02:00
return
}
} else {
trigger.SectionID, err = strconv.Atoi(r.FormValue("sectionID"))
if err != nil {
2024-05-05 10:10:04 +02:00
httpError(w, werr.Wrap(err).Log())
2024-05-01 10:02:33 +02:00
return
}
2024-04-29 08:36:13 +02:00
}
trigger.Name = r.FormValue("name")
trigger.Expression = r.FormValue("expression")
2024-05-01 10:02:33 +02:00
trigger.Datapoints = r.Form["datapoints[]"]
2024-04-29 08:36:13 +02:00
err = trigger.Update()
if err != nil {
2024-05-05 10:10:04 +02:00
httpError(w, werr.Wrap(err).Log())
2024-04-29 08:36:13 +02:00
return
}
2024-04-30 08:04:16 +02:00
w.Header().Add("Location", "/triggers")
2024-04-29 08:36:13 +02:00
w.WriteHeader(302)
} // }}}
func pageTriggerRun(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
idStr := r.PathValue("id")
id, err := strconv.Atoi(idStr)
if err != nil {
2024-05-05 10:10:04 +02:00
httpError(w, werr.Wrap(err).Log())
2024-04-29 08:36:13 +02:00
return
}
var trigger Trigger
trigger, err = TriggerRetrieve(id)
if err != nil {
2024-05-05 10:10:04 +02:00
httpError(w, werr.Wrap(err).Log())
2024-04-29 08:36:13 +02:00
return
}
expr, _ := io.ReadAll(r.Body)
trigger.Expression = string(expr)
resp := struct {
OK bool
Output any
}{
OK: true,
}
2024-04-30 20:10:05 +02:00
resp.Output, err = trigger.Run()
2024-04-29 08:36:13 +02:00
if err != nil {
2024-05-05 10:10:04 +02:00
werr.Wrap(err).Log()
2024-04-29 08:36:13 +02:00
httpError(w, err)
return
}
j, _ := json.Marshal(resp)
w.Header().Add("Content-Type", "application/json")
w.Write(j)
} // }}}
2024-05-01 10:02:33 +02:00
2024-04-29 08:36:13 +02:00
func pageConfiguration(w http.ResponseWriter, _ *http.Request, _ *session.T) { // {{{
areas, err := AreaRetrieve()
if err != nil {
2024-05-05 10:10:04 +02:00
httpError(w, werr.Wrap(err).Log())
2024-04-29 08:36:13 +02:00
return
}
sort.SliceStable(areas, func(i, j int) bool {
return areas[i].Name < areas[j].Name
})
page := Page{
LAYOUT: "main",
PAGE: "configuration",
Data: map[string]any{
"Areas": areas,
},
}
page.Render(w)
} // }}}