Skip to content

Commit bcc7336

Browse files
committed
added monitor incident endpoints
1 parent e9bbe77 commit bcc7336

File tree

14 files changed

+341
-14
lines changed

14 files changed

+341
-14
lines changed

cmd/api.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/opsway-io/backend/internal/connectors/redis"
1313
"github.com/opsway-io/backend/internal/entities"
1414
"github.com/opsway-io/backend/internal/event"
15+
"github.com/opsway-io/backend/internal/incident"
1516
"github.com/opsway-io/backend/internal/monitor"
1617
"github.com/opsway-io/backend/internal/notification/email"
1718
"github.com/opsway-io/backend/internal/rest"
@@ -120,6 +121,9 @@ func runAPI(cmd *cobra.Command, args []string) {
120121

121122
changelogService := changelog.NewService(db)
122123

124+
incidentRepository := incident.NewRepository(db)
125+
incidentService := incident.NewService(incidentRepository)
126+
123127
srv, err := rest.NewServer(
124128
conf.REST,
125129
conf.OAuth,
@@ -132,6 +136,7 @@ func runAPI(cmd *cobra.Command, args []string) {
132136
httpResultService,
133137
billingService,
134138
changelogService,
139+
incidentService,
135140
)
136141
if err != nil {
137142
l.WithError(err).Fatal("Failed to create REST server")

cmd/api_list.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ func runGenerate(cmd *cobra.Command, args []string) {
4343
nil,
4444
nil,
4545
nil,
46+
nil,
4647
)
4748

4849
var routes []Route

cmd/prober.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -232,12 +232,13 @@ func triggerIncident(ctx context.Context, m *entities.Monitor, hr *http.Result,
232232
for i, assertion := range *failed {
233233

234234
incidents[i] = entities.Incident{
235-
MonitorID: assertion.MonitorID,
236-
TeamID: m.TeamID,
237-
Title: assertion.Source,
238-
Description: &assertion.Source,
235+
MonitorID: assertion.MonitorID,
236+
TeamID: m.TeamID,
237+
Title: assertion.Source,
238+
Description: &assertion.Source,
239+
MonitorAssertionID: assertion.ID,
239240
}
240241
}
241242

242-
return i.Create(ctx, &incidents)
243+
return i.Upsert(ctx, &incidents)
243244
}

internal/entities/incident.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ import (
55
)
66

77
type Incident struct {
8-
ID uint
9-
TeamID uint `gorm:"index;not null"`
10-
MonitorID uint `gorm:"index;not null"`
8+
ID uint
9+
TeamID uint `gorm:"index;not null"`
10+
MonitorID uint `gorm:"index;not null"`
11+
MonitorAssertionID uint `gorm:"uniqueIndex:unresolved_incident;not null"`
12+
Resolved bool `gorm:"uniqueIndex:unresolved_incident;not null;default:false"`
1113

1214
Title string `gorm:"index;not null"`
1315
Description *string

internal/incident/repository.go

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,18 @@ import (
44
"context"
55
"errors"
66

7+
"github.com/opsway-io/backend/internal/connectors/postgres"
78
"github.com/opsway-io/backend/internal/entities"
89
"gorm.io/gorm"
10+
"gorm.io/gorm/clause"
911
)
1012

1113
var ErrNotFound = errors.New("incident not found")
1214

1315
type Repository interface {
1416
GetByID(ctx context.Context, id uint) (*entities.Incident, error)
15-
GetByTeamID(ctx context.Context, teamID uint) (*[]entities.Incident, error)
17+
GetByTeamIDPaginated(ctx context.Context, teamID uint, offset, limit *int) (*[]entities.Incident, error)
18+
Upsert(ctx context.Context, incidents *[]entities.Incident) error
1619
Create(ctx context.Context, incidents *[]entities.Incident) error
1720
Update(ctx context.Context, incident *entities.Incident) error
1821
Delete(ctx context.Context, incident *entities.Incident) error
@@ -43,19 +46,30 @@ func (r *RepositoryImpl) GetByID(ctx context.Context, id uint) (*entities.Incide
4346
return &incident, nil
4447
}
4548

46-
func (r *RepositoryImpl) GetByTeamID(ctx context.Context, teamID uint) (*[]entities.Incident, error) {
49+
func (r *RepositoryImpl) GetByTeamIDPaginated(ctx context.Context, teamID uint, offset, limit *int) (*[]entities.Incident, error) {
4750
var incidents []entities.Incident
4851
if err := r.db.WithContext(
4952
ctx,
5053
).Where(entities.Incident{
5154
TeamID: teamID,
52-
}).Find(&incidents).Error; err != nil {
55+
}).Order(
56+
"created_at desc",
57+
).Scopes(
58+
postgres.Paginated(offset, limit),
59+
).Find(&incidents).Error; err != nil {
5360
return nil, err
5461
}
5562

5663
return &incidents, nil
5764
}
5865

66+
func (r *RepositoryImpl) Upsert(ctx context.Context, incidents *[]entities.Incident) error {
67+
return r.db.WithContext(ctx).Clauses(clause.OnConflict{
68+
Columns: []clause.Column{{Name: "monitor_assertion_id"}, {Name: "resolved"}},
69+
DoUpdates: clause.AssignmentColumns([]string{"updated_at"}),
70+
}).Create(incidents).Error
71+
}
72+
5973
func (r *RepositoryImpl) Create(ctx context.Context, incidents *[]entities.Incident) error {
6074
return r.db.WithContext(ctx).Create(incidents).Error
6175
}

internal/incident/service.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import (
88

99
type Service interface {
1010
GetByID(ctx context.Context, id uint) (*entities.Incident, error)
11-
GetByTeamID(ctx context.Context, teamID uint) (*[]entities.Incident, error)
11+
GetByTeamIDPaginated(ctx context.Context, teamID uint, offset, limit *int) (*[]entities.Incident, error)
12+
Upsert(ctx context.Context, incidents *[]entities.Incident) error
1213
Create(ctx context.Context, incidents *[]entities.Incident) error
1314
Update(ctx context.Context, incident *entities.Incident) error
1415
Delete(ctx context.Context, incident *entities.Incident) error
@@ -28,8 +29,12 @@ func (s *ServiceImpl) GetByID(ctx context.Context, id uint) (*entities.Incident,
2829
return s.repository.GetByID(ctx, id)
2930
}
3031

31-
func (s *ServiceImpl) GetByTeamID(ctx context.Context, teamID uint) (*[]entities.Incident, error) {
32-
return s.repository.GetByTeamID(ctx, teamID)
32+
func (s *ServiceImpl) GetByTeamIDPaginated(ctx context.Context, teamID uint, offset, limit *int) (*[]entities.Incident, error) {
33+
return s.repository.GetByTeamIDPaginated(ctx, teamID, offset, limit)
34+
}
35+
36+
func (s *ServiceImpl) Upsert(ctx context.Context, incidents *[]entities.Incident) error {
37+
return s.repository.Upsert(ctx, incidents)
3338
}
3439

3540
func (s *ServiceImpl) Create(ctx context.Context, incidents *[]entities.Incident) error {

internal/monitor/repository.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ var ErrNotFound = errors.New("monitor not found")
1414
type Repository interface {
1515
GetMonitorAndSettingsByTeamIDAndID(ctx context.Context, teamID uint, monitorID uint) (*entities.Monitor, error)
1616
GetMonitorsAndSettingsByTeamID(ctx context.Context, teamID uint, offset *int, limit *int, query *string) (*[]MonitorWithTotalCount, error)
17+
GetMonitorsAndIncidentsByTeamID(ctx context.Context, teamID uint) (*[]entities.Monitor, error)
1718
SetState(ctx context.Context, teamID, monitorID uint, state entities.MonitorState) error
1819
Create(ctx context.Context, monitor *entities.Monitor) error
1920
Update(ctx context.Context, teamID, monitorID uint, monitor *entities.Monitor) error
@@ -82,6 +83,25 @@ func (r *RepositoryImpl) GetMonitorsAndSettingsByTeamID(ctx context.Context, tea
8283
return &monitors, err
8384
}
8485

86+
func (r *RepositoryImpl) GetMonitorsAndIncidentsByTeamID(ctx context.Context, teamID uint) (*[]entities.Monitor, error) {
87+
var monitors []entities.Monitor
88+
err := r.db.WithContext(
89+
ctx,
90+
).Preload("Incidents", "resolved = ?", false).
91+
Where(entities.Monitor{
92+
TeamID: teamID,
93+
}).Find(&monitors).Error
94+
if err != nil {
95+
if errors.Is(err, gorm.ErrRecordNotFound) {
96+
return nil, nil
97+
}
98+
99+
return nil, err
100+
}
101+
102+
return &monitors, err
103+
}
104+
85105
func (r *RepositoryImpl) SetState(ctx context.Context, teamID, monitorID uint, state entities.MonitorState) error {
86106
err := r.db.WithContext(ctx).Model(
87107
&entities.Monitor{},

internal/monitor/service.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
type Service interface {
1414
GetMonitorAndSettingsByTeamIDAndID(ctx context.Context, teamID uint, monitorID uint) (*entities.Monitor, error)
1515
GetMonitorsAndSettingsByTeamID(ctx context.Context, teamID uint, offset *int, limit *int, query *string) (*[]MonitorWithTotalCount, error)
16+
GetMonitorsAndIncidentsByTeamID(ctx context.Context, teamID uint) (*[]entities.Monitor, error)
1617
SetState(ctx context.Context, teamID, monitorID uint, state entities.MonitorState) error
1718
Create(ctx context.Context, monitor *entities.Monitor) error
1819
Update(ctx context.Context, teamID, monitorID uint, monitor *entities.Monitor) error
@@ -39,6 +40,10 @@ func (s *ServiceImpl) GetMonitorsAndSettingsByTeamID(ctx context.Context, teamID
3940
return s.repository.GetMonitorsAndSettingsByTeamID(ctx, teamID, offset, limit, query)
4041
}
4142

43+
func (s *ServiceImpl) GetMonitorsAndIncidentsByTeamID(ctx context.Context, teamID uint) (*[]entities.Monitor, error) {
44+
return s.repository.GetMonitorsAndIncidentsByTeamID(ctx, teamID)
45+
}
46+
4247
func (s *ServiceImpl) SetState(ctx context.Context, teamID, monitorID uint, state entities.MonitorState) error {
4348
err := s.repository.SetState(ctx, teamID, monitorID, state)
4449
if err != nil {
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
package incidents
2+
3+
import (
4+
"net/http"
5+
6+
"github.com/labstack/echo/v4"
7+
"github.com/opsway-io/backend/internal/entities"
8+
hs "github.com/opsway-io/backend/internal/rest/handlers"
9+
"github.com/opsway-io/backend/internal/rest/helpers"
10+
)
11+
12+
type GetIncidentsRequest struct {
13+
TeamID uint `param:"teamId" validate:"required,numeric,gte=0"`
14+
Offset *int `query:"offset" validate:"omitempty,numeric,gte=0"`
15+
Limit *int `query:"limit" validate:"omitempty,numeric,gte=0,max=255"`
16+
}
17+
18+
type GetIncidentsResponse struct {
19+
Checks []GetIncidentsResponseIncident `json:"incidents"`
20+
}
21+
22+
type GetIncidentsResponseIncident struct {
23+
ID uint `json:"id"`
24+
TeamID uint `json:"teamId"`
25+
MonitorID uint `json:"monitorId"`
26+
Title string `json:"title"`
27+
Description string `json:"description"`
28+
CreatedAt string `json:"createdAt"`
29+
}
30+
31+
func (h *Handlers) GetIncidents(c hs.AuthenticatedContext) error {
32+
req, err := helpers.Bind[GetIncidentsRequest](c)
33+
if err != nil {
34+
c.Log.WithError(err).Debug("failed to bind GetIncidentsRequest")
35+
36+
return echo.ErrBadRequest
37+
}
38+
39+
ctx := c.Request().Context()
40+
41+
incidents, err := h.IncidentService.GetByTeamIDPaginated(
42+
ctx,
43+
req.TeamID,
44+
req.Offset,
45+
req.Limit)
46+
if err != nil {
47+
c.Log.WithError(err).Error("failed to get incidents")
48+
49+
return echo.ErrInternalServerError
50+
}
51+
52+
resp := h.newGetIncidentResponse(incidents)
53+
54+
return c.JSON(http.StatusOK, resp)
55+
}
56+
57+
func (h *Handlers) newGetIncidentResponse(incidents *[]entities.Incident) *GetIncidentsResponse {
58+
resp := &GetIncidentsResponse{
59+
Checks: make([]GetIncidentsResponseIncident, len(*incidents)),
60+
}
61+
62+
for i, incident := range *incidents {
63+
resp.Checks[i] = GetIncidentsResponseIncident{
64+
ID: incident.ID,
65+
TeamID: incident.TeamID,
66+
MonitorID: incident.MonitorID,
67+
Title: incident.Title,
68+
Description: *incident.Description,
69+
CreatedAt: incident.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
70+
}
71+
}
72+
73+
return resp
74+
}
75+
76+
type GetIncidentOverviewRequest struct {
77+
TeamID uint `param:"teamId" validate:"required,numeric,gte=0"`
78+
Offset *int `query:"offset" validate:"omitempty,numeric,gte=0"`
79+
Limit *int `query:"limit" validate:"omitempty,numeric,gte=0,max=255"`
80+
}
81+
82+
type GetIncidentOverviewResponse struct {
83+
Checks []GetIncidentOverviewResponseIncident `json:"incidents"`
84+
}
85+
86+
type GetIncidentOverviewResponseIncident struct {
87+
ID uint `json:"id"`
88+
TeamID uint `json:"teamId"`
89+
MonitorID uint `json:"monitorId"`
90+
CreatedAt string `json:"createdAt"`
91+
Count int `json:"count"`
92+
}
93+
94+
func (h *Handlers) GetIncidentOverview(c hs.AuthenticatedContext) error {
95+
req, err := helpers.Bind[GetIncidentOverviewRequest](c)
96+
if err != nil {
97+
c.Log.WithError(err).Debug("failed to bind GetIncidentsRequest")
98+
99+
return echo.ErrBadRequest
100+
}
101+
102+
ctx := c.Request().Context()
103+
104+
incidents, err := h.IncidentService.GetByTeamIDPaginated(
105+
ctx,
106+
req.TeamID,
107+
req.Offset,
108+
req.Limit)
109+
if err != nil {
110+
c.Log.WithError(err).Error("failed to get incidents")
111+
112+
return echo.ErrInternalServerError
113+
}
114+
115+
resp := h.newGetIncidentOverviewResponse(incidents)
116+
117+
return c.JSON(http.StatusOK, resp)
118+
}
119+
120+
func (h *Handlers) newGetIncidentOverviewResponse(incidents *[]entities.Incident) *GetIncidentOverviewResponse {
121+
122+
resp := &GetIncidentOverviewResponse{
123+
Checks: make([]GetIncidentOverviewResponseIncident, len(*incidents)),
124+
}
125+
126+
for i, incident := range *incidents {
127+
resp.Checks[i] = GetIncidentOverviewResponseIncident{
128+
ID: incident.ID,
129+
TeamID: incident.TeamID,
130+
MonitorID: incident.MonitorID,
131+
Count: 0,
132+
CreatedAt: incident.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
133+
}
134+
}
135+
136+
return resp
137+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package incidents
2+
3+
import (
4+
"github.com/labstack/echo/v4"
5+
"github.com/opsway-io/backend/internal/authentication"
6+
"github.com/opsway-io/backend/internal/incident"
7+
"github.com/opsway-io/backend/internal/rest/handlers"
8+
mw "github.com/opsway-io/backend/internal/rest/middleware"
9+
"github.com/opsway-io/backend/internal/team"
10+
"github.com/sirupsen/logrus"
11+
)
12+
13+
type Handlers struct {
14+
AuthenticationService authentication.Service
15+
TeamService team.Service
16+
IncidentService incident.Service
17+
}
18+
19+
func Register(
20+
e *echo.Group,
21+
logger *logrus.Entry,
22+
teamService team.Service,
23+
incidentService incident.Service,
24+
) {
25+
h := &Handlers{
26+
IncidentService: incidentService,
27+
}
28+
29+
TeamGuard := mw.TeamGuardFactory(logger, teamService)
30+
31+
AuthHandler := handlers.AuthenticatedHandlerFactory(logger)
32+
33+
monitorsGroup := e.Group(
34+
"/teams/:teamId/incidents",
35+
TeamGuard(),
36+
)
37+
38+
monitorsGroup.GET("", AuthHandler(h.GetIncidents))
39+
monitorsGroup.GET("/overview", AuthHandler(h.GetIncidents))
40+
}

0 commit comments

Comments
 (0)