feat(index): more information about checks and worker groups

This commit is contained in:
Tine 2024-05-27 20:44:40 +02:00
parent 2a0447bc7b
commit 3e8832709a
Signed by: mentos1386
SSH key fingerprint: SHA256:MNtTsLbihYaWF8j1fkOHfkKNlnN1JQfxEU/rBU8nCGw
8 changed files with 232 additions and 117 deletions

View file

@ -4,6 +4,7 @@ import (
"context"
"github.com/dop251/goja"
"github.com/mentos1386/zdravko/database/models"
"github.com/mentos1386/zdravko/internal/server/services"
"github.com/mentos1386/zdravko/internal/temporal"
"github.com/mentos1386/zdravko/pkg/script"
@ -25,6 +26,10 @@ func (a *Activities) TargetsFilter(ctx context.Context, param temporal.ActivityT
}
for _, target := range allTargets {
if target.State == models.TargetStatePaused {
continue
}
vm := goja.New()
vm.SetFieldNameMapper(goja.UncapFieldNameMapper())

View file

@ -3,6 +3,7 @@ package handlers
import (
"context"
"net/http"
"sort"
"time"
"github.com/labstack/echo/v4"
@ -24,19 +25,33 @@ type Target struct {
Visibility models.TargetVisibility
Group string
Status models.TargetStatus
History *History
}
type HistoryItem struct {
Status models.TargetStatus
Date time.Time
History []*HistoryItem
Uptime float64
}
type History struct {
List []HistoryItem
List []*HistoryItem
Uptime float64
}
type HistoryItem struct {
Status models.TargetStatus
StatusCounts map[models.TargetStatus]int
Counts int
Date time.Time
Checks []*HistoryItemCheck
SuccessRate float64
}
type HistoryItemCheck struct {
Name string
WorkerGroupName string
Status models.TargetStatus
StatusCounts map[models.TargetStatus]int
Counts int
SuccessRate float64
}
type TargetsAndStatus struct {
Status models.TargetStatus
Targets []*Target
@ -47,20 +62,31 @@ func getDateString(date time.Time) string {
}
func getHistory(history []*services.TargetHistory, period time.Duration, buckets int) *History {
historyMap := map[string]models.TargetStatus{}
historyMap := map[string]*HistoryItem{}
numOfSuccess := 0.0
numTotal := 0.0
mapKeys := make([]string, buckets)
for i := 0; i < buckets; i++ {
dateString := getDateString(time.Now().Add(period * time.Duration(-i)).Truncate(period))
historyMap[dateString] = models.TargetStatusUnknown
date := time.Now().Add(period * time.Duration(-i)).Truncate(period)
dateString := getDateString(date)
mapKeys[i] = dateString
historyMap[dateString] = &HistoryItem{
Status: models.TargetStatusUnknown,
StatusCounts: map[models.TargetStatus]int{},
Date: date,
Checks: []*HistoryItemCheck{},
SuccessRate: 0.0,
}
}
for _, _history := range history {
dateString := getDateString(_history.CreatedAt.Time.Truncate(period))
// Skip if not part of the "buckets"
if _, ok := historyMap[dateString]; !ok {
entry, ok := historyMap[dateString]
if !ok {
continue
}
@ -69,24 +95,58 @@ func getHistory(history []*services.TargetHistory, period time.Duration, buckets
numOfSuccess++
}
// skip if it is already set to failure
if historyMap[dateString] == models.TargetStatusFailure {
continue
if entry.Status == models.TargetStatusUnknown {
entry.Status = _history.Status
}
// FIXME: This is wrong! As we can have multiple targets in dateString.
// We should look at only the newest one.
historyMap[dateString] = _history.Status
}
historyItems := make([]HistoryItem, buckets)
for i := 0; i < buckets; i++ {
date := time.Now().Add(period * time.Duration(-buckets+i+1)).Truncate(period)
datestring := getDateString(date)
historyItems[i] = HistoryItem{
Status: historyMap[datestring],
Date: date,
// If not yet failure, and failing check. Mark as failing.
if _history.Status == models.TargetStatusFailure && entry.Status != models.TargetStatusFailure {
entry.Status = models.TargetStatusFailure
}
entry.StatusCounts[_history.Status]++
entry.Counts++
entry.SuccessRate = 100.0 * float64(entry.StatusCounts[models.TargetStatusSuccess]) / float64(entry.Counts)
foundCheck := false
for _, check := range entry.Checks {
if check.Name == _history.CheckName && check.WorkerGroupName == _history.WorkerGroupName {
foundCheck = true
check.StatusCounts[_history.Status]++
check.Counts++
check.SuccessRate = 100.0 * float64(check.StatusCounts[models.TargetStatusSuccess]) / float64(check.Counts)
if check.Status != models.TargetStatusFailure && _history.Status == models.TargetStatusFailure {
check.Status = models.TargetStatusFailure
}
}
}
if !foundCheck {
successRate := 0.0
if _history.Status == models.TargetStatusSuccess {
successRate = 100.0
}
entry.Checks = append(entry.Checks, &HistoryItemCheck{
Name: _history.CheckName,
WorkerGroupName: _history.WorkerGroupName,
Status: _history.Status,
StatusCounts: map[models.TargetStatus]int{
_history.Status: 1,
},
Counts: 1,
SuccessRate: successRate,
})
sort.Slice(entry.Checks, func(i, j int) bool {
byName := entry.Checks[i].Name < entry.Checks[j].Name
byWorkerGroupName := entry.Checks[i].WorkerGroupName < entry.Checks[j].WorkerGroupName
return byName || (entry.Checks[i].Name == entry.Checks[j].Name && byWorkerGroupName)
})
}
historyMap[dateString] = entry
}
uptime := 0.0
@ -94,6 +154,12 @@ func getHistory(history []*services.TargetHistory, period time.Duration, buckets
uptime = 100.0 * numOfSuccess / numTotal
}
historyItems := make([]*HistoryItem, 0, len(historyMap))
for i := buckets - 1; i >= 0; i-- {
key := mapKeys[i]
historyItems = append(historyItems, historyMap[key])
}
return &History{
List: historyItems,
Uptime: uptime,
@ -107,9 +173,27 @@ func (h *BaseHandler) Index(c echo.Context) error {
return err
}
timeRange := c.QueryParam("time-range")
if timeRange != "48hours" && timeRange != "90days" && timeRange != "90minutes" {
timeRange = "90days"
timeRangeQuery := c.QueryParam("time-range")
if timeRangeQuery != "48hours" && timeRangeQuery != "90days" && timeRangeQuery != "90minutes" {
timeRangeQuery = "90days"
}
var timeBuckets int
var timeInterval time.Duration
var timeRange services.TargetHistoryDateRange
switch timeRangeQuery {
case "90days":
timeRange = services.TargetHistoryDateRange90Days
timeInterval = time.Hour * 24
timeBuckets = 90
case "48hours":
timeRange = services.TargetHistoryDateRange48Hours
timeInterval = time.Hour
timeBuckets = 48
case "90minutes":
timeRange = services.TargetHistoryDateRange90Minutes
timeInterval = time.Minute
timeBuckets = 90
}
overallStatus := models.TargetStatusUnknown
@ -117,20 +201,12 @@ func (h *BaseHandler) Index(c echo.Context) error {
targetsWithHistory := make([]*Target, len(targets))
for i, target := range targets {
history, err := services.GetTargetHistoryForTarget(ctx, h.db, target.Id)
history, err := services.GetTargetHistoryForTarget(ctx, h.db, target.Id, timeRange)
if err != nil {
return err
}
var historyResult *History
switch timeRange {
case "48hours":
historyResult = getHistory(history, time.Hour, 48)
case "90days":
historyResult = getHistory(history, time.Hour*24, 90)
case "90minutes":
historyResult = getHistory(history, time.Minute, 90)
}
historyResult := getHistory(history, timeInterval, timeBuckets)
if statusByGroup[target.Group] == "" {
statusByGroup[target.Group] = models.TargetStatusUnknown
@ -155,7 +231,8 @@ func (h *BaseHandler) Index(c echo.Context) error {
Visibility: target.Visibility,
Group: target.Group,
Status: status.Status,
History: historyResult,
History: historyResult.List,
Uptime: historyResult.Uptime,
}
}
@ -175,7 +252,7 @@ func (h *BaseHandler) Index(c echo.Context) error {
Navbar: Pages,
},
Targets: targetsByGroup,
TimeRange: timeRange,
TimeRange: timeRangeQuery,
Status: overallStatus,
})
}

View file

@ -83,7 +83,7 @@ func (h *BaseHandler) SettingsTargetsDescribeGET(c echo.Context) error {
return err
}
history, err := services.GetTargetHistoryForTarget(context.Background(), h.db, slug)
history, err := services.GetTargetHistoryForTarget(context.Background(), h.db, slug, services.TargetHistoryDateRange90Minutes)
if err != nil {
return err
}

View file

@ -2,6 +2,7 @@ package services
import (
"context"
"fmt"
"github.com/jmoiron/sqlx"
"github.com/mentos1386/zdravko/database/models"
@ -14,28 +15,30 @@ type TargetHistory struct {
CheckName string `db:"check_name"`
}
func GetLastNTargetHistory(ctx context.Context, db *sqlx.DB, n int) ([]*TargetHistory, error) {
var targetHistory []*TargetHistory
err := db.SelectContext(ctx, &targetHistory, `
SELECT
th.*,
t.name AS target_name,
wg.name AS worker_group_name,
c.name AS check_name
FROM target_histories th
LEFT JOIN targets t ON th.target_id = t.id
LEFT JOIN worker_groups wg ON th.worker_group_id = wg.id
LEFT JOIN checks c ON th.check_id = c.id
WHERE th.target_id = $1
ORDER BY th.created_at DESC
LIMIT $1
`, n)
return targetHistory, err
}
type TargetHistoryDateRange string
const (
TargetHistoryDateRange90Days TargetHistoryDateRange = "90_DAYS"
TargetHistoryDateRange48Hours TargetHistoryDateRange = "48_HOURS"
TargetHistoryDateRange90Minutes TargetHistoryDateRange = "90_MINUTES"
)
func GetTargetHistoryForTarget(ctx context.Context, db *sqlx.DB, targetId string, dateRange TargetHistoryDateRange) ([]*TargetHistory, error) {
dateRangeFilter := ""
switch dateRange {
case TargetHistoryDateRange90Days:
dateRangeFilter = "AND strftime('%Y-%m-%dT%H:%M:%fZ', th.created_at) >= datetime('now', 'localtime', '-90 days')"
case TargetHistoryDateRange48Hours:
dateRangeFilter = "AND strftime('%Y-%m-%dT%H:%M:%fZ', th.created_at) >= datetime('now', 'localtime', '-48 hours')"
case TargetHistoryDateRange90Minutes:
dateRangeFilter = "AND strftime('%Y-%m-%dT%H:%M:%fZ', th.created_at) >= datetime('now', 'localtime', '-90 minutes')"
}
func GetTargetHistoryForTarget(ctx context.Context, db *sqlx.DB, targetId string) ([]*TargetHistory, error) {
var targetHistory []*TargetHistory
err := db.SelectContext(ctx, &targetHistory, `
err := db.SelectContext(
ctx,
&targetHistory,
fmt.Sprintf(`
SELECT
th.*,
t.name AS target_name,
@ -45,9 +48,13 @@ func GetTargetHistoryForTarget(ctx context.Context, db *sqlx.DB, targetId string
LEFT JOIN targets t ON th.target_id = t.id
LEFT JOIN worker_groups wg ON th.worker_group_id = wg.id
LEFT JOIN checks c ON th.check_id = c.id
WHERE th.target_id = $1
WHERE
th.target_id = $1
%s
ORDER BY th.created_at DESC
`, targetId)
`, dateRangeFilter),
targetId,
)
return targetHistory, err
}

View file

@ -730,10 +730,6 @@ video {
height: 5rem;
}
.h-3 {
height: 0.75rem;
}
.h-4 {
height: 1rem;
}
@ -768,10 +764,6 @@ video {
width: 5rem;
}
.w-3 {
width: 0.75rem;
}
.w-4 {
width: 1rem;
}
@ -845,6 +837,10 @@ video {
justify-content: center;
}
.justify-between {
justify-content: space-between;
}
.gap-1 {
gap: 0.25rem;
}

View file

@ -101,19 +101,17 @@
<summary
class="flex flex-row gap-2 p-3 py-2 -mx-3 cursor-pointer hover:bg-blue-50 rounded-lg"
>
{{ if eq $targetsAndStatus.Status "SUCCESS" }}
<span
class="flex w-3 h-3 bg-green-400 rounded-full self-center"
></span>
{{ else if eq $targetsAndStatus.Status "FAILURE" }}
<span
class="flex w-3 h-3 bg-red-400 rounded-full self-center"
></span>
{{ else }}
<span
class="flex w-3 h-3 bg-gray-200 rounded-full self-center"
></span>
{{ end }}
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full
{{ if eq $targetsAndStatus.Status "SUCCESS" }}
bg-green-100 text-green-800
{{ else if eq $targetsAndStatus.Status "FAILURE" }}
bg-red-100 text-red-800
{{ else }}
bg-gray-100 text-gray-800
{{ end }}"
>{{ .Status }}</span
>
<h2 class="flex-1 font-semibold capitalize">
{{ $group }}
</h2>
@ -128,13 +126,17 @@
class="grid grid-cols-1 sm:grid-cols-2 gap-2 mt-2 pb-2 border-b last-of-type:pb-0 last-of-type:border-0 border-gray-100"
>
<div class="flex items-center gap-2">
{{ if eq .Status "SUCCESS" }}
<span class="flex w-3 h-3 bg-green-400 rounded-full"></span>
{{ else if eq .Status "FAILURE" }}
<span class="flex w-3 h-3 bg-red-400 rounded-full"></span>
{{ else }}
<span class="flex w-3 h-3 bg-gray-200 rounded-full"></span>
{{ end }}
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full
{{ if eq .Status "SUCCESS" }}
bg-green-100 text-green-800
{{ else if eq .Status "FAILURE" }}
bg-red-100 text-red-800
{{ else }}
bg-gray-100 text-gray-800
{{ end }}"
>{{ .Status }}</span
>
<h4>
{{ .Name }}
{{ if eq .Visibility "PUBLIC" }}
@ -159,12 +161,12 @@
</h4>
</div>
<div class="justify-self-end text-sm">
{{ printf "%.2f" .History.Uptime }}% uptime
{{ printf "%.2f" .Uptime }}% uptime
</div>
<div
class="grid gap-px col-span-2 grid-flow-col h-8 rounded overflow-hidden"
>
{{ range .History.List }}
{{ range .History }}
<div
class="has-tooltip [&_.tooltip]:hover:flex [&_.tooltip]:hover:visible flex"
>
@ -182,27 +184,51 @@
></div>
{{ end }}
<div
class="tooltip gap-2 bg-white border border-gray-200 rounded p-2 shadow-lg hidden z-50 absolute mt-10 -ml-4 flex-row text-xs"
class="tooltip flex flex-col gap-2 bg-white border border-gray-200 rounded p-2 shadow-lg hidden z-50 absolute mt-10 -ml-4 flex-row text-xs"
>
{{ if eq .Status "SUCCESS" }}
<div
class="flex flex-row gap-2 justify-between pb-2 border-b border-gray-200"
>
{{ if eq $.TimeRange "90days" }}
{{ .Date.Format "Jan 02" }}
{{ else if eq $.TimeRange "48hours" }}
{{ .Date.Format "Jan 02, 15:00 MST" }}
{{ else if eq $.TimeRange "90minutes" }}
{{ .Date.Format "Jan 02, 15:04 MST" }}
{{ end }}
<span
class="flex w-3 h-3 bg-green-400 rounded-full self-center"
></span>
{{ else if eq .Status "FAILURE" }}
<span
class="flex w-3 h-3 bg-red-400 rounded-full self-center"
></span>
{{ else }}
<span
class="flex w-3 h-3 bg-gray-200 rounded-full self-center"
></span>
{{ end }}
{{ if eq $.TimeRange "90days" }}
{{ .Date.Format "Jan 02" }}
{{ else if eq $.TimeRange "48hours" }}
{{ .Date.Format "Jan 02, 15:00 MST" }}
{{ else if eq $.TimeRange "90minutes" }}
{{ .Date.Format "Jan 02, 15:04 MST" }}
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full
{{ if eq .Status "SUCCESS" }}
bg-green-100 text-green-800
{{ else if eq .Status "FAILURE" }}
bg-red-100 text-red-800
{{ else }}
bg-gray-100 text-gray-800
{{ end }}"
>{{ .Status }}
({{ printf "%.2f" .SuccessRate }}%)
</span>
</div>
{{ range .Checks }}
<div class="flex flex-row gap-2 justify-between">
<p>
{{ .Name }} on
{{ .WorkerGroupName }}
</p>
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full
{{ if eq .Status "SUCCESS" }}
bg-green-100 text-green-800
{{ else if eq .Status "FAILURE" }}
bg-red-100 text-red-800
{{ else }}
bg-gray-100 text-gray-800
{{ end }}"
>{{ .Status }}
({{ printf "%.2f" .SuccessRate }}%)
</span>
</div>
{{ end }}
</div>
</div>

View file

@ -117,8 +117,7 @@
<th>Status</th>
<th>Worker Group</th>
<th>Check</th>
<th>Created At</th>
<th>Duration</th>
<th>Checked At</th>
<th>Note</th>
</tr>
</thead>
@ -153,7 +152,6 @@
<td>
{{ .CreatedAt.Time.Format "2006-01-02 15:04:05" }}
</td>
<td>{ .Duration }</td>
<td class="whitespace-normal">
{{ .Note }}
</td>

View file

@ -26,6 +26,12 @@ func load(files ...string) *template.Template {
t := template.New("default").Funcs(
template.FuncMap{
"MathDivide": func(a, b int) float64 {
if b == 0 {
return 0
}
return float64(a) / float64(b)
},
"DurationRoundSecond": func(d time.Duration) time.Duration {
return d.Round(time.Second)
},