From fbb5d48bf7c420b34887fe4379646e60216fcb1f Mon Sep 17 00:00:00 2001 From: TwinProduction Date: Thu, 28 Jan 2021 22:44:31 -0500 Subject: [PATCH] Add events to service detail page --- controller/controller.go | 11 ++++- core/event.go | 26 ++++++++++ core/service-status.go | 35 +++++++++++++- storage/memory_test.go | 2 +- web/app/src/App.vue | 10 ++-- web/app/src/components/Service.vue | 17 ++----- web/app/src/mixins/helper.js | 16 +++++++ web/app/src/views/Details.vue | 77 ++++++++++++++++++++++++++---- web/app/vue.config.js | 4 ++ 9 files changed, 166 insertions(+), 32 deletions(-) create mode 100644 core/event.go create mode 100644 web/app/src/mixins/helper.js create mode 100644 web/app/vue.config.js diff --git a/controller/controller.go b/controller/controller.go index 7ca9973c..f04cfa4b 100644 --- a/controller/controller.go +++ b/controller/controller.go @@ -124,7 +124,14 @@ func serviceStatusHandler(writer http.ResponseWriter, r *http.Request) { _, _ = writer.Write([]byte("not found")) return } - data, err := json.Marshal(serviceStatus) + data := map[string]interface{}{ + "serviceStatus": serviceStatus, + // This is my lazy way of exposing events even though they're not visible from the json annotation + // present in ServiceStatus. We do this because creating a separate object for each endpoints + // would be wasteful (one with and one without Events) + "events": serviceStatus.Events, + } + output, err := json.Marshal(data) if err != nil { log.Printf("[controller][serviceStatusHandler] Unable to marshal object to JSON: %s", err.Error()) writer.WriteHeader(http.StatusInternalServerError) @@ -133,7 +140,7 @@ func serviceStatusHandler(writer http.ResponseWriter, r *http.Request) { } writer.Header().Add("Content-Type", "application/json") writer.WriteHeader(http.StatusOK) - _, _ = writer.Write(data) + _, _ = writer.Write(output) } func healthHandler(writer http.ResponseWriter, _ *http.Request) { diff --git a/core/event.go b/core/event.go new file mode 100644 index 00000000..2414e958 --- /dev/null +++ b/core/event.go @@ -0,0 +1,26 @@ +package core + +import "time" + +// Event is something that happens at a specific time +type Event struct { + // Type is the kind of event + Type EventType `json:"type"` + + // Timestamp is the moment at which the event happened + Timestamp time.Time `json:"timestamp"` +} + +// EventType is, uh, the types of events? +type EventType string + +var ( + // EventStart is a type of event that represents when a service starts being monitored + EventStart EventType = "START" + + // EventHealthy is a type of event that represents a service passing all of its conditions + EventHealthy EventType = "HEALTHY" + + // EventUnhealthy is a type of event that represents a service failing one or more of its conditions + EventUnhealthy EventType = "UNHEALTHY" +) diff --git a/core/service-status.go b/core/service-status.go index 4d6be667..d6ec34f1 100644 --- a/core/service-status.go +++ b/core/service-status.go @@ -1,6 +1,10 @@ package core -import "github.com/TwinProduction/gatus/util" +import ( + "time" + + "github.com/TwinProduction/gatus/util" +) // ServiceStatus contains the evaluation Results of a Service type ServiceStatus struct { @@ -16,6 +20,13 @@ type ServiceStatus struct { // Results is the list of service evaluation results Results []*Result `json:"results"` + // Events is a list of events + // + // We don't expose this through JSON, because the main dashboard doesn't need to have these events. + // However, the detailed service page does leverage this by including it to a map that will be + // marshalled alongside the ServiceStatus. + Events []*Event `json:"-"` + // Uptime information on the service's uptime Uptime *Uptime `json:"uptime"` } @@ -27,13 +38,33 @@ func NewServiceStatus(service *Service) *ServiceStatus { Group: service.Group, Key: util.ConvertGroupAndServiceToKey(service.Group, service.Name), Results: make([]*Result, 0), - Uptime: NewUptime(), + Events: []*Event{{ + Type: EventStart, + Timestamp: time.Now(), + }}, + Uptime: NewUptime(), } } // AddResult adds a Result to ServiceStatus.Results and makes sure that there are // no more than 20 results in the Results slice func (ss *ServiceStatus) AddResult(result *Result) { + if len(ss.Results) > 0 { + // Check if there's any change since the last result + // OR there's only 1 event, which only happens when there's a start event + if ss.Results[len(ss.Results)-1].Success != result.Success || len(ss.Events) == 1 { + event := &Event{Timestamp: result.Timestamp} + if result.Success { + event.Type = EventHealthy + } else { + event.Type = EventUnhealthy + } + ss.Events = append(ss.Events, event) + if len(ss.Events) > 20 { + ss.Events = ss.Events[1:] + } + } + } ss.Results = append(ss.Results, result) if len(ss.Results) > 20 { ss.Results = ss.Results[1:] diff --git a/storage/memory_test.go b/storage/memory_test.go index 1701a0b6..e1f23230 100644 --- a/storage/memory_test.go +++ b/storage/memory_test.go @@ -217,7 +217,7 @@ func TestInMemoryStore_GetAllAsJSON(t *testing.T) { if err != nil { t.Fatal("shouldn't have returned an error, got", err.Error()) } - expectedOutput := `{"group_name":{"name":"name","group":"group","results":[{"status":200,"hostname":"example.org","duration":150000000,"errors":null,"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":true},{"condition":"[CERTIFICATE_EXPIRATION] \u003c 72h","success":true}],"success":true,"timestamp":"0001-01-01T00:00:00Z"},{"status":200,"hostname":"example.org","duration":750000000,"errors":["error-1","error-2"],"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":false},{"condition":"[CERTIFICATE_EXPIRATION] \u003c 72h","success":false}],"success":false,"timestamp":"0001-01-01T00:00:00Z"}],"uptime":{"7d":0.5,"24h":0.5,"1h":0.5}}}` + expectedOutput := `{"group_name":{"name":"name","group":"group","key":"group_name","results":[{"status":200,"hostname":"example.org","duration":150000000,"errors":null,"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":true},{"condition":"[CERTIFICATE_EXPIRATION] \u003c 72h","success":true}],"success":true,"timestamp":"0001-01-01T00:00:00Z"},{"status":200,"hostname":"example.org","duration":750000000,"errors":["error-1","error-2"],"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":false},{"condition":"[CERTIFICATE_EXPIRATION] \u003c 72h","success":false}],"success":false,"timestamp":"0001-01-01T00:00:00Z"}],"uptime":{"7d":0.5,"24h":0.5,"1h":0.5}}}` if string(output) != expectedOutput { t.Errorf("expected:\n %s\n\ngot:\n %s", expectedOutput, string(output)) } diff --git a/web/app/src/App.vue b/web/app/src/App.vue index 962aa43d..31885b7f 100644 --- a/web/app/src/App.vue +++ b/web/app/src/App.vue @@ -2,11 +2,11 @@
-
+
Health Status
-
- Gatus +
+ Gatus
@@ -42,9 +42,11 @@ export default {