Add liveness websocket notification and test ui

This commit is contained in:
sfigato 2023-02-16 23:29:31 +01:00
parent 60cb7a55c6
commit a9c604102b
Signed by: blallo
GPG Key ID: C530464EEDCF489A
6 changed files with 247 additions and 0 deletions

View File

@ -3,9 +3,11 @@ package main
import ( import (
"context" "context"
"flag" "flag"
"fmt"
"net/http" "net/http"
"os" "os"
"os/signal" "os/signal"
"strings"
"syscall" "syscall"
"time" "time"
@ -172,10 +174,35 @@ func setupHandler(radio *broadcast.Radio, logger logz.Logger, addr string) error
logger: logger, logger: logger,
} }
wsHandler := &livenessHandler{
radio: radio,
logger: logger,
}
testUI := &testUIHandler{
baseAddr: getBaseAddr(addr),
logger: logger,
}
router := chi.NewRouter() router := chi.NewRouter()
router.Post("/start", handler.Start) router.Post("/start", handler.Start)
router.Post("/stop", handler.Stop) router.Post("/stop", handler.Stop)
router.Get("/status", handler.Status) router.Get("/status", handler.Status)
router.Handle("/liveness", wsHandler)
router.Get("/test_ui", testUI.TestUI)
return http.ListenAndServe(addr, router) return http.ListenAndServe(addr, router)
} }
func getBaseAddr(addr string) string {
parts := strings.Split(addr, ":")
if parts[0] == "" {
parts[0] = "localhost"
}
if len(parts) == 2 {
return fmt.Sprintf("%s:%s", parts[0], parts[1])
}
return parts[0]
}

52
cmd/broadcast/test_ui.go Normal file
View File

@ -0,0 +1,52 @@
package main
import (
"html/template"
"net/http"
"git.sr.ht/~blallo/logz/interface"
)
var uiTmpl = `
<!DOCTYPE html>
<html>
<head>
<title>WebSocket test</title>
<script>
const socket = new WebSocket("ws://{{ .BaseAddr }}/liveness");
socket.onmessage = (event) => {
console.log(event.data);
};
</script>
</head>
<body>
Open the console...
</body>
</html>
`
var tmpl *template.Template
func init() {
tmpl = template.Must(template.New("testUI").Parse(uiTmpl))
}
type testUIHandler struct {
baseAddr string
logger logz.Logger
}
func (h *testUIHandler) TestUI(w http.ResponseWriter, r *http.Request) {
h.logger.Debug(map[string]any{
"msg": "Serving test ui",
"context": "http",
})
if err := tmpl.ExecuteTemplate(w, "testUI", map[string]string{"BaseAddr": h.baseAddr}); err != nil {
h.logger.Err(map[string]any{
"msg": "Failed to render testUI template",
"context": "http",
"err": err.Error(),
})
}
}

137
cmd/broadcast/ws_handler.go Normal file
View File

