Add first version of UI
This commit is contained in:
parent
f1497bec49
commit
88c35e30b4
@ -1,6 +1,7 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -21,13 +22,14 @@ type ServerMessage struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Result struct {
|
type Result struct {
|
||||||
HttpStatus int `json:"status"`
|
HttpStatus int `json:"status"`
|
||||||
Hostname string `json:"hostname"`
|
Hostname string `json:"hostname"`
|
||||||
Ip string `json:"ip"`
|
Ip string `json:"ip"`
|
||||||
Duration time.Duration `json:"duration"`
|
Duration time.Duration `json:"duration"`
|
||||||
Errors []error `json:"errors"`
|
Errors []error `json:"errors"`
|
||||||
ConditionResult []*ConditionResult `json:"condition-results"`
|
ConditionResults []*ConditionResult `json:"condition-results"`
|
||||||
Timestamp time.Time `json:"timestamp"`
|
Success bool `json:"success"`
|
||||||
|
Timestamp time.Time `json:"timestamp"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
@ -68,11 +70,17 @@ func (service *Service) getStatus(result *Result) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (service *Service) EvaluateConditions() *Result {
|
func (service *Service) EvaluateConditions() *Result {
|
||||||
result := &Result{}
|
result := &Result{Success: true}
|
||||||
service.getStatus(result)
|
service.getStatus(result)
|
||||||
service.getIp(result)
|
service.getIp(result)
|
||||||
|
if len(result.Errors) > 0 {
|
||||||
|
result.Success = false
|
||||||
|
}
|
||||||
for _, condition := range service.Conditions {
|
for _, condition := range service.Conditions {
|
||||||
condition.Evaluate(result)
|
success := condition.Evaluate(result)
|
||||||
|
if !success {
|
||||||
|
result.Success = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
result.Timestamp = time.Now()
|
result.Timestamp = time.Now()
|
||||||
return result
|
return result
|
||||||
@ -86,38 +94,45 @@ type ConditionResult struct {
|
|||||||
|
|
||||||
type Condition string
|
type Condition string
|
||||||
|
|
||||||
func (c *Condition) Evaluate(result *Result) {
|
func (c *Condition) Evaluate(result *Result) bool {
|
||||||
condition := string(*c)
|
condition := string(*c)
|
||||||
if strings.Contains(condition, "==") {
|
if strings.Contains(condition, "==") {
|
||||||
parts := sanitizeAndResolve(strings.Split(condition, "=="), result)
|
parts := sanitizeAndResolve(strings.Split(condition, "=="), result)
|
||||||
if parts[0] == parts[1] {
|
if parts[0] == parts[1] {
|
||||||
result.ConditionResult = append(result.ConditionResult, &ConditionResult{
|
result.ConditionResults = append(result.ConditionResults, &ConditionResult{
|
||||||
Condition: c,
|
Condition: c,
|
||||||
Success: true,
|
Success: true,
|
||||||
Explanation: fmt.Sprintf("%s is equal to %s", parts[0], parts[1]),
|
Explanation: fmt.Sprintf("%s is equal to %s", parts[0], parts[1]),
|
||||||
})
|
})
|
||||||
|
return true
|
||||||
} else {
|
} else {
|
||||||
result.ConditionResult = append(result.ConditionResult, &ConditionResult{
|
result.ConditionResults = append(result.ConditionResults, &ConditionResult{
|
||||||
Condition: c,
|
Condition: c,
|
||||||
Success: false,
|
Success: false,
|
||||||
Explanation: fmt.Sprintf("%s is not equal to %s", parts[0], parts[1]),
|
Explanation: fmt.Sprintf("%s is not equal to %s", parts[0], parts[1]),
|
||||||
})
|
})
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
} else if strings.Contains(condition, "!=") {
|
} else if strings.Contains(condition, "!=") {
|
||||||
parts := sanitizeAndResolve(strings.Split(condition, "!="), result)
|
parts := sanitizeAndResolve(strings.Split(condition, "!="), result)
|
||||||
if parts[0] != parts[1] {
|
if parts[0] != parts[1] {
|
||||||
result.ConditionResult = append(result.ConditionResult, &ConditionResult{
|
result.ConditionResults = append(result.ConditionResults, &ConditionResult{
|
||||||
Condition: c,
|
Condition: c,
|
||||||
Success: true,
|
Success: true,
|
||||||
Explanation: fmt.Sprintf("%s is not equal to %s", parts[0], parts[1]),
|
Explanation: fmt.Sprintf("%s is not equal to %s", parts[0], parts[1]),
|
||||||
})
|
})
|
||||||
|
return true
|
||||||
} else {
|
} else {
|
||||||
result.ConditionResult = append(result.ConditionResult, &ConditionResult{
|
result.ConditionResults = append(result.ConditionResults, &ConditionResult{
|
||||||
Condition: c,
|
Condition: c,
|
||||||
Success: false,
|
Success: false,
|
||||||
Explanation: fmt.Sprintf("%s is equal to %s", parts[0], parts[1]),
|
Explanation: fmt.Sprintf("%s is equal to %s", parts[0], parts[1]),
|
||||||
})
|
})
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
result.Errors = append(result.Errors, errors.New(fmt.Sprintf("invalid condition '%s' has been provided", condition)))
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,8 +8,8 @@ func TestEvaluateWithIp(t *testing.T) {
|
|||||||
condition := Condition("$IP == 127.0.0.1")
|
condition := Condition("$IP == 127.0.0.1")
|
||||||
result := &Result{Ip: "127.0.0.1"}
|
result := &Result{Ip: "127.0.0.1"}
|
||||||
condition.Evaluate(result)
|
condition.Evaluate(result)
|
||||||
if result.ConditionResult[0].Success != true {
|
if !result.ConditionResults[0].Success {
|
||||||
t.Error("Condition '$IP == 127.0.0.1' should have been a success")
|
t.Errorf("Condition '%s' should have been a success", condition)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -17,7 +17,48 @@ func TestEvaluateWithStatus(t *testing.T) {
|
|||||||
condition := Condition("$STATUS == 201")
|
condition := Condition("$STATUS == 201")
|
||||||
result := &Result{HttpStatus: 201}
|
result := &Result{HttpStatus: 201}
|
||||||
condition.Evaluate(result)
|
condition.Evaluate(result)
|
||||||
if result.ConditionResult[0].Success != true {
|
if !result.ConditionResults[0].Success {
|
||||||
t.Error("Condition '$STATUS == 201' should have been a success")
|
t.Errorf("Condition '%s' should have been a success", condition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEvaluateWithFailure(t *testing.T) {
|
||||||
|
condition := Condition("$STATUS == 200")
|
||||||
|
result := &Result{HttpStatus: 500}
|
||||||
|
condition.Evaluate(result)
|
||||||
|
if result.ConditionResults[0].Success {
|
||||||
|
t.Errorf("Condition '%s' should have been a failure", condition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIntegrationEvaluateConditions(t *testing.T) {
|
||||||
|
condition := Condition("$STATUS == 200")
|
||||||
|
service := Service{
|
||||||
|
Name: "GitHub",
|
||||||
|
Url: "https://api.github.com/healthz",
|
||||||
|
Conditions: []*Condition{&condition},
|
||||||
|
}
|
||||||
|
result := service.EvaluateConditions()
|
||||||
|
if !result.ConditionResults[0].Success {
|
||||||
|
t.Errorf("Condition '%s' should have been a success", condition)
|
||||||
|
}
|
||||||
|
if !result.Success {
|
||||||
|
t.Error("Because all conditions passed, this should have been a success")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIntegrationEvaluateConditionsWithFailure(t *testing.T) {
|
||||||
|
condition := Condition("$STATUS == 500")
|
||||||
|
service := Service{
|
||||||
|
Name: "GitHub",
|
||||||
|
Url: "https://api.github.com/healthz",
|
||||||
|
Conditions: []*Condition{&condition},
|
||||||
|
}
|
||||||
|
result := service.EvaluateConditions()
|
||||||
|
if result.ConditionResults[0].Success {
|
||||||
|
t.Errorf("Condition '%s' should have been a failure", condition)
|
||||||
|
}
|
||||||
|
if result.Success {
|
||||||
|
t.Error("Because one of the conditions failed, success should have been false")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
7
main.go
7
main.go
@ -12,7 +12,7 @@ func main() {
|
|||||||
go watchdog.Monitor()
|
go watchdog.Monitor()
|
||||||
http.HandleFunc("/api/v1/results", serviceResultsHandler)
|
http.HandleFunc("/api/v1/results", serviceResultsHandler)
|
||||||
http.HandleFunc("/health", healthHandler)
|
http.HandleFunc("/health", healthHandler)
|
||||||
http.HandleFunc("/", indexHandler)
|
http.Handle("/", http.FileServer(http.Dir("./static")))
|
||||||
log.Println("[main][main] Listening on port 80")
|
log.Println("[main][main] Listening on port 80")
|
||||||
log.Fatal(http.ListenAndServe(":80", nil))
|
log.Fatal(http.ListenAndServe(":80", nil))
|
||||||
}
|
}
|
||||||
@ -23,11 +23,6 @@ func serviceResultsHandler(writer http.ResponseWriter, request *http.Request) {
|
|||||||
_, _ = writer.Write(structToJsonBytes(serviceResults))
|
_, _ = writer.Write(structToJsonBytes(serviceResults))
|
||||||
}
|
}
|
||||||
|
|
||||||
func indexHandler(writer http.ResponseWriter, request *http.Request) {
|
|
||||||
writer.WriteHeader(http.StatusNotImplemented)
|
|
||||||
_, _ = writer.Write(structToJsonBytes(&core.ServerMessage{Error: true, Message: "Not implemented yet"}))
|
|
||||||
}
|
|
||||||
|
|
||||||
func healthHandler(writer http.ResponseWriter, request *http.Request) {
|
func healthHandler(writer http.ResponseWriter, request *http.Request) {
|
||||||
writer.WriteHeader(http.StatusOK)
|
writer.WriteHeader(http.StatusOK)
|
||||||
_, _ = writer.Write(structToJsonBytes(&core.HealthStatus{Status: "UP"}))
|
_, _ = writer.Write(structToJsonBytes(&core.HealthStatus{Status: "UP"}))
|
||||||
|
62
static/index.html
Normal file
62
static/index.html
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>gatus</title>
|
||||||
|
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container my-3 bg-light rounded p-4 border shadow">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="display-4">Gatus</div>
|
||||||
|
</div>
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">Name</th>
|
||||||
|
<th scope="col">Status</th>
|
||||||
|
<th scope="col">Hostname</th>
|
||||||
|
<th scope="col">Response time</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="results">
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
|
||||||
|
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
|
||||||
|
<script>
|
||||||
|
const OK = "<span class=\"badge badge-success\">✓</span> ";
|
||||||
|
const NOK = "<span class=\"badge badge-danger\">X</span> ";
|
||||||
|
|
||||||
|
function refreshTable() {
|
||||||
|
$.getJSON("/api/v1/results", function (data) {
|
||||||
|
let tableBody = "";
|
||||||
|
for (let serviceName in data) {
|
||||||
|
let serviceStatus = "";
|
||||||
|
for (let key in data[serviceName]) {
|
||||||
|
let entry = data[serviceName][key];
|
||||||
|
console.log(data[serviceName][key]);
|
||||||
|
serviceStatus += entry.success ? OK : NOK;
|
||||||
|
}
|
||||||
|
tableBody += ""
|
||||||
|
+ "<tr>"
|
||||||
|
+ " <td>" + serviceName + "</td>"
|
||||||
|
+ " <td>" + serviceStatus + "</td>"
|
||||||
|
+ " <td>" + data[serviceName][0].hostname + "</td>"
|
||||||
|
+ " <td>" + parseInt(data[serviceName][0].duration / 1000000) + "ms </td>"
|
||||||
|
+ "</tr>";
|
||||||
|
}
|
||||||
|
$("#results").html(tableBody);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshTable();
|
||||||
|
setInterval(function() {
|
||||||
|
refreshTable();
|
||||||
|
}, 10000);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
x
Reference in New Issue
Block a user