feat: check history and ui fixes

This commit is contained in:
Tine 2024-05-25 19:52:37 +02:00
parent 813c76fc3e
commit da53c04659
Signed by: mentos1386
SSH key fingerprint: SHA256:MNtTsLbihYaWF8j1fkOHfkKNlnN1JQfxEU/rBU8nCGw
15 changed files with 215 additions and 97 deletions

View file

@ -142,7 +142,10 @@ const (
type TargetHistory struct { type TargetHistory struct {
CreatedAt *Time `db:"created_at"` CreatedAt *Time `db:"created_at"`
TargetId string `db:"target_id"` TargetId string `db:"target_id"`
Status TargetStatus `db:"status"` WorkerGroupId string `db:"worker_group_id"`
Note string `db:"note"` CheckId string `db:"check_id"`
Status TargetStatus `db:"status"`
Note string `db:"note"`
} }

View file

@ -92,15 +92,19 @@ END;
-- +migrate StatementEnd -- +migrate StatementEnd
CREATE TABLE target_histories ( CREATE TABLE target_histories (
target_id TEXT NOT NULL, target_id TEXT NOT NULL,
worker_group_id TEXT NOT NULL,
check_id TEXT NOT NULL,
status TEXT NOT NULL, status TEXT NOT NULL,
note TEXT NOT NULL, note TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ')), created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ')),
PRIMARY KEY (target_id, created_at), PRIMARY KEY (target_id, worker_group_id, check_id, created_at),
CONSTRAINT fk_target_histories_target FOREIGN KEY (target_id) REFERENCES targets(id) ON DELETE CASCADE CONSTRAINT fk_target_histories_target FOREIGN KEY (target_id) REFERENCES targets(id) ON DELETE CASCADE,
CONSTRAINT fk_target_histories_worker_group FOREIGN KEY (worker_group_id) REFERENCES worker_groups(id) ON DELETE CASCADE,
CONSTRAINT fk_target_histories_check FOREIGN KEY (check_id) REFERENCES checks(id) ON DELETE CASCADE
) STRICT; ) STRICT;
-- +migrate Down -- +migrate Down

View file

@ -19,9 +19,11 @@ func (a *Activities) AddTargetHistory(ctx context.Context, param temporal.Activi
} }
err := services.AddHistoryForTarget(ctx, a.db, &models.TargetHistory{ err := services.AddHistoryForTarget(ctx, a.db, &models.TargetHistory{
TargetId: param.Target.Id, TargetId: param.Target.Id,
Status: status, WorkerGroupId: param.WorkerGroupId,
Note: param.Note, CheckId: param.CheckId,
Status: status,
Note: param.Note,
}) })
return &temporal.ActivityAddTargetHistoryResult{}, err return &temporal.ActivityAddTargetHistoryResult{}, err

View file

