384 lines
9.6 KiB
Go
384 lines
9.6 KiB
Go
package monitoring
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/atlasos/calypso/internal/common/database"
|
|
"github.com/atlasos/calypso/internal/common/logger"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// AlertSeverity represents the severity level of an alert
|
|
type AlertSeverity string
|
|
|
|
const (
|
|
AlertSeverityInfo AlertSeverity = "info"
|
|
AlertSeverityWarning AlertSeverity = "warning"
|
|
AlertSeverityCritical AlertSeverity = "critical"
|
|
)
|
|
|
|
// AlertSource represents where the alert originated
|
|
type AlertSource string
|
|
|
|
const (
|
|
AlertSourceSystem AlertSource = "system"
|
|
AlertSourceStorage AlertSource = "storage"
|
|
AlertSourceSCST AlertSource = "scst"
|
|
AlertSourceTape AlertSource = "tape"
|
|
AlertSourceVTL AlertSource = "vtl"
|
|
AlertSourceTask AlertSource = "task"
|
|
AlertSourceAPI AlertSource = "api"
|
|
)
|
|
|
|
// Alert represents a system alert
|
|
type Alert struct {
|
|
ID string `json:"id"`
|
|
Severity AlertSeverity `json:"severity"`
|
|
Source AlertSource `json:"source"`
|
|
Title string `json:"title"`
|
|
Message string `json:"message"`
|
|
ResourceType string `json:"resource_type,omitempty"`
|
|
ResourceID string `json:"resource_id,omitempty"`
|
|
IsAcknowledged bool `json:"is_acknowledged"`
|
|
AcknowledgedBy string `json:"acknowledged_by,omitempty"`
|
|
AcknowledgedAt *time.Time `json:"acknowledged_at,omitempty"`
|
|
ResolvedAt *time.Time `json:"resolved_at,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
|
}
|
|
|
|
// AlertService manages alerts
|
|
type AlertService struct {
|
|
db *database.DB
|
|
logger *logger.Logger
|
|
eventHub *EventHub
|
|
}
|
|
|
|
// NewAlertService creates a new alert service
|
|
func NewAlertService(db *database.DB, log *logger.Logger) *AlertService {
|
|
return &AlertService{
|
|
db: db,
|
|
logger: log,
|
|
}
|
|
}
|
|
|
|
// SetEventHub sets the event hub for broadcasting alerts
|
|
func (s *AlertService) SetEventHub(eventHub *EventHub) {
|
|
s.eventHub = eventHub
|
|
}
|
|
|
|
// CreateAlert creates a new alert
|
|
func (s *AlertService) CreateAlert(ctx context.Context, alert *Alert) error {
|
|
alert.ID = uuid.New().String()
|
|
alert.CreatedAt = time.Now()
|
|
|
|
var metadataJSON *string
|
|
if alert.Metadata != nil {
|
|
bytes, err := json.Marshal(alert.Metadata)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal metadata: %w", err)
|
|
}
|
|
jsonStr := string(bytes)
|
|
metadataJSON = &jsonStr
|
|
}
|
|
|
|
query := `
|
|
INSERT INTO alerts (id, severity, source, title, message, resource_type, resource_id, metadata)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
`
|
|
|
|
_, err := s.db.ExecContext(ctx, query,
|
|
alert.ID,
|
|
string(alert.Severity),
|
|
string(alert.Source),
|
|
alert.Title,
|
|
alert.Message,
|
|
alert.ResourceType,
|
|
alert.ResourceID,
|
|
metadataJSON,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create alert: %w", err)
|
|
}
|
|
|
|
s.logger.Info("Alert created",
|
|
"alert_id", alert.ID,
|
|
"severity", alert.Severity,
|
|
"source", alert.Source,
|
|
"title", alert.Title,
|
|
)
|
|
|
|
// Broadcast alert via WebSocket if event hub is set
|
|
if s.eventHub != nil {
|
|
s.eventHub.BroadcastAlert(alert)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ListAlerts retrieves alerts with optional filters
|
|
func (s *AlertService) ListAlerts(ctx context.Context, filters *AlertFilters) ([]*Alert, error) {
|
|
query := `
|
|
SELECT id, severity, source, title, message, resource_type, resource_id,
|
|
is_acknowledged, acknowledged_by, acknowledged_at, resolved_at,
|
|
created_at, metadata
|
|
FROM alerts
|
|
WHERE 1=1
|
|
`
|
|
args := []interface{}{}
|
|
argIndex := 1
|
|
|
|
if filters != nil {
|
|
if filters.Severity != "" {
|
|
query += fmt.Sprintf(" AND severity = $%d", argIndex)
|
|
args = append(args, string(filters.Severity))
|
|
argIndex++
|
|
}
|
|
if filters.Source != "" {
|
|
query += fmt.Sprintf(" AND source = $%d", argIndex)
|
|
args = append(args, string(filters.Source))
|
|
argIndex++
|
|
}
|
|
if filters.IsAcknowledged != nil {
|
|
query += fmt.Sprintf(" AND is_acknowledged = $%d", argIndex)
|
|
args = append(args, *filters.IsAcknowledged)
|
|
argIndex++
|
|
}
|
|
if filters.ResourceType != "" {
|
|
query += fmt.Sprintf(" AND resource_type = $%d", argIndex)
|
|
args = append(args, filters.ResourceType)
|
|
argIndex++
|
|
}
|
|
if filters.ResourceID != "" {
|
|
query += fmt.Sprintf(" AND resource_id = $%d", argIndex)
|
|
args = append(args, filters.ResourceID)
|
|
argIndex++
|
|
}
|
|
}
|
|
|
|
query += " ORDER BY created_at DESC"
|
|
|
|
if filters != nil && filters.Limit > 0 {
|
|
query += fmt.Sprintf(" LIMIT $%d", argIndex)
|
|
args = append(args, filters.Limit)
|
|
}
|
|
|
|
rows, err := s.db.QueryContext(ctx, query, args...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to query alerts: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var alerts []*Alert
|
|
for rows.Next() {
|
|
alert, err := s.scanAlert(rows)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to scan alert: %w", err)
|
|
}
|
|
alerts = append(alerts, alert)
|
|
}
|
|
|
|
if err := rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("error iterating alerts: %w", err)
|
|
}
|
|
|
|
return alerts, nil
|
|
}
|
|
|
|
// GetAlert retrieves a single alert by ID
|
|
func (s *AlertService) GetAlert(ctx context.Context, alertID string) (*Alert, error) {
|
|
query := `
|
|
SELECT id, severity, source, title, message, resource_type, resource_id,
|
|
is_acknowledged, acknowledged_by, acknowledged_at, resolved_at,
|
|
created_at, metadata
|
|
FROM alerts
|
|
WHERE id = $1
|
|
`
|
|
|
|
row := s.db.QueryRowContext(ctx, query, alertID)
|
|
alert, err := s.scanAlertRow(row)
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return nil, fmt.Errorf("alert not found")
|
|
}
|
|
return nil, fmt.Errorf("failed to get alert: %w", err)
|
|
}
|
|
|
|
return alert, nil
|
|
}
|
|
|
|
// AcknowledgeAlert marks an alert as acknowledged
|
|
func (s *AlertService) AcknowledgeAlert(ctx context.Context, alertID string, userID string) error {
|
|
query := `
|
|
UPDATE alerts
|
|
SET is_acknowledged = true, acknowledged_by = $1, acknowledged_at = NOW()
|
|
WHERE id = $2 AND is_acknowledged = false
|
|
`
|
|
|
|
result, err := s.db.ExecContext(ctx, query, userID, alertID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to acknowledge alert: %w", err)
|
|
}
|
|
|
|
rows, err := result.RowsAffected()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get rows affected: %w", err)
|
|
}
|
|
|
|
if rows == 0 {
|
|
return fmt.Errorf("alert not found or already acknowledged")
|
|
}
|
|
|
|
s.logger.Info("Alert acknowledged", "alert_id", alertID, "user_id", userID)
|
|
return nil
|
|
}
|
|
|
|
// ResolveAlert marks an alert as resolved
|
|
func (s *AlertService) ResolveAlert(ctx context.Context, alertID string) error {
|
|
query := `
|
|
UPDATE alerts
|
|
SET resolved_at = NOW()
|
|
WHERE id = $1 AND resolved_at IS NULL
|
|
`
|
|
|
|
result, err := s.db.ExecContext(ctx, query, alertID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to resolve alert: %w", err)
|
|
}
|
|
|
|
rows, err := result.RowsAffected()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get rows affected: %w", err)
|
|
}
|
|
|
|
if rows == 0 {
|
|
return fmt.Errorf("alert not found or already resolved")
|
|
}
|
|
|
|
s.logger.Info("Alert resolved", "alert_id", alertID)
|
|
return nil
|
|
}
|
|
|
|
// DeleteAlert deletes an alert (soft delete by resolving it)
|
|
func (s *AlertService) DeleteAlert(ctx context.Context, alertID string) error {
|
|
// For safety, we'll just resolve it instead of hard delete
|
|
return s.ResolveAlert(ctx, alertID)
|
|
}
|
|
|
|
// AlertFilters represents filters for listing alerts
|
|
type AlertFilters struct {
|
|
Severity AlertSeverity
|
|
Source AlertSource
|
|
IsAcknowledged *bool
|
|
ResourceType string
|
|
ResourceID string
|
|
Limit int
|
|
}
|
|
|
|
// scanAlert scans a row into an Alert struct
|
|
func (s *AlertService) scanAlert(rows *sql.Rows) (*Alert, error) {
|
|
var alert Alert
|
|
var severity, source string
|
|
var resourceType, resourceID, acknowledgedBy sql.NullString
|
|
var acknowledgedAt, resolvedAt sql.NullTime
|
|
var metadata sql.NullString
|
|
|
|
err := rows.Scan(
|
|
&alert.ID,
|
|
&severity,
|
|
&source,
|
|
&alert.Title,
|
|
&alert.Message,
|
|
&resourceType,
|
|
&resourceID,
|
|
&alert.IsAcknowledged,
|
|
&acknowledgedBy,
|
|
&acknowledgedAt,
|
|
&resolvedAt,
|
|
&alert.CreatedAt,
|
|
&metadata,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
alert.Severity = AlertSeverity(severity)
|
|
alert.Source = AlertSource(source)
|
|
if resourceType.Valid {
|
|
alert.ResourceType = resourceType.String
|
|
}
|
|
if resourceID.Valid {
|
|
alert.ResourceID = resourceID.String
|
|
}
|
|
if acknowledgedBy.Valid {
|
|
alert.AcknowledgedBy = acknowledgedBy.String
|
|
}
|
|
if acknowledgedAt.Valid {
|
|
alert.AcknowledgedAt = &acknowledgedAt.Time
|
|
}
|
|
if resolvedAt.Valid {
|
|
alert.ResolvedAt = &resolvedAt.Time
|
|
}
|
|
if metadata.Valid && metadata.String != "" {
|
|
json.Unmarshal([]byte(metadata.String), &alert.Metadata)
|
|
}
|
|
|
|
return &alert, nil
|
|
}
|
|
|
|
// scanAlertRow scans a single row into an Alert struct
|
|
func (s *AlertService) scanAlertRow(row *sql.Row) (*Alert, error) {
|
|
var alert Alert
|
|
var severity, source string
|
|
var resourceType, resourceID, acknowledgedBy sql.NullString
|
|
var acknowledgedAt, resolvedAt sql.NullTime
|
|
var metadata sql.NullString
|
|
|
|
err := row.Scan(
|
|
&alert.ID,
|
|
&severity,
|
|
&source,
|
|
&alert.Title,
|
|
&alert.Message,
|
|
&resourceType,
|
|
&resourceID,
|
|
&alert.IsAcknowledged,
|
|
&acknowledgedBy,
|
|
&acknowledgedAt,
|
|
&resolvedAt,
|
|
&alert.CreatedAt,
|
|
&metadata,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
alert.Severity = AlertSeverity(severity)
|
|
alert.Source = AlertSource(source)
|
|
if resourceType.Valid {
|
|
alert.ResourceType = resourceType.String
|
|
}
|
|
if resourceID.Valid {
|
|
alert.ResourceID = resourceID.String
|
|
}
|
|
if acknowledgedBy.Valid {
|
|
alert.AcknowledgedBy = acknowledgedBy.String
|
|
}
|
|
if acknowledgedAt.Valid {
|
|
alert.AcknowledgedAt = &acknowledgedAt.Time
|
|
}
|
|
if resolvedAt.Valid {
|
|
alert.ResolvedAt = &resolvedAt.Time
|
|
}
|
|
if metadata.Valid && metadata.String != "" {
|
|
json.Unmarshal([]byte(metadata.String), &alert.Metadata)
|
|
}
|
|
|
|
return &alert, nil
|
|
}
|
|
|