chore: establish local platform baseline
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
package alerts
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"data-terminal-cloud/internal/audit"
|
||||
"data-terminal-cloud/internal/idempotency"
|
||||
"data-terminal-cloud/internal/identity"
|
||||
"data-terminal-cloud/internal/outbox"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
const alertDeliveryTestEventType = "alert.webhook_test_requested"
|
||||
|
||||
type DeliveryTestInput struct {
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
type DeliveryTest struct {
|
||||
ID string `json:"id"`
|
||||
TestRef string `json:"test_ref"`
|
||||
RoutingPolicyID string `json:"routing_policy_id"`
|
||||
Category string `json:"category"`
|
||||
Severity string `json:"severity"`
|
||||
ReceiverGroup string `json:"receiver_group"`
|
||||
RunbookRef string `json:"runbook_ref"`
|
||||
Status string `json:"status"`
|
||||
AttemptCount int `json:"attempt_count"`
|
||||
ManualRetryCount int `json:"manual_retry_count"`
|
||||
LastErrorCode string `json:"last_error_code,omitempty"`
|
||||
LastManualRetryAt *time.Time `json:"last_manual_retry_at,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
||||
DeadAt *time.Time `json:"dead_at,omitempty"`
|
||||
}
|
||||
|
||||
type deliveryTestPayload struct {
|
||||
Action string `json:"action"`
|
||||
TestRef string `json:"test_ref"`
|
||||
TenantID string `json:"tenant_id"`
|
||||
RoutingPolicyID string `json:"routing_policy_id"`
|
||||
Category string `json:"category"`
|
||||
Severity string `json:"severity"`
|
||||
ReceiverGroup string `json:"receiver_group"`
|
||||
RunbookRef string `json:"runbook_ref"`
|
||||
Title string `json:"title"`
|
||||
Summary string `json:"summary"`
|
||||
OccurredAt time.Time `json:"occurred_at"`
|
||||
}
|
||||
|
||||
func (s *Service) CreateDeliveryTest(ctx context.Context, principal identity.Principal, tenantPublicID, routingPolicyPublicID, key, requestID string, input DeliveryTestInput) (DeliveryTest, bool, error) {
|
||||
input.Reason = strings.TrimSpace(input.Reason)
|
||||
if !validUUID(routingPolicyPublicID) || input.Reason == "" || len([]rune(input.Reason)) > 500 {
|
||||
return DeliveryTest{}, false, ErrInvalid
|
||||
}
|
||||
a, err := s.resolveActor(ctx, principal, tenantPublicID, "ops_operator", "ops_approver", "ops_admin")
|
||||
if err != nil {
|
||||
return DeliveryTest{}, false, err
|
||||
}
|
||||
requestBody, _ := json.Marshal(struct {
|
||||
RoutingPolicyID string `json:"routing_policy_id"`
|
||||
Reason string `json:"reason"`
|
||||
}{routingPolicyPublicID, input.Reason})
|
||||
result, err := s.idem.Execute(ctx, idempotency.Request{
|
||||
TenantID: &a.tenantID, ActorKey: "ops:" + a.userPublicID,
|
||||
Scope: "POST /api/v1/ops/alert-routing-policies/{id}/delivery-tests", Key: key,
|
||||
RequestHash: idempotency.Hash(requestBody), ExpiresAfter: 24 * time.Hour,
|
||||
}, func(op context.Context, tx pgx.Tx) (idempotency.Result, error) {
|
||||
var routeInternalID, category, severity, receiverGroup, runbookRef string
|
||||
err := tx.QueryRow(op, `SELECT id::text,category,severity,receiver_group,runbook_ref FROM alert_routing_policy WHERE tenant_id=$1::uuid AND public_id=$2::uuid AND status='ACTIVE'`, a.tenantID, routingPolicyPublicID).
|
||||
Scan(&routeInternalID, &category, &severity, &receiverGroup, &runbookRef)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return idempotency.Result{}, ErrNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return idempotency.Result{}, err
|
||||
}
|
||||
testRef := uuid.NewString()
|
||||
payload, _ := json.Marshal(deliveryTestPayload{
|
||||
Action: "TEST", TestRef: testRef, TenantID: a.tenantPublicID, RoutingPolicyID: routingPolicyPublicID,
|
||||
Category: category, Severity: severity, ReceiverGroup: receiverGroup, RunbookRef: runbookRef,
|
||||
Title: "告警接收器联通测试", Summary: "这是一条由运营工作台发起的受控测试通知,不代表真实故障。", OccurredAt: time.Now().UTC(),
|
||||
})
|
||||
dedupe := "alert-delivery-test:" + testRef
|
||||
eventID, err := outbox.NewStore(s.pool).Enqueue(op, tx, outbox.NewEvent{
|
||||
TenantID: &a.tenantID, AggregateType: "alert_delivery_test", AggregateID: testRef,
|
||||
EventType: alertDeliveryTestEventType, DeduplicationKey: &dedupe, Payload: payload,
|
||||
})
|
||||
if err != nil {
|
||||
return idempotency.Result{}, err
|
||||
}
|
||||
if err = s.audit.Record(op, tx, audit.Event{
|
||||
TenantID: &a.tenantID, ActorType: "user", ActorPublicID: &a.userPublicID,
|
||||
Action: "alert.delivery_test.create", ResourceType: "alert_delivery_test", ResourceID: &eventID,
|
||||
RequestID: requestID, Purpose: input.Reason, Result: "success",
|
||||
Detail: map[string]any{"routing_policy_id": routingPolicyPublicID, "receiver_group": receiverGroup, "test_ref": testRef},
|
||||
}); err != nil {
|
||||
return idempotency.Result{}, err
|
||||
}
|
||||
item, err := getDeliveryTestTx(op, tx, a.tenantID, eventID)
|
||||
if err != nil {
|
||||
return idempotency.Result{}, err
|
||||
}
|
||||
body, _ := json.Marshal(item)
|
||||
return idempotency.Result{StatusCode: 201, Body: body, ResourceID: &eventID}, nil
|
||||
})
|
||||
if err != nil {
|
||||
return DeliveryTest{}, false, err
|
||||
}
|
||||
var item DeliveryTest
|
||||
if err = json.Unmarshal(result.Body, &item); err != nil {
|
||||
return DeliveryTest{}, false, err
|
||||
}
|
||||
return item, !result.Replayed, nil
|
||||
}
|
||||
|
||||
func (s *Service) ListDeliveryTests(ctx context.Context, principal identity.Principal, tenantPublicID string) ([]DeliveryTest, error) {
|
||||
a, err := s.resolveActor(ctx, principal, tenantPublicID, "ops_viewer", "ops_operator", "ops_approver", "ops_admin")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rows, err := s.pool.Query(ctx, deliveryTestSelect+` WHERE o.tenant_id=$1::uuid AND o.event_type=$2 ORDER BY o.created_at DESC LIMIT 50`, a.tenantID, alertDeliveryTestEventType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []DeliveryTest{}
|
||||
for rows.Next() {
|
||||
item, scanErr := scanDeliveryTest(rows)
|
||||
if scanErr != nil {
|
||||
return nil, scanErr
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
return items, rows.Err()
|
||||
}
|
||||
|
||||
const deliveryTestSelect = `SELECT o.public_id::text,o.aggregate_id::text,COALESCE(o.payload->>'routing_policy_id',''),COALESCE(o.payload->>'category',''),COALESCE(o.payload->>'severity',''),COALESCE(o.payload->>'receiver_group',''),COALESCE(o.payload->>'runbook_ref',''),CASE o.status WHEN 'pending' THEN 'PENDING' WHEN 'processing' THEN 'PROCESSING' WHEN 'completed' THEN 'DELIVERED' ELSE 'FAILED' END,o.attempt_count,o.manual_retry_count,COALESCE(o.last_error_code,''),o.last_manual_retry_at,o.created_at,o.completed_at,o.dead_at FROM outbox_event o`
|
||||
|
||||
type deliveryTestScanner interface{ Scan(...any) error }
|
||||
|
||||
func scanDeliveryTest(row deliveryTestScanner) (DeliveryTest, error) {
|
||||
var item DeliveryTest
|
||||
err := row.Scan(&item.ID, &item.TestRef, &item.RoutingPolicyID, &item.Category, &item.Severity, &item.ReceiverGroup, &item.RunbookRef, &item.Status, &item.AttemptCount, &item.ManualRetryCount, &item.LastErrorCode, &item.LastManualRetryAt, &item.CreatedAt, &item.CompletedAt, &item.DeadAt)
|
||||
return item, err
|
||||
}
|
||||
|
||||
func getDeliveryTestTx(ctx context.Context, tx pgx.Tx, tenantID, eventID string) (DeliveryTest, error) {
|
||||
return scanDeliveryTest(tx.QueryRow(ctx, deliveryTestSelect+` WHERE o.tenant_id=$1::uuid AND o.id=$2::uuid AND o.event_type=$3`, tenantID, eventID, alertDeliveryTestEventType))
|
||||
}
|
||||
Reference in New Issue
Block a user