@ -71,7 +71,8 @@ func (h *BaseHandler) SettingsChecksGET(c echo.Context) error {
for i, check := range checks { for i, check := range checks {
state, err := services.GetCheckState(context.Background(), h.temporal, check.Id) state, err := services.GetCheckState(context.Background(), h.temporal, check.Id)
if err != nil { if err != nil {
return err h.logger.Error("Failed to get check state", "error", err)
state = models.CheckStateUnknown
} }
checksWithState[i] = &CheckWithWorkerGroupsAndState{ checksWithState[i] = &CheckWithWorkerGroupsAndState{
CheckWithWorkerGroups: check, CheckWithWorkerGroups: check,

View file

@ -267,7 +267,7 @@ func CreateOrUpdateCheckSchedule(
ID: getScheduleId(check.Id), ID: getScheduleId(check.Id),
Workflow: internaltemporal.WorkflowCheckName, Workflow: internaltemporal.WorkflowCheckName,
Args: args, Args: args,
TaskQueue: "default", TaskQueue: internaltemporal.TEMPORAL_SERVER_QUEUE,
RetryPolicy: &temporal.RetryPolicy{ RetryPolicy: &temporal.RetryPolicy{
MaximumAttempts: 3, MaximumAttempts: 3,
}, },

View file

@ -5,20 +5,26 @@ import (
"fmt" "fmt"
"time" "time"
"github.com/mentos1386/zdravko/internal/temporal"
"go.temporal.io/api/enums/v1"
"go.temporal.io/api/workflowservice/v1" "go.temporal.io/api/workflowservice/v1"
"go.temporal.io/sdk/client" "go.temporal.io/sdk/client"
) )
type CheckHistory struct { type CheckHistory struct {
CheckId string CheckId string
Status string Status string
Duration time.Duration Duration time.Duration
StartTime time.Time
EndTime time.Time
WorkerGroupName string
Note string
} }
func GetLastNCheckHistory(ctx context.Context, temporal client.Client, n int32) ([]*CheckHistory, error) { func GetLastNCheckHistory(ctx context.Context, t client.Client, n int32) ([]*CheckHistory, error) {
var checkHistory []*CheckHistory var checkHistory []*CheckHistory
response, err := temporal.ListWorkflow(ctx, &workflowservice.ListWorkflowExecutionsRequest{ response, err := t.ListWorkflow(ctx, &workflowservice.ListWorkflowExecutionsRequest{
PageSize: n, PageSize: n,
}) })
if err != nil { if err != nil {
@ -29,21 +35,37 @@ func GetLastNCheckHistory(ctx context.Context, temporal client.Client, n int32)
for _, execution := range executions { for _, execution := range executions {
scheduleId := string(execution.GetSearchAttributes().GetIndexedFields()["TemporalScheduledById"].Data) scheduleId := string(execution.GetSearchAttributes().GetIndexedFields()["TemporalScheduledById"].Data)
checkId := scheduleId[len("check-"):]
// Remove the quotes around the checkId and the prefix.
checkId := scheduleId[len("\"check-") : len(scheduleId)-1]
var result temporal.WorkflowCheckResult
if execution.Status != enums.WORKFLOW_EXECUTION_STATUS_RUNNING {
workflowRun := t.GetWorkflow(ctx, execution.GetExecution().GetWorkflowId(), execution.GetExecution().GetRunId())
err := workflowRun.Get(ctx, &result)
if err != nil {
return nil, err
}
}
checkHistory = append(checkHistory, &CheckHistory{ checkHistory = append(checkHistory, &CheckHistory{
CheckId: checkId, CheckId: checkId,
Duration: execution.CloseTime.AsTime().Sub(execution.StartTime.AsTime()), Duration: execution.CloseTime.AsTime().Sub(execution.StartTime.AsTime()),
Status: execution.Status.String(), StartTime: execution.StartTime.AsTime(),
EndTime: execution.CloseTime.AsTime(),
Status: execution.Status.String(),
WorkerGroupName: execution.GetTaskQueue(),
Note: result.Note,
}) })
} }
return checkHistory, nil return checkHistory, nil
} }
func GetCheckHistoryForCheck(ctx context.Context, temporal client.Client, checkId string) ([]*CheckHistory, error) { func GetCheckHistoryForCheck(ctx context.Context, t client.Client, checkId string) ([]*CheckHistory, error) {
var checkHistory []*CheckHistory var checkHistory []*CheckHistory
response, err := temporal.ListWorkflow(ctx, &workflowservice.ListWorkflowExecutionsRequest{ response, err := t.ListWorkflow(ctx, &workflowservice.ListWorkflowExecutionsRequest{
PageSize: 10, PageSize: 10,
Query: fmt.Sprintf(`TemporalScheduledById = "%s"`, getScheduleId(checkId)), Query: fmt.Sprintf(`TemporalScheduledById = "%s"`, getScheduleId(checkId)),
}) })
@ -55,11 +77,27 @@ func GetCheckHistoryForCheck(ctx context.Context, temporal client.Client, checkI
for _, execution := range executions { for _, execution := range executions {
scheduleId := string(execution.GetSearchAttributes().GetIndexedFields()["TemporalScheduledById"].Data) scheduleId := string(execution.GetSearchAttributes().GetIndexedFields()["TemporalScheduledById"].Data)
checkId := scheduleId[len("check-"):]
// Remove the quotes around the checkId and the prefix.
checkId := scheduleId[len("\"check-") : len(scheduleId)-1]
var result temporal.WorkflowCheckResult
if execution.Status != enums.WORKFLOW_EXECUTION_STATUS_RUNNING {
workflowRun := t.GetWorkflow(ctx, execution.GetExecution().GetWorkflowId(), execution.GetExecution().GetRunId())
err := workflowRun.Get(ctx, &result)
if err != nil {
return nil, err
}
}
checkHistory = append(checkHistory, &CheckHistory{ checkHistory = append(checkHistory, &CheckHistory{
CheckId: checkId, CheckId: checkId,
Duration: execution.CloseTime.AsTime().Sub(execution.StartTime.AsTime()), Duration: execution.CloseTime.AsTime().Sub(execution.StartTime.AsTime()),
Status: execution.Status.String(), StartTime: execution.StartTime.AsTime(),
EndTime: execution.CloseTime.AsTime(),
Status: execution.Status.String(),
WorkerGroupName: execution.GetTaskQueue(),
Note: result.Note,
}) })
} }

View file

@ -3,13 +3,15 @@ package services
import ( import (
"context" "context"
"github.com/mentos1386/zdravko/database/models"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"github.com/mentos1386/zdravko/database/models"
) )
type TargetHistory struct { type TargetHistory struct {
*models.TargetHistory *models.TargetHistory
TargetName string `db:"target_name"` TargetName string `db:"target_name"`
WorkerGroupName string `db:"worker_group_name"`
CheckName string `db:"check_name"`
} }
func GetLastNTargetHistory(ctx context.Context, db *sqlx.DB, n int) ([]*TargetHistory, error) { func GetLastNTargetHistory(ctx context.Context, db *sqlx.DB, n int) ([]*TargetHistory, error) {
@ -17,9 +19,13 @@ func GetLastNTargetHistory(ctx context.Context, db *sqlx.DB, n int) ([]*TargetHi
err := db.SelectContext(ctx, &targetHistory, ` err := db.SelectContext(ctx, &targetHistory, `
SELECT SELECT
th.*, th.*,
t.name AS target_name t.name AS target_name,
wg.name AS worker_group_name,
c.name AS check_name
FROM target_histories th FROM target_histories th
LEFT JOIN targets t ON th.target_id = t.id 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
ORDER BY th.created_at DESC ORDER BY th.created_at DESC
LIMIT $1 LIMIT $1
@ -32,9 +38,13 @@ func GetTargetHistoryForTarget(ctx context.Context, db *sqlx.DB, targetId string
err := db.SelectContext(ctx, &targetHistory, ` err := db.SelectContext(ctx, &targetHistory, `
SELECT SELECT
th.*, th.*,
t.name AS target_name t.name AS target_name,
wg.name AS worker_group_name,
c.name AS check_name
FROM target_histories th FROM target_histories th
LEFT JOIN targets t ON th.target_id = t.id 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
ORDER BY th.created_at DESC ORDER BY th.created_at DESC
`, targetId) `, targetId)
@ -46,10 +56,14 @@ func AddHistoryForTarget(ctx context.Context, db *sqlx.DB, history *models.Targe
` `
INSERT INTO target_histories ( INSERT INTO target_histories (
target_id, target_id,
worker_group_id,
check_id,
status, status,
note note
) VALUES ( ) VALUES (
:target_id, :target_id,
:worker_group_id,
:check_id,
:status, :status,
:note :note
)`, )`,

View file

@ -1,7 +1,6 @@
package workflows package workflows
import ( import (
"log/slog"
"sort" "sort"
"time" "time"
@ -9,7 +8,7 @@ import (
"go.temporal.io/sdk/workflow" "go.temporal.io/sdk/workflow"
) )
func (w *Workflows) CheckWorkflowDefinition(ctx workflow.Context, param temporal.WorkflowCheckParam) error { func (w *Workflows) CheckWorkflowDefinition(ctx workflow.Context, param temporal.WorkflowCheckParam) (*temporal.WorkflowCheckResult, error) {
workerGroupIds := param.WorkerGroupIds workerGroupIds := param.WorkerGroupIds
sort.Strings(workerGroupIds) sort.Strings(workerGroupIds)
@ -25,7 +24,7 @@ func (w *Workflows) CheckWorkflowDefinition(ctx workflow.Context, param temporal
}, },
).Get(ctx, &targetsFilterResult) ).Get(ctx, &targetsFilterResult)
if err != nil { if err != nil {
return err return nil, err
} }
for _, target := range targetsFilterResult.Targets { for _, target := range targetsFilterResult.Targets {
@ -43,7 +42,7 @@ func (w *Workflows) CheckWorkflowDefinition(ctx workflow.Context, param temporal
}, },
).Get(ctx, &checkResult) ).Get(ctx, &checkResult)
if err != nil { if err != nil {
return err return nil, err
} }
status := temporal.AddTargetHistoryStatusFailure status := temporal.AddTargetHistoryStatusFailure
@ -59,18 +58,20 @@ func (w *Workflows) CheckWorkflowDefinition(ctx workflow.Context, param temporal
}), }),
temporal.ActivityAddTargetHistoryName, temporal.ActivityAddTargetHistoryName,
&temporal.ActivityAddTargetHistoryParam{ &temporal.ActivityAddTargetHistoryParam{
Target: target, Target: target,
Status: status, WorkerGroupId: workerGroupId,
Note: checkResult.Note, CheckId: param.CheckId,
Status: status,
Note: checkResult.Note,
}, },
).Get(ctx, &addTargetHistoryResult) ).Get(ctx, &addTargetHistoryResult)
if err != nil { if err != nil {
return err return nil, err
} }
slog.Info("Check %s status: %s", param.CheckId, status)
} }
} }
return nil return &temporal.WorkflowCheckResult{
Note: "Check workflow completed",
}, nil
} }

View file

@ -9,9 +9,11 @@ const (
) )
type ActivityAddTargetHistoryParam struct { type ActivityAddTargetHistoryParam struct {
Target *Target Target *Target
Status AddTargetHistoryStatus WorkerGroupId string
Note string CheckId string
Status AddTargetHistoryStatus
Note string
} }
type ActivityAddTargetHistoryResult struct { type ActivityAddTargetHistoryResult struct {

View file

@ -7,4 +7,8 @@ type WorkflowCheckParam struct {
WorkerGroupIds []string WorkerGroupIds []string
} }
type WorkflowCheckResult struct {
Note string
}
const WorkflowCheckName = "CHECK_WORKFLOW" const WorkflowCheckName = "CHECK_WORKFLOW"

View file

@ -991,6 +991,11 @@ video {
background-color: rgb(253 186 116 / var(--tw-bg-opacity)); background-color: rgb(253 186 116 / var(--tw-bg-opacity));
} }
.bg-purple-100 {
--tw-bg-opacity: 1;
background-color: rgb(243 232 255 / var(--tw-bg-opacity));
}
.bg-red-100 { .bg-red-100 {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(254 226 226 / var(--tw-bg-opacity)); background-color: rgb(254 226 226 / var(--tw-bg-opacity));
@ -1225,6 +1230,11 @@ video {
color: rgb(22 101 52 / var(--tw-text-opacity)); color: rgb(22 101 52 / var(--tw-text-opacity));
} }
.text-purple-800 {
--tw-text-opacity: 1;
color: rgb(107 33 168 / var(--tw-text-opacity));
}
.text-red-600 { .text-red-600 {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(220 38 38 / var(--tw-text-opacity)); color: rgb(220 38 38 / var(--tw-text-opacity));

View file

@ -118,7 +118,8 @@
<th>Check ID</th> <th>Check ID</th>
<th>Status</th> <th>Status</th>
<th>Worker Group</th> <th>Worker Group</th>
<th>Created At</th> <th>Started At</th>
<th>Ended At</th>
<th>Duration</th> <th>Duration</th>
<th>Note</th> <th>Note</th>
</tr> </tr>
@ -130,36 +131,35 @@
<td>{{ .CheckId }}</td> <td>{{ .CheckId }}</td>
<td> <td>
<span <span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full {{ if eq .Status "SUCCESS" }} class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800"
bg-green-100 text-green-800
{{ else }}
bg-red-100 text-red-800
{{ end }}"
> >
{{ .Status }} {{ .Status }}...
</span> </span>
</td> </td>
<td> <td>
<span <span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800" class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800"
> >
{ .WorkerGroupName } {{ .WorkerGroupName }}
</span> </span>
</td> </td>
<td>{ .CreatedAt.Time.Format "2006-01-02 15:04:05" }</td> <td>{{ .StartTime.Format "2006-01-02 15:04:05" }}</td>
<td>/</td> <td></td>
<td class="whitespace-normal">/</td> <td></td>
<td class="whitespace-normal"></td>
</tr> </tr>
{{ else }} {{ else }}
<tr> <tr>
<td>{{ .CheckId }}</td> <td>{{ .CheckId }}</td>
<td> <td>
<span <span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full {{ if eq .Status "SUCCESS" }} class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full
bg-green-100 text-green-800 {{ if eq .Status "Completed" }}
bg-purple-100 text-purple-800
{{ else }} {{ else }}
bg-red-100 text-red-800 bg-red-100 text-red-800
{{ end }}" {{ end }}
"
> >
{{ .Status }} {{ .Status }}
</span> </span>
@ -168,12 +168,13 @@
<span <span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800" class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800"
> >
{ .WorkerGroupName } {{ .WorkerGroupName }}
</span> </span>
</td> </td>
<td>{ .CreatedAt.Time.Format "2006-01-02 15:04:05" }</td> <td>{{ .StartTime.Format "2006-01-02 15:04:05" }}</td>
<td>{{ .Duration }}</td> <td>{{ .EndTime.Format "2006-01-02 15:04:05" }}</td>
<td class="whitespace-normal">{ .Note }</td> <td>{{ DurationRoundMillisecond .Duration }}</td>
<td class="whitespace-normal">{{ .Note }}</td>
</tr> </tr>
{{ end }} {{ end }}
{{ end }} {{ end }}

View file

@ -42,52 +42,76 @@
</div> </div>
</div> </div>
<section class="mt-4"> <section>
<table> <table>
<caption> <caption>
Execution History History
<p>Last 10 executions for all checks and worker groups.</p> <p>Last 10 executions.</p>
</caption> </caption>
<thead> <thead>
<tr> <tr>
<th>Check</th> <th>Check ID</th>
<th>Worker Group</th>
<th>Status</th> <th>Status</th>
<th>Executed At</th> <th>Worker Group</th>
<th>Started At</th>
<th>Ended At</th>
<th>Duration</th>
<th>Note</th> <th>Note</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{{ range .History }} {{ range .History }}
<tr> {{ if eq .Status "Running" }}
<th> <tr>
<a <td>{{ .CheckId }}</td>
class="underline hover:text-blue-600" <td>
href="/settings/checks/{{ .CheckId }}" <span
>{{ .CheckId }}</a class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800"
> >
</th> {{ .Status }}...
<td> </span>
<span </td>
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800" <td>
> <span
{ .WorkerGroupName } class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800"
</span> >
</td> {{ .WorkerGroupName }}
<td> </span>
<span </td>
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full {{ if eq .Status "SUCCESS" }} <td>{{ .StartTime.Format "2006-01-02 15:04:05" }}</td>
bg-green-100 text-green-800 <td></td>
{{ else }} <td></td>
bg-red-100 text-red-800 <td class="whitespace-normal"></td>
{{ end }}" </tr>
> {{ else }}
{{ .Status }} <tr>
</span> <td>{{ .CheckId }}</td>
</td> <td>
<td>{ .CreatedAt.Time.Format "2006-01-02 15:04:05" }</td> <span
<td class="whitespace-normal">{ .Note }</td> class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full
</tr> {{ if eq .Status "Completed" }}
bg-purple-100 text-purple-800
{{ else }}
bg-red-100 text-red-800
{{ end }}
"
>
{{ .Status }}
</span>
</td>
<td>
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800"
>
{{ .WorkerGroupName }}
</span>
</td>
<td>{{ .StartTime.Format "2006-01-02 15:04:05" }}</td>
<td>{{ .EndTime.Format "2006-01-02 15:04:05" }}</td>
<td>{{ DurationRoundMillisecond .Duration }}</td>
<td class="whitespace-normal">{{ .Note }}</td>
</tr>
{{ end }}
{{ end }} {{ end }}
</tbody> </tbody>
</table> </table>

View file

@ -116,6 +116,7 @@
<tr> <tr>
<th>Status</th> <th>Status</th>
<th>Worker Group</th> <th>Worker Group</th>
<th>Check</th>
<th>Created At</th> <th>Created At</th>
<th>Duration</th> <th>Duration</th>
<th>Note</th> <th>Note</th>
@ -142,6 +143,13 @@
{{ .WorkerGroupName }} {{ .WorkerGroupName }}
</span> </span>
</td> </td>
<td>
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800"
>
{{ .CheckName }}
</span>
</td>
<td> <td>
{{ .CreatedAt.Time.Format "2006-01-02 15:04:05" }} {{ .CreatedAt.Time.Format "2006-01-02 15:04:05" }}
</td> </td>

View file

@ -26,6 +26,12 @@ func load(files ...string) *template.Template {
t := template.New("default").Funcs( t := template.New("default").Funcs(
template.FuncMap{ template.FuncMap{
"DurationRoundSecond": func(d time.Duration) time.Duration {
return d.Round(time.Second)
},
"DurationRoundMillisecond": func(d time.Duration) time.Duration {
return d.Round(time.Millisecond)
},
"StringsJoin": strings.Join, "StringsJoin": strings.Join,
"Now": time.Now, "Now": time.Now,
"ScriptUnescapeString": script.UnescapeString, "ScriptUnescapeString": script.UnescapeString,