Skip to content

Commit e783925

Browse files
authored
Merge pull request #151 from flashcatcloud/feat/monit-query-data
feat(monit-query): add data command, deprecate rows
2 parents ff14421 + 3e7f679 commit e783925

20 files changed

Lines changed: 412 additions & 89 deletions

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ module github.com/flashcatcloud/flashduty-cli
33
go 1.25.1
44

55
require (
6-
github.com/flashcatcloud/go-flashduty v0.12.0
6+
github.com/flashcatcloud/go-flashduty v0.13.1
77
github.com/mattn/go-runewidth v0.0.27
88
github.com/spf13/cobra v1.10.2
99
github.com/spf13/pflag v1.0.10

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY=
22
github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
33
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
4-
github.com/flashcatcloud/go-flashduty v0.12.0 h1:2jjQsTB212XvwpRcX8W6uQ+Zjzmlu857T7YtTs5nH8Q=
5-
github.com/flashcatcloud/go-flashduty v0.12.0/go.mod h1:aA0RtZEs0AYOwwdNKdtVeD8YMOdnmVY1zAlVD+9Ovx8=
4+
github.com/flashcatcloud/go-flashduty v0.13.1 h1:6AzKyeaY+dvcVOod8DqOhzjA6ilK/tTTALGrjIW+RdY=
5+
github.com/flashcatcloud/go-flashduty v0.13.1/go.mod h1:aA0RtZEs0AYOwwdNKdtVeD8YMOdnmVY1zAlVD+9Ovx8=
66
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
77
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
88
github.com/mattn/go-runewidth v0.0.27 h1:Feg/Oou5zI/wnpgDF6omIU0OokC9GxLC/WRknhVlIR0=

internal/cli/incident.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -822,10 +822,16 @@ personal channels, or a template.`,
822822
var notify flashduty.AddIncidentResponderRequestNotify
823823
if followPreference || notifyChannel != "" || templateID != "" {
824824
notify = flashduty.AddIncidentResponderRequestNotify{
825-
FollowPreference: followPreference,
826825
PersonalChannels: parseStringSlice(notifyChannel),
827826
TemplateID: templateID,
828827
}
828+
// Explicit wire value whenever the intent is "use these channels"
829+
// (channels given without the flag) or the flag was set explicitly.
830+
// Template-only keeps the server default (nil = personal
831+
// preference), so a template alone never suppresses delivery.
832+
if cmd.Flags().Changed("follow-preference") || notifyChannel != "" {
833+
notify.FollowPreference = &followPreference
834+
}
829835
}
830836

831837
return runCommand(cmd, args, func(ctx *RunContext) error {

internal/cli/monit_query.go

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@ import (
1212
)
1313

1414
func newMonitQueryCmd() *cobra.Command {
15-
cmd := newGroupCmd("monit-query", "Probe monit-backed datasources (prometheus|victorialogs|loki|mysql)")
15+
cmd := newGroupCmd("monit-query", "Probe monit-backed datasources (9 types via data; diagnose/rows support prometheus|victorialogs|loki|mysql)")
1616
cmd.AddCommand(newMonitQueryDiagnoseCmd())
17+
cmd.AddCommand(newMonitQueryDataCmd())
1718
cmd.AddCommand(newMonitQueryRowsCmd())
1819
return cmd
1920
}
@@ -82,16 +83,69 @@ func newMonitQueryDiagnoseCmd() *cobra.Command {
8283
return cmd
8384
}
8485

86+
func newMonitQueryDataCmd() *cobra.Command {
87+
var (
88+
dsType, dsName, expr string
89+
delaySeconds int64
90+
argsKV []string
91+
)
92+
93+
cmd := &cobra.Command{
94+
Use: "data",
95+
Short: "Structured datasource query (returns a stable query_result.v1: frames/records/samples)",
96+
Long: curatedLong("Structured datasource query returning the stable query_result.v1 result — frames, records, or samples — instead of the legacy flattened rows.", "Diagnostics", "QueryData"),
97+
RunE: func(cmd *cobra.Command, args []string) error {
98+
if dsType == "" || dsName == "" || expr == "" {
99+
return fmt.Errorf("--ds-type, --ds-name, --expr are required")
100+
}
101+
argsMap, err := parseKVSlice(argsKV)
102+
if err != nil {
103+
return fmt.Errorf("invalid --args: %w", err)
104+
}
105+
if err := normalizeRawTimeArgs(dsType, argsMap); err != nil {
106+
return err
107+
}
108+
109+
return runCommand(cmd, args, func(ctx *RunContext) error {
110+
input := &flashduty.QueryDataRequest{
111+
DsType: dsType,
112+
DsName: dsName,
113+
Expr: expr,
114+
DelaySeconds: delaySeconds,
115+
Args: argsMap,
116+
}
117+
result, _, err := ctx.Client.Diagnostics.QueryData(cmdContext(ctx.Cmd), input)
118+
if err != nil {
119+
return err
120+
}
121+
return ctx.Printer.Print(result, nil)
122+
})
123+
},
124+
}
125+
126+
cmd.Flags().StringVar(&dsType, "ds-type", "", "Datasource type (required)")
127+
cmd.Flags().StringVar(&dsName, "ds-name", "", "Datasource name as configured (required)")
128+
registerEnumFlag(cmd, "ds-type", "prometheus", "victorialogs", "loki", "mysql", "sls", "elasticsearch", "postgres", "oracle", "clickhouse")
129+
cmd.Flags().StringVar(&expr, "expr", "", "Query expression (required)")
130+
cmd.Flags().Int64Var(&delaySeconds, "delay-seconds", 0, "Look-back offset in seconds for point-in-time queries (default 0)")
131+
cmd.Flags().StringSliceVar(&argsKV, "args", nil, "Arg entries KEY=VALUE (repeatable; values must be strings per monit-query contract). "+
132+
"For loki/victorialogs raw mode, <ds-type>.start/<ds-type>.end accept a relative duration ('15m'), 'now', a date/RFC3339 timestamp, "+
133+
"or a unix epoch in seconds or milliseconds — normalized to the form the datasource requires before sending")
134+
135+
return cmd
136+
}
137+
85138
func newMonitQueryRowsCmd() *cobra.Command {
86139
var (
87140
dsType, dsName, expr string
88141
argsKV []string
89142
)
90143

91144
cmd := &cobra.Command{
92-
Use: "rows",
93-
Short: "Raw datasource passthrough (returns values/rows as the datasource itself would)",
94-
Long: curatedLong("Raw datasource passthrough returning values/rows as the datasource itself would.", "Diagnostics", "QueryRows"),
145+
Use: "rows",
146+
Short: "Raw datasource passthrough (returns values/rows as the datasource itself would). Deprecated — prefer 'monit-query data'",
147+
Deprecated: "use 'monit-query data' instead",
148+
Long: curatedLong("Deprecated. Raw datasource passthrough returning values/rows as the datasource itself would. Migrate to 'monit-query data', which preserves frames/records/samples without forcing results into legacy rows.", "Diagnostics", "QueryRows"),
95149
RunE: func(cmd *cobra.Command, args []string) error {
96150
if dsType == "" || dsName == "" || expr == "" {
97151
return fmt.Errorf("--ds-type, --ds-name, --expr are required")

internal/cli/monit_query_test.go

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,15 @@ func TestMonitQueryRowsFlags(t *testing.T) {
3131
}
3232
}
3333

34+
func TestMonitQueryDataFlags(t *testing.T) {
35+
cmd := newMonitQueryDataCmd()
36+
for _, name := range []string{"ds-type", "ds-name", "expr", "args", "delay-seconds"} {
37+
if cmd.Flags().Lookup(name) == nil {
38+
t.Errorf("flag --%s missing", name)
39+
}
40+
}
41+
}
42+
3443
// --- monit-query diagnose -------------------------------------------------
3544

3645
func TestMonitQueryDiagnoseHappyPath(t *testing.T) {
@@ -202,6 +211,104 @@ func TestMonitQueryDiagnoseInvalidTimeStart(t *testing.T) {
202211

203212
// --- monit-query rows -----------------------------------------------------
204213

214+
func TestMonitQueryDataHappyPath(t *testing.T) {
215+
saveAndResetGlobals(t)
216+
stub := newGFStub(t)
217+
// data returns the stable query_result.v1 envelope: data.{format,result}.
218+
stub.data = map[string]any{
219+
"format": "query_result.v1",
220+
"result": map[string]any{
221+
"kind": "samples",
222+
"samples": []any{
223+
map[string]any{"labels": map[string]any{"job": "api"}, "value": 1.25},
224+
},
225+
},
226+
}
227+
228+
out, err := execCommand(
229+
"monit-query", "data",
230+
"--ds-type", "prometheus",
231+
"--ds-name", "prom-prod",
232+
"--expr", "up",
233+
"--delay-seconds", "30",
234+
"--args", "step=15s",
235+
"--output-format", "json",
236+
)
237+
if err != nil {
238+
t.Fatalf("unexpected error: %v", err)
239+
}
240+
if stub.lastPath != "/monit/query/data" {
241+
t.Fatalf("expected /monit/query/data, got %q", stub.lastPath)
242+
}
243+
body := stub.lastBody
244+
if body["ds_type"] != "prometheus" || body["ds_name"] != "prom-prod" || body["expr"] != "up" {
245+
t.Errorf("unexpected data input: %#v", body)
246+
}
247+
if fmt.Sprint(body["delay_seconds"]) != "30" {
248+
t.Errorf("expected delay_seconds 30, got %v", body["delay_seconds"])
249+
}
250+
args, _ := body["args"].(map[string]any)
251+
if args["step"] != "15s" {
252+
t.Errorf("expected args step=15s, got %#v", args)
253+
}
254+
var rendered map[string]any
255+
if err := json.Unmarshal([]byte(out), &rendered); err != nil {
256+
t.Fatalf("decode CLI JSON: %v\n%s", err, out)
257+
}
258+
if rendered["format"] != "query_result.v1" {
259+
t.Errorf("expected format query_result.v1, got %v", rendered["format"])
260+
}
261+
}
262+
263+
func TestMonitQueryDataRequiredFlags(t *testing.T) {
264+
cases := []struct {
265+
name string
266+
args []string
267+
}{
268+
{
269+
name: "missing ds-type",
270+
args: []string{
271+
"monit-query", "data",
272+
"--ds-name", "prom-prod",
273+
"--expr", "up",
274+
},
275+
},
276+
{
277+
name: "missing ds-name",
278+
args: []string{
279+
"monit-query", "data",
280+
"--ds-type", "prometheus",
281+
"--expr", "up",
282+
},
283+
},
284+
{
285+
name: "missing expr",
286+
args: []string{
287+
"monit-query", "data",
288+
"--ds-type", "prometheus",
289+
"--ds-name", "prom-prod",
290+
},
291+
},
292+
}
293+
for _, tc := range cases {
294+
t.Run(tc.name, func(t *testing.T) {
295+
saveAndResetGlobals(t)
296+
stub := newGFStub(t)
297+
298+
_, err := execCommand(tc.args...)
299+
if err == nil {
300+
t.Fatal("expected required-flag error, got nil")
301+
}
302+
if !strings.Contains(err.Error(), "required") {
303+
t.Errorf("expected error to mention 'required', got %q", err.Error())
304+
}
305+
if stub.requests != 0 {
306+
t.Errorf("data should not have been called: %d request(s)", stub.requests)
307+
}
308+
})
309+
}
310+
}
311+
205312
func TestMonitQueryRowsHappyPath(t *testing.T) {
206313
saveAndResetGlobals(t)
207314
stub := newGFStub(t)
@@ -406,6 +513,28 @@ func TestMonitQueryRowsRawModeNormalizesRFC3339(t *testing.T) {
406513
}
407514
}
408515

516+
func TestMonitQueryDataInvalidArgs(t *testing.T) {
517+
saveAndResetGlobals(t)
518+
stub := newGFStub(t)
519+
520+
_, err := execCommand(
521+
"monit-query", "data",
522+
"--ds-type", "prometheus",
523+
"--ds-name", "prom-prod",
524+
"--expr", "up",
525+
"--args", "no-equals-sign",
526+
)
527+
if err == nil {
528+
t.Fatal("expected error for malformed --args, got nil")
529+
}
530+
if !strings.Contains(err.Error(), "--args") {
531+
t.Errorf("expected error to mention --args, got %q", err.Error())
532+
}
533+
if stub.requests != 0 {
534+
t.Errorf("data should not have been called: %d request(s)", stub.requests)
535+
}
536+
}
537+
409538
func TestMonitQueryRowsInvalidArgs(t *testing.T) {
410539
saveAndResetGlobals(t)
411540
stub := newGFStub(t)

internal/cli/zz_generated_account.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/cli/zz_generated_applications.go

Lines changed: 11 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)