|
| 1 | +package cli |
| 2 | + |
| 3 | +import ( |
| 4 | + "fmt" |
| 5 | + "io" |
| 6 | + "strings" |
| 7 | + |
| 8 | + flashduty "github.com/flashcatcloud/flashduty-sdk" |
| 9 | + "github.com/spf13/cobra" |
| 10 | + |
| 11 | + "github.com/flashcatcloud/flashduty-cli/internal/output" |
| 12 | + "github.com/flashcatcloud/flashduty-cli/internal/timeutil" |
| 13 | +) |
| 14 | + |
| 15 | +func newAlertCmd() *cobra.Command { |
| 16 | + cmd := &cobra.Command{ |
| 17 | + Use: "alert", |
| 18 | + Short: "Manage alerts", |
| 19 | + } |
| 20 | + cmd.AddCommand(newAlertListCmd()) |
| 21 | + cmd.AddCommand(newAlertGetCmd()) |
| 22 | + cmd.AddCommand(newAlertEventsCmd()) |
| 23 | + cmd.AddCommand(newAlertTimelineCmd()) |
| 24 | + cmd.AddCommand(newAlertMergeCmd()) |
| 25 | + return cmd |
| 26 | +} |
| 27 | + |
| 28 | +func newAlertListCmd() *cobra.Command { |
| 29 | + var severity, channel, title, since, until string |
| 30 | + var active, recovered, muted bool |
| 31 | + var limit, page int |
| 32 | + |
| 33 | + cmd := &cobra.Command{ |
| 34 | + Use: "list", |
| 35 | + Short: "List alerts", |
| 36 | + RunE: func(cmd *cobra.Command, args []string) error { |
| 37 | + return runCommand(cmd, args, func(ctx *RunContext) error { |
| 38 | + if active && recovered { |
| 39 | + return fmt.Errorf("--active and --recovered are mutually exclusive") |
| 40 | + } |
| 41 | + |
| 42 | + startTime, err := timeutil.Parse(since) |
| 43 | + if err != nil { |
| 44 | + return fmt.Errorf("invalid --since: %w", err) |
| 45 | + } |
| 46 | + endTime, err := timeutil.Parse(until) |
| 47 | + if err != nil { |
| 48 | + return fmt.Errorf("invalid --until: %w", err) |
| 49 | + } |
| 50 | + |
| 51 | + input := &flashduty.ListAlertsInput{ |
| 52 | + StartTime: startTime, |
| 53 | + EndTime: endTime, |
| 54 | + AlertSeverity: severity, |
| 55 | + Title: title, |
| 56 | + Limit: limit, |
| 57 | + Page: page, |
| 58 | + } |
| 59 | + |
| 60 | + if active { |
| 61 | + input.IsActive = boolPtr(true) |
| 62 | + } else if recovered { |
| 63 | + input.IsActive = boolPtr(false) |
| 64 | + } |
| 65 | + |
| 66 | + if muted { |
| 67 | + input.EverMuted = boolPtr(true) |
| 68 | + } |
| 69 | + |
| 70 | + if channel != "" { |
| 71 | + channelIDs, err := parseIntSlice(channel) |
| 72 | + if err != nil { |
| 73 | + return fmt.Errorf("invalid --channel: %w", err) |
| 74 | + } |
| 75 | + input.ChannelIDs = channelIDs |
| 76 | + } |
| 77 | + |
| 78 | + result, err := ctx.Client.ListAlerts(cmdContext(ctx.Cmd), input) |
| 79 | + if err != nil { |
| 80 | + return err |
| 81 | + } |
| 82 | + |
| 83 | + cols := []output.Column{ |
| 84 | + {Header: "ID", Field: func(v any) string { return v.(flashduty.Alert).AlertID }}, |
| 85 | + {Header: "TITLE", MaxWidth: 50, Field: func(v any) string { return v.(flashduty.Alert).Title }}, |
| 86 | + {Header: "SEVERITY", Field: func(v any) string { return v.(flashduty.Alert).AlertSeverity }}, |
| 87 | + {Header: "STATUS", Field: func(v any) string { return v.(flashduty.Alert).AlertStatus }}, |
| 88 | + {Header: "EVENTS", Field: func(v any) string { return fmt.Sprintf("%d", v.(flashduty.Alert).EventCnt) }}, |
| 89 | + {Header: "CHANNEL", Field: func(v any) string { return v.(flashduty.Alert).ChannelName }}, |
| 90 | + {Header: "STARTED", Field: func(v any) string { return output.FormatTime(v.(flashduty.Alert).StartTime) }}, |
| 91 | + } |
| 92 | + |
| 93 | + return ctx.PrintList(result.Alerts, cols, len(result.Alerts), page, result.Total) |
| 94 | + }) |
| 95 | + }, |
| 96 | + } |
| 97 | + |
| 98 | + cmd.Flags().StringVar(&severity, "severity", "", "Filter: Critical,Warning,Info") |
| 99 | + cmd.Flags().BoolVar(&active, "active", false, "Show active only") |
| 100 | + cmd.Flags().BoolVar(&recovered, "recovered", false, "Show recovered only") |
| 101 | + cmd.Flags().StringVar(&channel, "channel", "", "Comma-separated channel IDs") |
| 102 | + cmd.Flags().BoolVar(&muted, "muted", false, "Show ever-muted only") |
| 103 | + cmd.Flags().StringVar(&title, "title", "", "Search by title keyword") |
| 104 | + cmd.Flags().StringVar(&since, "since", "24h", "Start time") |
| 105 | + cmd.Flags().StringVar(&until, "until", "now", "End time") |
| 106 | + cmd.Flags().IntVar(&limit, "limit", 20, "Max results") |
| 107 | + cmd.Flags().IntVar(&page, "page", 1, "Page number") |
| 108 | + |
| 109 | + return cmd |
| 110 | +} |
| 111 | + |
| 112 | +func newAlertGetCmd() *cobra.Command { |
| 113 | + return &cobra.Command{ |
| 114 | + Use: "get <alert_id>", |
| 115 | + Short: "Get alert detail", |
| 116 | + Args: requireArgs("alert_id"), |
| 117 | + RunE: func(cmd *cobra.Command, args []string) error { |
| 118 | + return runCommand(cmd, args, func(ctx *RunContext) error { |
| 119 | + result, err := ctx.Client.GetAlertDetail(cmdContext(ctx.Cmd), &flashduty.GetAlertDetailInput{ |
| 120 | + AlertID: ctx.Args[0], |
| 121 | + }) |
| 122 | + if err != nil { |
| 123 | + return err |
| 124 | + } |
| 125 | + |
| 126 | + if ctx.JSON { |
| 127 | + return ctx.Printer.Print(result.Alert, nil) |
| 128 | + } |
| 129 | + |
| 130 | + printAlertDetail(ctx.Writer, result.Alert) |
| 131 | + return nil |
| 132 | + }) |
| 133 | + }, |
| 134 | + } |
| 135 | +} |
| 136 | + |
| 137 | +func printAlertDetail(w io.Writer, a flashduty.Alert) { |
| 138 | + labels := make([]string, 0, len(a.Labels)) |
| 139 | + for k, v := range a.Labels { |
| 140 | + labels = append(labels, k+"="+v) |
| 141 | + } |
| 142 | + |
| 143 | + incidentInfo := "-" |
| 144 | + if a.Incident != nil { |
| 145 | + incidentInfo = fmt.Sprintf("%s (%s)", a.Incident.IncidentID, a.Incident.Progress) |
| 146 | + } |
| 147 | + |
| 148 | + mutedStr := "No" |
| 149 | + if a.EverMuted { |
| 150 | + mutedStr = "Yes" |
| 151 | + } |
| 152 | + |
| 153 | + _, _ = fmt.Fprintf(w, "ID: %s\n", a.AlertID) |
| 154 | + _, _ = fmt.Fprintf(w, "Title: %s\n", a.Title) |
| 155 | + _, _ = fmt.Fprintf(w, "Severity: %s\n", a.AlertSeverity) |
| 156 | + _, _ = fmt.Fprintf(w, "Status: %s\n", a.AlertStatus) |
| 157 | + _, _ = fmt.Fprintf(w, "Alert Key: %s\n", orDash(a.AlertKey)) |
| 158 | + _, _ = fmt.Fprintf(w, "Channel: %s\n", a.ChannelName) |
| 159 | + _, _ = fmt.Fprintf(w, "Integration: %s (%s)\n", a.IntegrationName, a.IntegrationType) |
| 160 | + _, _ = fmt.Fprintf(w, "Events: %d\n", a.EventCnt) |
| 161 | + _, _ = fmt.Fprintf(w, "Started: %s\n", output.FormatTime(a.StartTime)) |
| 162 | + _, _ = fmt.Fprintf(w, "Last Event: %s\n", output.FormatTime(a.LastTime)) |
| 163 | + _, _ = fmt.Fprintf(w, "Recovered: %s\n", output.FormatTime(a.EndTime)) |
| 164 | + _, _ = fmt.Fprintf(w, "Muted: %s\n", mutedStr) |
| 165 | + _, _ = fmt.Fprintf(w, "Incident: %s\n", incidentInfo) |
| 166 | + _, _ = fmt.Fprintf(w, "Labels: %s\n", orDash(strings.Join(labels, ", "))) |
| 167 | + _, _ = fmt.Fprintf(w, "Description: %s\n", orDash(a.Description)) |
| 168 | +} |
| 169 | + |
| 170 | +func newAlertEventsCmd() *cobra.Command { |
| 171 | + return &cobra.Command{ |
| 172 | + Use: "events <alert_id>", |
| 173 | + Short: "List alert events", |
| 174 | + Args: requireArgs("alert_id"), |
| 175 | + RunE: func(cmd *cobra.Command, args []string) error { |
| 176 | + return runCommand(cmd, args, func(ctx *RunContext) error { |
| 177 | + result, err := ctx.Client.ListAlertEvents(cmdContext(ctx.Cmd), &flashduty.ListAlertEventsInput{ |
| 178 | + AlertID: ctx.Args[0], |
| 179 | + }) |
| 180 | + if err != nil { |
| 181 | + return err |
| 182 | + } |
| 183 | + |
| 184 | + if len(result.AlertEvents) == 0 { |
| 185 | + ctx.WriteResult("No alert events found.") |
| 186 | + return nil |
| 187 | + } |
| 188 | + |
| 189 | + cols := []output.Column{ |
| 190 | + {Header: "EVENT_ID", Field: func(v any) string { return v.(flashduty.AlertEvent).EventID }}, |
| 191 | + {Header: "SEVERITY", Field: func(v any) string { return v.(flashduty.AlertEvent).EventSeverity }}, |
| 192 | + {Header: "STATUS", Field: func(v any) string { return v.(flashduty.AlertEvent).EventStatus }}, |
| 193 | + {Header: "TIME", Field: func(v any) string { return output.FormatTime(v.(flashduty.AlertEvent).EventTime) }}, |
| 194 | + {Header: "TITLE", MaxWidth: 50, Field: func(v any) string { return v.(flashduty.AlertEvent).Title }}, |
| 195 | + } |
| 196 | + |
| 197 | + return ctx.PrintTotal(result.AlertEvents, cols, len(result.AlertEvents)) |
| 198 | + }) |
| 199 | + }, |
| 200 | + } |
| 201 | +} |
| 202 | + |
| 203 | +func newAlertTimelineCmd() *cobra.Command { |
| 204 | + var limit, page int |
| 205 | + |
| 206 | + cmd := &cobra.Command{ |
| 207 | + Use: "timeline <alert_id>", |
| 208 | + Short: "View alert timeline", |
| 209 | + Args: requireArgs("alert_id"), |
| 210 | + RunE: func(cmd *cobra.Command, args []string) error { |
| 211 | + return runCommand(cmd, args, func(ctx *RunContext) error { |
| 212 | + result, err := ctx.Client.GetAlertFeed(cmdContext(ctx.Cmd), &flashduty.GetAlertFeedInput{ |
| 213 | + AlertID: ctx.Args[0], |
| 214 | + Limit: limit, |
| 215 | + Page: page, |
| 216 | + }) |
| 217 | + if err != nil { |
| 218 | + return err |
| 219 | + } |
| 220 | + |
| 221 | + if len(result.Items) == 0 { |
| 222 | + ctx.WriteResult("No timeline events.") |
| 223 | + return nil |
| 224 | + } |
| 225 | + |
| 226 | + cols := []output.Column{ |
| 227 | + {Header: "TIME", Field: func(v any) string { return output.FormatTime(v.(flashduty.TimelineEvent).Timestamp) }}, |
| 228 | + {Header: "TYPE", Field: func(v any) string { return v.(flashduty.TimelineEvent).Type }}, |
| 229 | + {Header: "OPERATOR", Field: func(v any) string { return v.(flashduty.TimelineEvent).OperatorName }}, |
| 230 | + {Header: "DETAIL", MaxWidth: 80, Field: func(v any) string { |
| 231 | + d := v.(flashduty.TimelineEvent).Detail |
| 232 | + if d == nil { |
| 233 | + return "-" |
| 234 | + } |
| 235 | + return fmt.Sprintf("%v", d) |
| 236 | + }}, |
| 237 | + } |
| 238 | + |
| 239 | + return ctx.Printer.Print(result.Items, cols) |
| 240 | + }) |
| 241 | + }, |
| 242 | + } |
| 243 | + |
| 244 | + cmd.Flags().IntVar(&limit, "limit", 20, "Max events") |
| 245 | + cmd.Flags().IntVar(&page, "page", 1, "Page number") |
| 246 | + |
| 247 | + return cmd |
| 248 | +} |
| 249 | + |
| 250 | +func newAlertMergeCmd() *cobra.Command { |
| 251 | + var incidentID, comment string |
| 252 | + |
| 253 | + cmd := &cobra.Command{ |
| 254 | + Use: "merge <alert_id> [<alert_id2> ...]", |
| 255 | + Short: "Merge alerts into an incident", |
| 256 | + Args: requireArgs("alert_id"), |
| 257 | + RunE: func(cmd *cobra.Command, args []string) error { |
| 258 | + return runCommand(cmd, args, func(ctx *RunContext) error { |
| 259 | + if err := ctx.Client.MergeAlertsToIncident(cmdContext(ctx.Cmd), &flashduty.MergeAlertsInput{ |
| 260 | + AlertIDs: ctx.Args, |
| 261 | + IncidentID: incidentID, |
| 262 | + Comment: comment, |
| 263 | + }); err != nil { |
| 264 | + return err |
| 265 | + } |
| 266 | + |
| 267 | + ctx.WriteResult(fmt.Sprintf("Merged %d alert(s) into incident %s.", len(ctx.Args), incidentID)) |
| 268 | + return nil |
| 269 | + }) |
| 270 | + }, |
| 271 | + } |
| 272 | + |
| 273 | + cmd.Flags().StringVar(&incidentID, "incident", "", "Target incident ID") |
| 274 | + cmd.Flags().StringVar(&comment, "comment", "", "Merge comment") |
| 275 | + _ = cmd.MarkFlagRequired("incident") |
| 276 | + |
| 277 | + return cmd |
| 278 | +} |
0 commit comments