sermoni

"Service monitor" / cronjob status service
Log | Files | Refs

commit d9bb3fc26ddd7fb8213c1decfea5c36e45cbd5b7
parent 2a651d2c6909d8e3a5a735159459f5b54720eb92
Author: Vetle Haflan <vetle@haflan.dev>
Date:   Sat, 11 Apr 2020 02:18:53 +0200

Add events.go, tests++

Diffstat:
Mdatabase/database.go | 22++++++++++++++++++++--
Aevents/events.go | 150+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aevents/events_test.go | 38++++++++++++++++++++++++++++++++++++++
Mservices/services.go | 15++++++---------
Mservices/services_test.go | 6++++++
5 files changed, 220 insertions(+), 11 deletions(-)

diff --git a/database/database.go b/database/database.go @@ -3,6 +3,8 @@ package database import ( "errors" "fmt" + "log" + "strconv" "time" "golang.org/x/crypto/bcrypt" @@ -125,4 +127,21 @@ func Close() { // GetDB gets the database structure func GetDB() *bbolt.DB { return db -}- \ No newline at end of file +} + +// BytesToUint64 converts a byte array to a uint64 number, an operation that is +// often repeated for IDs. It is assumed that the data will parse successfully +// (i.e. type checking is performed in an earlier stage). +// If the parsing fails, the function therefore panics +func BytesToUint64(byteData []byte) uint64 { + uint64Data, err := strconv.ParseUint(string(byteData), 10, 64) + if err != nil { + log.Panic("couldn't parse byte data to uint64") + } + return uint64Data +} + +// Uint64ToBytes converts a uint64 formatted number to a byte array +func Uint64ToBytes(uint64Data uint64) []byte { + return []byte(strconv.FormatUint(uint64Data, 10)) +} diff --git a/events/events.go b/events/events.go @@ -0,0 +1,150 @@ +package events + +import ( + "errors" + "log" + "sermoni/database" + "sermoni/services" + "strconv" + + "go.etcd.io/bbolt" +) + +var ( + keyEventID = []byte("id") + keyEventService = []byte("service") + keyEventTimestamp = []byte("timestamp") + keyEventStatus = []byte("status") + keyEventTitle = []byte("title") + keyEventDetails = []byte("details") +) + +// Event contains data sent to sermoni from a service +type Event struct { + ID uint64 `json:"id"` + Service uint64 `json:"service"` // ID of a Service (to be mapped to service name client-side) + Timestamp uint64 `json:"timestamp"` + Status string `json:"status"` + Title string `json:"title"` + Details string `json:"details"` +} + +// GetAll returns all events in the database +func GetAll() (events []*Event) { + db := database.GetDB() + db.View(func(tx *bbolt.Tx) error { + b := tx.Bucket(database.BucketKeyEvents) + if b == nil { + log.Panic("The events bucket does not exist") + } + // ForEach doesn't return buckets (nil instead), so only the key is useful + return b.ForEach(func(id, _ []byte) error { + eb := b.Bucket(id) + event := &Event{} + if err := event.fromBucket(eb); err != nil { + return err + } + events = append(events, event) + return nil + }) + }) + return +} + +// Delete a service event +func Delete() { + +} + +// Add a new service event if the token matches any services in the database +// The event.Service will be set to the service ID automatically, given a valid token +func Add(serviceToken string, event *Event) error { + db := database.GetDB() + service := services.GetByToken(serviceToken) + if service == nil { + return errors.New("no service found for the given token") + } + return db.Update(func(tx *bbolt.Tx) error { + b := tx.Bucket(database.BucketKeyEvents) + if b == nil { + log.Panic("The events bucket does not exist") + } + + // Create a new event ID + idInt, err := b.NextSequence() + if err != nil { + return err + } + id := []byte(strconv.FormatUint(idInt, 10)) + event.ID = idInt + event.Service = service.ID + + // Create the event bucket and fill it with data from event + eb, err := b.CreateBucket(id) + if err != nil { + return err + } + return event.toBucket(eb) + }) +} + +// Writes the event data to the given bucket +func (event *Event) toBucket(eb *bbolt.Bucket) error { + var err error + id := database.Uint64ToBytes(event.ID) + service := database.Uint64ToBytes(event.Service) + if err = eb.Put(keyEventID, id); err != nil { + return err + } + if eb.Put(keyEventService, service); err != nil { + return err + } + if eb.Put(keyEventStatus, []byte(event.Status)); err != nil { + return err + } + if eb.Put(keyEventTitle, []byte(event.Title)); err != nil { + return err + } + if eb.Put(keyEventDetails, []byte(event.Details)); err != nil { + return err + } + return nil +} + +// Reads data from the given bucket into the fields of event +// Returns error if any of the fields cannot be found +func (event *Event) fromBucket(eb *bbolt.Bucket) error { + var id, service, timestamp []byte + var status, title, details []byte + err := errors.New("missing field from database") + + // Get data from database + if id := eb.Get(keyEventID); id == nil { + return err + } + if service = eb.Get(keyEventService); service == nil { + return err + } + if timestamp = eb.Get(keyEventTimestamp); timestamp == nil { + return err + } + if status = eb.Get(keyEventStatus); status == nil { + return err + } + if title = eb.Get(keyEventTitle); title == nil { + return err + } + if details = eb.Get(keyEventDetails); details == nil { + return err + } + + // Format data and set fields of event + event.ID = database.BytesToUint64(id) + event.Service = database.BytesToUint64(service) + event.Timestamp = database.BytesToUint64(timestamp) + event.Status = string(status) + event.Title = string(title) + event.Details = string(details) + + return nil +} diff --git a/events/events_test.go b/events/events_test.go @@ -0,0 +1,38 @@ +package events + +import ( + "os" + "sermoni/database" + "sermoni/services" + "testing" +) + +const serviceToken = "test-service" + +func TestAddEvent(t *testing.T) { + Add(serviceToken, &Event{ + Timestamp: 1586558825515, + Title: "Backup completed successfully", + }) +} + +func TestMain(m *testing.M) { + // (Re)create the test database + testDB := "test.db" + os.Remove(testDB) + var err error + if err = database.Init(testDB); err != nil { + print("Couldn't initialize test database") + os.Exit(1) + } + err = services.Add(serviceToken, &services.Service{ + Name: "test @ dev-laptop", + Description: "Service used for testing only", + }) + if err != nil { + print("Couldn't add test service") + os.Exit(1) + } + defer database.Close() + os.Exit(m.Run()) +} diff --git a/services/services.go b/services/services.go @@ -24,13 +24,12 @@ type Service struct { ExpectationPeriod uint64 `json:"period"` // set if the service is expected to report periodically, format is UnixTime (milli?) } - // GetByToken returns the service structure associated with the token string, if there // are any matching entries in service-tokens bucket. Returns nil if there are no matches func GetByToken(token string) *Service { id := getIDFromToken(token) if id == nil { - log.Printf("No service found for the token '%v'\n", token) + log.Printf("no service found for the token '%v'\n", token) return nil } return get(id) @@ -43,9 +42,8 @@ func GetByID(id uint64) *Service { } // GetAll returns all services in the database (TODO) -func GetAll() []*Service { +func GetAll() (services []*Service) { db := database.GetDB() - var services []*Service db.View(func(tx *bbolt.Tx) error { b := tx.Bucket(database.BucketKeyServices) stb := tx.Bucket(database.BucketKeyServiceTokens) @@ -66,7 +64,7 @@ func GetAll() []*Service { return nil }) }) - return services + return } // Delete deletes the given service if it exists @@ -86,7 +84,7 @@ func Delete(intID uint64) error { if b.Bucket(serviceID) == nil { return errors.New("no service for the given id") } - if err := b.DeleteBucket(serviceID); err != nil { + if err := b.DeleteBucket(serviceID); err != nil { return err } @@ -150,16 +148,15 @@ func Add(token string, service *Service) error { }) } - - // // Package-local helpers // // fromBucket populates the service struct with data from the given service bucket +// TODO: Consider failing on missing fields and generally choosing an approach more similar to Event.fromBucket func (service *Service) fromBucket(id []byte, sb *bbolt.Bucket) { // Ignoring this error, because it shouldn't be possible - idInt, _ := strconv.ParseUint(string(id), 10, 64) + idInt := database.BytesToUint64(id) service.ID = idInt if name := sb.Get(keyServiceName); name != nil { service.Name = string(name) diff --git a/services/services_test.go b/services/services_test.go @@ -99,6 +99,9 @@ func TestGetByID(t *testing.T) { if !service.equals(testService) { t.Fatal("stored service doesn't match original") } + if service = GetByID(23423); service != nil { + t.Fatal("returned service for invalid id") + } } func TestGetByToken(t *testing.T) { @@ -107,6 +110,9 @@ func TestGetByToken(t *testing.T) { if !service.equals(testService) { t.Fatal("stored service doesn't match original") } + if service = GetByToken("not-a-token"); service != nil { + t.Fatal("returned service for invalid token") + } } func TestGetAll(t *testing.T) {