2026-08-26 06:41:07 +08:00
package alerts
import (
"context"
"encoding/json"
"errors"
"regexp"
"strings"
"time"
"data-terminal-cloud/internal/audit"
"data-terminal-cloud/internal/idempotency"
"data-terminal-cloud/internal/identity"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
var (
ErrInvalid = errors . New ( "alert input is invalid" )
ErrAccessDenied = errors . New ( "alert operations access denied" )
ErrNotFound = errors . New ( "alert resource not found" )
ErrStateConflict = errors . New ( "alert state conflict" )
ErrVersionConflict = errors . New ( "alert version conflict" )
)
type RoutingInput struct {
Category string `json:"category"`
Severity string `json:"severity"`
ReceiverGroup string `json:"receiver_group"`
ResponseMinutes int `json:"response_minutes"`
EscalationAfterMinutes int `json:"escalation_after_minutes"`
EscalationReceiverGroup string `json:"escalation_receiver_group"`
RunbookRef string `json:"runbook_ref"`
ChangeSummary string `json:"change_summary"`
}
type RoutingPolicy struct {
ID string `json:"id"`
TenantID string `json:"tenant_id"`
Category string `json:"category"`
Severity string `json:"severity"`
VersionNo int `json:"version_no"`
Status string `json:"status"`
ReceiverGroup string `json:"receiver_group"`
ResponseMinutes int `json:"response_minutes"`
EscalationAfterMinutes int `json:"escalation_after_minutes"`
EscalationReceiverGroup string `json:"escalation_receiver_group"`
RunbookRef string `json:"runbook_ref"`
ChangeSummary string `json:"change_summary"`
CreatedBy string `json:"created_by"`
RetiredAt * time . Time `json:"retired_at,omitempty"`
RowVersion int64 `json:"row_version"`
CreatedAt time . Time `json:"created_at"`
UpdatedAt time . Time `json:"updated_at"`
}
type SilenceInput struct {
RuleCode string `json:"rule_code,omitempty"`
Category string `json:"category,omitempty"`
ResourceType string `json:"resource_type,omitempty"`
ResourcePublicID string `json:"resource_id,omitempty"`
Matcher map [ string ] any `json:"matcher"`
Reason string `json:"reason"`
StartsAt time . Time `json:"starts_at"`
EndsAt time . Time `json:"ends_at"`
}
type Silence struct {
ID string `json:"id"`
RuleCode * string `json:"rule_code,omitempty"`
Category * string `json:"category,omitempty"`
ResourceType * string `json:"resource_type,omitempty"`
ResourcePublicID * string `json:"resource_id,omitempty"`
Matcher map [ string ] any `json:"matcher"`
Reason string `json:"reason"`
StartsAt time . Time `json:"starts_at"`
EndsAt time . Time `json:"ends_at"`
CreatedBy string `json:"created_by"`
RevokedAt * time . Time `json:"revoked_at,omitempty"`
RowVersion int64 `json:"row_version"`
CreatedAt time . Time `json:"created_at"`
}
type Action struct {
ID string `json:"id"`
ActionType string `json:"action_type"`
FromStatus * string `json:"from_status,omitempty"`
ToStatus * string `json:"to_status,omitempty"`
ActorType string `json:"actor_type"`
ActorUser * string `json:"actor_user_id,omitempty"`
Note string `json:"note"`
CreatedAt time . Time `json:"created_at"`
}
type Incident struct {
ID string `json:"id"`
RuleCode string `json:"rule_code"`
Category string `json:"category"`
Severity string `json:"severity"`
Status string `json:"status"`
Fingerprint string `json:"fingerprint"`
Title string `json:"title"`
Summary string `json:"summary"`
ResourceType * string `json:"resource_type,omitempty"`
ResourcePublicID * string `json:"resource_id,omitempty"`
CorrelationRequestID * string `json:"request_id,omitempty"`
ReceiverGroup * string `json:"receiver_group,omitempty"`
RunbookRef * string `json:"runbook_ref,omitempty"`
CurrentOwner * string `json:"current_owner,omitempty"`
FirstDetectedAt time . Time `json:"first_detected_at"`
LastDetectedAt time . Time `json:"last_detected_at"`
OccurrenceCount int64 `json:"occurrence_count"`
AcknowledgedAt * time . Time `json:"acknowledged_at,omitempty"`
MitigatedAt * time . Time `json:"mitigated_at,omitempty"`
ResolvedAt * time . Time `json:"resolved_at,omitempty"`
SilencedUntil * time . Time `json:"silenced_until,omitempty"`
EscalatedAt * time . Time `json:"escalated_at,omitempty"`
RowVersion int64 `json:"row_version"`
Actions [] Action `json:"actions"`
}
type actor struct { tenantID , tenantPublicID , userID , userPublicID , role string }
type Service struct {
pool * pgxpool . Pool
audit * audit . Repository
idem * idempotency . Executor
}
func NewService ( pool * pgxpool . Pool , auditRepository * audit . Repository ) ( * Service , error ) {
if pool == nil || auditRepository == nil {
return nil , errors . New ( "alert service dependencies are required" )
}
return & Service { pool : pool , audit : auditRepository , idem : idempotency . NewExecutor ( pool )}, nil
}
func ( s * Service ) CreateRoutingPolicy ( ctx context . Context , principal identity . Principal , tenantPublicID , key , requestID string , input RoutingInput ) ( RoutingPolicy , bool , error ) {
normalizeRouting ( & input )
if ! validRoutingInput ( input ) {
return RoutingPolicy {}, false , ErrInvalid
}
a , err := s . resolveActor ( ctx , principal , tenantPublicID , "ops_approver" , "ops_admin" )
if err != nil {
return RoutingPolicy {}, false , err
}
body , _ := json . Marshal ( input )
result , err := s . idem . Execute ( ctx , idempotency . Request { TenantID : & a . tenantID , ActorKey : "ops:" + a . userPublicID , Scope : "POST /api/v1/ops/alert-routing-policies" , Key : key , RequestHash : idempotency . Hash ( body ), ExpiresAfter : 24 * time . Hour }, func ( op context . Context , tx pgx . Tx ) ( idempotency . Result , error ) {
if _ , e := tx . Exec ( op , `SELECT pg_advisory_xact_lock(hashtextextended($1,0))` , a . tenantID + ":" + input . Category + ":" + input . Severity ); e != nil {
return idempotency . Result {}, e
}
var version int
if e := tx . QueryRow ( op , `SELECT COALESCE(MAX(version_no),0)+1 FROM alert_routing_policy WHERE tenant_id=$1::uuid AND category=$2 AND severity=$3` , a . tenantID , input . Category , input . Severity ). Scan ( & version ); e != nil {
return idempotency . Result {}, e
}
if _ , e := tx . Exec ( op , `UPDATE alert_routing_policy SET status='RETIRED',retired_at=now(),row_version=row_version+1,updated_at=now() WHERE tenant_id=$1::uuid AND category=$2 AND severity=$3 AND status='ACTIVE'` , a . tenantID , input . Category , input . Severity ); e != nil {
return idempotency . Result {}, e
}
publicID := uuid . NewString ()
var internalID string
if e := tx . QueryRow ( op , `INSERT INTO alert_routing_policy(public_id,tenant_id,category,severity,version_no,receiver_group,response_minutes,escalation_after_minutes,escalation_receiver_group,runbook_ref,change_summary,created_by) VALUES($1::uuid,$2::uuid,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12::uuid) RETURNING id::text` , publicID , a . tenantID , input . Category , input . Severity , version , input . ReceiverGroup , input . ResponseMinutes , input . EscalationAfterMinutes , input . EscalationReceiverGroup , input . RunbookRef , input . ChangeSummary , a . userID ). Scan ( & internalID ); e != nil {
return idempotency . Result {}, e
}
if e := s . audit . Record ( op , tx , audit . Event { TenantID : & a . tenantID , ActorType : "user" , ActorPublicID : & a . userPublicID , Action : "alert.routing.activate" , ResourceType : "alert_routing_policy" , ResourceID : & internalID , RequestID : requestID , Purpose : input . ChangeSummary , Result : "success" , Detail : map [ string ] any { "category" : input . Category , "severity" : input . Severity , "version_no" : version }}); e != nil {
return idempotency . Result {}, e
}
item , e := getRoutingTx ( op , tx , a . tenantID , publicID )
if e != nil {
return idempotency . Result {}, e
}
encoded , _ := json . Marshal ( item )
return idempotency . Result { StatusCode : 201 , Body : encoded , ResourceID : & internalID }, nil
})
if err != nil {
return RoutingPolicy {}, false , err
}
var item RoutingPolicy
if err = json . Unmarshal ( result . Body , & item ); err != nil {
return RoutingPolicy {}, false , err
}
return item , ! result . Replayed , nil
}
func ( s * Service ) ListRoutingPolicies ( ctx context . Context , principal identity . Principal , tenantPublicID string ) ([] RoutingPolicy , error ) {
a , e := s . resolveActor ( ctx , principal , tenantPublicID , "ops_viewer" , "ops_operator" , "ops_approver" , "ops_admin" )
if e != nil {
return nil , e
}
rows , e := s . pool . Query ( ctx , routingSelect + ` WHERE arp.tenant_id=$1::uuid ORDER BY CASE arp.status WHEN 'ACTIVE' THEN 1 ELSE 2 END,arp.category,arp.severity,arp.version_no DESC` , a . tenantID )
if e != nil {
return nil , e
}
defer rows . Close ()
items := [] RoutingPolicy {}
for rows . Next () {
item , se := scanRouting ( rows )
if se != nil {
return nil , se
}
items = append ( items , item )
}
return items , rows . Err ()
}
func ( s * Service ) ListIncidents ( ctx context . Context , principal identity . Principal , tenantPublicID , status string ) ([] Incident , error ) {
a , e := s . resolveActor ( ctx , principal , tenantPublicID , "ops_viewer" , "ops_operator" , "ops_approver" , "ops_admin" )
if e != nil {
return nil , e
}
status = strings . ToUpper ( strings . TrimSpace ( status ))
if status != "" && ! validStatus ( status ) {
return nil , ErrInvalid
}
rows , e := s . pool . Query ( ctx , incidentSelect + ` WHERE ai.tenant_id=$1::uuid AND (NULLIF($2,'') IS NULL OR ai.status=$2) ORDER BY CASE ai.severity WHEN 'CRITICAL' THEN 1 WHEN 'HIGH' THEN 2 WHEN 'MEDIUM' THEN 3 ELSE 4 END,ai.last_detected_at DESC LIMIT 1000` , a . tenantID , status )
if e != nil {
return nil , e
}
defer rows . Close ()
items := [] Incident {}
for rows . Next () {
item , se := scanIncident ( rows )
if se != nil {
return nil , se
}
items = append ( items , item )
}
return items , rows . Err ()
}
func ( s * Service ) GetIncident ( ctx context . Context , principal identity . Principal , tenantPublicID , publicID string ) ( Incident , error ) {
a , e := s . resolveActor ( ctx , principal , tenantPublicID , "ops_viewer" , "ops_operator" , "ops_approver" , "ops_admin" )
if e != nil {
return Incident {}, e
}
if ! validUUID ( publicID ) {
return Incident {}, ErrInvalid
}
tx , e := s . pool . BeginTx ( ctx , pgx . TxOptions { AccessMode : pgx . ReadOnly })
if e != nil {
return Incident {}, e
}
defer tx . Rollback ( context . Background ())
item , e := getIncidentTx ( ctx , tx , a . tenantID , publicID )
if errors . Is ( e , pgx . ErrNoRows ) {
return Incident {}, ErrNotFound
}
if e == nil {
e = tx . Commit ( ctx )
}
return item , e
}
func ( s * Service ) CreateSilence ( ctx context . Context , principal identity . Principal , tenantPublicID , key , requestID string , input SilenceInput ) ( Silence , bool , error ) {
normalizeSilence ( & input )
if ! validSilenceInput ( input , time . Now (). UTC ()) {
return Silence {}, false , ErrInvalid
}
a , e := s . resolveActor ( ctx , principal , tenantPublicID , "ops_operator" , "ops_approver" , "ops_admin" )
if e != nil {
return Silence {}, false , e
}
encoded , _ := json . Marshal ( input )
result , e := s . idem . Execute ( ctx , idempotency . Request { TenantID : & a . tenantID , ActorKey : "ops:" + a . userPublicID , Scope : "POST /api/v1/ops/alert-silences" , Key : key , RequestHash : idempotency . Hash ( encoded ), ExpiresAfter : 24 * time . Hour }, func ( op context . Context , tx pgx . Tx ) ( idempotency . Result , error ) {
matcher , _ := json . Marshal ( input . Matcher )
publicID := uuid . NewString ()
var internalID string
e := tx . QueryRow ( op , `INSERT INTO alert_silence(public_id,tenant_id,rule_code,category,resource_type,resource_public_id,matcher,reason,starts_at,ends_at,created_by) VALUES($1::uuid,$2::uuid,NULLIF($3,''),NULLIF($4,''),NULLIF($5,''),$6::uuid,$7::jsonb,$8,$9,$10,$11::uuid) RETURNING id::text` , publicID , a . tenantID , input . RuleCode , input . Category , input . ResourceType , nullStringUUID ( input . ResourcePublicID ), matcher , input . Reason , input . StartsAt . UTC (), input . EndsAt . UTC (), a . userID ). Scan ( & internalID )
if e != nil {
return idempotency . Result {}, e
}
if e = s . audit . Record ( op , tx , audit . Event { TenantID : & a . tenantID , ActorType : "user" , ActorPublicID : & a . userPublicID , Action : "alert.silence.create" , ResourceType : "alert_silence" , ResourceID : & internalID , RequestID : requestID , Purpose : input . Reason , Result : "success" }); e != nil {
return idempotency . Result {}, e
}
item , e := getSilenceTx ( op , tx , a . tenantID , publicID )
if e != nil {
return idempotency . Result {}, e
}
body , _ := json . Marshal ( item )
return idempotency . Result { StatusCode : 201 , Body : body , ResourceID : & internalID }, nil
})
if e != nil {
return Silence {}, false , e
}
var item Silence
if e = json . Unmarshal ( result . Body , & item ); e != nil {
return Silence {}, false , e
}
return item , ! result . Replayed , nil
}
func ( s * Service ) ListSilences ( ctx context . Context , principal identity . Principal , tenantPublicID string ) ([] Silence , error ) {
a , e := s . resolveActor ( ctx , principal , tenantPublicID , "ops_viewer" , "ops_operator" , "ops_approver" , "ops_admin" )
if e != nil {
return nil , e
}
rows , e := s . pool . Query ( ctx , silenceSelect + ` WHERE als.tenant_id=$1::uuid ORDER BY CASE WHEN revoked_at IS NULL AND ends_at>now() THEN 1 ELSE 2 END,ends_at DESC LIMIT 1000` , a . tenantID )
if e != nil {
return nil , e
}
defer rows . Close ()
items := [] Silence {}
for rows . Next () {
item , se := scanSilence ( rows )
if se != nil {
return nil , se
}
items = append ( items , item )
}
return items , rows . Err ()
}
func ( s * Service ) RevokeSilence ( ctx context . Context , principal identity . Principal , tenantPublicID , publicID , requestID , reason string , expected int64 ) ( Silence , error ) {
reason = strings . TrimSpace ( reason )
if reason == "" || len ([] rune ( reason )) > 500 {
return Silence {}, ErrInvalid
}
a , e := s . resolveActor ( ctx , principal , tenantPublicID , "ops_operator" , "ops_approver" , "ops_admin" )
if e != nil {
return Silence {}, e
}
tx , e := s . pool . BeginTx ( ctx , pgx . TxOptions {})
if e != nil {
return Silence {}, e
}
defer tx . Rollback ( context . Background ())
var id string
var version int64
var revoked * time . Time
e = tx . QueryRow ( ctx , `SELECT id::text,row_version,revoked_at FROM alert_silence WHERE tenant_id=$1::uuid AND public_id=$2::uuid FOR UPDATE` , a . tenantID , publicID ). Scan ( & id , & version , & revoked )
if errors . Is ( e , pgx . ErrNoRows ) {
return Silence {}, ErrNotFound
}
if e != nil {
return Silence {}, e
}
if version != expected {
return Silence {}, ErrVersionConflict
}
if revoked != nil {
return Silence {}, ErrStateConflict
}
_ , e = tx . Exec ( ctx , `UPDATE alert_silence SET revoked_by=$2::uuid,revoked_at=now(),row_version=row_version+1,updated_at=now() WHERE id=$1::uuid` , id , a . userID )
if e != nil {
return Silence {}, e
}
if e = s . audit . Record ( ctx , tx , audit . Event { TenantID : & a . tenantID , ActorType : "user" , ActorPublicID : & a . userPublicID , Action : "alert.silence.revoke" , ResourceType : "alert_silence" , ResourceID : & id , RequestID : requestID , Purpose : reason , Result : "success" }); e != nil {
return Silence {}, e
}
item , e := getSilenceTx ( ctx , tx , a . tenantID , publicID )
if e == nil {
e = tx . Commit ( ctx )
}
return item , e
}
func ( s * Service ) TransitionIncident ( ctx context . Context , principal identity . Principal , tenantPublicID , publicID , requestID , action , note string , expected int64 ) ( Incident , error ) {
action = strings . ToUpper ( strings . TrimSpace ( action ))
note = strings . TrimSpace ( note )
if note == "" || len ([] rune ( note )) > 1000 {
return Incident {}, ErrInvalid
}
targets := map [ string ] string { "ACKNOWLEDGE" : "ACKNOWLEDGED" , "MITIGATE" : "MITIGATED" , "RESOLVE" : "RESOLVED" }
target , ok := targets [ action ]
if ! ok {
return Incident {}, ErrInvalid
}
a , e := s . resolveActor ( ctx , principal , tenantPublicID , "ops_operator" , "ops_approver" , "ops_admin" )
if e != nil {
return Incident {}, e
}
tx , e := s . pool . BeginTx ( ctx , pgx . TxOptions {})
if e != nil {
return Incident {}, e
}
defer tx . Rollback ( context . Background ())
var id , status string
var version int64
e = tx . QueryRow ( ctx , `SELECT id::text,status,row_version FROM alert_incident WHERE tenant_id=$1::uuid AND public_id=$2::uuid FOR UPDATE` , a . tenantID , publicID ). Scan ( & id , & status , & version )
if errors . Is ( e , pgx . ErrNoRows ) {
return Incident {}, ErrNotFound
}
if e != nil {
return Incident {}, e
}
if version != expected {
return Incident {}, ErrVersionConflict
}
if status == "RESOLVED" || status == target {
return Incident {}, ErrStateConflict
}
now := time . Now (). UTC ()
_ , e = tx . Exec ( ctx , `UPDATE alert_incident SET status=$2::varchar,current_owner_id=COALESCE(current_owner_id,$3::uuid),acknowledged_at=CASE WHEN $2::varchar='ACKNOWLEDGED' THEN $4::timestamptz ELSE acknowledged_at END,mitigated_at=CASE WHEN $2::varchar='MITIGATED' THEN $4::timestamptz ELSE mitigated_at END,resolved_at=CASE WHEN $2::varchar='RESOLVED' THEN $4::timestamptz ELSE resolved_at END,row_version=row_version+1,updated_at=$4::timestamptz WHERE id=$1::uuid` , id , target , a . userID , now )
if e != nil {
return Incident {}, e
}
_ , e = tx . Exec ( ctx , `INSERT INTO alert_action(tenant_id,incident_id,action_type,from_status,to_status,actor_type,actor_user_id,note) VALUES($1::uuid,$2::uuid,$3,$4,$5,'user',$6::uuid,$7)` , a . tenantID , id , target , status , target , a . userID , note )
if e != nil {
return Incident {}, e
}
if e = s . audit . Record ( ctx , tx , audit . Event { TenantID : & a . tenantID , ActorType : "user" , ActorPublicID : & a . userPublicID , Action : "alert.incident." + strings . ToLower ( action ), ResourceType : "alert_incident" , ResourceID : & id , RequestID : requestID , Purpose : note , Result : "success" }); e != nil {
return Incident {}, e
}
item , e := getIncidentTx ( ctx , tx , a . tenantID , publicID )
if e == nil {
e = tx . Commit ( ctx )
}
return item , e
}
func ( s * Service ) resolveActor ( ctx context . Context , principal identity . Principal , tenantPublicID string , roles ... string ) ( actor , error ) {
if _ , e := uuid . Parse ( tenantPublicID ); e != nil {
return actor {}, ErrInvalid
}
var a actor
2026-08-29 21:56:51 +08:00
e := s . pool . QueryRow ( ctx , `SELECT t.id::text,t.public_id::text,ua.id::text,ua.public_id::text,ua.platform_role FROM user_account ua JOIN tenant t ON t.id=ua.tenant_id AND t.status='active' WHERE t.public_id=$1::uuid AND ua.oidc_issuer=$2 AND ua.oidc_subject=$3 AND ua.status='active' AND ua.account_kind='operator'` , tenantPublicID , principal . Issuer , principal . Subject ). Scan ( & a . tenantID , & a . tenantPublicID , & a . userID , & a . userPublicID , & a . role )
2026-08-26 06:41:07 +08:00
if errors . Is ( e , pgx . ErrNoRows ) {
return actor {}, ErrAccessDenied
}
if e != nil {
return actor {}, e
}
for _ , role := range roles {
if a . role == role {
return a , nil
}
}
return actor {}, ErrAccessDenied
}
func normalizeRouting ( i * RoutingInput ) {
i . Category = strings . ToUpper ( strings . TrimSpace ( i . Category ))
i . Severity = strings . ToUpper ( strings . TrimSpace ( i . Severity ))
i . ReceiverGroup = strings . TrimSpace ( i . ReceiverGroup )
i . EscalationReceiverGroup = strings . TrimSpace ( i . EscalationReceiverGroup )
i . RunbookRef = strings . TrimSpace ( i . RunbookRef )
i . ChangeSummary = strings . TrimSpace ( i . ChangeSummary )
}
func validRoutingInput ( i RoutingInput ) bool {
return validCategory ( i . Category ) && validSeverity ( i . Severity ) && i . ReceiverGroup != "" && len ([] rune ( i . ReceiverGroup )) <= 120 && i . ResponseMinutes >= 1 && i . ResponseMinutes <= 10080 && i . EscalationAfterMinutes >= i . ResponseMinutes && i . EscalationAfterMinutes <= 10080 && i . EscalationReceiverGroup != "" && len ([] rune ( i . EscalationReceiverGroup )) <= 120 && i . RunbookRef != "" && len ([] rune ( i . RunbookRef )) <= 300 && i . ChangeSummary != "" && len ([] rune ( i . ChangeSummary )) <= 500
}
func normalizeSilence ( i * SilenceInput ) {
i . RuleCode = strings . ToUpper ( strings . TrimSpace ( i . RuleCode ))
i . Category = strings . ToUpper ( strings . TrimSpace ( i . Category ))
i . ResourceType = strings . TrimSpace ( i . ResourceType )
i . ResourcePublicID = strings . TrimSpace ( i . ResourcePublicID )
i . Reason = strings . TrimSpace ( i . Reason )
}
func validSilenceInput ( i SilenceInput , now time . Time ) bool {
return !( i . RuleCode == "" && i . Category == "" && i . ResourcePublicID == "" ) && ( i . RuleCode == "" || rulePattern . MatchString ( i . RuleCode )) && ( i . Category == "" || validCategory ( i . Category )) && ( i . ResourcePublicID == "" || validUUID ( i . ResourcePublicID )) && i . Reason != "" && len ([] rune ( i . Reason )) <= 500 && ! i . StartsAt . IsZero () && i . EndsAt . After ( i . StartsAt ) && i . EndsAt . Sub ( i . StartsAt ) <= 30 * 24 * time . Hour && ! i . EndsAt . Before ( now )
}
func validCategory ( v string ) bool {
return v == "BUSINESS" || v == "TECHNICAL" || v == "SECURITY" || v == "COST"
}
func validSeverity ( v string ) bool {
return v == "CRITICAL" || v == "HIGH" || v == "MEDIUM" || v == "LOW"
}
func validStatus ( v string ) bool {
return v == "OPEN" || v == "ACKNOWLEDGED" || v == "MITIGATED" || v == "RESOLVED" || v == "SILENCED"
}
func validUUID ( v string ) bool { _ , e := uuid . Parse ( v ); return e == nil }
func nullStringUUID ( v string ) any {
if strings . TrimSpace ( v ) == "" {
return nil
}
return v
}
var rulePattern = regexp . MustCompile ( `^[A-Z][A-Z0-9_]{2,99}$` )
type scanner interface { Scan ( ... any ) error }
const routingSelect = `SELECT arp.public_id::text,t.public_id::text,arp.category,arp.severity,arp.version_no,arp.status,arp.receiver_group,arp.response_minutes,arp.escalation_after_minutes,arp.escalation_receiver_group,arp.runbook_ref,arp.change_summary,ua.public_id::text,arp.retired_at,arp.row_version,arp.created_at,arp.updated_at FROM alert_routing_policy arp JOIN tenant t ON t.id=arp.tenant_id JOIN user_account ua ON ua.id=arp.created_by`
func scanRouting ( r scanner ) ( RoutingPolicy , error ) {
var i RoutingPolicy
e := r . Scan ( & i . ID , & i . TenantID , & i . Category , & i . Severity , & i . VersionNo , & i . Status , & i . ReceiverGroup , & i . ResponseMinutes , & i . EscalationAfterMinutes , & i . EscalationReceiverGroup , & i . RunbookRef , & i . ChangeSummary , & i . CreatedBy , & i . RetiredAt , & i . RowVersion , & i . CreatedAt , & i . UpdatedAt )
return i , e
}
func getRoutingTx ( ctx context . Context , tx pgx . Tx , tenantID , publicID string ) ( RoutingPolicy , error ) {
return scanRouting ( tx . QueryRow ( ctx , routingSelect + ` WHERE arp.tenant_id=$1::uuid AND arp.public_id=$2::uuid` , tenantID , publicID ))
}
const incidentSelect = `SELECT ai.public_id::text,ai.rule_code,ai.category,ai.severity,ai.status,ai.fingerprint,ai.title,ai.summary,ai.resource_type,ai.resource_public_id::text,ai.correlation_request_id,arp.receiver_group,arp.runbook_ref,owner.public_id::text,ai.first_detected_at,ai.last_detected_at,ai.occurrence_count,ai.acknowledged_at,ai.mitigated_at,ai.resolved_at,ai.silenced_until,ai.escalated_at,ai.row_version FROM alert_incident ai LEFT JOIN alert_routing_policy arp ON arp.id=ai.routing_policy_id LEFT JOIN user_account owner ON owner.id=ai.current_owner_id`
func scanIncident ( r scanner ) ( Incident , error ) {
var i Incident
e := r . Scan ( & i . ID , & i . RuleCode , & i . Category , & i . Severity , & i . Status , & i . Fingerprint , & i . Title , & i . Summary , & i . ResourceType , & i . ResourcePublicID , & i . CorrelationRequestID , & i . ReceiverGroup , & i . RunbookRef , & i . CurrentOwner , & i . FirstDetectedAt , & i . LastDetectedAt , & i . OccurrenceCount , & i . AcknowledgedAt , & i . MitigatedAt , & i . ResolvedAt , & i . SilencedUntil , & i . EscalatedAt , & i . RowVersion )
i . Actions = [] Action {}
return i , e
}
func getIncidentTx ( ctx context . Context , tx pgx . Tx , tenantID , publicID string ) ( Incident , error ) {
i , e := scanIncident ( tx . QueryRow ( ctx , incidentSelect + ` WHERE ai.tenant_id=$1::uuid AND ai.public_id=$2::uuid` , tenantID , publicID ))
if e != nil {
return i , e
}
rows , e := tx . Query ( ctx , `SELECT aa.public_id::text,aa.action_type,aa.from_status,aa.to_status,aa.actor_type,ua.public_id::text,aa.note,aa.created_at FROM alert_action aa LEFT JOIN user_account ua ON ua.id=aa.actor_user_id WHERE aa.incident_id=(SELECT id FROM alert_incident WHERE tenant_id=$1::uuid AND public_id=$2::uuid) ORDER BY aa.created_at,aa.id` , tenantID , publicID )
if e != nil {
return i , e
}
defer rows . Close ()
for rows . Next () {
var a Action
if e = rows . Scan ( & a . ID , & a . ActionType , & a . FromStatus , & a . ToStatus , & a . ActorType , & a . ActorUser , & a . Note , & a . CreatedAt ); e != nil {
return i , e
}
i . Actions = append ( i . Actions , a )
}
return i , rows . Err ()
}
const silenceSelect = `SELECT als.public_id::text,als.rule_code,als.category,als.resource_type,als.resource_public_id::text,als.matcher,als.reason,als.starts_at,als.ends_at,ua.public_id::text,als.revoked_at,als.row_version,als.created_at FROM alert_silence als JOIN user_account ua ON ua.id=als.created_by`
func scanSilence ( r scanner ) ( Silence , error ) {
var i Silence
var raw [] byte
e := r . Scan ( & i . ID , & i . RuleCode , & i . Category , & i . ResourceType , & i . ResourcePublicID , & raw , & i . Reason , & i . StartsAt , & i . EndsAt , & i . CreatedBy , & i . RevokedAt , & i . RowVersion , & i . CreatedAt )
if e == nil {
e = json . Unmarshal ( raw , & i . Matcher )
}
if i . Matcher == nil {
i . Matcher = map [ string ] any {}
}
return i , e
}
func getSilenceTx ( ctx context . Context , tx pgx . Tx , tenantID , publicID string ) ( Silence , error ) {
return scanSilence ( tx . QueryRow ( ctx , silenceSelect + ` WHERE als.tenant_id=$1::uuid AND als.public_id=$2::uuid` , tenantID , publicID ))
}