From df9c4fe002dfe8b89a8743103170af0726d3b700 Mon Sep 17 00:00:00 2001 From: Onkar Date: Wed, 17 Jun 2026 13:14:55 +0530 Subject: [PATCH] feat(veye): add --debug flag, scan --ai full detail panel, stage progress labels - veye scan --ai now shows numbered Stage N/6 progress lines as each stage starts and completes - After AI pipeline finishes per alert: renders full incident detail box with SEV badge, resource, LLM model, root cause, analysis, remediation plan with [DESTRUCTIVE] badges, and hint to run veye apply - Add --debug persistent flag to root cmd: prints HTTP method/path/status via slog - client.Client gains SetDebug(bool) and logDebug() for transparent request tracing --- veye/cmd/root.go | 9 +++- veye/cmd/scan.go | 87 +++++++++++++++++++++++++++++----- veye/internal/client/client.go | 16 ++++++- 3 files changed, 97 insertions(+), 15 deletions(-) diff --git a/veye/cmd/root.go b/veye/cmd/root.go index 029150b..5ea65fa 100644 --- a/veye/cmd/root.go +++ b/veye/cmd/root.go @@ -14,8 +14,9 @@ import ( ) var ( - apiURL string - api *client.Client + apiURL string + debugLog bool + api *client.Client ) const bannerArt = ` @@ -43,6 +44,9 @@ var rootCmd = &cobra.Command{ styles.Mute.Render(" Connect to a running VisualEyes backend and inspect metrics, alerts, logs and RCA results."), PersistentPreRunE: func(cmd *cobra.Command, args []string) error { api = client.New(apiURL) + if debugLog { + api.SetDebug(true) + } if cmd.Name() != "help" { if _, err := api.Health(); err != nil { return fmt.Errorf("cannot reach VisualEyes backend at %s: %w\nHint: is the server running? (./bin/server)", apiURL, err) @@ -70,6 +74,7 @@ func init() { rootCmd.PersistentFlags().StringVar(&apiURL, "api", defaultURL, "VisualEyes backend URL (env: VEYE_API_URL, or set in ~/.veye/.env)") + rootCmd.PersistentFlags().BoolVar(&debugLog, "debug", false, "Print HTTP request/response details") rootCmd.AddCommand(statusCmd, alertsCmd, logsCmd, rcaCmd, watchCmd, scanCmd, incidentsCmd, applyCmd, showCmd, reportCmd, clustersCmd) } diff --git a/veye/cmd/scan.go b/veye/cmd/scan.go index 428c1ce..debbdbc 100644 --- a/veye/cmd/scan.go +++ b/veye/cmd/scan.go @@ -164,37 +164,46 @@ func runAIScan() error { continue } - stageStatus := make([]string, 7) // index 1-6 + stageEmoji := []string{"", "πŸ”", "πŸ“ˆ", "πŸ“‹", "πŸ—", "πŸ“–", "⚑"} for ev := range ch { if ev.Stage < 1 || ev.Stage > 6 { continue } label := stageLabels[ev.Stage] + emoji := stageEmoji[ev.Stage] switch ev.Status { case "start": - stageStatus[ev.Stage] = styles.SevWarning.Render("…") + fmt.Printf(" %s %s %s %s\n", + styles.SevWarning.Render("…"), + emoji, + styles.SectionHeader.Render(fmt.Sprintf("Stage %d/6: %s", ev.Stage, label)), + styles.Mute.Render("running…"), + ) case "done": detail := "" if ev.Detail != "" { detail = " " + styles.Mute.Render(ev.Detail) } - stageStatus[ev.Stage] = styles.Good.Render("βœ“") - fmt.Printf(" %s %s%s\n", - stageStatus[ev.Stage], - styles.KeyStyle.Render(label), + fmt.Printf(" %s %s %s%s\n", + styles.Good.Render("βœ“"), + emoji, + styles.Good.Render(fmt.Sprintf("Stage %d/6: %s", ev.Stage, label)), detail, ) case "failed": - stageStatus[ev.Stage] = styles.Bad.Render("βœ—") - fmt.Printf(" %s %s\n", stageStatus[ev.Stage], styles.Bad.Render(label+" failed")) + fmt.Printf(" %s %s %s\n", + styles.Bad.Render("βœ—"), + emoji, + styles.Bad.Render(fmt.Sprintf("Stage %d/6: %s β€” failed", ev.Stage, label)), + ) } } - // Show RCA result summary. + // Show full RCA detail after pipeline completes. rca, err := api.RCA(a.ID) - if err == nil && rca.Status == "done" && rca.RootCause != "" { + if err == nil && rca.Status == "done" { fmt.Println() - fmt.Printf(" %s %s\n", styles.KeyStyle.Render("Root cause"), wordWrap(rca.RootCause, 64)) + renderScanRCADetail(a, rca) } fmt.Println() } @@ -416,6 +425,62 @@ func printScanResult(r *client.ScanResult) { ) } +// renderScanRCADetail prints the full incident-style panel after a scan --ai completes. +func renderScanRCADetail(a client.ScanAllItem, rca *client.RCAResult) { + box := lipgloss.NewStyle(). + Border(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("#303060")). + Padding(0, 2).Width(72) + + header := fmt.Sprintf("%s %s %s", + styles.SeverityBadge(a.Severity), + styles.ValStyle.Bold(true).Render(a.Message), + styles.Mute.Render(fmt.Sprintf("alert #%d", a.ID)), + ) + fmt.Println(box.Render(header)) + fmt.Println() + + fmt.Printf(" %s %s\n", styles.KeyStyle.Width(20).Render("Resource"), styles.ValStyle.Render(a.Resource)) + if rca.Model != "" { + fmt.Printf(" %s %s\n", styles.KeyStyle.Width(20).Render("LLM Model"), styles.Mute.Render(rca.Model)) + } + fmt.Println() + + if rca.RootCause != "" { + fmt.Println(styles.SectionHeader.Render(" Root Cause")) + for _, line := range wrap(rca.RootCause, 72) { + fmt.Printf(" %s\n", styles.ValStyle.Render(line)) + } + fmt.Println() + } + + if rca.Explanation != "" { + fmt.Println(styles.SectionHeader.Render(" Analysis")) + for _, line := range wrap(rca.Explanation, 72) { + fmt.Printf(" %s\n", styles.Mute.Render(line)) + } + fmt.Println() + } + + var cmds []client.FixCommand + if rca.Commands != "" { + _ = json.Unmarshal([]byte(rca.Commands), &cmds) + } + if len(cmds) > 0 { + fmt.Println(styles.SectionHeader.Render(" Remediation Plan")) + fmt.Println() + for i, c := range cmds { + safety := styles.Good.Render("[auto-safe]") + if !c.IsAutoSafe { + safety = styles.DestructiveBadge.Render("[DESTRUCTIVE]") + } + fmt.Printf(" Step %d: %s %s\n", i+1, safety, styles.ValStyle.Render("$ "+c.Command)) + fmt.Println() + } + fmt.Println(styles.Mute.Render(fmt.Sprintf(" run 'veye apply %d' to execute Β· 'veye apply %d --dry-run' to preview", a.ID, a.ID))) + } +} + func formatPercent(v float64) string { if v == 0 { return styles.Mute.Render("n/a") diff --git a/veye/internal/client/client.go b/veye/internal/client/client.go index 521c650..45775cd 100644 --- a/veye/internal/client/client.go +++ b/veye/internal/client/client.go @@ -5,6 +5,7 @@ import ( "bufio" "encoding/json" "fmt" + "log/slog" "net/http" "strings" "time" @@ -177,8 +178,9 @@ type K8sMetrics struct { // Client is the VisualEyes API client. type Client struct { - base string - http *http.Client + base string + http *http.Client + debug bool } // New creates a Client pointed at the given base URL (e.g. "http://localhost:8080"). @@ -189,12 +191,22 @@ func New(base string) *Client { } } +// SetDebug enables verbose HTTP request/response logging. +func (c *Client) SetDebug(on bool) { c.debug = on } + +func (c *Client) logDebug(method, path string, status int) { + if c.debug { + slog.Debug("veye http", "method", method, "path", path, "status", status) + } +} + func (c *Client) get(path string, out any) error { resp, err := c.http.Get(c.base + path) if err != nil { return fmt.Errorf("GET %s: %w", path, err) } defer resp.Body.Close() + c.logDebug("GET", path, resp.StatusCode) if resp.StatusCode != http.StatusOK { return fmt.Errorf("GET %s: HTTP %d", path, resp.StatusCode) }