854 lines
18 KiB
Go
854 lines
18 KiB
Go
package repository
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
|
)
|
|
|
|
func (r *opsRepository) ListAlertRules(ctx context.Context) ([]*service.OpsAlertRule, error) {
|
|
if r == nil || r.db == nil {
|
|
return nil, fmt.Errorf("nil ops repository")
|
|
}
|
|
|
|
q := `
|
|
SELECT
|
|
id,
|
|
name,
|
|
COALESCE(description, ''),
|
|
enabled,
|
|
COALESCE(severity, ''),
|
|
metric_type,
|
|
operator,
|
|
threshold,
|
|
window_minutes,
|
|
sustained_minutes,
|
|
cooldown_minutes,
|
|
COALESCE(notify_email, true),
|
|
filters,
|
|
last_triggered_at,
|
|
created_at,
|
|
updated_at
|
|
FROM ops_alert_rules
|
|
ORDER BY id DESC`
|
|
|
|
rows, err := r.db.QueryContext(ctx, q)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = rows.Close() }()
|
|
|
|
out := []*service.OpsAlertRule{}
|
|
for rows.Next() {
|
|
var rule service.OpsAlertRule
|
|
var filtersRaw []byte
|
|
var lastTriggeredAt sql.NullTime
|
|
if err := rows.Scan(
|
|
&rule.ID,
|
|
&rule.Name,
|
|
&rule.Description,
|
|
&rule.Enabled,
|
|
&rule.Severity,
|
|
&rule.MetricType,
|
|
&rule.Operator,
|
|
&rule.Threshold,
|
|
&rule.WindowMinutes,
|
|
&rule.SustainedMinutes,
|
|
&rule.CooldownMinutes,
|
|
&rule.NotifyEmail,
|
|
&filtersRaw,
|
|
&lastTriggeredAt,
|
|
&rule.CreatedAt,
|
|
&rule.UpdatedAt,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
if lastTriggeredAt.Valid {
|
|
v := lastTriggeredAt.Time
|
|
rule.LastTriggeredAt = &v
|
|
}
|
|
if len(filtersRaw) > 0 && string(filtersRaw) != "null" {
|
|
var decoded map[string]any
|
|
if err := json.Unmarshal(filtersRaw, &decoded); err == nil {
|
|
rule.Filters = decoded
|
|
}
|
|
}
|
|
out = append(out, &rule)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (r *opsRepository) CreateAlertRule(ctx context.Context, input *service.OpsAlertRule) (*service.OpsAlertRule, error) {
|
|
if r == nil || r.db == nil {
|
|
return nil, fmt.Errorf("nil ops repository")
|
|
}
|
|
if input == nil {
|
|
return nil, fmt.Errorf("nil input")
|
|
}
|
|
|
|
filtersArg, err := opsNullJSONMap(input.Filters)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
q := `
|
|
INSERT INTO ops_alert_rules (
|
|
name,
|
|
description,
|
|
enabled,
|
|
severity,
|
|
metric_type,
|
|
operator,
|
|
threshold,
|
|
window_minutes,
|
|
sustained_minutes,
|
|
cooldown_minutes,
|
|
notify_email,
|
|
filters,
|
|
created_at,
|
|
updated_at
|
|
) VALUES (
|
|
$1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,NOW(),NOW()
|
|
)
|
|
RETURNING
|
|
id,
|
|
name,
|
|
COALESCE(description, ''),
|
|
enabled,
|
|
COALESCE(severity, ''),
|
|
metric_type,
|
|
operator,
|
|
threshold,
|
|
window_minutes,
|
|
sustained_minutes,
|
|
cooldown_minutes,
|
|
COALESCE(notify_email, true),
|
|
filters,
|
|
last_triggered_at,
|
|
created_at,
|
|
updated_at`
|
|
|
|
var out service.OpsAlertRule
|
|
var filtersRaw []byte
|
|
var lastTriggeredAt sql.NullTime
|
|
|
|
if err := r.db.QueryRowContext(
|
|
ctx,
|
|
q,
|
|
strings.TrimSpace(input.Name),
|
|
strings.TrimSpace(input.Description),
|
|
input.Enabled,
|
|
strings.TrimSpace(input.Severity),
|
|
strings.TrimSpace(input.MetricType),
|
|
strings.TrimSpace(input.Operator),
|
|
input.Threshold,
|
|
input.WindowMinutes,
|
|
input.SustainedMinutes,
|
|
input.CooldownMinutes,
|
|
input.NotifyEmail,
|
|
filtersArg,
|
|
).Scan(
|
|
&out.ID,
|
|
&out.Name,
|
|
&out.Description,
|
|
&out.Enabled,
|
|
&out.Severity,
|
|
&out.MetricType,
|
|
&out.Operator,
|
|
&out.Threshold,
|
|
&out.WindowMinutes,
|
|
&out.SustainedMinutes,
|
|
&out.CooldownMinutes,
|
|
&out.NotifyEmail,
|
|
&filtersRaw,
|
|
&lastTriggeredAt,
|
|
&out.CreatedAt,
|
|
&out.UpdatedAt,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
if lastTriggeredAt.Valid {
|
|
v := lastTriggeredAt.Time
|
|
out.LastTriggeredAt = &v
|
|
}
|
|
if len(filtersRaw) > 0 && string(filtersRaw) != "null" {
|
|
var decoded map[string]any
|
|
if err := json.Unmarshal(filtersRaw, &decoded); err == nil {
|
|
out.Filters = decoded
|
|
}
|
|
}
|
|
|
|
return &out, nil
|
|
}
|
|
|
|
func (r *opsRepository) UpdateAlertRule(ctx context.Context, input *service.OpsAlertRule) (*service.OpsAlertRule, error) {
|
|
if r == nil || r.db == nil {
|
|
return nil, fmt.Errorf("nil ops repository")
|
|
}
|
|
if input == nil {
|
|
return nil, fmt.Errorf("nil input")
|
|
}
|
|
if input.ID <= 0 {
|
|
return nil, fmt.Errorf("invalid id")
|
|
}
|
|
|
|
filtersArg, err := opsNullJSONMap(input.Filters)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
q := `
|
|
UPDATE ops_alert_rules
|
|
SET
|
|
name = $2,
|
|
description = $3,
|
|
enabled = $4,
|
|
severity = $5,
|
|
metric_type = $6,
|
|
operator = $7,
|
|
threshold = $8,
|
|
window_minutes = $9,
|
|
sustained_minutes = $10,
|
|
cooldown_minutes = $11,
|
|
notify_email = $12,
|
|
filters = $13,
|
|
updated_at = NOW()
|
|
WHERE id = $1
|
|
RETURNING
|
|
id,
|
|
name,
|
|
COALESCE(description, ''),
|
|
enabled,
|
|
COALESCE(severity, ''),
|
|
metric_type,
|
|
operator,
|
|
threshold,
|
|
window_minutes,
|
|
sustained_minutes,
|
|
cooldown_minutes,
|
|
COALESCE(notify_email, true),
|
|
filters,
|
|
last_triggered_at,
|
|
created_at,
|
|
updated_at`
|
|
|
|
var out service.OpsAlertRule
|
|
var filtersRaw []byte
|
|
var lastTriggeredAt sql.NullTime
|
|
|
|
if err := r.db.QueryRowContext(
|
|
ctx,
|
|
q,
|
|
input.ID,
|
|
strings.TrimSpace(input.Name),
|
|
strings.TrimSpace(input.Description),
|
|
input.Enabled,
|
|
strings.TrimSpace(input.Severity),
|
|
strings.TrimSpace(input.MetricType),
|
|
strings.TrimSpace(input.Operator),
|
|
input.Threshold,
|
|
input.WindowMinutes,
|
|
input.SustainedMinutes,
|
|
input.CooldownMinutes,
|
|
input.NotifyEmail,
|
|
filtersArg,
|
|
).Scan(
|
|
&out.ID,
|
|
&out.Name,
|
|
&out.Description,
|
|
&out.Enabled,
|
|
&out.Severity,
|
|
&out.MetricType,
|
|
&out.Operator,
|
|
&out.Threshold,
|
|
&out.WindowMinutes,
|
|
&out.SustainedMinutes,
|
|
&out.CooldownMinutes,
|
|
&out.NotifyEmail,
|
|
&filtersRaw,
|
|
&lastTriggeredAt,
|
|
&out.CreatedAt,
|
|
&out.UpdatedAt,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if lastTriggeredAt.Valid {
|
|
v := lastTriggeredAt.Time
|
|
out.LastTriggeredAt = &v
|
|
}
|
|
if len(filtersRaw) > 0 && string(filtersRaw) != "null" {
|
|
var decoded map[string]any
|
|
if err := json.Unmarshal(filtersRaw, &decoded); err == nil {
|
|
out.Filters = decoded
|
|
}
|
|
}
|
|
|
|
return &out, nil
|
|
}
|
|
|
|
func (r *opsRepository) DeleteAlertRule(ctx context.Context, id int64) error {
|
|
if r == nil || r.db == nil {
|
|
return fmt.Errorf("nil ops repository")
|
|
}
|
|
if id <= 0 {
|
|
return fmt.Errorf("invalid id")
|
|
}
|
|
|
|
res, err := r.db.ExecContext(ctx, "DELETE FROM ops_alert_rules WHERE id = $1", id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
affected, err := res.RowsAffected()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if affected == 0 {
|
|
return sql.ErrNoRows
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *opsRepository) ListAlertEvents(ctx context.Context, filter *service.OpsAlertEventFilter) ([]*service.OpsAlertEvent, error) {
|
|
if r == nil || r.db == nil {
|
|
return nil, fmt.Errorf("nil ops repository")
|
|
}
|
|
if filter == nil {
|
|
filter = &service.OpsAlertEventFilter{}
|
|
}
|
|
|
|
limit := filter.Limit
|
|
if limit <= 0 {
|
|
limit = 100
|
|
}
|
|
if limit > 500 {
|
|
limit = 500
|
|
}
|
|
|
|
where, args := buildOpsAlertEventsWhere(filter)
|
|
args = append(args, limit)
|
|
limitArg := "$" + itoa(len(args))
|
|
|
|
q := `
|
|
SELECT
|
|
id,
|
|
COALESCE(rule_id, 0),
|
|
COALESCE(severity, ''),
|
|
COALESCE(status, ''),
|
|
COALESCE(title, ''),
|
|
COALESCE(description, ''),
|
|
metric_value,
|
|
threshold_value,
|
|
dimensions,
|
|
fired_at,
|
|
resolved_at,
|
|
email_sent,
|
|
created_at
|
|
FROM ops_alert_events
|
|
` + where + `
|
|
ORDER BY fired_at DESC, id DESC
|
|
LIMIT ` + limitArg
|
|
|
|
rows, err := r.db.QueryContext(ctx, q, args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = rows.Close() }()
|
|
|
|
out := []*service.OpsAlertEvent{}
|
|
for rows.Next() {
|
|
var ev service.OpsAlertEvent
|
|
var metricValue sql.NullFloat64
|
|
var thresholdValue sql.NullFloat64
|
|
var dimensionsRaw []byte
|
|
var resolvedAt sql.NullTime
|
|
if err := rows.Scan(
|
|
&ev.ID,
|
|
&ev.RuleID,
|
|
&ev.Severity,
|
|
&ev.Status,
|
|
&ev.Title,
|
|
&ev.Description,
|
|
&metricValue,
|
|
&thresholdValue,
|
|
&dimensionsRaw,
|
|
&ev.FiredAt,
|
|
&resolvedAt,
|
|
&ev.EmailSent,
|
|
&ev.CreatedAt,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
if metricValue.Valid {
|
|
v := metricValue.Float64
|
|
ev.MetricValue = &v
|
|
}
|
|
if thresholdValue.Valid {
|
|
v := thresholdValue.Float64
|
|
ev.ThresholdValue = &v
|
|
}
|
|
if resolvedAt.Valid {
|
|
v := resolvedAt.Time
|
|
ev.ResolvedAt = &v
|
|
}
|
|
if len(dimensionsRaw) > 0 && string(dimensionsRaw) != "null" {
|
|
var decoded map[string]any
|
|
if err := json.Unmarshal(dimensionsRaw, &decoded); err == nil {
|
|
ev.Dimensions = decoded
|
|
}
|
|
}
|
|
out = append(out, &ev)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (r *opsRepository) GetAlertEventByID(ctx context.Context, eventID int64) (*service.OpsAlertEvent, error) {
|
|
if r == nil || r.db == nil {
|
|
return nil, fmt.Errorf("nil ops repository")
|
|
}
|
|
if eventID <= 0 {
|
|
return nil, fmt.Errorf("invalid event id")
|
|
}
|
|
|
|
q := `
|
|
SELECT
|
|
id,
|
|
COALESCE(rule_id, 0),
|
|
COALESCE(severity, ''),
|
|
COALESCE(status, ''),
|
|
COALESCE(title, ''),
|
|
COALESCE(description, ''),
|
|
metric_value,
|
|
threshold_value,
|
|
dimensions,
|
|
fired_at,
|
|
resolved_at,
|
|
email_sent,
|
|
created_at
|
|
FROM ops_alert_events
|
|
WHERE id = $1`
|
|
|
|
row := r.db.QueryRowContext(ctx, q, eventID)
|
|
ev, err := scanOpsAlertEvent(row)
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return nil, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
return ev, nil
|
|
}
|
|
|
|
func (r *opsRepository) GetActiveAlertEvent(ctx context.Context, ruleID int64) (*service.OpsAlertEvent, error) {
|
|
if r == nil || r.db == nil {
|
|
return nil, fmt.Errorf("nil ops repository")
|
|
}
|
|
if ruleID <= 0 {
|
|
return nil, fmt.Errorf("invalid rule id")
|
|
}
|
|
|
|
q := `
|
|
SELECT
|
|
id,
|
|
COALESCE(rule_id, 0),
|
|
COALESCE(severity, ''),
|
|
COALESCE(status, ''),
|
|
COALESCE(title, ''),
|
|
COALESCE(description, ''),
|
|
metric_value,
|
|
threshold_value,
|
|
dimensions,
|
|
fired_at,
|
|
resolved_at,
|
|
email_sent,
|
|
created_at
|
|
FROM ops_alert_events
|
|
WHERE rule_id = $1 AND status = $2
|
|
ORDER BY fired_at DESC
|
|
LIMIT 1`
|
|
|
|
row := r.db.QueryRowContext(ctx, q, ruleID, service.OpsAlertStatusFiring)
|
|
ev, err := scanOpsAlertEvent(row)
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return nil, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
return ev, nil
|
|
}
|
|
|
|
func (r *opsRepository) GetLatestAlertEvent(ctx context.Context, ruleID int64) (*service.OpsAlertEvent, error) {
|
|
if r == nil || r.db == nil {
|
|
return nil, fmt.Errorf("nil ops repository")
|
|
}
|
|
if ruleID <= 0 {
|
|
return nil, fmt.Errorf("invalid rule id")
|
|
}
|
|
|
|
q := `
|
|
SELECT
|
|
id,
|
|
COALESCE(rule_id, 0),
|
|
COALESCE(severity, ''),
|
|
COALESCE(status, ''),
|
|
COALESCE(title, ''),
|
|
COALESCE(description, ''),
|
|
metric_value,
|
|
threshold_value,
|
|
dimensions,
|
|
fired_at,
|
|
resolved_at,
|
|
email_sent,
|
|
created_at
|
|
FROM ops_alert_events
|
|
WHERE rule_id = $1
|
|
ORDER BY fired_at DESC
|
|
LIMIT 1`
|
|
|
|
row := r.db.QueryRowContext(ctx, q, ruleID)
|
|
ev, err := scanOpsAlertEvent(row)
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return nil, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
return ev, nil
|
|
}
|
|
|
|
func (r *opsRepository) CreateAlertEvent(ctx context.Context, event *service.OpsAlertEvent) (*service.OpsAlertEvent, error) {
|
|
if r == nil || r.db == nil {
|
|
return nil, fmt.Errorf("nil ops repository")
|
|
}
|
|
if event == nil {
|
|
return nil, fmt.Errorf("nil event")
|
|
}
|
|
|
|
dimensionsArg, err := opsNullJSONMap(event.Dimensions)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
q := `
|
|
INSERT INTO ops_alert_events (
|
|
rule_id,
|
|
severity,
|
|
status,
|
|
title,
|
|
description,
|
|
metric_value,
|
|
threshold_value,
|
|
dimensions,
|
|
fired_at,
|
|
resolved_at,
|
|
email_sent,
|
|
created_at
|
|
) VALUES (
|
|
$1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,NOW()
|
|
)
|
|
RETURNING
|
|
id,
|
|
COALESCE(rule_id, 0),
|
|
COALESCE(severity, ''),
|
|
COALESCE(status, ''),
|
|
COALESCE(title, ''),
|
|
COALESCE(description, ''),
|
|
metric_value,
|
|
threshold_value,
|
|
dimensions,
|
|
fired_at,
|
|
resolved_at,
|
|
email_sent,
|
|
created_at`
|
|
|
|
row := r.db.QueryRowContext(
|
|
ctx,
|
|
q,
|
|
opsNullInt64(&event.RuleID),
|
|
opsNullString(event.Severity),
|
|
opsNullString(event.Status),
|
|
opsNullString(event.Title),
|
|
opsNullString(event.Description),
|
|
opsNullFloat64(event.MetricValue),
|
|
opsNullFloat64(event.ThresholdValue),
|
|
dimensionsArg,
|
|
event.FiredAt,
|
|
opsNullTime(event.ResolvedAt),
|
|
event.EmailSent,
|
|
)
|
|
return scanOpsAlertEvent(row)
|
|
}
|
|
|
|
func (r *opsRepository) UpdateAlertEventStatus(ctx context.Context, eventID int64, status string, resolvedAt *time.Time) error {
|
|
if r == nil || r.db == nil {
|
|
return fmt.Errorf("nil ops repository")
|
|
}
|
|
if eventID <= 0 {
|
|
return fmt.Errorf("invalid event id")
|
|
}
|
|
if strings.TrimSpace(status) == "" {
|
|
return fmt.Errorf("invalid status")
|
|
}
|
|
|
|
q := `
|
|
UPDATE ops_alert_events
|
|
SET status = $2,
|
|
resolved_at = $3
|
|
WHERE id = $1`
|
|
|
|
_, err := r.db.ExecContext(ctx, q, eventID, strings.TrimSpace(status), opsNullTime(resolvedAt))
|
|
return err
|
|
}
|
|
|
|
func (r *opsRepository) UpdateAlertEventEmailSent(ctx context.Context, eventID int64, emailSent bool) error {
|
|
if r == nil || r.db == nil {
|
|
return fmt.Errorf("nil ops repository")
|
|
}
|
|
if eventID <= 0 {
|
|
return fmt.Errorf("invalid event id")
|
|
}
|
|
|
|
_, err := r.db.ExecContext(ctx, "UPDATE ops_alert_events SET email_sent = $2 WHERE id = $1", eventID, emailSent)
|
|
return err
|
|
}
|
|
|
|
type opsAlertEventRow interface {
|
|
Scan(dest ...any) error
|
|
}
|
|
|
|
func (r *opsRepository) CreateAlertSilence(ctx context.Context, input *service.OpsAlertSilence) (*service.OpsAlertSilence, error) {
|
|
if r == nil || r.db == nil {
|
|
return nil, fmt.Errorf("nil ops repository")
|
|
}
|
|
if input == nil {
|
|
return nil, fmt.Errorf("nil input")
|
|
}
|
|
if input.RuleID <= 0 {
|
|
return nil, fmt.Errorf("invalid rule_id")
|
|
}
|
|
platform := strings.TrimSpace(input.Platform)
|
|
if platform == "" {
|
|
return nil, fmt.Errorf("invalid platform")
|
|
}
|
|
if input.Until.IsZero() {
|
|
return nil, fmt.Errorf("invalid until")
|
|
}
|
|
|
|
q := `
|
|
INSERT INTO ops_alert_silences (
|
|
rule_id,
|
|
platform,
|
|
group_id,
|
|
region,
|
|
until,
|
|
reason,
|
|
created_by,
|
|
created_at
|
|
) VALUES (
|
|
$1,$2,$3,$4,$5,$6,$7,NOW()
|
|
)
|
|
RETURNING id, rule_id, platform, group_id, region, until, COALESCE(reason,''), created_by, created_at`
|
|
|
|
row := r.db.QueryRowContext(
|
|
ctx,
|
|
q,
|
|
input.RuleID,
|
|
platform,
|
|
opsNullInt64(input.GroupID),
|
|
opsNullString(input.Region),
|
|
input.Until,
|
|
opsNullString(input.Reason),
|
|
opsNullInt64(input.CreatedBy),
|
|
)
|
|
|
|
var out service.OpsAlertSilence
|
|
var groupID sql.NullInt64
|
|
var region sql.NullString
|
|
var createdBy sql.NullInt64
|
|
if err := row.Scan(
|
|
&out.ID,
|
|
&out.RuleID,
|
|
&out.Platform,
|
|
&groupID,
|
|
®ion,
|
|
&out.Until,
|
|
&out.Reason,
|
|
&createdBy,
|
|
&out.CreatedAt,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
if groupID.Valid {
|
|
v := groupID.Int64
|
|
out.GroupID = &v
|
|
}
|
|
if region.Valid {
|
|
v := strings.TrimSpace(region.String)
|
|
if v != "" {
|
|
out.Region = &v
|
|
}
|
|
}
|
|
if createdBy.Valid {
|
|
v := createdBy.Int64
|
|
out.CreatedBy = &v
|
|
}
|
|
return &out, nil
|
|
}
|
|
|
|
func (r *opsRepository) IsAlertSilenced(ctx context.Context, ruleID int64, platform string, groupID *int64, region *string, now time.Time) (bool, error) {
|
|
if r == nil || r.db == nil {
|
|
return false, fmt.Errorf("nil ops repository")
|
|
}
|
|
if ruleID <= 0 {
|
|
return false, fmt.Errorf("invalid rule id")
|
|
}
|
|
platform = strings.TrimSpace(platform)
|
|
if platform == "" {
|
|
return false, nil
|
|
}
|
|
if now.IsZero() {
|
|
now = time.Now().UTC()
|
|
}
|
|
|
|
q := `
|
|
SELECT 1
|
|
FROM ops_alert_silences
|
|
WHERE rule_id = $1
|
|
AND platform = $2
|
|
AND (group_id IS NOT DISTINCT FROM $3)
|
|
AND (region IS NOT DISTINCT FROM $4)
|
|
AND until > $5
|
|
LIMIT 1`
|
|
|
|
var dummy int
|
|
err := r.db.QueryRowContext(ctx, q, ruleID, platform, opsNullInt64(groupID), opsNullString(region), now).Scan(&dummy)
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return false, nil
|
|
}
|
|
return false, err
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func scanOpsAlertEvent(row opsAlertEventRow) (*service.OpsAlertEvent, error) {
|
|
var ev service.OpsAlertEvent
|
|
var metricValue sql.NullFloat64
|
|
var thresholdValue sql.NullFloat64
|
|
var dimensionsRaw []byte
|
|
var resolvedAt sql.NullTime
|
|
|
|
if err := row.Scan(
|
|
&ev.ID,
|
|
&ev.RuleID,
|
|
&ev.Severity,
|
|
&ev.Status,
|
|
&ev.Title,
|
|
&ev.Description,
|
|
&metricValue,
|
|
&thresholdValue,
|
|
&dimensionsRaw,
|
|
&ev.FiredAt,
|
|
&resolvedAt,
|
|
&ev.EmailSent,
|
|
&ev.CreatedAt,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
if metricValue.Valid {
|
|
v := metricValue.Float64
|
|
ev.MetricValue = &v
|
|
}
|
|
if thresholdValue.Valid {
|
|
v := thresholdValue.Float64
|
|
ev.ThresholdValue = &v
|
|
}
|
|
if resolvedAt.Valid {
|
|
v := resolvedAt.Time
|
|
ev.ResolvedAt = &v
|
|
}
|
|
if len(dimensionsRaw) > 0 && string(dimensionsRaw) != "null" {
|
|
var decoded map[string]any
|
|
if err := json.Unmarshal(dimensionsRaw, &decoded); err == nil {
|
|
ev.Dimensions = decoded
|
|
}
|
|
}
|
|
return &ev, nil
|
|
}
|
|
|
|
func buildOpsAlertEventsWhere(filter *service.OpsAlertEventFilter) (string, []any) {
|
|
clauses := []string{"1=1"}
|
|
args := []any{}
|
|
|
|
if filter == nil {
|
|
return "WHERE " + strings.Join(clauses, " AND "), args
|
|
}
|
|
|
|
if status := strings.TrimSpace(filter.Status); status != "" {
|
|
args = append(args, status)
|
|
clauses = append(clauses, "status = $"+itoa(len(args)))
|
|
}
|
|
if severity := strings.TrimSpace(filter.Severity); severity != "" {
|
|
args = append(args, severity)
|
|
clauses = append(clauses, "severity = $"+itoa(len(args)))
|
|
}
|
|
if filter.EmailSent != nil {
|
|
args = append(args, *filter.EmailSent)
|
|
clauses = append(clauses, "email_sent = $"+itoa(len(args)))
|
|
}
|
|
if filter.StartTime != nil && !filter.StartTime.IsZero() {
|
|
args = append(args, *filter.StartTime)
|
|
clauses = append(clauses, "fired_at >= $"+itoa(len(args)))
|
|
}
|
|
if filter.EndTime != nil && !filter.EndTime.IsZero() {
|
|
args = append(args, *filter.EndTime)
|
|
clauses = append(clauses, "fired_at < $"+itoa(len(args)))
|
|
}
|
|
|
|
// Cursor pagination (descending by fired_at, then id)
|
|
if filter.BeforeFiredAt != nil && !filter.BeforeFiredAt.IsZero() && filter.BeforeID != nil && *filter.BeforeID > 0 {
|
|
args = append(args, *filter.BeforeFiredAt)
|
|
tsArg := "$" + itoa(len(args))
|
|
args = append(args, *filter.BeforeID)
|
|
idArg := "$" + itoa(len(args))
|
|
clauses = append(clauses, fmt.Sprintf("(fired_at < %s OR (fired_at = %s AND id < %s))", tsArg, tsArg, idArg))
|
|
}
|
|
// Dimensions are stored in JSONB. We filter best-effort without requiring GIN indexes.
|
|
if platform := strings.TrimSpace(filter.Platform); platform != "" {
|
|
args = append(args, platform)
|
|
clauses = append(clauses, "(dimensions->>'platform') = $"+itoa(len(args)))
|
|
}
|
|
if filter.GroupID != nil && *filter.GroupID > 0 {
|
|
args = append(args, fmt.Sprintf("%d", *filter.GroupID))
|
|
clauses = append(clauses, "(dimensions->>'group_id') = $"+itoa(len(args)))
|
|
}
|
|
|
|
return "WHERE " + strings.Join(clauses, " AND "), args
|
|
}
|
|
|
|
func opsNullJSONMap(v map[string]any) (any, error) {
|
|
if v == nil {
|
|
return sql.NullString{}, nil
|
|
}
|
|
b, err := json.Marshal(v)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(b) == 0 {
|
|
return sql.NullString{}, nil
|
|
}
|
|
return sql.NullString{String: string(b), Valid: true}, nil
|
|
}
|