@ -0,0 +1,137 @@
package main
import (
"bytes"
"encoding/json"
"io"
"net/http"
"time"
"github.com/gorilla/websocket"
"git.abbiamoundominio.org/blallo/broadcast"
"git.sr.ht/~blallo/logz/interface"
)
const (
livenessInterval = 5 * time.Second
livenessTimeout = 4 * time.Second
statusStarted = "STARTED"
statusStopped = "STOPPED"
statusUnknown = "UNKNOWN"
)
var upgrader = websocket.Upgrader{}
type livenessHandler struct {
radio *broadcast.Radio
logger logz.Logger
}
func (h *livenessHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.logger.Info(map[string]any{
"msg": "Starting Liveness handler",
"context": "ws",
})
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
h.logger.Err(map[string]any{
"msg": "Failed to upgrade",
"context": "ws",
"err": err.Error(),
})
return
}
defer conn.Close()
ticker := time.NewTicker(livenessInterval)
for {
<-ticker.C
timer := time.NewTimer(livenessTimeout)
select {
case <-timer.C:
h.logger.Warn(map[string]any{
"msg": "Liveness request timed out",
"context": "ws",
})
h.writeMessage(conn, statusUnknown)
case err := <-h.radio.IsAlive():
switch err {
case nil:
h.logger.Debug(map[string]any{
"msg": "Still alive",
"context": "ws",
"status": statusStarted,
})
h.writeMessage(conn, statusStarted)
case broadcast.ErrNotRunning:
h.logger.Debug(map[string]any{
"msg": "Still alive",
"context": "ws",
"status": statusStopped,
})
h.writeMessage(conn, statusStopped)
default:
h.logger.Debug(map[string]any{
"msg": "Other error",
"context": "ws",
"err": err.(error).Error(),
"status": statusUnknown,
})
h.writeMessage(conn, statusUnknown)
}
}
timer.Stop()
}
}
func (h *livenessHandler) writeMessage(conn *websocket.Conn, status string) {
msg, err := json.Marshal(map[string]any{
"status": status,
})
if err != nil {
h.logger.Err(map[string]any{
"msg": "Failed to marshal",
"context": "ws",
"status": status,
"err": err.Error(),
})
return
}
w, err := conn.NextWriter(websocket.TextMessage)
if err != nil {
h.logger.Err(map[string]any{
"msg": "Failed to get next writer",
"context": "ws",
"status": status,
"err": err.Error(),
})
return
}
if _, err := io.Copy(w, bytes.NewReader(msg)); err != nil {
h.logger.Err(map[string]any{
"msg": "Failed to write message",
"context": "ws",
"status": status,
"err": err.Error(),
})
return
}
if err := w.Close(); err != nil {
h.logger.Err(map[string]any{
"msg": "Failed to close writer",
"context": "ws",
"status": status,
"err": err.Error(),
})
return
}
}

2
go.mod
View File

@ -13,9 +13,11 @@ require (
git.sr.ht/~blallo/logz/zlog v0.0.0-20220324191132-95d94ae8e337 // indirect git.sr.ht/~blallo/logz/zlog v0.0.0-20220324191132-95d94ae8e337 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-chi/chi/v5 v5.0.8 // indirect github.com/go-chi/chi/v5 v5.0.8 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rs/zerolog v1.26.1 // indirect github.com/rs/zerolog v1.26.1 // indirect
github.com/stretchr/objx v0.5.0 // indirect github.com/stretchr/objx v0.5.0 // indirect
github.com/stretchr/testify v1.8.1 // indirect github.com/stretchr/testify v1.8.1 // indirect
golang.org/x/net v0.6.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

4
go.sum
View File

@ -15,6 +15,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0= github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0=
github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@ -38,6 +40,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.6.0 h1:L4ZwwTvKW9gr0ZMS1yrHD9GZhIuVjOBBnaKH+SPQK0Q=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"os" "os"
"syscall"
"git.sr.ht/~blallo/logz/interface" "git.sr.ht/~blallo/logz/interface"
) )
@ -28,6 +29,7 @@ const (
CommandStart = 1 << iota CommandStart = 1 << iota
CommandStop CommandStop
CommandStatus CommandStatus
CommandLiveness
) )
type Radio struct { type Radio struct {
@ -90,6 +92,11 @@ func (r *Radio) Run(ctx context.Context) error {
"msg": "Received Status command", "msg": "Received Status command",
}) })
cmd.Resp <- r.printLogs() cmd.Resp <- r.printLogs()
case CommandLiveness:
r.logger.Info(map[string]any{
"msg": "Received Liveness command",
})
cmd.Resp <- r.liveness()
} }
} }
} }
@ -125,6 +132,16 @@ func (r *Radio) Status() <-chan any {
return resp return resp
} }
func (r *Radio) IsAlive() <-chan any {
resp := make(chan any)
r.commands <- Command{
CommandType: CommandLiveness,
Resp: resp,
}
return resp
}
func (r *Radio) start() error { func (r *Radio) start() error {
if r.proc != nil { if r.proc != nil {
return ErrAlreadyRunning return ErrAlreadyRunning
@ -197,3 +214,11 @@ func (r *Radio) stop() error {
func (r *Radio) printLogs() []string { func (r *Radio) printLogs() []string {
return r.lines.Lines() return r.lines.Lines()
} }
func (r *Radio) liveness() error {
if r.proc == nil {
return ErrNotRunning
}
return r.proc.Signal(syscall.Signal(0))
}