diff --git a/internal/server/activities/targets_filter.go b/internal/server/activities/targets_filter.go index 537b84f..cc56059 100644 --- a/internal/server/activities/targets_filter.go +++ b/internal/server/activities/targets_filter.go @@ -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()) diff --git a/internal/server/handlers/index.go b/internal/server/handlers/index.go index 33fb000..183ab11 100644 --- a/internal/server/handlers/index.go +++ b/internal/server/handlers/index.go @@ -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, }) } diff --git a/internal/server/handlers/settings_targets.go b/internal/server/handlers/settings_targets.go index fbac171..453639e 100644 --- a/internal/server/handlers/settings_targets.go +++ b/internal/server/handlers/settings_targets.go @@ -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 } diff --git a/internal/server/services/targets_history.go b/internal/server/services/targets_history.go index e233fd2..4ecf9b4 100644 --- a/internal/server/services/targets_history.go +++ b/internal/server/services/targets_history.go @@ -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 } diff --git a/web/static/css/tailwind.css b/web/static/css/tailwind.css index aff86a8..2e545df 100644 --- a/web/static/css/tailwind.css +++ b/web/static/css/tailwind.css @@ -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; } diff --git a/web/templates/pages/index.tmpl b/web/templates/pages/index.tmpl index 288bee0..89c1905 100644 --- a/web/templates/pages/index.tmpl +++ b/web/templates/pages/index.tmpl @@ -101,19 +101,17 @@ - {{ if eq $targetsAndStatus.Status "SUCCESS" }} - - {{ else if eq $targetsAndStatus.Status "FAILURE" }} - - {{ else }} - - {{ end }} + {{ .Status }}

{{ $group }}

@@ -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" >
- {{ if eq .Status "SUCCESS" }} - - {{ else if eq .Status "FAILURE" }} - - {{ else }} - - {{ end }} + {{ .Status }}

{{ .Name }} {{ if eq .Visibility "PUBLIC" }} @@ -159,12 +161,12 @@

- {{ printf "%.2f" .History.Uptime }}% uptime + {{ printf "%.2f" .Uptime }}% uptime
- {{ range .History.List }} + {{ range .History }}
@@ -182,27 +184,51 @@ >
{{ end }}
diff --git a/web/templates/pages/settings_targets_describe.tmpl b/web/templates/pages/settings_targets_describe.tmpl index 55090a1..24994a2 100644 --- a/web/templates/pages/settings_targets_describe.tmpl +++ b/web/templates/pages/settings_targets_describe.tmpl @@ -117,8 +117,7 @@ Status Worker Group Check - Created At - Duration + Checked At Note @@ -153,7 +152,6 @@ {{ .CreatedAt.Time.Format "2006-01-02 15:04:05" }} - { .Duration } {{ .Note }} diff --git a/web/templates/templates.go b/web/templates/templates.go index 8eb7c4e..8f5ec3a 100644 --- a/web/templates/templates.go +++ b/web/templates/templates.go @@ -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) },