feat: renaming things and thus breaking existing database schema

This commit is contained in:
Tine 2024-05-16 22:15:14 +02:00
parent 4603f7a79a
commit 1afde077a6
Signed by: mentos1386
SSH key fingerprint: SHA256:MNtTsLbihYaWF8j1fkOHfkKNlnN1JQfxEU/rBU8nCGw
39 changed files with 890 additions and 786 deletions

4
.gitignore vendored
View file

@ -6,9 +6,7 @@ package.json
node_modules/
# Database
zdravko.db*
zdravko_kv.db*
temporal.db*
store/
# Keys
*.pem

View file

@ -45,16 +45,16 @@ type OAuth2State struct {
ExpiresAt *Time `db:"expires_at"`
}
type MonitorStatus string
type CheckStatus string
const (
MonitorSuccess MonitorStatus = "SUCCESS"
MonitorFailure MonitorStatus = "FAILURE"
MonitorError MonitorStatus = "ERROR"
MonitorUnknown MonitorStatus = "UNKNOWN"
CheckSuccess CheckStatus = "SUCCESS"
CheckFailure CheckStatus = "FAILURE"
CheckError CheckStatus = "ERROR"
CheckUnknown CheckStatus = "UNKNOWN"
)
type Monitor struct {
type Check struct {
CreatedAt *Time `db:"created_at"`
UpdatedAt *Time `db:"updated_at"`
@ -66,18 +66,18 @@ type Monitor struct {
Script string `db:"script"`
}
type MonitorWithWorkerGroups struct {
Monitor
type CheckWithWorkerGroups struct {
Check
// List of worker group names
WorkerGroups []string
}
type MonitorHistory struct {
type CheckHistory struct {
CreatedAt *Time `db:"created_at"`
MonitorId string `db:"monitor_id"`
Status MonitorStatus `db:"status"`
CheckId string `db:"check_id"`
Status CheckStatus `db:"status"`
Note string `db:"note"`
WorkerGroupId string `db:"worker_group_id"`
@ -92,11 +92,11 @@ type WorkerGroup struct {
Name string `db:"name"`
}
type WorkerGroupWithMonitors struct {
type WorkerGroupWithChecks struct {
WorkerGroup
// List of worker group names
Monitors []string
Checks []string
}
type TriggerStatus string

View file

@ -6,7 +6,7 @@ CREATE TABLE oauth2_states (
PRIMARY KEY (state)
) STRICT;
CREATE TABLE monitors (
CREATE TABLE checks (
id TEXT NOT NULL,
name TEXT NOT NULL,
"group" TEXT NOT NULL DEFAULT 'default',
@ -17,12 +17,12 @@ CREATE TABLE monitors (
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ')),
PRIMARY KEY (id),
CONSTRAINT unique_monitors_name UNIQUE (name)
CONSTRAINT unique_checks_name UNIQUE (name)
) STRICT;
--CREATE TRIGGER monitors_updated_timestamp AFTER UPDATE ON monitors BEGIN
-- update monitors set updated_at = strftime('%Y-%m-%dT%H:%M:%fZ') where id = new.id;
--CREATE TRIGGER checks_updated_timestamp AFTER UPDATE ON checks BEGIN
-- update checks set updated_at = strftime('%Y-%m-%dT%H:%M:%fZ') where id = new.id;
--END;
CREATE TABLE worker_groups (
@ -40,17 +40,17 @@ CREATE TABLE worker_groups (
-- update worker_groups set updated_at = strftime('%Y-%m-%dT%H:%M:%fZ') where id = new.id;
--END;
CREATE TABLE monitor_worker_groups (
CREATE TABLE check_worker_groups (
worker_group_id TEXT NOT NULL,
monitor_id TEXT NOT NULL,
check_id TEXT NOT NULL,
PRIMARY KEY (worker_group_id,monitor_id),
CONSTRAINT fk_monitor_worker_groups_worker_group FOREIGN KEY (worker_group_id) REFERENCES worker_groups(id) ON DELETE CASCADE,
CONSTRAINT fk_monitor_worker_groups_monitor FOREIGN KEY (monitor_id) REFERENCES monitors(id) ON DELETE CASCADE
PRIMARY KEY (worker_group_id,check_id),
CONSTRAINT fk_check_worker_groups_worker_group FOREIGN KEY (worker_group_id) REFERENCES worker_groups(id) ON DELETE CASCADE,
CONSTRAINT fk_check_worker_groups_check FOREIGN KEY (check_id) REFERENCES checks(id) ON DELETE CASCADE
) STRICT;
CREATE TABLE monitor_histories (
monitor_id TEXT NOT NULL,
CREATE TABLE check_histories (
check_id TEXT NOT NULL,
worker_group_id TEXT NOT NULL,
status TEXT NOT NULL,
@ -58,14 +58,41 @@ CREATE TABLE monitor_histories (
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ')),
PRIMARY KEY (monitor_id, worker_group_id, created_at),
CONSTRAINT fk_monitor_histories_monitor FOREIGN KEY (monitor_id) REFERENCES monitors(id) ON DELETE CASCADE,
CONSTRAINT fk_monitor_histories_worker_group FOREIGN KEY (worker_group_id) REFERENCES worker_groups(id) ON DELETE CASCADE
PRIMARY KEY (check_id, worker_group_id, created_at),
CONSTRAINT fk_check_histories_check FOREIGN KEY (check_id) REFERENCES checks(id) ON DELETE CASCADE,
CONSTRAINT fk_check_histories_worker_group FOREIGN KEY (worker_group_id) REFERENCES worker_groups(id) ON DELETE CASCADE
) STRICT;
CREATE TABLE triggers (
id TEXT NOT NULL,
name TEXT NOT NULL,
script TEXT NOT NULL,
status TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ')),
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ')),
PRIMARY KEY (id),
CONSTRAINT unique_triggers_name UNIQUE (name)
) STRICT;
CREATE TABLE trigger_histories (
trigger_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 (trigger_id, created_at),
CONSTRAINT fk_trigger_histories_trigger FOREIGN KEY (trigger_id) REFERENCES triggers(id) ON DELETE CASCADE
) STRICT;
-- +migrate Down
DROP TABLE oauth2_states;
DROP TABLE monitor_worker_groups;
DROP TABLE check_worker_groups;
DROP TABLE worker_groups;
DROP TABLE monitor_histories;
DROP TABLE monitors;
DROP TABLE check_histories;
DROP TABLE checks;
DROP TABLE triggers;
DROP TABLE trigger_histories;

View file

@ -1,29 +0,0 @@
-- +migrate Up
CREATE TABLE triggers (
id TEXT NOT NULL,
name TEXT NOT NULL,
script TEXT NOT NULL,
status TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ')),
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ')),
PRIMARY KEY (id),
CONSTRAINT unique_triggers_name UNIQUE (name)
) STRICT;
CREATE TABLE trigger_histories (
trigger_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 (trigger_id, created_at),
CONSTRAINT fk_trigger_histories_trigger FOREIGN KEY (trigger_id) REFERENCES triggers(id) ON DELETE CASCADE
) STRICT;
-- +migrate Down
DROP TABLE triggers;
DROP TABLE trigger_histories;

View file

@ -17,9 +17,9 @@ primary_region = 'waw'
ROOT_URL = 'https://zdravko.mnts.dev'
TEMPORAL_SERVER_HOST = 'server.process.zdravko.internal:7233'
TEMPORAL_DATABASE_PATH = '/data/temporal-9.db'
SQLITE_DATABASE_PATH = '/data/zdravko-9.db'
KEYVALUE_DATABASE_PATH = '/data/zdravko_kv.db'
TEMPORAL_DATABASE_PATH = '/data/temporal-10.db'
SQLITE_DATABASE_PATH = '/data/zdravko-10.db'
KEYVALUE_DATABASE_PATH = '/data/zdravko_kv-10.db'
[processes]
server = '--temporal --server'

View file

@ -18,12 +18,12 @@ type HealtcheckParam struct {
Script string
}
type MonitorResult struct {
type CheckResult struct {
Success bool
Note string
}
func (a *Activities) Monitor(ctx context.Context, param HealtcheckParam) (*MonitorResult, error) {
func (a *Activities) Check(ctx context.Context, param HealtcheckParam) (*CheckResult, error) {
execution := k6.NewExecution(slog.Default(), script.UnescapeString(param.Script))
result, err := execution.Run(ctx)
@ -31,23 +31,23 @@ func (a *Activities) Monitor(ctx context.Context, param HealtcheckParam) (*Monit
return nil, err
}
return &MonitorResult{Success: result.Success, Note: result.Note}, nil
return &CheckResult{Success: result.Success, Note: result.Note}, nil
}
type HealtcheckAddToHistoryParam struct {
MonitorId string
Status models.MonitorStatus
CheckId string
Status models.CheckStatus
Note string
WorkerGroupId string
}
type MonitorAddToHistoryResult struct {
type CheckAddToHistoryResult struct {
}
func (a *Activities) MonitorAddToHistory(ctx context.Context, param HealtcheckAddToHistoryParam) (*MonitorAddToHistoryResult, error) {
url := fmt.Sprintf("%s/api/v1/monitors/%s/history", a.config.ApiUrl, param.MonitorId)
func (a *Activities) CheckAddToHistory(ctx context.Context, param HealtcheckAddToHistoryParam) (*CheckAddToHistoryResult, error) {
url := fmt.Sprintf("%s/api/v1/checks/%s/history", a.config.ApiUrl, param.CheckId)
body := api.ApiV1MonitorsHistoryPOSTBody{
body := api.ApiV1ChecksHistoryPOSTBody{
Status: param.Status,
Note: param.Note,
WorkerGroupId: param.WorkerGroupId,
@ -73,5 +73,5 @@ func (a *Activities) MonitorAddToHistory(ctx context.Context, param HealtcheckAd
return nil, fmt.Errorf("unexpected status code: %d", response.StatusCode)
}
return &MonitorAddToHistoryResult{}, nil
return &CheckAddToHistoryResult{}, nil
}

View file

@ -48,8 +48,8 @@ func NewServerConfig() *ServerConfig {
// Set defaults
v.SetDefault("port", GetEnvOrDefault("PORT", "8000"))
v.SetDefault("rooturl", GetEnvOrDefault("ROOT_URL", "http://localhost:8000"))
v.SetDefault("sqlitedatabasepath", GetEnvOrDefault("SQLITE_DATABASE_PATH", "zdravko.db"))
v.SetDefault("keyvaluedatabasepath", GetEnvOrDefault("KEYVALUE_DATABASE_PATH", "zdravko_kv.db"))
v.SetDefault("sqlitedatabasepath", GetEnvOrDefault("SQLITE_DATABASE_PATH", "store/zdravko.db"))
v.SetDefault("keyvaluedatabasepath", GetEnvOrDefault("KEYVALUE_DATABASE_PATH", "store/zdravko_kv.db"))
v.SetDefault("sessionsecret", os.Getenv("SESSION_SECRET"))
v.SetDefault("temporal.uihost", GetEnvOrDefault("TEMPORAL_UI_HOST", "127.0.0.1:8223"))
v.SetDefault("temporal.serverhost", GetEnvOrDefault("TEMPORAL_SERVER_HOST", "127.0.0.1:7233"))

View file

@ -23,7 +23,7 @@ func NewTemporalConfig() *TemporalConfig {
v := newViper()
// Set defaults
v.SetDefault("databasepath", GetEnvOrDefault("TEMPORAL_DATABASE_PATH", "temporal.db"))
v.SetDefault("databasepath", GetEnvOrDefault("TEMPORAL_DATABASE_PATH", "store/temporal.db"))
v.SetDefault("listenaddress", GetEnvOrDefault("TEMPORAL_LISTEN_ADDRESS", "0.0.0.0"))
v.SetDefault("jwt.publickey", os.Getenv("JWT_PUBLIC_KEY"))

View file

@ -40,26 +40,26 @@ func (h *BaseHandler) ApiV1WorkersConnectGET(c echo.Context) error {
// TODO: Can we instead get this from the Workflow outcome?
//
// To somehow listen for the outcomes and then store them automatically.
func (h *BaseHandler) ApiV1MonitorsHistoryPOST(c echo.Context) error {
func (h *BaseHandler) ApiV1ChecksHistoryPOST(c echo.Context) error {
ctx := context.Background()
id := c.Param("id")
var body api.ApiV1MonitorsHistoryPOSTBody
var body api.ApiV1ChecksHistoryPOSTBody
err := (&echo.DefaultBinder{}).BindBody(c, &body)
if err != nil {
return err
}
_, err = services.GetMonitor(ctx, h.db, id)
_, err = services.GetCheck(ctx, h.db, id)
if err != nil {
if err == sql.ErrNoRows {
return echo.NewHTTPError(http.StatusNotFound, "Monitor not found")
return echo.NewHTTPError(http.StatusNotFound, "Check not found")
}
return err
}
err = services.AddHistoryForMonitor(ctx, h.db, &models.MonitorHistory{
MonitorId: id,
err = services.AddHistoryForCheck(ctx, h.db, &models.CheckHistory{
CheckId: id,
WorkerGroupId: body.WorkerGroupId,
Status: body.Status,
Note: body.Note,

View file

@ -18,7 +18,7 @@ import (
var examplesYaml embed.FS
type examples struct {
Monitor string `yaml:"monitor"`
Check string `yaml:"check"`
Trigger string `yaml:"trigger"`
}
@ -63,7 +63,7 @@ func NewBaseHandler(db *sqlx.DB, kvStore kv.KeyValueStore, temporal client.Clien
panic(err)
}
examples.Monitor = script.EscapeString(examples.Monitor)
examples.Check = script.EscapeString(examples.Check)
examples.Trigger = script.EscapeString(examples.Trigger)
return &BaseHandler{

View file

@ -13,21 +13,21 @@ import (
type IndexData struct {
*components.Base
Monitors map[string]MonitorsAndStatus
MonitorsLength int
Checks map[string]ChecksAndStatus
ChecksLength int
TimeRange string
Status models.MonitorStatus
Status models.CheckStatus
}
type Monitor struct {
type Check struct {
Name string
Group string
Status models.MonitorStatus
Status models.CheckStatus
History *History
}
type HistoryItem struct {
Status models.MonitorStatus
Status models.CheckStatus
Date time.Time
}
@ -36,23 +36,23 @@ type History struct {
Uptime int
}
type MonitorsAndStatus struct {
Status models.MonitorStatus
Monitors []*Monitor
type ChecksAndStatus struct {
Status models.CheckStatus
Checks []*Check
}
func getDateString(date time.Time) string {
return date.UTC().Format("2006-01-02T15:04:05")
}
func getHistory(history []*models.MonitorHistory, period time.Duration, buckets int) *History {
historyMap := map[string]models.MonitorStatus{}
func getHistory(history []*models.CheckHistory, period time.Duration, buckets int) *History {
historyMap := map[string]models.CheckStatus{}
numOfSuccess := 0
numTotal := 0
for i := 0; i < buckets; i++ {
dateString := getDateString(time.Now().Add(period * time.Duration(-i)).Truncate(period))
historyMap[dateString] = models.MonitorUnknown
historyMap[dateString] = models.CheckUnknown
}
for _, _history := range history {
@ -64,12 +64,12 @@ func getHistory(history []*models.MonitorHistory, period time.Duration, buckets
}
numTotal++
if _history.Status == models.MonitorSuccess {
if _history.Status == models.CheckSuccess {
numOfSuccess++
}
// skip if it is already set to failure
if historyMap[dateString] == models.MonitorFailure {
if historyMap[dateString] == models.CheckFailure {
continue
}
@ -101,7 +101,7 @@ func getHistory(history []*models.MonitorHistory, period time.Duration, buckets
func (h *BaseHandler) Index(c echo.Context) error {
ctx := context.Background()
monitors, err := services.GetMonitors(ctx, h.db)
checks, err := services.GetChecks(ctx, h.db)
if err != nil {
return err
}
@ -111,12 +111,12 @@ func (h *BaseHandler) Index(c echo.Context) error {
timeRange = "90days"
}
overallStatus := models.MonitorUnknown
statusByGroup := make(map[string]models.MonitorStatus)
overallStatus := models.CheckUnknown
statusByGroup := make(map[string]models.CheckStatus)
monitorsWithHistory := make([]*Monitor, len(monitors))
for i, monitor := range monitors {
history, err := services.GetMonitorHistoryForMonitor(ctx, h.db, monitor.Id)
checksWithHistory := make([]*Check, len(checks))
for i, check := range checks {
history, err := services.GetCheckHistoryForCheck(ctx, h.db, check.Id)
if err != nil {
return err
}
@ -131,37 +131,37 @@ func (h *BaseHandler) Index(c echo.Context) error {
historyResult = getHistory(history, time.Minute, 90)
}
if statusByGroup[monitor.Group] == "" {
statusByGroup[monitor.Group] = models.MonitorUnknown
if statusByGroup[check.Group] == "" {
statusByGroup[check.Group] = models.CheckUnknown
}
status := historyResult.List[len(historyResult.List)-1]
if status.Status == models.MonitorSuccess {
if overallStatus == models.MonitorUnknown {
if status.Status == models.CheckSuccess {
if overallStatus == models.CheckUnknown {
overallStatus = status.Status
}
if statusByGroup[monitor.Group] == models.MonitorUnknown {
statusByGroup[monitor.Group] = status.Status
if statusByGroup[check.Group] == models.CheckUnknown {
statusByGroup[check.Group] = status.Status
}
}
if status.Status != models.MonitorSuccess && status.Status != models.MonitorUnknown {
if status.Status != models.CheckSuccess && status.Status != models.CheckUnknown {
overallStatus = status.Status
statusByGroup[monitor.Group] = status.Status
statusByGroup[check.Group] = status.Status
}
monitorsWithHistory[i] = &Monitor{
Name: monitor.Name,
Group: monitor.Group,
checksWithHistory[i] = &Check{
Name: check.Name,
Group: check.Group,
Status: status.Status,
History: historyResult,
}
}
monitorsByGroup := map[string]MonitorsAndStatus{}
for _, monitor := range monitorsWithHistory {
monitorsByGroup[monitor.Group] = MonitorsAndStatus{
Status: statusByGroup[monitor.Group],
Monitors: append(monitorsByGroup[monitor.Group].Monitors, monitor),
checksByGroup := map[string]ChecksAndStatus{}
for _, check := range checksWithHistory {
checksByGroup[check.Group] = ChecksAndStatus{
Status: statusByGroup[check.Group],
Checks: append(checksByGroup[check.Group].Checks, check),
}
}
@ -172,7 +172,7 @@ func (h *BaseHandler) Index(c echo.Context) error {
NavbarActive: GetPageByTitle(Pages, "Status"),
Navbar: Pages,
},
Monitors: monitorsByGroup,
Checks: checksByGroup,
TimeRange: timeRange,
Status: overallStatus,
})

View file

@ -36,11 +36,13 @@ func NewSettings(user *AuthenticatedUser, page *components.Page, breadCrumbs []*
var SettingsPages = []*components.Page{
{Path: "/settings", Title: "Overview", Breadcrumb: "Overview"},
{Path: "/settings/targets", Title: "Incidents", Breadcrumb: "Incidents"},
{Path: "/settings/incidents", Title: "Incidents", Breadcrumb: "Incidents"},
{Path: "/settings/targets", Title: "Targets", Breadcrumb: "Targets"},
{Path: "/settings/targets/create", Title: "Targets Create", Breadcrumb: "Create"},
{Path: "/settings/monitors", Title: "Checks", Breadcrumb: "Checks"},
{Path: "/settings/monitors/create", Title: "Checks Create", Breadcrumb: "Create"},
{Path: "/settings/hooks", Title: "Hooks", Breadcrumb: "Hooks"},
{Path: "/settings/hooks/create", Title: "Hooks Create", Breadcrumb: "Create"},
{Path: "/settings/checks", Title: "Checks", Breadcrumb: "Checks"},
{Path: "/settings/checks/create", Title: "Checks Create", Breadcrumb: "Create"},
{Path: "/settings/worker-groups", Title: "Worker Groups", Breadcrumb: "Worker Groups"},
{Path: "/settings/worker-groups/create", Title: "Worker Groups Create", Breadcrumb: "Create"},
{Path: "/settings/notifications", Title: "Notifications", Breadcrumb: "Notifications"},
@ -63,10 +65,11 @@ var SettingsSidebar = []SettingsSidebarGroup{
Pages: []*components.Page{
GetPageByTitle(SettingsPages, "Targets"),
GetPageByTitle(SettingsPages, "Checks"),
GetPageByTitle(SettingsPages, "Hooks"),
},
},
{
Group: "Decide",
Group: "Alert",
Pages: []*components.Page{
GetPageByTitle(SettingsPages, "Triggers"),
},
@ -91,9 +94,9 @@ var SettingsSidebar = []SettingsSidebarGroup{
type SettingsOverview struct {
*Settings
WorkerGroupsCount int
MonitorsCount int
ChecksCount int
NotificationsCount int
History []*services.MonitorHistoryWithMonitor
History []*services.CheckHistoryWithCheck
}
func (h *BaseHandler) SettingsOverviewGET(c echo.Context) error {
@ -105,12 +108,12 @@ func (h *BaseHandler) SettingsOverviewGET(c echo.Context) error {
return err
}
monitors, err := services.CountMonitors(ctx, h.db)
checks, err := services.CountChecks(ctx, h.db)
if err != nil {
return err
}
history, err := services.GetLastNMonitorHistory(ctx, h.db, 10)
history, err := services.GetLastNCheckHistory(ctx, h.db, 10)
if err != nil {
return err
}
@ -122,7 +125,7 @@ func (h *BaseHandler) SettingsOverviewGET(c echo.Context) error {
[]*components.Page{GetPageByTitle(SettingsPages, "Overview")},
),
WorkerGroupsCount: workerGroups,
MonitorsCount: monitors,
ChecksCount: checks,
NotificationsCount: 42,
History: history,
})

View file

@ -0,0 +1,30 @@
package handlers
import (
"net/http"
"code.tjo.space/mentos1386/zdravko/web/templates/components"
"github.com/labstack/echo/v4"
)
type Incident struct{}
type SettingsIncidents struct {
*Settings
Incidents []*Incident
}
func (h *BaseHandler) SettingsIncidentsGET(c echo.Context) error {
cc := c.(AuthenticatedContext)
incidents := make([]*Incident, 0)
return c.Render(http.StatusOK, "settings_incidents.tmpl", &SettingsIncidents{
Settings: NewSettings(
cc.Principal.User,
GetPageByTitle(SettingsPages, "Incidents"),
[]*components.Page{GetPageByTitle(SettingsPages, "Incidents")},
),
Incidents: incidents,
})
}

View file

@ -14,13 +14,6 @@ import (
"github.com/labstack/echo/v4"
)
type Incident struct{}
type SettingsIncidents struct {
*Settings
Incidents []*Incident
}
type CreateTrigger struct {
Name string `validate:"required"`
Script string `validate:"required"`

View file

@ -17,7 +17,7 @@ import (
"github.com/labstack/echo/v4"
)
type CreateMonitor struct {
type CreateCheck struct {
Name string `validate:"required"`
Group string `validate:"required"`
WorkerGroups string `validate:"required"`
@ -25,96 +25,96 @@ type CreateMonitor struct {
Script string `validate:"required"`
}
type UpdateMonitor struct {
type UpdateCheck struct {
Group string `validate:"required"`
WorkerGroups string `validate:"required"`
Schedule string `validate:"required,cron"`
Script string `validate:"required"`
}
type MonitorWithWorkerGroupsAndStatus struct {
*models.MonitorWithWorkerGroups
Status services.MonitorStatus
type CheckWithWorkerGroupsAndStatus struct {
*models.CheckWithWorkerGroups
Status services.CheckStatus
}
type SettingsMonitors struct {
type SettingsChecks struct {
*Settings
Monitors map[string][]*MonitorWithWorkerGroupsAndStatus
MonitorGroups []string
Checks map[string][]*CheckWithWorkerGroupsAndStatus
CheckGroups []string
}
type SettingsMonitor struct {
type SettingsCheck struct {
*Settings
Monitor *MonitorWithWorkerGroupsAndStatus
History []*models.MonitorHistory
Check *CheckWithWorkerGroupsAndStatus
History []*models.CheckHistory
}
type SettingsMonitorCreate struct {
type SettingsCheckCreate struct {
*Settings
Example string
}
func (h *BaseHandler) SettingsMonitorsGET(c echo.Context) error {
func (h *BaseHandler) SettingsChecksGET(c echo.Context) error {
cc := c.(AuthenticatedContext)
monitors, err := services.GetMonitorsWithWorkerGroups(context.Background(), h.db)
checks, err := services.GetChecksWithWorkerGroups(context.Background(), h.db)
if err != nil {
return err
}
monitorsWithStatus := make([]*MonitorWithWorkerGroupsAndStatus, len(monitors))
for i, monitor := range monitors {
status, err := services.GetMonitorStatus(context.Background(), h.temporal, monitor.Id)
checksWithStatus := make([]*CheckWithWorkerGroupsAndStatus, len(checks))
for i, check := range checks {
status, err := services.GetCheckStatus(context.Background(), h.temporal, check.Id)
if err != nil {
return err
}
monitorsWithStatus[i] = &MonitorWithWorkerGroupsAndStatus{
MonitorWithWorkerGroups: monitor,
checksWithStatus[i] = &CheckWithWorkerGroupsAndStatus{
CheckWithWorkerGroups: check,
Status: status,
}
}
monitorGroups := []string{}
monitorsByGroup := map[string][]*MonitorWithWorkerGroupsAndStatus{}
for _, monitor := range monitorsWithStatus {
monitorsByGroup[monitor.Group] = append(monitorsByGroup[monitor.Group], monitor)
if slices.Contains(monitorGroups, monitor.Group) == false {
monitorGroups = append(monitorGroups, monitor.Group)
checkGroups := []string{}
checksByGroup := map[string][]*CheckWithWorkerGroupsAndStatus{}
for _, check := range checksWithStatus {
checksByGroup[check.Group] = append(checksByGroup[check.Group], check)
if slices.Contains(checkGroups, check.Group) == false {
checkGroups = append(checkGroups, check.Group)
}
}
return c.Render(http.StatusOK, "settings_monitors.tmpl", &SettingsMonitors{
return c.Render(http.StatusOK, "settings_checks.tmpl", &SettingsChecks{
Settings: NewSettings(
cc.Principal.User,
GetPageByTitle(SettingsPages, "Checks"),
[]*components.Page{GetPageByTitle(SettingsPages, "Checks")},
),
Monitors: monitorsByGroup,
MonitorGroups: monitorGroups,
Checks: checksByGroup,
CheckGroups: checkGroups,
})
}
func (h *BaseHandler) SettingsMonitorsDescribeGET(c echo.Context) error {
func (h *BaseHandler) SettingsChecksDescribeGET(c echo.Context) error {
cc := c.(AuthenticatedContext)
slug := c.Param("id")
monitor, err := services.GetMonitorWithWorkerGroups(context.Background(), h.db, slug)
check, err := services.GetCheckWithWorkerGroups(context.Background(), h.db, slug)
if err != nil {
return err
}
status, err := services.GetMonitorStatus(context.Background(), h.temporal, monitor.Id)
status, err := services.GetCheckStatus(context.Background(), h.temporal, check.Id)
if err != nil {
return err
}
monitorWithStatus := &MonitorWithWorkerGroupsAndStatus{
MonitorWithWorkerGroups: monitor,
checkWithStatus := &CheckWithWorkerGroupsAndStatus{
CheckWithWorkerGroups: check,
Status: status,
}
history, err := services.GetMonitorHistoryForMonitor(context.Background(), h.db, slug)
history, err := services.GetCheckHistoryForCheck(context.Background(), h.db, slug)
if err != nil {
return err
}
@ -124,76 +124,76 @@ func (h *BaseHandler) SettingsMonitorsDescribeGET(c echo.Context) error {
maxElements = len(history)
}
return c.Render(http.StatusOK, "settings_monitors_describe.tmpl", &SettingsMonitor{
return c.Render(http.StatusOK, "settings_checks_describe.tmpl", &SettingsCheck{
Settings: NewSettings(
cc.Principal.User,
GetPageByTitle(SettingsPages, "Checks"),
[]*components.Page{
GetPageByTitle(SettingsPages, "Checks"),
{
Path: fmt.Sprintf("/settings/monitors/%s", slug),
Path: fmt.Sprintf("/settings/checks/%s", slug),
Title: "Describe",
Breadcrumb: monitor.Name,
Breadcrumb: check.Name,
},
}),
Monitor: monitorWithStatus,
Check: checkWithStatus,
History: history[:maxElements],
})
}
func (h *BaseHandler) SettingsMonitorsDescribeDELETE(c echo.Context) error {
func (h *BaseHandler) SettingsChecksDescribeDELETE(c echo.Context) error {
slug := c.Param("id")
err := services.DeleteMonitor(context.Background(), h.db, slug)
err := services.DeleteCheck(context.Background(), h.db, slug)
if err != nil {
return err
}
err = services.DeleteMonitorSchedule(context.Background(), h.temporal, slug)
err = services.DeleteCheckSchedule(context.Background(), h.temporal, slug)
if err != nil {
return err
}
return c.Redirect(http.StatusSeeOther, "/settings/monitors")
return c.Redirect(http.StatusSeeOther, "/settings/checks")
}
func (h *BaseHandler) SettingsMonitorsDisableGET(c echo.Context) error {
func (h *BaseHandler) SettingsChecksDisableGET(c echo.Context) error {
slug := c.Param("id")
monitor, err := services.GetMonitor(context.Background(), h.db, slug)
check, err := services.GetCheck(context.Background(), h.db, slug)
if err != nil {
return err
}
err = services.SetMonitorStatus(context.Background(), h.temporal, monitor.Id, services.MonitorStatusPaused)
err = services.SetCheckStatus(context.Background(), h.temporal, check.Id, services.CheckStatusPaused)
if err != nil {
return err
}
return c.Redirect(http.StatusSeeOther, fmt.Sprintf("/settings/monitors/%s", slug))
return c.Redirect(http.StatusSeeOther, fmt.Sprintf("/settings/checks/%s", slug))
}
func (h *BaseHandler) SettingsMonitorsEnableGET(c echo.Context) error {
func (h *BaseHandler) SettingsChecksEnableGET(c echo.Context) error {
slug := c.Param("id")
monitor, err := services.GetMonitor(context.Background(), h.db, slug)
check, err := services.GetCheck(context.Background(), h.db, slug)
if err != nil {
return err
}
err = services.SetMonitorStatus(context.Background(), h.temporal, monitor.Id, services.MonitorStatusActive)
err = services.SetCheckStatus(context.Background(), h.temporal, check.Id, services.CheckStatusActive)
if err != nil {
return err
}
return c.Redirect(http.StatusSeeOther, fmt.Sprintf("/settings/monitors/%s", slug))
return c.Redirect(http.StatusSeeOther, fmt.Sprintf("/settings/checks/%s", slug))
}
func (h *BaseHandler) SettingsMonitorsDescribePOST(c echo.Context) error {
func (h *BaseHandler) SettingsChecksDescribePOST(c echo.Context) error {
ctx := context.Background()
monitorId := c.Param("id")
checkId := c.Param("id")
update := UpdateMonitor{
update := UpdateCheck{
Group: strings.ToLower(c.FormValue("group")),
WorkerGroups: strings.ToLower(strings.TrimSpace(c.FormValue("workergroups"))),
Schedule: c.FormValue("schedule"),
@ -204,18 +204,18 @@ func (h *BaseHandler) SettingsMonitorsDescribePOST(c echo.Context) error {
return err
}
monitor, err := services.GetMonitor(ctx, h.db, monitorId)
check, err := services.GetCheck(ctx, h.db, checkId)
if err != nil {
return err
}
monitor.Group = update.Group
monitor.Schedule = update.Schedule
monitor.Script = update.Script
check.Group = update.Group
check.Schedule = update.Schedule
check.Script = update.Script
err = services.UpdateMonitor(
err = services.UpdateCheck(
ctx,
h.db,
monitor,
check,
)
if err != nil {
return err
@ -241,23 +241,23 @@ func (h *BaseHandler) SettingsMonitorsDescribePOST(c echo.Context) error {
workerGroups = append(workerGroups, workerGroup)
}
err = services.UpdateMonitorWorkerGroups(ctx, h.db, monitor, workerGroups)
err = services.UpdateCheckWorkerGroups(ctx, h.db, check, workerGroups)
if err != nil {
return err
}
err = services.CreateOrUpdateMonitorSchedule(ctx, h.temporal, monitor, workerGroups)
err = services.CreateOrUpdateCheckSchedule(ctx, h.temporal, check, workerGroups)
if err != nil {
return err
}
return c.Redirect(http.StatusSeeOther, fmt.Sprintf("/settings/monitors/%s", monitorId))
return c.Redirect(http.StatusSeeOther, fmt.Sprintf("/settings/checks/%s", checkId))
}
func (h *BaseHandler) SettingsMonitorsCreateGET(c echo.Context) error {
func (h *BaseHandler) SettingsChecksCreateGET(c echo.Context) error {
cc := c.(AuthenticatedContext)
return c.Render(http.StatusOK, "settings_monitors_create.tmpl", &SettingsMonitorCreate{
return c.Render(http.StatusOK, "settings_checks_create.tmpl", &SettingsCheckCreate{
Settings: NewSettings(
cc.Principal.User,
GetPageByTitle(SettingsPages, "Checks"),
@ -266,15 +266,15 @@ func (h *BaseHandler) SettingsMonitorsCreateGET(c echo.Context) error {
GetPageByTitle(SettingsPages, "Checks Create"),
},
),
Example: h.examples.Monitor,
Example: h.examples.Check,
})
}
func (h *BaseHandler) SettingsMonitorsCreatePOST(c echo.Context) error {
func (h *BaseHandler) SettingsChecksCreatePOST(c echo.Context) error {
ctx := context.Background()
monitorId := slug.Make(c.FormValue("name"))
checkId := slug.Make(c.FormValue("name"))
create := CreateMonitor{
create := CreateCheck{
Name: c.FormValue("name"),
Group: strings.ToLower(c.FormValue("group")),
WorkerGroups: strings.ToLower(strings.TrimSpace(c.FormValue("workergroups"))),
@ -306,32 +306,32 @@ func (h *BaseHandler) SettingsMonitorsCreatePOST(c echo.Context) error {
workerGroups = append(workerGroups, workerGroup)
}
monitor := &models.Monitor{
check := &models.Check{
Name: create.Name,
Group: create.Group,
Id: monitorId,
Id: checkId,
Schedule: create.Schedule,
Script: create.Script,
}
err = services.CreateMonitor(
err = services.CreateCheck(
ctx,
h.db,
monitor,
check,
)
if err != nil {
return err
}
err = services.UpdateMonitorWorkerGroups(ctx, h.db, monitor, workerGroups)
err = services.UpdateCheckWorkerGroups(ctx, h.db, check, workerGroups)
if err != nil {
return err
}
err = services.CreateOrUpdateMonitorSchedule(ctx, h.temporal, monitor, workerGroups)
err = services.CreateOrUpdateCheckSchedule(ctx, h.temporal, check, workerGroups)
if err != nil {
return err
}
return c.Redirect(http.StatusSeeOther, "/settings/monitors")
return c.Redirect(http.StatusSeeOther, "/settings/checks")
}

View file

@ -22,7 +22,7 @@ type WorkerWithTokenAndActiveWorkers struct {
}
type WorkerGroupWithActiveWorkers struct {
*models.WorkerGroupWithMonitors
*models.WorkerGroupWithChecks
ActiveWorkers []string
}
@ -39,7 +39,7 @@ type SettingsWorker struct {
func (h *BaseHandler) SettingsWorkerGroupsGET(c echo.Context) error {
cc := c.(AuthenticatedContext)
workerGroups, err := services.GetWorkerGroupsWithMonitors(context.Background(), h.db)
workerGroups, err := services.GetWorkerGroupsWithChecks(context.Background(), h.db)
if err != nil {
return err
}
@ -51,7 +51,7 @@ func (h *BaseHandler) SettingsWorkerGroupsGET(c echo.Context) error {
return err
}
workerGroupsWithActiveWorkers[i] = &WorkerGroupWithActiveWorkers{
WorkerGroupWithMonitors: workerGroup,
WorkerGroupWithChecks: workerGroup,
ActiveWorkers: activeWorkers,
}
}

316
internal/services/check.go Normal file
View file

@ -0,0 +1,316 @@
package services
import (
"context"
"log"
"sort"
"time"
"code.tjo.space/mentos1386/zdravko/database/models"
"code.tjo.space/mentos1386/zdravko/internal/workflows"
"github.com/jmoiron/sqlx"
"go.temporal.io/sdk/client"
"go.temporal.io/sdk/temporal"
"golang.org/x/exp/maps"
)
type CheckStatus string
const (
CheckStatusUnknown CheckStatus = "UNKNOWN"
CheckStatusPaused CheckStatus = "PAUSED"
CheckStatusActive CheckStatus = "ACTIVE"
)
func getScheduleId(id string) string {
return "check-" + id
}
func CountChecks(ctx context.Context, db *sqlx.DB) (int, error) {
var count int
err := db.GetContext(ctx, &count, "SELECT COUNT(*) FROM checks")
return count, err
}
func GetCheckStatus(ctx context.Context, temporal client.Client, id string) (CheckStatus, error) {
schedule := temporal.ScheduleClient().GetHandle(ctx, getScheduleId(id))
description, err := schedule.Describe(ctx)
if err != nil {
return CheckStatusUnknown, err
}
if description.Schedule.State.Paused {
return CheckStatusPaused, nil
}
return CheckStatusActive, nil
}
func SetCheckStatus(ctx context.Context, temporal client.Client, id string, status CheckStatus) error {
schedule := temporal.ScheduleClient().GetHandle(ctx, getScheduleId(id))
if status == CheckStatusActive {
return schedule.Unpause(ctx, client.ScheduleUnpauseOptions{Note: "Unpaused by user"})
}
if status == CheckStatusPaused {
return schedule.Pause(ctx, client.SchedulePauseOptions{Note: "Paused by user"})
}
return nil
}
func CreateCheck(ctx context.Context, db *sqlx.DB, check *models.Check) error {
_, err := db.NamedExecContext(ctx,
`INSERT INTO checks (id, name, "group", script, schedule) VALUES (:id, :name, :group, :script, :schedule)`,
check,
)
return err
}
func UpdateCheck(ctx context.Context, db *sqlx.DB, check *models.Check) error {
_, err := db.NamedExecContext(ctx,
`UPDATE checks SET "group"=:group, script=:script, schedule=:schedule WHERE id=:id`,
check,
)
return err
}
func DeleteCheck(ctx context.Context, db *sqlx.DB, id string) error {
_, err := db.ExecContext(ctx,
"DELETE FROM checks WHERE id=$1",
id,
)
return err
}
func UpdateCheckWorkerGroups(ctx context.Context, db *sqlx.DB, check *models.Check, workerGroups []*models.WorkerGroup) error {
tx, err := db.BeginTxx(ctx, nil)
if err != nil {
return err
}
_, err = tx.ExecContext(ctx,
"DELETE FROM check_worker_groups WHERE check_id=$1",
check.Id,
)
if err != nil {
tx.Rollback()
return err
}
for _, group := range workerGroups {
_, err = tx.ExecContext(ctx,
"INSERT INTO check_worker_groups (check_id, worker_group_id) VALUES ($1, $2)",
check.Id,
group.Id,
)
if err != nil {
tx.Rollback()
return err
}
}
return tx.Commit()
}
func GetCheck(ctx context.Context, db *sqlx.DB, id string) (*models.Check, error) {
check := &models.Check{}
err := db.GetContext(ctx, check,
"SELECT * FROM checks WHERE id=$1",
id,
)
return check, err
}
func GetCheckWithWorkerGroups(ctx context.Context, db *sqlx.DB, id string) (*models.CheckWithWorkerGroups, error) {
rows, err := db.QueryContext(ctx,
`
SELECT
checks.id,
checks.name,
checks."group",
checks.script,
checks.schedule,
checks.created_at,
checks.updated_at,
worker_groups.name as worker_group_name
FROM checks
LEFT OUTER JOIN check_worker_groups ON checks.id = check_worker_groups.check_id
LEFT OUTER JOIN worker_groups ON check_worker_groups.worker_group_id = worker_groups.id
WHERE checks.id=$1
ORDER BY checks.name
`,
id,
)
if err != nil {
return nil, err
}
defer rows.Close()
check := &models.CheckWithWorkerGroups{}
for rows.Next() {
var workerGroupName *string
err = rows.Scan(
&check.Id,
&check.Name,
&check.Group,
&check.Script,
&check.Schedule,
&check.CreatedAt,
&check.UpdatedAt,
&workerGroupName,
)
if err != nil {
return nil, err
}
if workerGroupName != nil {
check.WorkerGroups = append(check.WorkerGroups, *workerGroupName)
}
}
return check, err
}
func GetChecks(ctx context.Context, db *sqlx.DB) ([]*models.Check, error) {
checks := []*models.Check{}
err := db.SelectContext(ctx, &checks,
"SELECT * FROM checks ORDER BY name",
)
return checks, err
}
func GetChecksWithWorkerGroups(ctx context.Context, db *sqlx.DB) ([]*models.CheckWithWorkerGroups, error) {
rows, err := db.QueryContext(ctx,
`
SELECT
checks.id,
checks.name,
checks."group",
checks.script,
checks.schedule,
checks.created_at,
checks.updated_at,
worker_groups.name as worker_group_name
FROM checks
LEFT OUTER JOIN check_worker_groups ON checks.id = check_worker_groups.check_id
LEFT OUTER JOIN worker_groups ON check_worker_groups.worker_group_id = worker_groups.id
ORDER BY checks.name
`)
if err != nil {
return nil, err
}
defer rows.Close()
checks := map[string]*models.CheckWithWorkerGroups{}
for rows.Next() {
check := &models.CheckWithWorkerGroups{}
var workerGroupName *string
err = rows.Scan(
&check.Id,
&check.Name,
&check.Group,
&check.Script,
&check.Schedule,
&check.CreatedAt,
&check.UpdatedAt,
&workerGroupName,
)
if err != nil {
return nil, err
}
if workerGroupName != nil {
workerGroups := []string{}
if checks[check.Id] != nil {
workerGroups = checks[check.Id].WorkerGroups
}
check.WorkerGroups = append(workerGroups, *workerGroupName)
}
checks[check.Id] = check
}
checksWithWorkerGroups := maps.Values(checks)
sort.SliceStable(checksWithWorkerGroups, func(i, j int) bool {
return checksWithWorkerGroups[i].Name < checksWithWorkerGroups[j].Name
})
return checksWithWorkerGroups, err
}
func DeleteCheckSchedule(ctx context.Context, t client.Client, id string) error {
schedule := t.ScheduleClient().GetHandle(ctx, getScheduleId(id))
return schedule.Delete(ctx)
}
func CreateOrUpdateCheckSchedule(
ctx context.Context,
t client.Client,
check *models.Check,
workerGroups []*models.WorkerGroup,
) error {
log.Println("Creating or Updating Check Schedule")
workerGroupStrings := make([]string, len(workerGroups))
for i, group := range workerGroups {
workerGroupStrings[i] = group.Id
}
args := make([]interface{}, 1)
args[0] = workflows.CheckWorkflowParam{
Script: check.Script,
CheckId: check.Id,
WorkerGroupIds: workerGroupStrings,
}
options := client.ScheduleOptions{
ID: getScheduleId(check.Id),
Spec: client.ScheduleSpec{
CronExpressions: []string{check.Schedule},
Jitter: time.Second * 10,
},
Action: &client.ScheduleWorkflowAction{
ID: getScheduleId(check.Id),
Workflow: workflows.NewWorkflows(nil).CheckWorkflowDefinition,
Args: args,
TaskQueue: "default",
RetryPolicy: &temporal.RetryPolicy{
MaximumAttempts: 3,
},
},
}
schedule := t.ScheduleClient().GetHandle(ctx, getScheduleId(check.Id))
// If exists, we update
_, err := schedule.Describe(ctx)
if err == nil {
err = schedule.Update(ctx, client.ScheduleUpdateOptions{
DoUpdate: func(input client.ScheduleUpdateInput) (*client.ScheduleUpdate, error) {
return &client.ScheduleUpdate{
Schedule: &client.Schedule{
Spec: &options.Spec,
Action: options.Action,
Policy: input.Description.Schedule.Policy,
State: input.Description.Schedule.State,
},
}, nil
},
})
if err != nil {
return err
}
} else {
schedule, err = t.ScheduleClient().Create(ctx, options)
if err != nil {
return err
}
}
err = schedule.Trigger(ctx, client.ScheduleTriggerOptions{})
if err != nil {
return err
}
return nil
}

View file

@ -0,0 +1,67 @@
package services
import (
"context"
"code.tjo.space/mentos1386/zdravko/database/models"
"github.com/jmoiron/sqlx"
)
type CheckHistoryWithCheck struct {
*models.CheckHistory
CheckName string `db:"check_name"`
CheckId string `db:"check_id"`
}
func GetLastNCheckHistory(ctx context.Context, db *sqlx.DB, n int) ([]*CheckHistoryWithCheck, error) {
var checkHistory []*CheckHistoryWithCheck
err := db.SelectContext(ctx, &checkHistory, `
SELECT
mh.*,
wg.name AS worker_group_name,
m.name AS check_name,
m.id AS check_id
FROM check_histories mh
LEFT JOIN worker_groups wg ON mh.worker_group_id = wg.id
LEFT JOIN check_worker_groups mwg ON mh.check_id = mwg.check_id
LEFT JOIN checks m ON mwg.check_id = m.id
ORDER BY mh.created_at DESC
LIMIT $1
`, n)
return checkHistory, err
}
func GetCheckHistoryForCheck(ctx context.Context, db *sqlx.DB, checkId string) ([]*models.CheckHistory, error) {
var checkHistory []*models.CheckHistory
err := db.SelectContext(ctx, &checkHistory, `
SELECT
mh.*,
wg.name AS worker_group_name,
wg.id AS worker_group_id
FROM check_histories as mh
LEFT JOIN worker_groups wg ON mh.worker_group_id = wg.id
LEFT JOIN check_worker_groups mwg ON mh.check_id = mwg.check_id
WHERE mh.check_id = $1
ORDER BY mh.created_at DESC
`, checkId)
return checkHistory, err
}
func AddHistoryForCheck(ctx context.Context, db *sqlx.DB, history *models.CheckHistory) error {
_, err := db.NamedExecContext(ctx,
`
INSERT INTO check_histories (
check_id,
worker_group_id,
status,
note
) VALUES (
:check_id,
:worker_group_id,
:status,
:note
)`,
history,
)
return err
}

View file

@ -1,316 +0,0 @@
package services
import (
"context"
"log"
"sort"
"time"
"code.tjo.space/mentos1386/zdravko/database/models"
"code.tjo.space/mentos1386/zdravko/internal/workflows"
"github.com/jmoiron/sqlx"
"go.temporal.io/sdk/client"
"go.temporal.io/sdk/temporal"
"golang.org/x/exp/maps"
)
type MonitorStatus string
const (
MonitorStatusUnknown MonitorStatus = "UNKNOWN"
MonitorStatusPaused MonitorStatus = "PAUSED"
MonitorStatusActive MonitorStatus = "ACTIVE"
)
func getScheduleId(id string) string {
return "monitor-" + id
}
func CountMonitors(ctx context.Context, db *sqlx.DB) (int, error) {
var count int
err := db.GetContext(ctx, &count, "SELECT COUNT(*) FROM monitors")
return count, err
}
func GetMonitorStatus(ctx context.Context, temporal client.Client, id string) (MonitorStatus, error) {
schedule := temporal.ScheduleClient().GetHandle(ctx, getScheduleId(id))
description, err := schedule.Describe(ctx)
if err != nil {
return MonitorStatusUnknown, err
}
if description.Schedule.State.Paused {
return MonitorStatusPaused, nil
}
return MonitorStatusActive, nil
}
func SetMonitorStatus(ctx context.Context, temporal client.Client, id string, status MonitorStatus) error {
schedule := temporal.ScheduleClient().GetHandle(ctx, getScheduleId(id))
if status == MonitorStatusActive {
return schedule.Unpause(ctx, client.ScheduleUnpauseOptions{Note: "Unpaused by user"})
}
if status == MonitorStatusPaused {
return schedule.Pause(ctx, client.SchedulePauseOptions{Note: "Paused by user"})
}
return nil
}
func CreateMonitor(ctx context.Context, db *sqlx.DB, monitor *models.Monitor) error {
_, err := db.NamedExecContext(ctx,
`INSERT INTO monitors (id, name, "group", script, schedule) VALUES (:id, :name, :group, :script, :schedule)`,
monitor,
)
return err
}
func UpdateMonitor(ctx context.Context, db *sqlx.DB, monitor *models.Monitor) error {
_, err := db.NamedExecContext(ctx,
`UPDATE monitors SET "group"=:group, script=:script, schedule=:schedule WHERE id=:id`,
monitor,
)
return err
}
func DeleteMonitor(ctx context.Context, db *sqlx.DB, id string) error {
_, err := db.ExecContext(ctx,
"DELETE FROM monitors WHERE id=$1",
id,
)
return err
}
func UpdateMonitorWorkerGroups(ctx context.Context, db *sqlx.DB, monitor *models.Monitor, workerGroups []*models.WorkerGroup) error {
tx, err := db.BeginTxx(ctx, nil)
if err != nil {
return err
}
_, err = tx.ExecContext(ctx,
"DELETE FROM monitor_worker_groups WHERE monitor_id=$1",
monitor.Id,
)
if err != nil {
tx.Rollback()
return err
}
for _, group := range workerGroups {
_, err = tx.ExecContext(ctx,
"INSERT INTO monitor_worker_groups (monitor_id, worker_group_id) VALUES ($1, $2)",
monitor.Id,
group.Id,
)
if err != nil {
tx.Rollback()
return err
}
}
return tx.Commit()
}
func GetMonitor(ctx context.Context, db *sqlx.DB, id string) (*models.Monitor, error) {
monitor := &models.Monitor{}
err := db.GetContext(ctx, monitor,
"SELECT * FROM monitors WHERE id=$1",
id,
)
return monitor, err
}
func GetMonitorWithWorkerGroups(ctx context.Context, db *sqlx.DB, id string) (*models.MonitorWithWorkerGroups, error) {
rows, err := db.QueryContext(ctx,
`
SELECT
monitors.id,
monitors.name,
monitors."group",
monitors.script,
monitors.schedule,
monitors.created_at,
monitors.updated_at,
worker_groups.name as worker_group_name
FROM monitors
LEFT OUTER JOIN monitor_worker_groups ON monitors.id = monitor_worker_groups.monitor_id
LEFT OUTER JOIN worker_groups ON monitor_worker_groups.worker_group_id = worker_groups.id
WHERE monitors.id=$1
ORDER BY monitors.name
`,
id,
)
if err != nil {
return nil, err
}
defer rows.Close()
monitor := &models.MonitorWithWorkerGroups{}
for rows.Next() {
var workerGroupName *string
err = rows.Scan(
&monitor.Id,
&monitor.Name,
&monitor.Group,
&monitor.Script,
&monitor.Schedule,
&monitor.CreatedAt,
&monitor.UpdatedAt,
&workerGroupName,
)
if err != nil {
return nil, err
}
if workerGroupName != nil {
monitor.WorkerGroups = append(monitor.WorkerGroups, *workerGroupName)
}
}
return monitor, err
}
func GetMonitors(ctx context.Context, db *sqlx.DB) ([]*models.Monitor, error) {
monitors := []*models.Monitor{}
err := db.SelectContext(ctx, &monitors,
"SELECT * FROM monitors ORDER BY name",
)
return monitors, err
}
func GetMonitorsWithWorkerGroups(ctx context.Context, db *sqlx.DB) ([]*models.MonitorWithWorkerGroups, error) {
rows, err := db.QueryContext(ctx,
`
SELECT
monitors.id,
monitors.name,
monitors."group",
monitors.script,
monitors.schedule,
monitors.created_at,
monitors.updated_at,
worker_groups.name as worker_group_name
FROM monitors
LEFT OUTER JOIN monitor_worker_groups ON monitors.id = monitor_worker_groups.monitor_id
LEFT OUTER JOIN worker_groups ON monitor_worker_groups.worker_group_id = worker_groups.id
ORDER BY monitors.name
`)
if err != nil {
return nil, err
}
defer rows.Close()
monitors := map[string]*models.MonitorWithWorkerGroups{}
for rows.Next() {
monitor := &models.MonitorWithWorkerGroups{}
var workerGroupName *string
err = rows.Scan(
&monitor.Id,
&monitor.Name,
&monitor.Group,
&monitor.Script,
&monitor.Schedule,
&monitor.CreatedAt,
&monitor.UpdatedAt,
&workerGroupName,
)
if err != nil {
return nil, err
}
if workerGroupName != nil {
workerGroups := []string{}
if monitors[monitor.Id] != nil {
workerGroups = monitors[monitor.Id].WorkerGroups
}
monitor.WorkerGroups = append(workerGroups, *workerGroupName)
}
monitors[monitor.Id] = monitor
}
monitorsWithWorkerGroups := maps.Values(monitors)
sort.SliceStable(monitorsWithWorkerGroups, func(i, j int) bool {
return monitorsWithWorkerGroups[i].Name < monitorsWithWorkerGroups[j].Name
})
return monitorsWithWorkerGroups, err
}
func DeleteMonitorSchedule(ctx context.Context, t client.Client, id string) error {
schedule := t.ScheduleClient().GetHandle(ctx, getScheduleId(id))
return schedule.Delete(ctx)
}
func CreateOrUpdateMonitorSchedule(
ctx context.Context,
t client.Client,
monitor *models.Monitor,
workerGroups []*models.WorkerGroup,
) error {
log.Println("Creating or Updating Monitor Schedule")
workerGroupStrings := make([]string, len(workerGroups))
for i, group := range workerGroups {
workerGroupStrings[i] = group.Id
}
args := make([]interface{}, 1)
args[0] = workflows.MonitorWorkflowParam{
Script: monitor.Script,
MonitorId: monitor.Id,
WorkerGroupIds: workerGroupStrings,
}
options := client.ScheduleOptions{
ID: getScheduleId(monitor.Id),
Spec: client.ScheduleSpec{
CronExpressions: []string{monitor.Schedule},
Jitter: time.Second * 10,
},
Action: &client.ScheduleWorkflowAction{
ID: getScheduleId(monitor.Id),
Workflow: workflows.NewWorkflows(nil).MonitorWorkflowDefinition,
Args: args,
TaskQueue: "default",
RetryPolicy: &temporal.RetryPolicy{
MaximumAttempts: 3,
},
},
}
schedule := t.ScheduleClient().GetHandle(ctx, getScheduleId(monitor.Id))
// If exists, we update
_, err := schedule.Describe(ctx)
if err == nil {
err = schedule.Update(ctx, client.ScheduleUpdateOptions{
DoUpdate: func(input client.ScheduleUpdateInput) (*client.ScheduleUpdate, error) {
return &client.ScheduleUpdate{
Schedule: &client.Schedule{
Spec: &options.Spec,
Action: options.Action,
Policy: input.Description.Schedule.Policy,
State: input.Description.Schedule.State,
},
}, nil
},
})
if err != nil {
return err
}
} else {
schedule, err = t.ScheduleClient().Create(ctx, options)
if err != nil {
return err
}
}
err = schedule.Trigger(ctx, client.ScheduleTriggerOptions{})
if err != nil {
return err
}
return nil
}

View file

@ -1,67 +0,0 @@
package services
import (
"context"
"code.tjo.space/mentos1386/zdravko/database/models"
"github.com/jmoiron/sqlx"
)
type MonitorHistoryWithMonitor struct {
*models.MonitorHistory
MonitorName string `db:"monitor_name"`
MonitorId string `db:"monitor_id"`
}
func GetLastNMonitorHistory(ctx context.Context, db *sqlx.DB, n int) ([]*MonitorHistoryWithMonitor, error) {
var monitorHistory []*MonitorHistoryWithMonitor
err := db.SelectContext(ctx, &monitorHistory, `
SELECT
mh.*,
wg.name AS worker_group_name,
m.name AS monitor_name,
m.id AS monitor_id
FROM monitor_histories mh
LEFT JOIN worker_groups wg ON mh.worker_group_id = wg.id
LEFT JOIN monitor_worker_groups mwg ON mh.monitor_id = mwg.monitor_id
LEFT JOIN monitors m ON mwg.monitor_id = m.id
ORDER BY mh.created_at DESC
LIMIT $1
`, n)
return monitorHistory, err
}
func GetMonitorHistoryForMonitor(ctx context.Context, db *sqlx.DB, monitorId string) ([]*models.MonitorHistory, error) {
var monitorHistory []*models.MonitorHistory
err := db.SelectContext(ctx, &monitorHistory, `
SELECT
mh.*,
wg.name AS worker_group_name,
wg.id AS worker_group_id
FROM monitor_histories as mh
LEFT JOIN worker_groups wg ON mh.worker_group_id = wg.id
LEFT JOIN monitor_worker_groups mwg ON mh.monitor_id = mwg.monitor_id
WHERE mh.monitor_id = $1
ORDER BY mh.created_at DESC
`, monitorId)
return monitorHistory, err
}
func AddHistoryForMonitor(ctx context.Context, db *sqlx.DB, history *models.MonitorHistory) error {
_, err := db.NamedExecContext(ctx,
`
INSERT INTO monitor_histories (
monitor_id,
worker_group_id,
status,
note
) VALUES (
:monitor_id,
:worker_group_id,
:status,
:note
)`,
history,
)
return err
}

View file

@ -54,7 +54,7 @@ func GetWorkerGroups(ctx context.Context, db *sqlx.DB) ([]*models.WorkerGroup, e
return workerGroups, err
}
func GetWorkerGroupsWithMonitors(ctx context.Context, db *sqlx.DB) ([]*models.WorkerGroupWithMonitors, error) {
func GetWorkerGroupsWithChecks(ctx context.Context, db *sqlx.DB) ([]*models.WorkerGroupWithChecks, error) {
rows, err := db.QueryContext(ctx,
`
SELECT
@ -62,10 +62,10 @@ SELECT
worker_groups.name,
worker_groups.created_at,
worker_groups.updated_at,
monitors.name as monitor_name
checks.name as check_name
FROM worker_groups
LEFT OUTER JOIN monitor_worker_groups ON worker_groups.id = monitor_worker_groups.worker_group_id
LEFT OUTER JOIN monitors ON monitor_worker_groups.monitor_id = monitors.id
LEFT OUTER JOIN check_worker_groups ON worker_groups.id = check_worker_groups.worker_group_id
LEFT OUTER JOIN checks ON check_worker_groups.check_id = checks.id
ORDER BY worker_groups.name
`)
if err != nil {
@ -73,29 +73,29 @@ ORDER BY worker_groups.name
}
defer rows.Close()
workerGroups := map[string]*models.WorkerGroupWithMonitors{}
workerGroups := map[string]*models.WorkerGroupWithChecks{}
for rows.Next() {
workerGroup := &models.WorkerGroupWithMonitors{}
workerGroup := &models.WorkerGroupWithChecks{}
var monitorName *string
var checkName *string
err = rows.Scan(
&workerGroup.Id,
&workerGroup.Name,
&workerGroup.CreatedAt,
&workerGroup.UpdatedAt,
&monitorName,
&checkName,
)
if err != nil {
return nil, err
}
if monitorName != nil {
monitors := []string{}
if checkName != nil {
checks := []string{}
if workerGroups[workerGroup.Id] != nil {
monitors = workerGroups[workerGroup.Id].Monitors
checks = workerGroups[workerGroup.Id].Checks
}
workerGroup.Monitors = append(monitors, *monitorName)
workerGroup.Checks = append(checks, *checkName)
}
workerGroups[workerGroup.Id] = workerGroup
@ -122,7 +122,7 @@ func GetWorkerGroup(ctx context.Context, db *sqlx.DB, id string) (*models.Worker
return &workerGroup, err
}
func GetWorkerGroupWithMonitors(ctx context.Context, db *sqlx.DB, id string) (*models.WorkerGroupWithMonitors, error) {
func GetWorkerGroupWithChecks(ctx context.Context, db *sqlx.DB, id string) (*models.WorkerGroupWithChecks, error) {
rows, err := db.QueryContext(ctx,
`
SELECT
@ -130,10 +130,10 @@ SELECT
worker_groups.name,
worker_groups.created_at,
worker_groups.updated_at,
monitors.name as monitor_name
checks.name as check_name
FROM worker_groups
LEFT OUTER JOIN monitor_worker_groups ON worker_groups.id = monitor_worker_groups.worker_group_id
LEFT OUTER JOIN monitors ON monitor_worker_groups.monitor_id = monitors.id
LEFT OUTER JOIN check_worker_groups ON worker_groups.id = check_worker_groups.worker_group_id
LEFT OUTER JOIN checks ON check_worker_groups.check_id = checks.id
WHERE worker_groups.id=$1
`,
id,
@ -143,22 +143,22 @@ WHERE worker_groups.id=$1
}
defer rows.Close()
workerGroup := &models.WorkerGroupWithMonitors{}
workerGroup := &models.WorkerGroupWithChecks{}
for rows.Next() {
var monitorName *string
var checkName *string
err = rows.Scan(
&workerGroup.Id,
&workerGroup.Name,
&workerGroup.CreatedAt,
&workerGroup.UpdatedAt,
&monitorName,
&checkName,
)
if err != nil {
return nil, err
}
if monitorName != nil {
workerGroup.Monitors = append(workerGroup.Monitors, *monitorName)
if checkName != nil {
workerGroup.Checks = append(workerGroup.Checks, *checkName)
}
}

View file

@ -0,0 +1,58 @@
package workflows
import (
"sort"
"time"
"code.tjo.space/mentos1386/zdravko/database/models"
"code.tjo.space/mentos1386/zdravko/internal/activities"
"go.temporal.io/sdk/workflow"
)
type CheckWorkflowParam struct {
Script string
CheckId string
WorkerGroupIds []string
}
func (w *Workflows) CheckWorkflowDefinition(ctx workflow.Context, param CheckWorkflowParam) (models.CheckStatus, error) {
workerGroupIds := param.WorkerGroupIds
sort.Strings(workerGroupIds)
for _, workerGroupId := range workerGroupIds {
ctx = workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
StartToCloseTimeout: 60 * time.Second,
TaskQueue: workerGroupId,
})
heatlcheckParam := activities.HealtcheckParam{
Script: param.Script,
}
var checkResult *activities.CheckResult
err := workflow.ExecuteActivity(ctx, w.activities.Check, heatlcheckParam).Get(ctx, &checkResult)
if err != nil {
return models.CheckUnknown, err
}
status := models.CheckFailure
if checkResult.Success {
status = models.CheckSuccess
}
historyParam := activities.HealtcheckAddToHistoryParam{
CheckId: param.CheckId,
Status: status,
Note: checkResult.Note,
WorkerGroupId: workerGroupId,
}
var historyResult *activities.CheckAddToHistoryResult
err = workflow.ExecuteActivity(ctx, w.activities.CheckAddToHistory, historyParam).Get(ctx, &historyResult)
if err != nil {
return models.CheckUnknown, err
}
}
return models.CheckSuccess, nil
}

View file

@ -1,58 +0,0 @@
package workflows
import (
"sort"
"time"
"code.tjo.space/mentos1386/zdravko/database/models"
"code.tjo.space/mentos1386/zdravko/internal/activities"
"go.temporal.io/sdk/workflow"
)
type MonitorWorkflowParam struct {
Script string
MonitorId string
WorkerGroupIds []string
}
func (w *Workflows) MonitorWorkflowDefinition(ctx workflow.Context, param MonitorWorkflowParam) (models.MonitorStatus, error) {
workerGroupIds := param.WorkerGroupIds
sort.Strings(workerGroupIds)
for _, workerGroupId := range workerGroupIds {
ctx = workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
StartToCloseTimeout: 60 * time.Second,
TaskQueue: workerGroupId,
})
heatlcheckParam := activities.HealtcheckParam{
Script: param.Script,
}
var monitorResult *activities.MonitorResult
err := workflow.ExecuteActivity(ctx, w.activities.Monitor, heatlcheckParam).Get(ctx, &monitorResult)
if err != nil {
return models.MonitorUnknown, err
}
status := models.MonitorFailure
if monitorResult.Success {
status = models.MonitorSuccess
}
historyParam := activities.HealtcheckAddToHistoryParam{
MonitorId: param.MonitorId,
Status: status,
Note: monitorResult.Note,
WorkerGroupId: workerGroupId,
}
var historyResult *activities.MonitorAddToHistoryResult
err = workflow.ExecuteActivity(ctx, w.activities.MonitorAddToHistory, historyParam).Get(ctx, &historyResult)
if err != nil {
return models.MonitorUnknown, err
}
}
return models.MonitorSuccess, nil
}

View file

@ -2,8 +2,8 @@ package api
import "code.tjo.space/mentos1386/zdravko/database/models"
type ApiV1MonitorsHistoryPOSTBody struct {
Status models.MonitorStatus `json:"status"`
type ApiV1ChecksHistoryPOSTBody struct {
Status models.CheckStatus `json:"status"`
Note string `json:"note"`
WorkerGroupId string `json:"worker_group"`
}

View file

@ -72,14 +72,16 @@ func Routes(
//settings.GET("/targets/:id/disable", h.SettingsTargetsDisableGET)
//settings.GET("/targets/:id/enable", h.SettingsTargetsEnableGET)
settings.GET("/monitors", h.SettingsMonitorsGET)
settings.GET("/monitors/create", h.SettingsMonitorsCreateGET)
settings.POST("/monitors/create", h.SettingsMonitorsCreatePOST)
settings.GET("/monitors/:id", h.SettingsMonitorsDescribeGET)
settings.POST("/monitors/:id", h.SettingsMonitorsDescribePOST)
settings.GET("/monitors/:id/delete", h.SettingsMonitorsDescribeDELETE)
settings.GET("/monitors/:id/disable", h.SettingsMonitorsDisableGET)
settings.GET("/monitors/:id/enable", h.SettingsMonitorsEnableGET)
settings.GET("/incidents", h.SettingsIncidentsGET)
settings.GET("/checks", h.SettingsChecksGET)
settings.GET("/checks/create", h.SettingsChecksCreateGET)
settings.POST("/checks/create", h.SettingsChecksCreatePOST)
settings.GET("/checks/:id", h.SettingsChecksDescribeGET)
settings.POST("/checks/:id", h.SettingsChecksDescribePOST)
settings.GET("/checks/:id/delete", h.SettingsChecksDescribeDELETE)
settings.GET("/checks/:id/disable", h.SettingsChecksDisableGET)
settings.GET("/checks/:id/enable", h.SettingsChecksEnableGET)
settings.GET("/notifications", h.SettingsNotificationsGET)
@ -101,7 +103,6 @@ func Routes(
apiv1 := e.Group("/api/v1")
apiv1.Use(h.Authenticated)
apiv1.GET("/workers/connect", h.ApiV1WorkersConnectGET)
apiv1.POST("/monitors/:id/history", h.ApiV1MonitorsHistoryPOST)
// Error handler
e.HTTPErrorHandler = func(err error, c echo.Context) {

View file

@ -20,7 +20,7 @@ func NewWorker(temporalClient client.Client, cfg *config.ServerConfig) *Worker {
workerWorkflows := workflows.NewWorkflows(workerActivities)
// Register Workflows
w.RegisterWorkflow(workerWorkflows.MonitorWorkflowDefinition)
w.RegisterWorkflow(workerWorkflows.CheckWorkflowDefinition)
return &Worker{
worker: w,

View file

@ -92,11 +92,11 @@ func (w *Worker) Start() error {
workerWorkflows := workflows.NewWorkflows(workerActivities)
// Register Workflows
w.worker.RegisterWorkflow(workerWorkflows.MonitorWorkflowDefinition)
w.worker.RegisterWorkflow(workerWorkflows.CheckWorkflowDefinition)
// Register Activities
w.worker.RegisterActivity(workerActivities.Monitor)
w.worker.RegisterActivity(workerActivities.MonitorAddToHistory)
w.worker.RegisterActivity(workerActivities.Check)
w.worker.RegisterActivity(workerActivities.CheckAddToHistory)
return w.worker.Run(worker.InterruptCh())
}

View file

@ -51,9 +51,6 @@ code {
@apply shadow;
}
.sidebar {
@apply flex flex-row flex-wrap justify-center lg:flex-col lg:w-48 gap-2 h-fit text-sm font-medium text-gray-900;
}
.sidebar a {
@apply w-full block rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-700 focus:text-blue-700;
}

View file

@ -825,6 +825,10 @@ video {
flex-direction: column;
}
.flex-wrap {
flex-wrap: wrap;
}
.items-center {
align-items: center;
}
@ -1361,28 +1365,6 @@ code {
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
.sidebar {
display: flex;
height: -moz-fit-content;
height: fit-content;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
gap: 0.5rem;
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 500;
--tw-text-opacity: 1;
color: rgb(17 24 39 / var(--tw-text-opacity));
}
@media (min-width: 1024px) {
.sidebar {
width: 12rem;
flex-direction: column;
}
}
.sidebar a {
display: block;
width: 100%;
@ -1856,10 +1838,22 @@ code {
margin-top: 5rem;
}
.lg\:w-48 {
width: 12rem;
}
.lg\:grid-cols-\[min-content_minmax\(0\2c 1fr\)\] {
grid-template-columns: min-content minmax(0,1fr);
}
.lg\:flex-col {
flex-direction: column;
}
.lg\:items-start {
align-items: flex-start;
}
.lg\:justify-start {
justify-content: flex-start;
}

View file

@ -10,15 +10,17 @@
<div
class="md:px-4 lg:px-8 mx-auto mt-8 w-full max-w-screen-xl lg:mt-20 grid grid-cols-1 lg:grid-cols-[min-content_minmax(0,1fr)] gap-8"
>
<ul class="sidebar gap-4">
<ul
class="sidebar gap-2 flex flex-row flex-wrap justify-center lg:flex-col lg:w-48 h-fit text-sm font-medium text-gray-900"
>
{{ range .SettingsSidebar }}
<li>
<li class="flex items-center gap-1 lg:flex-col lg:items-start">
<p
class="mb-2 text-xs font-semibold text-gray-600 uppercase tracking-wider"
class="text-xs font-semibold text-gray-600 uppercase tracking-wider"
>
{{ .Group }}
</p>
<ul>
<ul class="flex flex-row flex-wrap gap-1 lg:flex-col">
{{ range .Pages }}
<li>
<a
@ -40,6 +42,9 @@
{{ if eq .Title "Checks" }}
<span class="text-slate-400">(3)</span>
{{ end }}
{{ if eq .Title "Hooks" }}
<span class="text-slate-400">(3)</span>
{{ end }}
{{ if eq .Title "Triggers" }}
<span class="text-slate-400">(3)</span>
{{ end }}

View file

@ -1,26 +1,26 @@
{{ define "main" }}
<div class="container max-w-screen-md flex flex-col mt-20 gap-20">
{{ $length := len .Monitors }}
{{ $length := len .Checks }}
{{ if eq $length 0 }}
<section>
<div class="py-8 px-4 mx-auto max-w-screen-xl text-center lg:py-16">
<h1
class="mb-4 text-2xl font-extrabold tracking-tight leading-none text-gray-900 md:text-3xl lg:text-4xl"
>
There are no monitors yet.
There are no checks yet.
</h1>
<p
class="mb-8 text-l font-normal text-gray-700 lg:text-l sm:px-8 lg:px-40"
>
Create a monitor to monitor your services and get notified when they
Create a check to check your services and get notified when they
are down.
</p>
<div class="flex flex-col gap-4 sm:flex-row sm:justify-center">
<a
href="/settings/monitors/create"
href="/settings/checks/create"
class="inline-flex justify-center items-center py-3 px-5 text-base font-medium text-center text-white rounded-lg bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300"
>
Create First Monitor
Create First Check
<svg class="feather ml-1 h-5 w-5 overflow-visible">
<use href="/static/icons/feather-sprite.svg#plus" />
</svg>
@ -69,7 +69,7 @@
</p>
</div>
{{ end }}
<div class="monitors flex flex-col gap-4">
<div class="checks flex flex-col gap-4">
<div
class="inline-flex gap-1 justify-center md:justify-end time-range"
role="group"
@ -93,7 +93,7 @@
>90 Minutes</a
>
</div>
{{ range $group, $monitorsAndStatus := .Monitors }}
{{ range $group, $checksAndStatus := .Checks }}
<details
open
class="bg-white shadow-md rounded-lg p-6 py-4 gap-2 [&_svg]:open:rotate-90"
@ -101,11 +101,11 @@
<summary
class="flex flex-row gap-2 p-3 py-2 -mx-3 cursor-pointer hover:bg-blue-50 rounded-lg"
>
{{ if eq $monitorsAndStatus.Status "SUCCESS" }}
{{ if eq $checksAndStatus.Status "SUCCESS" }}
<span
class="flex w-3 h-3 bg-green-400 rounded-full self-center"
></span>
{{ else if eq $monitorsAndStatus.Status "FAILURE" }}
{{ else if eq $checksAndStatus.Status "FAILURE" }}
<span
class="flex w-3 h-3 bg-red-400 rounded-full self-center"
></span>
@ -123,7 +123,7 @@
<use href="/static/icons/feather-sprite.svg#chevron-right" />
</svg>
</summary>
{{ range $monitorsAndStatus.Monitors }}
{{ range $checksAndStatus.Checks }}
<div
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"
>

View file

@ -1,13 +1,13 @@
{{ define "settings" }}
{{ $description := "Monitors are constantly determining if targets are healthy or not." }}
{{ $description := "Checks are constantly determining if targets are healthy or not." }}
{{ $length := len .Monitors }}
{{ $length := len .Checks }}
{{ if eq $length 0 }}
<div class="py-8 px-4 mx-auto max-w-screen-xl text-center lg:py-16">
<h1
class="mb-4 text-2xl font-extrabold tracking-tight leading-none text-gray-900 md:text-3xl lg:text-4xl"
>
There are no monitors yet.
There are no checks yet.
</h1>
<p
class="mb-8 text-l font-normal text-gray-700 lg:text-l sm:px-8 lg:px-40"
@ -16,10 +16,10 @@
</p>
<div class="flex flex-col gap-4 sm:flex-row sm:justify-center">
<a
href="/settings/monitors/create"
href="/settings/checks/create"
class="inline-flex justify-center items-center py-3 px-5 text-base font-medium text-center text-white rounded-lg bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300"
>
Create First Monitor
Create First Check
<svg class="feather ml-1 h-5 w-5 overflow-visible">
<use href="/static/icons/feather-sprite.svg#plus" />
</svg>
@ -30,13 +30,13 @@
<section>
<table>
<caption>
List of Monitors
List of Checks
<div class="mt-1 gap-4 grid grid-cols-1 md:grid-cols-[1fr,20%]">
<p>
{{ $description }}
</p>
<a
href="/settings/monitors/create"
href="/settings/checks/create"
class="h-min inline-flex justify-center items-center py-2 px-4 text-sm font-medium text-center text-white rounded-lg bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300"
>
Create New
@ -48,7 +48,7 @@
</caption>
<thead class="text-xs text-gray-700 uppercase bg-gray-50">
<tr>
<th scope="col">Monitor Group</th>
<th scope="col">Check Group</th>
<th scope="col">Name</th>
<th scope="col">Visibility</th>
<th scope="col">Worker Groups</th>
@ -58,7 +58,7 @@
</tr>
</thead>
<tbody>
{{ range .MonitorGroups }}
{{ range .CheckGroups }}
{{ $currentGroup := . }}
<tr class="row-special">
<th scope="row">
@ -71,9 +71,9 @@
<td></td>
<td></td>
</tr>
{{ range $group, $monitors := $.Monitors }}
{{ range $group, $checks := $.Checks }}
{{ if eq $group $currentGroup }}
{{ range $monitors }}
{{ range $checks }}
<tr>
<th scope="row">└─</th>
<th scope="row">
@ -125,7 +125,7 @@
{{ .Schedule }}
</td>
<td>
<a href="/settings/monitors/{{ .Id }}" class="link"
<a href="/settings/checks/{{ .Id }}" class="link"
>Details</a
>
</td>

View file

@ -1,10 +1,10 @@
{{ define "settings" }}
<section class="p-5">
<form action="/settings/monitors/create" method="post">
<form action="/settings/checks/create" method="post">
<label for="name">Name</label>
<input type="text" name="name" id="name" placeholder="Github.com" />
<p>Name of the monitor can be anything.</p>
<label list="existing-groups" for="group">Monitor Group</label>
<p>Name of the check can be anything.</p>
<label list="existing-groups" for="group">Check Group</label>
<input
type="text"
name="group"
@ -17,7 +17,7 @@
<option value="default"></option>
</datalist>
<p>
Group monitors together. This affects how they are presented on the
Group checks together. This affects how they are presented on the
homepage.
</p>
<label for="workergroups">Worker Groups</label>
@ -29,7 +29,7 @@
required
/>
<p>
Worker groups are used to distribute the monitor to specific workers.
Worker groups are used to distribute the check to specific workers.
</p>
<label for="schedule">Schedule</label>
<input
@ -41,7 +41,7 @@
required
/>
<p>
Schedule is a cron expression that defines when the monitor should be
Schedule is a cron expression that defines when the check should be
executed.
<br />
You can also use <code>@every [interval]</code> where interval is a

View file

@ -1,17 +1,17 @@
{{ define "settings" }}
<section class="p-5">
<form action="/settings/monitors/{{ .Monitor.Id }}" method="post">
<form action="/settings/checks/{{ .Check.Id }}" method="post">
<h2>Configuration</h2>
<label for="group">Monitor Group</label>
<label for="group">Check Group</label>
<input
type="text"
name="group"
id="group"
value="{{ .Monitor.Group }}"
value="{{ .Check.Group }}"
required
/>
<p>
Group monitors together. This affects how they are presented on the
Group checks together. This affects how they are presented on the
homepage.
</p>
<label for="workergroups">Worker Groups</label>
@ -19,22 +19,22 @@
type="text"
name="workergroups"
id="workergroups"
value="{{ range .Monitor.WorkerGroups }}{{ . }}{{ end }}"
value="{{ range .Check.WorkerGroups }}{{ . }}{{ end }}"
required
/>
<p>
Worker groups are used to distribute the monitor to specific workers.
Worker groups are used to distribute the check to specific workers.
</p>
<label for="schedule">Schedule</label>
<input
type="text"
name="schedule"
id="schedule"
value="{{ .Monitor.Schedule }}"
value="{{ .Check.Schedule }}"
required
/>
<p>
Schedule is a cron expression that defines when the monitor should be
Schedule is a cron expression that defines when the check should be
executed.
<br />
You can also use <code>@every [interval]</code> where interval is a
@ -44,7 +44,7 @@
</p>
<label for="script">Script</label>
<textarea required id="script" name="script" class="h-96">
{{ ScriptUnescapeString .Monitor.Script }}</textarea
{{ ScriptUnescapeString .Check.Script }}</textarea
>
<div
id="editor"
@ -65,13 +65,13 @@
<section class="p-5 flex-1">
<h2 class="mb-2 flex flex-row gap-2">
Status
{{ if eq .Monitor.Status "ACTIVE" }}
{{ if eq .Check.Status "ACTIVE" }}
<span
class="self-center h-fit w-fit px-2 text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800"
>
ACTIVE
</span>
{{ else if eq .Monitor.Status "PAUSED" }}
{{ else if eq .Check.Status "PAUSED" }}
<span
class="self-center h-fit w-fit px-2 text-xs leading-5 font-semibold rounded-full bg-yellow-100 text-yellow-800"
>
@ -80,19 +80,19 @@
{{ end }}
</h2>
<p class="text-sm mb-2">
Pausing the monitor will stop it from executing. This can be useful in
cases of expected downtime. Or when the monitor is not needed anymore.
Pausing the check will stop it from executing. This can be useful in
cases of expected downtime. Or when the check is not needed anymore.
</p>
{{ if eq .Monitor.Status "ACTIVE" }}
{{ if eq .Check.Status "ACTIVE" }}
<a
class="block text-center py-2.5 px-5 me-2 mb-2 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100"
href="/settings/monitors/{{ .Monitor.Id }}/disable"
href="/settings/checks/{{ .Check.Id }}/disable"
>Pause</a
>
{{ else if eq .Monitor.Status "PAUSED" }}
{{ else if eq .Check.Status "PAUSED" }}
<a
class="block text-center py-2.5 px-5 me-2 mb-2 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100"
href="/settings/monitors/{{ .Monitor.Id }}/enable"
href="/settings/checks/{{ .Check.Id }}/enable"
>Resume</a
>
{{ end }}
@ -100,10 +100,10 @@
<section class="p-2 flex-1 border-4 border-red-300">
<h2 class="mb-2">Danger Zone</h2>
<p class="text-sm mb-2">Permanently delete this monitor.</p>
<p class="text-sm mb-2">Permanently delete this check.</p>
<a
class="block text-center focus:outline-none text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2"
href="/settings/monitors/{{ .Monitor.Id }}/delete"
href="/settings/checks/{{ .Check.Id }}/delete"
>Delete</a
>
</section>
@ -113,7 +113,7 @@
<table>
<caption>
History
<p>Last 10 executions of monitor script.</p>
<p>Last 10 executions of check script.</p>
</caption>
<thead>
<tr>
@ -172,7 +172,7 @@
var doc = new DOMParser().parseFromString(input, "text/html");
return doc.documentElement.textContent;
}
script = htmlDecode("{{ .Monitor.Script }}")
script = htmlDecode("{{ .Check.Script }}")
require.config({ paths: { vs: '/static/monaco/vs' } });
require(['vs/editor/editor.main'], function () {

View file

@ -0,0 +1,84 @@
{{ define "settings" }}
{{ $description := "Incidents represent an event where some services were unavailable. Incidents can either be created automatically via the Triggers or manually. Incidents can be resolved manually or via triggers as well, once services are healthy again." }}
{{ $length := len .Incidents }}
{{ if eq $length 0 }}
<div class="py-8 px-4 mx-auto max-w-screen-xl text-center lg:py-16">
<h1
class="mb-4 text-2xl font-extrabold tracking-tight leading-none text-gray-900 md:text-3xl lg:text-4xl"
>
There are no incidents yet.
</h1>
<p
class="mb-8 text-l font-normal text-gray-700 lg:text-l sm:px-8 lg:px-40"
>
{{ $description }}
</p>
<div class="flex flex-col gap-4 sm:flex-row sm:justify-center">
<a
href="/settings/incidents/create"
class="inline-flex justify-center items-center py-3 px-5 text-base font-medium text-center text-white rounded-lg bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300"
>
Add Manual Incident
<svg class="feather ml-1 h-5 w-5 overflow-visible">
<use href="/static/icons/feather-sprite.svg#plus" />
</svg>
</a>
<a
href="/settings/triggers"
class="inline-flex justify-center items-center py-3 px-5 text-base font-medium text-center text-white rounded-lg bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300"
>
Go to Triggers
</a>
</div>
</div>
{{ else }}
<section>
<table>
<caption>
List of Incidents
<div class="mt-1 gap-4 grid grid-cols-1 md:grid-cols-[1fr,20%]">
<p>
{{ $description }}
</p>
<a
href="/settings/incidents/create"
class="h-min inline-flex justify-center items-center py-2 px-4 text-sm font-medium text-center text-white rounded-lg bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300"
>
Create New
<svg class="feather h-5 w-5 overflow-visible">
<use href="/static/icons/feather-sprite.svg#plus" />
</svg>
</a>
</div>
</caption>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Action</th>
</tr>
</thead>
{{ range .Incidents }}
<tbody>
<tr>
<th scope="row">
{{ .Name }}
</th>
<td>
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800"
>
{{ .Type }}
</span>
</td>
<td>
<a href="/settings/incidents/{{ .Id }}" class="link">Details</a>
</td>
</tr>
</tbody>
{{ end }}
</table>
</section>
{{ end }}
{{ end }}

View file

@ -7,7 +7,7 @@
</h1>
<p class="mb-8 text-l font-normal text-gray-500 lg:text-l sm:px-8 md:px-40">
Welcome to the settings page. Here you can manage your worker groups,
monitors, and notifications.
checks, and notifications.
</p>
</div>
@ -16,15 +16,15 @@
class="inline-block bg-white rounded-lg shadow p-5 text-center sm:text-left"
>
<h3 class="text-sm leading-6 font-medium text-gray-400">Total Targets</h3>
<p class="text-3xl font-bold text-black">{{ .MonitorsCount }}</p>
<p class="text-3xl font-bold text-black">{{ .ChecksCount }}</p>
</div>
<div
class="inline-block bg-white rounded-lg shadow p-5 text-center sm:text-left"
>
<h3 class="text-sm leading-6 font-medium text-gray-400">
Total Monitors
Total Checks
</h3>
<p class="text-3xl font-bold text-black">{{ .MonitorsCount }}</p>
<p class="text-3xl font-bold text-black">{{ .ChecksCount }}</p>
</div>
<div
class="inline-block bg-white rounded-lg shadow p-5 text-center sm:text-left"
@ -48,11 +48,11 @@
<table>
<caption>
Execution History
<p>Last 10 executions for all monitors and worker groups.</p>
<p>Last 10 executions for all checks and worker groups.</p>
</caption>
<thead>
<tr>
<th>Monitor</th>
<th>Check</th>
<th>Worker Group</th>
<th>Status</th>
<th>Executed At</th>
@ -65,8 +65,8 @@
<th>
<a
class="underline hover:text-blue-600"
href="/settings/monitors/{{ .MonitorId }}"
>{{ .MonitorName }}</a
href="/settings/checks/{{ .CheckId }}"
>{{ .CheckName }}</a
>
</th>
<td>

View file

@ -1,5 +1,5 @@
{{ define "settings" }}
{{ $description := "Triggers process monitor outcomes and determine if an incident should be created, updated or closed." }}
{{ $description := "Triggers process check outcomes and determine if an incident should be created, updated or closed." }}
{{ $length := len .Triggers }}
{{ if eq $length 0 }}

View file

@ -50,7 +50,7 @@
<tr>
<th>Name</th>
<th>Workers</th>
<th>Monitors</th>
<th>Checks</th>
<th>Action</th>
</tr>
</thead>
@ -76,7 +76,7 @@
{{ end }}
</td>
<td>
{{ len .Monitors }}
{{ len .Checks }}
</td>
<td>
<a href="/settings/worker-groups/{{ .Id }}" class="link"

View file

@ -51,13 +51,14 @@ func NewTemplates() *Templates {
"settings_triggers_create.tmpl": loadSettings("pages/settings_triggers_create.tmpl"),
"settings_triggers_describe.tmpl": loadSettings("pages/settings_triggers_describe.tmpl"),
"settings_targets.tmpl": loadSettings("pages/settings_targets.tmpl"),
"settings_incidents.tmpl": loadSettings("pages/settings_incidents.tmpl"),
"settings_notifications.tmpl": loadSettings("pages/settings_notifications.tmpl"),
"settings_worker_groups.tmpl": loadSettings("pages/settings_worker_groups.tmpl"),
"settings_worker_groups_create.tmpl": loadSettings("pages/settings_worker_groups_create.tmpl"),
"settings_worker_groups_describe.tmpl": loadSettings("pages/settings_worker_groups_describe.tmpl"),
"settings_monitors.tmpl": loadSettings("pages/settings_monitors.tmpl"),
"settings_monitors_create.tmpl": loadSettings("pages/settings_monitors_create.tmpl"),
"settings_monitors_describe.tmpl": loadSettings("pages/settings_monitors_describe.tmpl"),
"settings_checks.tmpl": loadSettings("pages/settings_checks.tmpl"),
"settings_checks_create.tmpl": loadSettings("pages/settings_checks_create.tmpl"),
"settings_checks_describe.tmpl": loadSettings("pages/settings_checks_describe.tmpl"),
},
}
}