Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 29 additions & 66 deletions cmd/investigations.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
"fmt"
"io"
"net/http"
"strings"

"github.com/DataDog/pup/pkg/formatter"
"github.com/spf13/cobra"
Expand All @@ -23,20 +22,17 @@ var investigationsCmd = &cobra.Command{
Long: `Manage Bits AI investigations.

Bits AI investigations allow you to trigger automated root cause analysis
for monitor alerts or general infrastructure issues.
for monitor alerts.

CAPABILITIES:
• Trigger a new investigation (monitor alert or general)
• Trigger a new investigation (monitor alert)
• Get investigation details by ID
• List investigations with optional filters

EXAMPLES:
# Trigger investigation from a monitor alert
pup investigations trigger --type=monitor_alert --monitor-id=123456 --event-id="evt-abc" --event-ts=1706918956000

# Trigger a general investigation
pup investigations trigger --type=general --tags="service:web-store" --description="High error rate"

# Get investigation details
pup investigations get <investigation-id>

Expand Down Expand Up @@ -67,29 +63,21 @@ var investigationsListCmd = &cobra.Command{
}

var (
invTriggerType string
invMonitorID int64
invEventID string
invEventTS int64
invTags string
invDescription string
invStartTime int64
invEndTime int64
invPageOffset int64
invPageLimit int64
invFilterMonID int64
invTriggerType string
invMonitorID int64
invEventID string
invEventTS int64
invPageOffset int64
invPageLimit int64
invFilterMonID int64
)

func init() {
// trigger flags
investigationsTriggerCmd.Flags().StringVar(&invTriggerType, "type", "", "Investigation type: monitor_alert or general (required)")
investigationsTriggerCmd.Flags().StringVar(&invTriggerType, "type", "", "Investigation type: monitor_alert (required)")
investigationsTriggerCmd.Flags().Int64Var(&invMonitorID, "monitor-id", 0, "Monitor ID (required for monitor_alert)")
investigationsTriggerCmd.Flags().StringVar(&invEventID, "event-id", "", "Event ID (required for monitor_alert)")
investigationsTriggerCmd.Flags().Int64Var(&invEventTS, "event-ts", 0, "Event timestamp in milliseconds (required for monitor_alert)")
investigationsTriggerCmd.Flags().StringVar(&invTags, "tags", "", "Comma-separated tags (required for general)")
investigationsTriggerCmd.Flags().StringVar(&invDescription, "description", "", "Problem description (required for general)")
investigationsTriggerCmd.Flags().Int64Var(&invStartTime, "start-time", 0, "Start time in milliseconds (optional for general)")
investigationsTriggerCmd.Flags().Int64Var(&invEndTime, "end-time", 0, "End time in milliseconds (optional for general)")
if err := investigationsTriggerCmd.MarkFlagRequired("type"); err != nil {
panic(fmt.Errorf("failed to mark flag as required: %w", err))
}
Expand Down Expand Up @@ -196,50 +184,25 @@ func runInvestigationsList(cmd *cobra.Command, args []string) error {
func buildTriggerRequestBody() (map[string]any, error) {
var trigger map[string]any

switch invTriggerType {
case "monitor_alert":
if invMonitorID == 0 {
return nil, fmt.Errorf("--monitor-id is required for monitor_alert investigations")
}
if invEventID == "" {
return nil, fmt.Errorf("--event-id is required for monitor_alert investigations")
}
if invEventTS == 0 {
return nil, fmt.Errorf("--event-ts is required for monitor_alert investigations")
}
trigger = map[string]any{
"type": "monitor_alert_trigger",
"monitor_alert_trigger": map[string]any{
"monitor_id": invMonitorID,
"event_id": invEventID,
"event_ts": invEventTS,
},
}

case "general":
if invTags == "" {
return nil, fmt.Errorf("--tags is required for general investigations")
}
if invDescription == "" {
return nil, fmt.Errorf("--description is required for general investigations")
}
general := map[string]any{
"tags": strings.Split(invTags, ","),
"description": invDescription,
}
if invStartTime != 0 {
general["start_time"] = invStartTime
}
if invEndTime != 0 {
general["end_time"] = invEndTime
}
trigger = map[string]any{
"type": "general_investigation",
"general_investigation": general,
}

default:
return nil, fmt.Errorf("invalid investigation type %q: must be monitor_alert or general", invTriggerType)
if invTriggerType != "monitor_alert" {
return nil, fmt.Errorf("invalid investigation type %q: must be monitor_alert", invTriggerType)
}
if invMonitorID == 0 {
return nil, fmt.Errorf("--monitor-id is required for monitor_alert investigations")
}
if invEventID == "" {
return nil, fmt.Errorf("--event-id is required for monitor_alert investigations")
}
if invEventTS == 0 {
return nil, fmt.Errorf("--event-ts is required for monitor_alert investigations")
}
trigger = map[string]any{
"type": "monitor_alert_trigger",
"monitor_alert_trigger": map[string]any{
"monitor_id": invMonitorID,
"event_id": invEventID,
"event_ts": invEventTS,
},
}

return map[string]any{
Expand Down
120 changes: 2 additions & 118 deletions cmd/investigations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ func TestInvestigationsTriggerCmd(t *testing.T) {

// Check flags
flags := investigationsTriggerCmd.Flags()
requiredFlags := []string{"type", "monitor-id", "event-id", "event-ts", "tags", "description", "start-time", "end-time"}
requiredFlags := []string{"type", "monitor-id", "event-id", "event-ts"}
for _, name := range requiredFlags {
if flags.Lookup(name) == nil {
t.Errorf("Missing --%s flag", name)
Expand Down Expand Up @@ -200,109 +200,13 @@ func TestBuildTriggerRequestBody_MonitorAlert(t *testing.T) {
}
}

func TestBuildTriggerRequestBody_General(t *testing.T) {
origType := invTriggerType
origTags := invTags
origDesc := invDescription
origStart := invStartTime
origEnd := invEndTime
defer func() {
invTriggerType = origType
invTags = origTags
invDescription = origDesc
invStartTime = origStart
invEndTime = origEnd
}()

invTriggerType = "general"
invTags = "service:web-store,env:prod"
invDescription = "High error rate"
invStartTime = 1706918956000
invEndTime = 1706919956000

body, err := buildTriggerRequestBody()
if err != nil {
t.Fatalf("buildTriggerRequestBody() error = %v", err)
}

data := body["data"].(map[string]any)
attrs := data["attributes"].(map[string]any)
trigger := attrs["trigger"].(map[string]any)

if trigger["type"] != "general_investigation" {
t.Errorf("trigger.type = %v, want general_investigation", trigger["type"])
}

gi := trigger["general_investigation"].(map[string]any)

tags, ok := gi["tags"].([]string)
if !ok {
t.Fatal("tags is not []string")
}
if len(tags) != 2 || tags[0] != "service:web-store" || tags[1] != "env:prod" {
t.Errorf("tags = %v, want [service:web-store env:prod]", tags)
}

if gi["description"] != "High error rate" {
t.Errorf("description = %v, want 'High error rate'", gi["description"])
}

if gi["start_time"] != int64(1706918956000) {
t.Errorf("start_time = %v, want 1706918956000", gi["start_time"])
}

if gi["end_time"] != int64(1706919956000) {
t.Errorf("end_time = %v, want 1706919956000", gi["end_time"])
}
}

func TestBuildTriggerRequestBody_GeneralNoOptionalTimes(t *testing.T) {
origType := invTriggerType
origTags := invTags
origDesc := invDescription
origStart := invStartTime
origEnd := invEndTime
defer func() {
invTriggerType = origType
invTags = origTags
invDescription = origDesc
invStartTime = origStart
invEndTime = origEnd
}()

invTriggerType = "general"
invTags = "service:web-store"
invDescription = "Something is wrong"
invStartTime = 0
invEndTime = 0

body, err := buildTriggerRequestBody()
if err != nil {
t.Fatalf("buildTriggerRequestBody() error = %v", err)
}

data := body["data"].(map[string]any)
attrs := data["attributes"].(map[string]any)
trigger := attrs["trigger"].(map[string]any)
gi := trigger["general_investigation"].(map[string]any)

if _, exists := gi["start_time"]; exists {
t.Error("start_time should not be present when zero")
}
if _, exists := gi["end_time"]; exists {
t.Error("end_time should not be present when zero")
}
}

func TestBuildTriggerRequestBody_Validation(t *testing.T) {
tests := []struct {
name string
triggerType string
monitorID int64
eventID string
eventTS int64
tags string
description string
wantErr string
}{
{
Expand All @@ -329,24 +233,10 @@ func TestBuildTriggerRequestBody_Validation(t *testing.T) {
eventTS: 0,
wantErr: "--event-ts is required",
},
{
name: "general missing tags",
triggerType: "general",
tags: "",
description: "Some issue",
wantErr: "--tags is required",
},
{
name: "general missing description",
triggerType: "general",
tags: "service:web",
description: "",
wantErr: "--description is required",
},
{
name: "invalid type",
triggerType: "invalid",
wantErr: "invalid investigation type",
wantErr: "must be monitor_alert",
},
}

Expand All @@ -356,23 +246,17 @@ func TestBuildTriggerRequestBody_Validation(t *testing.T) {
origMonitorID := invMonitorID
origEventID := invEventID
origEventTS := invEventTS
origTags := invTags
origDesc := invDescription
defer func() {
invTriggerType = origType
invMonitorID = origMonitorID
invEventID = origEventID
invEventTS = origEventTS
invTags = origTags
invDescription = origDesc
}()

invTriggerType = tt.triggerType
invMonitorID = tt.monitorID
invEventID = tt.eventID
invEventTS = tt.eventTS
invTags = tt.tags
invDescription = tt.description

_, err := buildTriggerRequestBody()
if err == nil {
Expand Down