Add Matrix notification
This commit is contained in:
parent
ed440f582f
commit
2b7ba22bfa
5 changed files with 140 additions and 29 deletions
|
@ -16,12 +16,6 @@ type Config struct {
|
||||||
Matrix MatrixConfig
|
Matrix MatrixConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
type MatrixConfig struct {
|
|
||||||
UserId string
|
|
||||||
AccessToken string
|
|
||||||
RoomId string
|
|
||||||
}
|
|
||||||
|
|
||||||
func LoadConfigFromTOML(configPath string) (*Config, error) {
|
func LoadConfigFromTOML(configPath string) (*Config, error) {
|
||||||
file, err := os.Open(configPath)
|
file, err := os.Open(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -14,13 +14,13 @@ import (
|
||||||
|
|
||||||
"github.com/oklog/ulid/v2"
|
"github.com/oklog/ulid/v2"
|
||||||
|
|
||||||
"git.exozy.me/exozyme/status/scanner"
|
. "git.exozy.me/exozyme/status/scanner"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Row struct {
|
type Row struct {
|
||||||
time time.Time
|
time time.Time
|
||||||
kind EventType
|
kind EventType
|
||||||
data scanner.ServiceStatus
|
data ServiceStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
type EventType int
|
type EventType int
|
||||||
|
@ -48,7 +48,8 @@ type Database interface {
|
||||||
QueryAllAfter(cutoff time.Time) ([]Row, error)
|
QueryAllAfter(cutoff time.Time) ([]Row, error)
|
||||||
|
|
||||||
// store service status records with timestamp
|
// store service status records with timestamp
|
||||||
StoreServiceStatusBatch(entries scanner.ServiceStatusBatch, when time.Time) error
|
// returns non-duplicate rows
|
||||||
|
StoreServiceStatusBatch(entries ServiceStatusBatch, when time.Time) ([]ServiceStatus, error)
|
||||||
|
|
||||||
// store self up/down event with timestamp
|
// store self up/down event with timestamp
|
||||||
//
|
//
|
||||||
|
@ -59,10 +60,10 @@ type Database interface {
|
||||||
type Span struct {
|
type Span struct {
|
||||||
start time.Time
|
start time.Time
|
||||||
stop time.Time
|
stop time.Time
|
||||||
status scanner.ServiceStatus
|
status ServiceStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
func append_close_span(res *[]Span, rows []Row, start_i, stop_i int, status scanner.ServiceStatus) {
|
func append_close_span(res *[]Span, rows []Row, start_i, stop_i int, status ServiceStatus) {
|
||||||
var stop_time time.Time
|
var stop_time time.Time
|
||||||
if stop_i < 0 {
|
if stop_i < 0 {
|
||||||
stop_time = time.Now()
|
stop_time = time.Now()
|
||||||
|
@ -78,7 +79,7 @@ func append_close_span(res *[]Span, rows []Row, start_i, stop_i int, status scan
|
||||||
func CalculateServiceStatusSpans(rows []Row, service_url string) []Span {
|
func CalculateServiceStatusSpans(rows []Row, service_url string) []Span {
|
||||||
res := []Span{}
|
res := []Span{}
|
||||||
start_i := -1
|
start_i := -1
|
||||||
var status scanner.ServiceStatus
|
var status ServiceStatus
|
||||||
|
|
||||||
for i, v := range rows {
|
for i, v := range rows {
|
||||||
switch v.kind {
|
switch v.kind {
|
||||||
|
@ -210,7 +211,9 @@ func (db *dumbDatabase) QueryAllAfter(cutoff time.Time) ([]Row, error) {
|
||||||
return db.rows[start:], nil
|
return db.rows[start:], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *dumbDatabase) StoreServiceStatusBatch(entries scanner.ServiceStatusBatch, when time.Time) error {
|
func (db *dumbDatabase) StoreServiceStatusBatch(entries ServiceStatusBatch, when time.Time) ([]ServiceStatus, error) {
|
||||||
|
non_duplicates := []ServiceStatus{}
|
||||||
|
|
||||||
db.mutex.Lock()
|
db.mutex.Lock()
|
||||||
defer db.mutex.Unlock()
|
defer db.mutex.Unlock()
|
||||||
|
|
||||||
|
@ -235,7 +238,7 @@ func (db *dumbDatabase) StoreServiceStatusBatch(entries scanner.ServiceStatusBat
|
||||||
db.rows = append(db.rows, new_row)
|
db.rows = append(db.rows, new_row)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return non_duplicates, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const max_rows = 10000
|
const max_rows = 10000
|
||||||
|
@ -247,7 +250,7 @@ func (db *dumbDatabase) StoreSelfStatus(status EventType, when time.Time) error
|
||||||
if status != EventSelfUp && status != EventSelfDown {
|
if status != EventSelfUp && status != EventSelfDown {
|
||||||
log.Panicf("(StoreSelfStatus) invalid value: %v", status)
|
log.Panicf("(StoreSelfStatus) invalid value: %v", status)
|
||||||
}
|
}
|
||||||
var placeholder scanner.ServiceStatus
|
var placeholder ServiceStatus
|
||||||
new_row := Row{when, EventType(status), placeholder}
|
new_row := Row{when, EventType(status), placeholder}
|
||||||
db.rows = append(db.rows, new_row)
|
db.rows = append(db.rows, new_row)
|
||||||
if len(db.rows) > max_rows {
|
if len(db.rows) > max_rows {
|
||||||
|
|
61
core/matrix.go
Normal file
61
core/matrix.go
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
m "maunium.net/go/mautrix"
|
||||||
|
. "maunium.net/go/mautrix/id"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MatrixConfig struct {
|
||||||
|
UserId string
|
||||||
|
// The AccessToken can be got by logging in with any Matrix client.
|
||||||
|
// In element.io, find it under Settings -> About & Help -> Advanced.
|
||||||
|
AccessToken string
|
||||||
|
RoomId string
|
||||||
|
}
|
||||||
|
|
||||||
|
type MatrixClient struct {
|
||||||
|
inner m.Client
|
||||||
|
config MatrixConfig
|
||||||
|
// Resolved/opaque Room Id
|
||||||
|
room_id RoomID
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMatrixClient(config MatrixConfig) (*MatrixClient, error) {
|
||||||
|
if config.AccessToken == "" {
|
||||||
|
return nil, fmt.Errorf("no access token")
|
||||||
|
}
|
||||||
|
|
||||||
|
user_id := UserID(config.UserId)
|
||||||
|
client, err := m.NewClient(user_id.Homeserver(), user_id, config.AccessToken)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var opaque_room_id RoomID
|
||||||
|
if strings.HasPrefix(config.RoomId, "!") {
|
||||||
|
opaque_room_id = RoomID(config.RoomId)
|
||||||
|
} else {
|
||||||
|
resp, err := client.ResolveAlias(RoomAlias(config.RoomId))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
opaque_room_id = resp.RoomID
|
||||||
|
}
|
||||||
|
|
||||||
|
return &MatrixClient{
|
||||||
|
*client,
|
||||||
|
config,
|
||||||
|
opaque_room_id,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *MatrixClient) SendNotice(message string) (*m.RespSendEvent, error) {
|
||||||
|
return client.inner.SendNotice(client.room_id, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *MatrixClient) RedactEvent(eventId EventID, reason string) (*m.RespSendEvent, error) {
|
||||||
|
return client.inner.RedactEvent(client.room_id, eventId, m.ReqRedact{Reason: reason})
|
||||||
|
}
|
66
main.go
66
main.go
|
@ -3,6 +3,8 @@ package main
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"embed"
|
"embed"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
|
@ -59,9 +61,18 @@ func rootPage(w http.ResponseWriter, r *http.Request) {
|
||||||
// get service status'
|
// get service status'
|
||||||
timeout := time.Second
|
timeout := time.Second
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
|
defer cancel()
|
||||||
data := scanner.CheckServiceBatch(ctx, config.Service)
|
data := scanner.CheckServiceBatch(ctx, config.Service)
|
||||||
dbm.StoreServiceStatusBatch(data, time.Now())
|
non_duplicates, err := dbm.StoreServiceStatusBatch(data, time.Now())
|
||||||
cancel()
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = notify(non_duplicates)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// don't uncomment this in production. verbose
|
// don't uncomment this in production. verbose
|
||||||
// log.Printf("%v", data)
|
// log.Printf("%v", data)
|
||||||
|
@ -69,20 +80,15 @@ func rootPage(w http.ResponseWriter, r *http.Request) {
|
||||||
parsedTemplate, err := parseAndRenderTemplate("templates/index.html", data)
|
parsedTemplate, err := parseAndRenderTemplate("templates/index.html", data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), 500)
|
http.Error(w, err.Error(), 500)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Write([]byte(parsedTemplate))
|
w.Write([]byte(parsedTemplate))
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeFile(path string) {
|
|
||||||
err := os.Remove(path)
|
|
||||||
if err != nil && !os.IsNotExist(err) {
|
|
||||||
log.Panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var config *core.Config
|
var config *core.Config
|
||||||
var dbm core.Database
|
var dbm core.Database
|
||||||
|
var client *core.MatrixClient
|
||||||
|
|
||||||
func cleanup() {
|
func cleanup() {
|
||||||
log.Println("Cleaning up")
|
log.Println("Cleaning up")
|
||||||
|
@ -102,6 +108,11 @@ func main() {
|
||||||
log.Panic(err)
|
log.Panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
client, err = core.NewMatrixClient(config.Matrix)
|
||||||
|
if err != nil {
|
||||||
|
log.Panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
http.HandleFunc("/", rootPage)
|
http.HandleFunc("/", rootPage)
|
||||||
|
|
||||||
// serve public folder from embedfs
|
// serve public folder from embedfs
|
||||||
|
@ -151,7 +162,7 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// log startup and register cleanup (on SIGTERM or panic)
|
// log startup and register cleanup (on SIGTERM or panic)
|
||||||
ch_sigterm := make(chan os.Signal)
|
ch_sigterm := make(chan os.Signal, 1)
|
||||||
os_signal.Notify(ch_sigterm, syscall.SIGTERM, syscall.SIGINT)
|
os_signal.Notify(ch_sigterm, syscall.SIGTERM, syscall.SIGINT)
|
||||||
|
|
||||||
log.Print("Starting up")
|
log.Print("Starting up")
|
||||||
|
@ -173,7 +184,11 @@ func main() {
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
routineCheck(5 * time.Minute)
|
err := routineCheck(5 * time.Minute)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error in routineCheck: %v", err)
|
||||||
|
// idea: maybe should delay here
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
@ -185,12 +200,37 @@ func main() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func routineCheck(timeout time.Duration) {
|
func routineCheck(timeout time.Duration) error {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
data := scanner.CheckServiceBatch(ctx, config.Service)
|
data := scanner.CheckServiceBatch(ctx, config.Service)
|
||||||
dbm.StoreServiceStatusBatch(data, time.Now())
|
non_duplicates, err := dbm.StoreServiceStatusBatch(data, time.Now())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = notify(non_duplicates)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
// log.Printf("Routine check: %v", data)
|
// log.Printf("Routine check: %v", data)
|
||||||
<-ctx.Done()
|
<-ctx.Done()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func notify(updates []scanner.ServiceStatus) error {
|
||||||
|
final_err := error(nil)
|
||||||
|
for _, v := range updates {
|
||||||
|
statusString := "???"
|
||||||
|
if v.Ok {
|
||||||
|
statusString = "up"
|
||||||
|
} else {
|
||||||
|
statusString = "down"
|
||||||
|
|
||||||
|
}
|
||||||
|
_, err := client.SendNotice(fmt.Sprintf("%s is %s!", v.Name, statusString))
|
||||||
|
final_err = errors.Join(final_err, err)
|
||||||
|
}
|
||||||
|
return final_err
|
||||||
}
|
}
|
||||||
|
|
13
util.go
Normal file
13
util.go
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func removeFile(path string) {
|
||||||
|
err := os.Remove(path)
|
||||||
|
if err != nil && !os.IsNotExist(err) {
|
||||||
|
log.Panic(err)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue