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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/flashcatcloud/flashduty-cli
go 1.25.1

require (
github.com/flashcatcloud/go-flashduty v0.15.1-0.20260908030757-f478f34797be
github.com/flashcatcloud/go-flashduty v0.15.1-0.20260908113851-5bf8f2902391
github.com/mattn/go-runewidth v0.0.28
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ github.com/flashcatcloud/go-flashduty v0.15.1-0.20260908025802-4fa9a76d8b57 h1:3
github.com/flashcatcloud/go-flashduty v0.15.1-0.20260908025802-4fa9a76d8b57/go.mod h1:YpHiTYXR5NXBI/rGRZfUy537XMkhdCkwA8NW1QoRHwk=
github.com/flashcatcloud/go-flashduty v0.15.1-0.20260908030757-f478f34797be h1:F3+A0vVRICnEeBshac70P+VBtuo64P5hBmxcfbFxiXk=
github.com/flashcatcloud/go-flashduty v0.15.1-0.20260908030757-f478f34797be/go.mod h1:YpHiTYXR5NXBI/rGRZfUy537XMkhdCkwA8NW1QoRHwk=
github.com/flashcatcloud/go-flashduty v0.15.1-0.20260908113851-5bf8f2902391 h1:u4IM9wE2/isYCAUMszeBsp4b9Na7Qh5mHlvPLpDgvkw=
github.com/flashcatcloud/go-flashduty v0.15.1-0.20260908113851-5bf8f2902391/go.mod h1:YpHiTYXR5NXBI/rGRZfUy537XMkhdCkwA8NW1QoRHwk=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/mattn/go-runewidth v0.0.28 h1:rPyg2ybwEKPebvpzVWe1gKBkH8EQFkxO4Y0hjBeLaBU=
Expand Down
66 changes: 0 additions & 66 deletions internal/cli/monit_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,76 +12,10 @@ import (

func newMonitQueryCmd() *cobra.Command {
cmd := newGroupCmd("monit-query", "Query configured datasources; structured diagnostics use monit datasource-tools-invoke")
cmd.AddCommand(newMonitQueryDiagnoseCmd())
cmd.AddCommand(newMonitQueryDataCmd())
return cmd
}

func newMonitQueryDiagnoseCmd() *cobra.Command {
var (
dsType, dsName, timeStart, timeEnd, inputQuery, operation string
maxLogs, maxPatterns, timeoutSeconds int
)

cmd := &cobra.Command{
Use: "diagnose",
Short: "Legacy log-pattern and metric-trend evidence (prefer monit datasource-tools-invoke)",
Long: curatedLong("Run pre-clustered RCA over a datasource window, returning log_patterns or metric_trends findings.", "Diagnostics", "QueryDiagnose"),
RunE: func(cmd *cobra.Command, args []string) error {
if dsType == "" || dsName == "" || inputQuery == "" {
return fmt.Errorf("--ds-type, --ds-name, --input-query are required")
}
startTime, err := timeutil.Parse(timeStart)
if err != nil {
return fmt.Errorf("invalid --time-start: %w", err)
}
endTime, err := timeutil.Parse(timeEnd)
if err != nil {
return fmt.Errorf("invalid --time-end: %w", err)
}

return runCommand(cmd, args, func(ctx *RunContext) error {
input := &flashduty.DiagnoseRequest{
DsType: dsType,
DsName: dsName,
Operation: operation,
Input: flashduty.DiagnoseRequestInput{Query: inputQuery},
TimeRange: flashduty.DiagnoseRequestTimeRange{Start: startTime, End: endTime},
}
if maxLogs > 0 {
input.Options.MaxLogsScanned = int64(maxLogs)
}
if maxPatterns > 0 {
input.Options.MaxPatterns = int64(maxPatterns)
}
if timeoutSeconds > 0 {
input.Options.TimeoutSeconds = int64(timeoutSeconds)
}

//nolint:staticcheck // Keep the legacy command working while callers migrate to datasource tools.
result, _, err := ctx.Client.Diagnostics.QueryDiagnose(cmdContext(ctx.Cmd), input)
if err != nil {
return err
}
return ctx.Printer.Print(result, nil)
})
},
}

cmd.Flags().StringVar(&dsType, "ds-type", "", "Datasource type: loki|victorialogs (log_patterns) or prometheus (metric_trends) (required)")
cmd.Flags().StringVar(&dsName, "ds-name", "", "Datasource name as configured (required)")
registerEnumFlag(cmd, "ds-type", "prometheus", "victorialogs", "loki")
cmd.Flags().StringVar(&timeStart, "time-start", "15m", "Window start: relative duration ('15m'/'1h'), 'now', a date/RFC3339 timestamp, or a unix epoch in seconds or milliseconds")
cmd.Flags().StringVar(&timeEnd, "time-end", "now", "Window end: same formats as --time-start; span capped at 6h")
cmd.Flags().StringVar(&inputQuery, "input-query", "", "Filter-only log query OR matrix PromQL (required)")
cmd.Flags().StringVar(&operation, "operation", "", "log_patterns or metric_trends (default inferred from ds-type)")
cmd.Flags().IntVar(&maxLogs, "max-logs", 0, "Max log lines scanned (default 10000, cap 50000)")
cmd.Flags().IntVar(&maxPatterns, "max-patterns", 0, "Max patterns returned (default 20, cap 50)")
cmd.Flags().IntVar(&timeoutSeconds, "timeout-seconds", 0, "Per-call timeout in seconds (default 25, cap 30)")

return cmd
}

func newMonitQueryDataCmd() *cobra.Command {
var (
dsType, dsName, expr string
Expand Down
185 changes: 12 additions & 173 deletions internal/cli/monit_query_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,6 @@ import (
"time"
)

func TestMonitQueryDiagnoseFlags(t *testing.T) {
cmd := newMonitQueryDiagnoseCmd()
for _, name := range []string{
"ds-type", "ds-name", "time-start", "time-end",
"input-query", "operation",
"max-logs", "max-patterns", "timeout-seconds",
} {
if cmd.Flags().Lookup(name) == nil {
t.Errorf("flag --%s missing", name)
}
}
}

func TestMonitQueryDataFlags(t *testing.T) {
cmd := newMonitQueryDataCmd()
for _, name := range []string{"ds-type", "ds-name", "expr", "args", "delay-seconds"} {
Expand All @@ -31,175 +18,27 @@ func TestMonitQueryDataFlags(t *testing.T) {
}
}

// --- monit-query diagnose -------------------------------------------------

func TestMonitQueryDiagnoseHappyPath(t *testing.T) {
saveAndResetGlobals(t)
stub := newGFStub(t)
stub.data = map[string]any{"operation": "log_patterns"}

_, err := execCommand(
"monit-query", "diagnose",
"--ds-type", "victorialogs",
"--ds-name", "vl-prod",
"--input-query", `{app="api"}`,
"--operation", "log_patterns",
"--max-logs", "5000",
"--max-patterns", "10",
"--timeout-seconds", "20",
)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if stub.lastPath != "/monit/query/diagnose" {
t.Fatalf("expected /monit/query/diagnose, got %q", stub.lastPath)
}
body := stub.lastBody
if body["ds_type"] != "victorialogs" || body["ds_name"] != "vl-prod" {
t.Errorf("unexpected ds fields: %#v", body)
}
input, _ := body["input"].(map[string]any)
if input["query"] != `{app="api"}` {
t.Errorf("expected input query %q, got %v", `{app="api"}`, input["query"])
}
if body["operation"] != "log_patterns" {
t.Errorf("expected operation log_patterns, got %v", body["operation"])
}
options, _ := body["options"].(map[string]any)
if fmt.Sprint(options["max_logs_scanned"]) != "5000" ||
fmt.Sprint(options["max_patterns"]) != "10" ||
fmt.Sprint(options["timeout_seconds"]) != "20" {
t.Errorf("unexpected caps: %#v", options)
}
timeRange, _ := body["time_range"].(map[string]any)
if fmt.Sprint(timeRange["start"]) == "0" || fmt.Sprint(timeRange["start"]) == "<nil>" ||
fmt.Sprint(timeRange["end"]) == "0" || fmt.Sprint(timeRange["end"]) == "<nil>" {
t.Errorf("expected non-zero default time range, got %#v", timeRange)
}
}

func TestMonitQueryDiagnoseRendersMetricEvidence(t *testing.T) {
saveAndResetGlobals(t)
stub := newGFStub(t)
stub.data = map[string]any{
"schema_version": "2",
"operation": "metric_trends",
"ds_type": "prometheus",
"ds_name": "prod-prometheus",
"query": "up",
"window": map[string]any{"start": "2026-07-14T06:00:00Z", "end": "2026-07-14T07:00:00Z"},
"results": []any{map[string]any{
"method": "window_compare",
"window": map[string]any{"start": "2026-07-14T06:00:00Z", "end": "2026-07-14T07:00:00Z"},
"summary": map[string]any{
"series_total": 1, "series_analyzed": 1, "selected_series_total": 1, "series_returned": 1,
"analysis_truncated": false, "evidence_summary": "One series changed.",
},
"series_evidence": []any{map[string]any{
"labels": map[string]any{"instance": "api-1"},
"observations": []any{"The current average increased."},
}},
"warnings": []any{},
}},
}

out, err := execCommand(
"monit-query", "diagnose",
"--ds-type", "prometheus",
"--ds-name", "prod-prometheus",
"--input-query", "up",
"--operation", "metric_trends",
"--output-format", "json",
)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
var rendered map[string]any
if err := json.Unmarshal([]byte(out), &rendered); err != nil {
t.Fatalf("decode CLI JSON: %v\n%s", err, out)
}
if _, found := rendered["data_handling"]; found {
t.Fatalf("metric output fabricated data_handling: %s", out)
}
evidence := rendered["results"].([]any)[0].(map[string]any)["series_evidence"].([]any)[0].(map[string]any)
for _, field := range []string{"comparison_status", "current_window_stats", "baseline_window_stats"} {
if _, found := evidence[field]; found {
t.Fatalf("metric evidence fabricated %s: %s", field, out)
}
}
}

func TestMonitQueryDiagnoseRequiredFlags(t *testing.T) {
cases := []struct {
name string
args []string
}{
{
name: "missing ds-type",
args: []string{
"monit-query", "diagnose",
"--ds-name", "vl-prod",
"--input-query", `{app="api"}`,
},
},
{
name: "missing ds-name",
args: []string{
"monit-query", "diagnose",
"--ds-type", "victorialogs",
"--input-query", `{app="api"}`,
},
},
{
name: "missing input-query",
args: []string{
"monit-query", "diagnose",
"--ds-type", "victorialogs",
"--ds-name", "vl-prod",
},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
func TestRetiredMonitCommandsRejectBeforeRequest(t *testing.T) {
for _, args := range [][]string{
{"monit-query", "diagnose"}, {"monit", "query-diagnose"},
{"monit", "rule-counter-status"},
{"monit", "store-ruleset-create"}, {"monit", "store-ruleset-update"},
{"monit", "store-ruleset-list"}, {"monit", "store-ruleset-info"}, {"monit", "store-ruleset-delete"},
} {
t.Run(strings.Join(args, " "), func(t *testing.T) {
saveAndResetGlobals(t)
stub := newGFStub(t)

_, err := execCommand(tc.args...)
if err == nil {
t.Fatal("expected required-flag error, got nil")
}
if !strings.Contains(err.Error(), "required") {
t.Errorf("expected error to mention 'required', got %q", err.Error())
_, err := execCommand(args...)
if err == nil || !strings.Contains(err.Error(), "unknown command") {
t.Fatalf("retired command error=%v", err)
}
if stub.requests != 0 {
t.Errorf("diagnose should not have been called: %d request(s)", stub.requests)
t.Fatalf("retired command sent %d requests", stub.requests)
}
})
}
}

func TestMonitQueryDiagnoseInvalidTimeStart(t *testing.T) {
saveAndResetGlobals(t)
stub := newGFStub(t)

_, err := execCommand(
"monit-query", "diagnose",
"--ds-type", "victorialogs",
"--ds-name", "vl-prod",
"--input-query", `{app="api"}`,
"--time-start", "not-a-time",
)
if err == nil {
t.Fatal("expected error for invalid --time-start, got nil")
}
if !strings.Contains(err.Error(), "--time-start") {
t.Errorf("expected error to mention --time-start, got %q", err.Error())
}
if stub.requests != 0 {
t.Errorf("diagnose should not have been called: %d request(s)", stub.requests)
}
}

// --- monit-query data -----------------------------------------------------

func TestMonitQueryDataHappyPath(t *testing.T) {
Expand Down
40 changes: 0 additions & 40 deletions internal/cli/zz_generated_alert_rules.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions internal/cli/zz_generated_data_sources.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading