Skip to content

Commit 3d31214

Browse files
feat[backend](alerts): added echoes alerts
1 parent 07ae657 commit 3d31214

7 files changed

Lines changed: 108 additions & 0 deletions

File tree

backend/modules/alerts/connectors/repository.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ type AlertRepository interface {
1919
CountOpenAlerts(ctx context.Context) (int64, error)
2020
CountByStatus(ctx context.Context, status int) (int64, error)
2121
SearchByIDs(ctx context.Context, alertIDs []string) ([]domain.UtmAlert, error)
22+
ListEchoes(ctx context.Context, parentID string, from, size int, sortBy, sortOrder string) ([]domain.UtmAlert, int64, error)
2223
GetRawByID(ctx context.Context, alertID string) (json.RawMessage, error)
2324
RelatedLogRefs(ctx context.Context, steps []domain.CorrelationStep, anchorTS time.Time, maxSize int) (refs []domain.LogRef, truncated bool, err error)
2425
}

backend/modules/alerts/connectors/usecase.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ type AlertUsecase interface {
2525
ConvertToIncident(ctx context.Context, userLogin string, req dto.ConvertToIncidentRequest) error
2626
CountOpenAlerts(ctx context.Context) (*dto.CountOpenAlertsResponse, error)
2727
RelatedLogs(ctx context.Context, alertID string) (*dto.RelatedLogsResponse, error)
28+
ListEchoes(ctx context.Context, parentID string, page, size int, sortBy, sortOrder string) ([]domain.UtmAlert, int64, error)
2829
SetCorrelationResolver(r CorrelationResolver)
2930
}
3031

backend/modules/alerts/handler/alerts.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,3 +185,37 @@ func (h *AlertHandler) CountOpenAlerts(c *gin.Context) {
185185
// Java returns bare Long — match that contract.
186186
c.JSON(http.StatusOK, resp.Count)
187187
}
188+
189+
// @Summary List echoes of an alert
190+
// @Description Returns the child alerts (parentId == :id) of a parent alert, paginated and sorted.
191+
// @Tags Alerts
192+
// @Security BearerAuth
193+
// @Produce json
194+
// @Param id path string true "Parent alert id"
195+
// @Param page query int false "Page number (1-based, default 1)"
196+
// @Param size query int false "Page size (default 20, max 100)"
197+
// @Param sortBy query string false "Sort field (default @timestamp)"
198+
// @Param sortOrder query string false "Sort order: asc|desc (default desc)"
199+
// @Success 200 {array} domain.UtmAlert
200+
// @Header 200 {string} X-Total-Count "Total matching echoes"
201+
// @Failure 400 {object} map[string]string
202+
// @Failure 500 {object} map[string]string
203+
// @Router /utm-alerts/{id}/echoes [get]
204+
func (h *AlertHandler) ListEchoes(c *gin.Context) {
205+
parentID := c.Param("id")
206+
if parentID == "" {
207+
c.JSON(http.StatusBadRequest, gin.H{"error": "missing alert id"})
208+
return
209+
}
210+
page := queryInt(c, "page", 1)
211+
size := queryInt(c, "size", 20)
212+
sortBy := c.Query("sortBy")
213+
sortOrder := c.DefaultQuery("sortOrder", "desc")
214+
215+
items, total, err := h.usecase.ListEchoes(c.Request.Context(), parentID, page, size, sortBy, sortOrder)
216+
if err != nil {
217+
writeAlertError(c, err)
218+
return
219+
}
220+
writePagedArray(c, items, total)
221+
}

backend/modules/alerts/repository/alert_os.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,3 +201,20 @@ func (r *osAlertRepo) SearchByIDs(ctx context.Context, alertIDs []string) ([]dom
201201
return alerts, nil
202202
}
203203

204+
func (r *osAlertRepo) ListEchoes(ctx context.Context, parentID string, from, size int, sortBy, sortOrder string) ([]domain.UtmAlert, int64, error) {
205+
query := termQuery("parentId.keyword", parentID)
206+
raws, total, err := osSearchPage(ctx, alertIndex, query, from, size, sortBy, sortOrder)
207+
if err != nil {
208+
return nil, 0, err
209+
}
210+
alerts := make([]domain.UtmAlert, 0, len(raws))
211+
for _, raw := range raws {
212+
var a domain.UtmAlert
213+
if err := json.Unmarshal(raw, &a); err != nil {
214+
continue
215+
}
216+
alerts = append(alerts, a)
217+
}
218+
return alerts, total, nil
219+
}
220+

backend/modules/alerts/repository/osquery.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,37 @@ func osSearchSources(ctx context.Context, index string, query map[string]any, si
100100
return docs, nil
101101
}
102102

103+
// osSearchPage runs a paged + sorted search and returns the raw source docs
104+
// alongside the matching total count.
105+
func osSearchPage(ctx context.Context, index string, query map[string]any, from, size int, sortBy, sortOrder string) ([]json.RawMessage, int64, error) {
106+
body := map[string]any{
107+
"from": from,
108+
"size": size,
109+
"track_total_hits": true,
110+
}
111+
if query != nil {
112+
body["query"] = query
113+
}
114+
if sortBy != "" {
115+
body["sort"] = []map[string]any{
116+
{sortBy: map[string]any{"order": sortOrder}},
117+
}
118+
}
119+
res, err := osdk.RawSearch(ctx, []string{index}, body)
120+
if err != nil {
121+
return nil, 0, err
122+
}
123+
docs := make([]json.RawMessage, 0, len(res.Hits.Hits))
124+
for _, h := range res.Hits.Hits {
125+
b, err := json.Marshal(h.Source)
126+
if err != nil {
127+
return nil, 0, err
128+
}
129+
docs = append(docs, b)
130+
}
131+
return docs, res.Hits.Total.Value, nil
132+
}
133+
103134
func osCount(ctx context.Context, index string, query map[string]any) (int64, error) {
104135
body := map[string]any{"size": 0, "track_total_hits": true}
105136
if query != nil {

backend/modules/alerts/routes.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ func RegisterRoutes(api *gin.RouterGroup, m *Module, userAuth gin.HandlerFunc) {
2424

2525
ag.GET("/count-open-alerts", read, h.CountOpenAlerts)
2626
ag.GET("/related-logs", read, h.RelatedLogs)
27+
ag.GET("/:id/echoes", read, h.ListEchoes)
2728

2829
tg := api.Group("/utm-alert-tags", userAuth)
2930
tg.POST("", write, th.Create)

backend/modules/alerts/usecase/alert.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,29 @@ func (u *alertUsecase) CountOpenAlerts(ctx context.Context) (*dto.CountOpenAlert
160160
return &dto.CountOpenAlertsResponse{Count: count}, nil
161161
}
162162

163+
func (u *alertUsecase) ListEchoes(ctx context.Context, parentID string, page, size int, sortBy, sortOrder string) ([]domain.UtmAlert, int64, error) {
164+
if parentID == "" {
165+
return nil, 0, domain.ErrMissingAlertID
166+
}
167+
if page < 1 {
168+
page = 1
169+
}
170+
if size < 1 {
171+
size = 20
172+
}
173+
if size > 100 {
174+
size = 100
175+
}
176+
if sortBy == "" {
177+
sortBy = "@timestamp"
178+
}
179+
if sortOrder != "asc" && sortOrder != "desc" {
180+
sortOrder = "desc"
181+
}
182+
from := (page - 1) * size
183+
return u.repo.ListEchoes(ctx, parentID, from, size, sortBy, sortOrder)
184+
}
185+
163186
// ---------------------------------------------------------------------------
164187
// Internal helpers for HistoryEntry construction
165188
// ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)