diff --git a/core/condition-result.go b/core/condition-result.go new file mode 100644 index 00000000..d8bdc1e9 --- /dev/null +++ b/core/condition-result.go @@ -0,0 +1,10 @@ +package core + +// ConditionResult result of a Condition +type ConditionResult struct { + // Condition that was evaluated + Condition string `json:"condition"` + + // Success whether the condition was met (successful) or not (failed) + Success bool `json:"success"` +} diff --git a/core/health-status.go b/core/health-status.go new file mode 100644 index 00000000..89e906eb --- /dev/null +++ b/core/health-status.go @@ -0,0 +1,11 @@ +package core + +// HealthStatus is the status of Gatus +type HealthStatus struct { + // Status is the state of Gatus (UP/DOWN) + Status string `json:"status"` + + // Message is an accompanying description of why the status is as reported. + // If the Status is UP, no message will be provided + Message string `json:"message,omitempty"` +} diff --git a/core/types.go b/core/result.go similarity index 64% rename from core/types.go rename to core/result.go index fabca387..5c538c64 100644 --- a/core/types.go +++ b/core/result.go @@ -4,22 +4,12 @@ import ( "time" ) -// HealthStatus is the status of Gatus -type HealthStatus struct { - // Status is the state of Gatus (UP/DOWN) - Status string `json:"status"` - - // Message is an accompanying description of why the status is as reported. - // If the Status is UP, no message will be provided - Message string `json:"message,omitempty"` -} - // Result of the evaluation of a Service type Result struct { // HTTPStatus is the HTTP response status code HTTPStatus int `json:"status"` - // DNSRCode is the response code of DNS query in human readable version + // DNSRCode is the response code of a DNS query in a human readable format DNSRCode string `json:"dns-rcode"` // Body is the response body @@ -52,12 +42,3 @@ type Result struct { // CertificateExpiration is the duration before the certificate expires CertificateExpiration time.Duration `json:"certificate-expiration,omitempty"` } - -// ConditionResult result of a Condition -type ConditionResult struct { - // Condition that was evaluated - Condition string `json:"condition"` - - // Success whether the condition was met (successful) or not (failed) - Success bool `json:"success"` -} diff --git a/core/service-status.go b/core/service-status.go new file mode 100644 index 00000000..92102dd7 --- /dev/null +++ b/core/service-status.go @@ -0,0 +1,27 @@ +package core + +// ServiceStatus contains the evaluation Results of a Service +type ServiceStatus struct { + // Group the service is a part of. Used for grouping multiple services together on the front end. + Group string `json:"group,omitempty"` + + // Results is the list of service evaluation results + Results []*Result `json:"results"` +} + +// NewServiceStatus creates a new ServiceStatus +func NewServiceStatus(service *Service) *ServiceStatus { + return &ServiceStatus{ + Group: service.Group, + Results: make([]*Result, 0), + } +} + +// 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) { + ss.Results = append(ss.Results, result) + if len(ss.Results) > 20 { + ss.Results = ss.Results[1:] + } +} diff --git a/core/service.go b/core/service.go index f735b2ca..928940cb 100644 --- a/core/service.go +++ b/core/service.go @@ -30,6 +30,9 @@ type Service struct { // Name of the service. Can be anything. Name string `yaml:"name"` + // Group the service is a part of. Used for grouping multiple services together on the front end. + Group string `yaml:"group,omitempty"` + // URL to send the request to URL string `yaml:"url"` diff --git a/main.go b/main.go index e3cfd931..e01a7ae4 100644 --- a/main.go +++ b/main.go @@ -18,19 +18,19 @@ import ( const cacheTTL = 10 * time.Second var ( - cachedServiceResults []byte - cachedServiceResultsGzipped []byte - cachedServiceResultsTimestamp time.Time + cachedServiceStatuses []byte + cachedServiceStatusesGzipped []byte + cachedServiceStatusesTimestamp time.Time ) func main() { cfg := loadConfiguration() - resultsHandler := serviceResultsHandler + statusesHandler := serviceStatusesHandler if cfg.Security != nil && cfg.Security.IsValid() { - resultsHandler = security.Handler(serviceResultsHandler, cfg.Security) + statusesHandler = security.Handler(serviceStatusesHandler, cfg.Security) } http.HandleFunc("/favicon.ico", favIconHandler) // favicon needs to be always served from the root - http.HandleFunc(cfg.Web.PrependWithContextRoot("/api/v1/results"), resultsHandler) + http.HandleFunc(cfg.Web.PrependWithContextRoot("/api/v1/statuses"), statusesHandler) http.HandleFunc(cfg.Web.PrependWithContextRoot("/health"), healthHandler) http.Handle(cfg.Web.ContextRoot, GzipHandler(http.StripPrefix(cfg.Web.ContextRoot, http.FileServer(http.Dir("./static"))))) @@ -56,29 +56,29 @@ func loadConfiguration() *config.Config { return config.Get() } -func serviceResultsHandler(writer http.ResponseWriter, r *http.Request) { - if isExpired := cachedServiceResultsTimestamp.IsZero() || time.Now().Sub(cachedServiceResultsTimestamp) > cacheTTL; isExpired { +func serviceStatusesHandler(writer http.ResponseWriter, r *http.Request) { + if isExpired := cachedServiceStatusesTimestamp.IsZero() || time.Now().Sub(cachedServiceStatusesTimestamp) > cacheTTL; isExpired { buffer := &bytes.Buffer{} gzipWriter := gzip.NewWriter(buffer) - data, err := watchdog.GetJSONEncodedServiceResults() + data, err := watchdog.GetJSONEncodedServiceStatuses() if err != nil { - log.Printf("[main][serviceResultsHandler] Unable to marshal object to JSON: %s", err.Error()) + log.Printf("[main][serviceStatusesHandler] Unable to marshal object to JSON: %s", err.Error()) writer.WriteHeader(http.StatusInternalServerError) _, _ = writer.Write([]byte("Unable to marshal object to JSON")) return } gzipWriter.Write(data) gzipWriter.Close() - cachedServiceResults = data - cachedServiceResultsGzipped = buffer.Bytes() - cachedServiceResultsTimestamp = time.Now() + cachedServiceStatuses = data + cachedServiceStatusesGzipped = buffer.Bytes() + cachedServiceStatusesTimestamp = time.Now() } var data []byte if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") { writer.Header().Set("Content-Encoding", "gzip") - data = cachedServiceResultsGzipped + data = cachedServiceStatusesGzipped } else { - data = cachedServiceResults + data = cachedServiceStatuses } writer.Header().Add("Content-type", "application/json") writer.WriteHeader(http.StatusOK) diff --git a/static/index.html b/static/index.html index 7ada745a..ac18811d 100644 --- a/static/index.html +++ b/static/index.html @@ -99,6 +99,13 @@ #settings select:focus { box-shadow: none; } + .service-group { + cursor: pointer; + user-select: none; + } + .service-group h5:hover { + color: #1b1e21 !important; + }
@@ -162,7 +169,7 @@ function showTooltip(serviceName, index, element) { userClickedStatus = false; clearTimeout(timerHandler); - let serviceResult = serviceStatuses[serviceName][index]; + let serviceResult = serviceStatuses[serviceName].results[index]; $("#tooltip-timestamp").text(prettifyTimestamp(serviceResult.timestamp)); $("#tooltip-response-time").text(parseInt(serviceResult.duration/1000000) + "ms"); // Populate the condition section @@ -219,8 +226,8 @@ return "X"; } - function refreshResults() { - $.getJSON("./api/v1/results", function (data) { + function refreshStatuses() { + $.getJSON("./api/v1/statuses", function (data) { // Update the table only if there's a change if (JSON.stringify(serviceStatuses) !== JSON.stringify(data)) { serviceStatuses = data; @@ -230,16 +237,17 @@ } function buildTable() { - let output = ""; + let outputByGroup = {}; for (let serviceName in serviceStatuses) { let serviceStatusOverTime = ""; - let hostname = serviceStatuses[serviceName][serviceStatuses[serviceName].length-1].hostname + let serviceStatus = serviceStatuses[serviceName]; + let hostname = serviceStatus.results[serviceStatus.results.length-1].hostname; let minResponseTime = null; let maxResponseTime = null; let newestTimestamp = null; let oldestTimestamp = null; - for (let key in serviceStatuses[serviceName]) { - let serviceResult = serviceStatuses[serviceName][key]; + for (let key in serviceStatus.results) { + let serviceResult = serviceStatus.results[key]; serviceStatusOverTime = createStatusBadge(serviceName, key, serviceResult.success) + serviceStatusOverTime; const responseTime = parseInt(serviceResult.duration/1000000); if (minResponseTime == null || minResponseTime > responseTime) { @@ -256,8 +264,8 @@ oldestTimestamp = timestamp; } } - output += "" - + "