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 {
CreatedAt *Time `db:"created_at"`
TargetId string `db:"target_id"`
Status TargetStatus `db:"status"`
Note string `db:"note"`
TargetId string `db:"target_id"`
WorkerGroupId string `db:"worker_group_id"`
CheckId string `db:"check_id"`
Status TargetStatus `db:"status"`
Note string `db:"note"`
}

View file

@ -92,15 +92,19 @@ END;
-- +migrate StatementEnd
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,
note TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ')),
PRIMARY KEY (target_id, created_at),
CONSTRAINT fk_target_histories_target FOREIGN KEY (target_id) REFERENCES targets(id) ON DELETE CASCADE
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_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;
-- +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{
TargetId: param.Target.Id,
Status: status,
Note: param.Note,
TargetId: param.Target.Id,
WorkerGroupId: param.WorkerGroupId,
CheckId: param.CheckId,
Status: status,
Note: param.Note,
})
return &temporal.ActivityAddTargetHistoryResult{}, err

View file

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

View file

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

View file

@ -5,20 +5,26 @@ import (
"fmt"
"time"
"github.com/mentos1386/zdravko/internal/temporal"
"go.temporal.io/api/enums/v1"
"go.temporal.io/api/workflowservice/v1"
"go.temporal.io/sdk/client"
)
type CheckHistory struct {
CheckId string
Status string
Duration time.Duration
CheckId string
Status string
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
response, err := temporal.ListWorkflow(ctx, &workflowservice.ListWorkflowExecutionsRequest{
response, err := t.ListWorkflow(ctx, &workflowservice.ListWorkflowExecutionsRequest{
PageSize: n,
})
if err != nil {
@ -29,21 +35,37 @@ func GetLastNCheckHistory(ctx context.Context, temporal client.Client, n int32)
for _, execution := range executions {
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{
CheckId: checkId,
Duration: execution.CloseTime.AsTime().Sub(execution.StartTime.AsTime()),
Status: execution.Status.String(),
CheckId: checkId,
Duration: execution.CloseTime.AsTime().Sub(execution.StartTime.AsTime()),
StartTime: execution.StartTime.AsTime(),
EndTime: execution.CloseTime.AsTime(),
Status: execution.Status.String(),
WorkerGroupName: execution.GetTaskQueue(),
Note: result.Note,
})
}
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
response, err := temporal.ListWorkflow(ctx, &workflowservice.ListWorkflowExecutionsRequest{
response, err := t.ListWorkflow(ctx, &workflowservice.ListWorkflowExecutionsRequest{
PageSize: 10,
Query: fmt.Sprintf(`TemporalScheduledById = "%s"`, getScheduleId(checkId)),
})
@ -55,11 +77,27 @@ func GetCheckHistoryForCheck(ctx context.Context, temporal client.Client, checkI
for _, execution := range executions {
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{
CheckId: checkId,
Duration: execution.CloseTime.AsTime().Sub(execution.StartTime.AsTime()),
Status: execution.Status.String(),
CheckId: checkId,
Duration: execution.CloseTime.AsTime().Sub(execution.StartTime.AsTime()),
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 (
"context"
"github.com/mentos1386/zdravko/database/models"
"github.com/jmoiron/sqlx"
"github.com/mentos1386/zdravko/database/models"
)
type TargetHistory struct {
*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) {
@ -17,9 +19,13 @@ func GetLastNTargetHistory(ctx context.Context, db *sqlx.DB, n int) ([]*TargetHi
err := db.SelectContext(ctx, &targetHistory, `
SELECT
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
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
@ -32,9 +38,13 @@ func GetTargetHistoryForTarget(ctx context.Context, db *sqlx.DB, targetId string
err := db.SelectContext(ctx, &targetHistory, `
SELECT
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
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
`, targetId)
@ -46,10 +56,14 @@ func AddHistoryForTarget(ctx context.Context, db *sqlx.DB, history *models.Targe
`
INSERT INTO target_histories (
target_id,
worker_group_id,
check_id,
status,
note
) VALUES (
:target_id,
:worker_group_id,
:check_id,
:status,
:note
)`,

View file

@ -1,7 +1,6 @@
package workflows
import (
"log/slog"
"sort"
"time"
@ -9,7 +8,7 @@ import (
"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
sort.Strings(workerGroupIds)
@ -25,7 +24,7 @@ func (w *Workflows) CheckWorkflowDefinition(ctx workflow.Context, param temporal
},
).Get(ctx, &targetsFilterResult)
if err != nil {
return err
return nil, err
}
for _, target := range targetsFilterResult.Targets {
@ -43,7 +42,7 @@ func (w *Workflows) CheckWorkflowDefinition(ctx workflow.Context, param temporal
},
).Get(ctx, &checkResult)
if err != nil {
return err
return nil, err
}
status := temporal.AddTargetHistoryStatusFailure
@ -59,18 +58,20 @@ func (w *Workflows) CheckWorkflowDefinition(ctx workflow.Context, param temporal
}),
temporal.ActivityAddTargetHistoryName,
&temporal.ActivityAddTargetHistoryParam{
Target: target,
Status: status,
Note: checkResult.Note,
Target: target,
WorkerGroupId: workerGroupId,
CheckId: param.CheckId,
Status: status,
Note: checkResult.Note,
},
).Get(ctx, &addTargetHistoryResult)
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 {
Target *Target
Status AddTargetHistoryStatus
Note string
Target *Target
WorkerGroupId string
CheckId string
Status AddTargetHistoryStatus
Note string
}
type ActivityAddTargetHistoryResult struct {

View file

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

View file

@ -991,6 +991,11 @@ video {
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 {
--tw-bg-opacity: 1;
background-color: rgb(254 226 226 / var(--tw-bg-opacity));
@ -1225,6 +1230,11 @@ video {
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 {
--tw-text-opacity: 1;
color: rgb(220 38 38 / var(--tw-text-opacity));

View file

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

View file

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

View file

@ -116,6 +116,7 @@
<tr>
<th>Status</th>
<th>Worker Group</th>
<th>Check</th>
<th>Created At</th>
<th>Duration</th>
<th>Note</th>
@ -142,6 +143,13 @@
{{ .WorkerGroupName }}
</span>
</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>
{{ .CreatedAt.Time.Format "2006-01-02 15:04:05" }}
</td>

View file

@ -26,6 +26,12 @@ func load(files ...string) *template.Template {
t := template.New("default").Funcs(
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,
"Now": time.Now,
"ScriptUnescapeString": script.UnescapeString,