Compare commits
3 commits
Author | SHA1 | Date | |
---|---|---|---|
|
e53554365b | ||
|
1052a35271 | ||
|
cb1a211ef4 |
6 changed files with 182 additions and 102 deletions
94
core/db_interface.go
Normal file
94
core/db_interface.go
Normal file
|
@ -0,0 +1,94 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
. "git.exozy.me/exozyme/status/scanner"
|
||||
)
|
||||
|
||||
// change implementation here
|
||||
func OpenDatabase(dir string) (Database, error) {
|
||||
return OpenPrototypeDb(dir)
|
||||
}
|
||||
|
||||
type Database interface {
|
||||
// dump data to disk
|
||||
Persist() error
|
||||
|
||||
// get all records after some time (`cutoff`)
|
||||
//
|
||||
// Usage: (get past 7 days of history)
|
||||
// db.QueryAllAfter(time.Now().Add(-7 * 24 * time.Hour))
|
||||
//
|
||||
// Then, parse the result with `GetServiceStatusSpans(rows, service_url)`
|
||||
QueryAllAfter(cutoff time.Time) ([]Row, error)
|
||||
|
||||
// store service status records with timestamp
|
||||
// returns non-duplicate rows
|
||||
StoreServiceStatusBatch(entries ServiceStatusBatch, when time.Time) ([]ServiceStatus, error)
|
||||
|
||||
// store self up/down event with timestamp
|
||||
//
|
||||
// `status` is EventSelfUp or EventSelfDown
|
||||
StoreSelfStatus(status EventType, when time.Time) error
|
||||
}
|
||||
|
||||
type Row struct {
|
||||
time time.Time
|
||||
kind EventType
|
||||
data ServiceStatus
|
||||
}
|
||||
|
||||
type EventType int
|
||||
|
||||
const (
|
||||
// represents when a servire's status text changes
|
||||
// start and stop events are represented with EventSelfUp and EventSelfDown
|
||||
EventService EventType = iota
|
||||
// represents when this program starts
|
||||
EventSelfUp
|
||||
// represents when this program stops
|
||||
EventSelfDown
|
||||
)
|
||||
|
||||
type Span struct {
|
||||
start time.Time
|
||||
stop time.Time
|
||||
status ServiceStatus
|
||||
}
|
||||
|
||||
func append_close_span(res *[]Span, rows []Row, start_i, stop_i int, status ServiceStatus) {
|
||||
var stop_time time.Time
|
||||
if stop_i < 0 {
|
||||
stop_time = time.Now()
|
||||
} else {
|
||||
stop_time = rows[stop_i].time
|
||||
}
|
||||
start_time := rows[start_i].time
|
||||
*res = append(*res, Span{start_time, stop_time, status})
|
||||
}
|
||||
|
||||
// Find when the service is up, and when the service is down
|
||||
// returns timespans. There might be gaps between two spans, meaning that we don't know the state of that service
|
||||
func CalculateServiceStatusSpans(rows []Row, service_url string) []Span {
|
||||
res := []Span{}
|
||||
start_i := -1
|
||||
var status ServiceStatus
|
||||
|
||||
for i, v := range rows {
|
||||
switch v.kind {
|
||||
case EventSelfDown:
|
||||
append_close_span(&res, rows, start_i, i, status)
|
||||
start_i = -1
|
||||
case EventService:
|
||||
if v.data.Url == service_url {
|
||||
append_close_span(&res, rows, start_i, i, status)
|
||||
start_i = i
|
||||
status = v.data
|
||||
}
|
||||
}
|
||||
}
|
||||
append_close_span(&res, rows, start_i, -1, status)
|
||||
|
||||
return res
|
||||
}
|
|
@ -13,98 +13,17 @@ import (
|
|||
|
||||
"github.com/oklog/ulid/v2"
|
||||
|
||||
. "git.exozy.me/exozyme/status/scanner"
|
||||
. "git.exozy.me/exozyme/status/scanner"
|
||||
)
|
||||
|
||||
type Row struct {
|
||||
time time.Time
|
||||
kind EventType
|
||||
data ServiceStatus
|
||||
}
|
||||
|
||||
type EventType int
|
||||
|
||||
const (
|
||||
// represents when a servire's status changes
|
||||
EventService EventType = iota
|
||||
// represents when this program starts
|
||||
EventSelfUp
|
||||
// represents when this program stops
|
||||
EventSelfDown
|
||||
)
|
||||
|
||||
type Database interface {
|
||||
// dump data to disk
|
||||
Persist() error
|
||||
|
||||
// get all records after some time (`cutoff`)
|
||||
//
|
||||
// Usage: (get past 7 days of history)
|
||||
// db.QueryAllAfter(time.Now().Add(-7 * 24 * time.Hour))
|
||||
//
|
||||
// Then, parse the result with `GetServiceStatusSpans(rows, service_url)`
|
||||
QueryAllAfter(cutoff time.Time) ([]Row, error)
|
||||
|
||||
// store service status records with timestamp
|
||||
// returns non-duplicate rows
|
||||
StoreServiceStatusBatch(entries ServiceStatusBatch, when time.Time) ([]ServiceStatus, error)
|
||||
|
||||
// store self up/down event with timestamp
|
||||
//
|
||||
// `status` is EventSelfUp or EventSelfDown
|
||||
StoreSelfStatus(status EventType, when time.Time) error
|
||||
}
|
||||
|
||||
type Span struct {
|
||||
start time.Time
|
||||
stop time.Time
|
||||
status ServiceStatus
|
||||
}
|
||||
|
||||
func append_close_span(res *[]Span, rows []Row, start_i, stop_i int, status ServiceStatus) {
|
||||
var stop_time time.Time
|
||||
if stop_i < 0 {
|
||||
stop_time = time.Now()
|
||||
} else {
|
||||
stop_time = rows[stop_i].time
|
||||
}
|
||||
start_time := rows[start_i].time
|
||||
*res = append(*res, Span{start_time, stop_time, status})
|
||||
}
|
||||
|
||||
// Find when the service is up, and when the service is down
|
||||
// returns timespans. There might be gaps between two spans, meaning that we don't know the state of that service
|
||||
func CalculateServiceStatusSpans(rows []Row, service_url string) []Span {
|
||||
res := []Span{}
|
||||
start_i := -1
|
||||
var status ServiceStatus
|
||||
|
||||
for i, v := range rows {
|
||||
switch v.kind {
|
||||
case EventSelfDown:
|
||||
append_close_span(&res, rows, start_i, i, status)
|
||||
start_i = -1
|
||||
case EventService:
|
||||
if v.data.Url == service_url {
|
||||
append_close_span(&res, rows, start_i, i, status)
|
||||
start_i = i
|
||||
status = v.data
|
||||
}
|
||||
}
|
||||
}
|
||||
append_close_span(&res, rows, start_i, -1, status)
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
type dumbDatabase struct {
|
||||
type PrototypeDb struct {
|
||||
rootDir string
|
||||
rows []Row
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
// Load the newest backup, or start anew
|
||||
func OpenDatabase(dir string) (Database, error) {
|
||||
func OpenPrototypeDb(dir string) (Database, error) {
|
||||
if dir == "" {
|
||||
return nil, fmt.Errorf("database path is empty. did you set it in config file?")
|
||||
}
|
||||
|
@ -140,13 +59,13 @@ func OpenDatabase(dir string) (Database, error) {
|
|||
}
|
||||
log.Printf("Loaded %d rows from: %v", len(rows), fullpath)
|
||||
|
||||
return &dumbDatabase{rootDir: dir, rows: rows}, nil
|
||||
return &PrototypeDb{rootDir: dir, rows: rows}, nil
|
||||
}
|
||||
|
||||
return &dumbDatabase{rootDir: dir, rows: []Row{}}, nil
|
||||
return &PrototypeDb{rootDir: dir, rows: []Row{}}, nil
|
||||
}
|
||||
|
||||
func (db *dumbDatabase) sortRows() {
|
||||
func (db *PrototypeDb) sortRows() {
|
||||
sort.Slice(db.rows, func(i, j int) bool {
|
||||
return db.rows[i].time.Compare(db.rows[j].time) == -1
|
||||
})
|
||||
|
@ -156,7 +75,7 @@ func (db *dumbDatabase) sortRows() {
|
|||
//
|
||||
// Steps:
|
||||
// create -> write -> fsync -> rename
|
||||
func (db *dumbDatabase) Persist() error {
|
||||
func (db *PrototypeDb) Persist() error {
|
||||
db.mutex.Lock()
|
||||
defer db.mutex.Unlock()
|
||||
|
||||
|
@ -214,7 +133,7 @@ func (db *dumbDatabase) Persist() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (db *dumbDatabase) QueryAllAfter(cutoff time.Time) ([]Row, error) {
|
||||
func (db *PrototypeDb) QueryAllAfter(cutoff time.Time) ([]Row, error) {
|
||||
db.mutex.Lock()
|
||||
defer db.mutex.Unlock()
|
||||
|
||||
|
@ -234,7 +153,7 @@ func (db *dumbDatabase) QueryAllAfter(cutoff time.Time) ([]Row, error) {
|
|||
return db.rows[start:], nil
|
||||
}
|
||||
|
||||
func (db *dumbDatabase) StoreServiceStatusBatch(entries ServiceStatusBatch, when time.Time) ([]ServiceStatus, error) {
|
||||
func (db *PrototypeDb) StoreServiceStatusBatch(entries ServiceStatusBatch, when time.Time) ([]ServiceStatus, error) {
|
||||
non_duplicates := []ServiceStatus{}
|
||||
|
||||
db.mutex.Lock()
|
||||
|
@ -263,7 +182,7 @@ func (db *dumbDatabase) StoreServiceStatusBatch(entries ServiceStatusBatch, when
|
|||
return non_duplicates, nil
|
||||
}
|
||||
|
||||
func (db *dumbDatabase) StoreSelfStatus(status EventType, when time.Time) error {
|
||||
func (db *PrototypeDb) StoreSelfStatus(status EventType, when time.Time) error {
|
||||
db.mutex.Lock()
|
||||
defer db.mutex.Unlock()
|
||||
|
53
core/db_tkrzw.go
Normal file
53
core/db_tkrzw.go
Normal file
|
@ -0,0 +1,53 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/oklog/ulid/v2"
|
||||
"github.com/estraier/tkrzw-go"
|
||||
|
||||
. "git.exozy.me/exozyme/status/scanner"
|
||||
)
|
||||
|
||||
type Tkrzw struct {
|
||||
*tkrzw.DBM
|
||||
}
|
||||
|
||||
func OpenTkrzw(dir string) (Database, error) {
|
||||
dbm := tkrzw.NewDBM()
|
||||
status := dbm.Open(path.Join(dir, "casket.tks"), true, map[string]string{})
|
||||
if !status.IsOK() {
|
||||
return nil, status
|
||||
}
|
||||
return &Tkrzw{DBM: dbm}, nil
|
||||
// dbm.Rebuild()
|
||||
}
|
||||
|
||||
func (dbm *Tkrzw) Persist() error {
|
||||
status := dbm.Synchronize(true, map[string]string{})
|
||||
if !status.IsOK() {
|
||||
return status
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dbm *Tkrzw) QueryAllAfter(cutoff time.Time) ([]Row, error) {
|
||||
panic("not implemented") // TODO: Implement
|
||||
}
|
||||
|
||||
func (dbm *Tkrzw) StoreServiceStatusBatch(entries ServiceStatusBatch, when time.Time) ([]ServiceStatus, error) {
|
||||
panic("not implemented") // TODO: Implement
|
||||
}
|
||||
|
||||
func (dbm *Tkrzw) StoreSelfStatus(status EventType, when time.Time) error {
|
||||
panic("not implemented") // TODO: Implement
|
||||
}
|
||||
|
2
go.mod
2
go.mod
|
@ -4,6 +4,7 @@ go 1.21.1
|
|||
|
||||
require (
|
||||
github.com/cbroglie/mustache v1.4.0
|
||||
github.com/estraier/tkrzw-go v0.0.0-20240603165139-89707b45d5ab
|
||||
github.com/oklog/ulid/v2 v2.1.0
|
||||
github.com/pelletier/go-toml/v2 v2.1.0
|
||||
github.com/prometheus-community/pro-bing v0.3.0
|
||||
|
@ -11,7 +12,6 @@ require (
|
|||
)
|
||||
|
||||
require (
|
||||
github.com/elnormous/contenttype v1.0.4
|
||||
github.com/google/uuid v1.3.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
|
|
4
go.sum
4
go.sum
|
@ -4,8 +4,8 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV
|
|||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/elnormous/contenttype v1.0.4 h1:FjmVNkvQOGqSX70yvocph7keC8DtmJaLzTTq6ZOQCI8=
|
||||
github.com/elnormous/contenttype v1.0.4/go.mod h1:5KTOW8m1kdX1dLMiUJeN9szzR2xkngiv2K+RVZwWBbI=
|
||||
github.com/estraier/tkrzw-go v0.0.0-20240603165139-89707b45d5ab h1:yQbaZGVW+i4XGqOCmngDuuusApVBBqIDPaLMI6JOyXU=
|
||||
github.com/estraier/tkrzw-go v0.0.0-20240603165139-89707b45d5ab/go.mod h1:vUOsQ39pa/SuhSmt/Nj+FiNFEHhGf58xM61e0EfZQcc=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
|
||||
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
|
|
30
main.go
30
main.go
|
@ -87,7 +87,12 @@ func index(w http.ResponseWriter, r *http.Request) {
|
|||
if (!accept_html || prefer_text) && accept_text {
|
||||
indexForVT100(w, r, data)
|
||||
} else if accept_html {
|
||||
indexForBrowser(w, r, data, config.SiteName, config.IconSrc)
|
||||
historical_data, err := dbm.QueryAllAfter(time.Now().Add(-7 * 24 * time.Hour))
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
indexForBrowserWithHistoricalData(w, r, historical_data, config.SiteName, config.IconSrc)
|
||||
} else { // when no valid content type is requested
|
||||
// not to-spec. still sends plain text anyway
|
||||
indexForVT100(w, r, data)
|
||||
|
@ -100,14 +105,23 @@ func indexForVT100(w http.ResponseWriter, _ *http.Request, data scanner.ServiceS
|
|||
w.Write([]byte(parsedTemplate))
|
||||
}
|
||||
|
||||
func indexForBrowser(w http.ResponseWriter, _ *http.Request, data scanner.ServiceStatusBatch, siteName, iconSrc string) {
|
||||
parsedTemplate, err := parseAndRenderTemplate("templates/index.html", siteName, iconSrc, data)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
// func indexForBrowser(w http.ResponseWriter, _ *http.Request, data scanner.ServiceStatusBatch, siteName string, iconSrc string) {
|
||||
// parsedTemplate, err := parseAndRenderTemplate("templates/index.html", siteName, iconSrc, data)
|
||||
// if err != nil {
|
||||
// http.Error(w, err.Error(), 500)
|
||||
// return
|
||||
// }
|
||||
|
||||
w.Write([]byte(parsedTemplate))
|
||||
// w.Write([]byte(parsedTemplate))
|
||||
// }
|
||||
|
||||
func indexForBrowserWithHistoricalData(w http.ResponseWriter, _ *http.Request, historical_data []core.Row, siteName string, iconSrc string) {
|
||||
// the basic idea here:
|
||||
// - create new template and render it with historical_data
|
||||
//
|
||||
// reference: see `indexForBrowser` above
|
||||
|
||||
panic("todo")
|
||||
}
|
||||
|
||||
var config *core.Config
|
||||
|
|
Loading…
Reference in a new issue