Skip to content

Commit 2ca70f0

Browse files
feat(gateway): add metrics query commands (Phase 5a-E) (#222)
* fix: display pagination cursors in all list commands Previously, pagination cursors (next/prev) were not displayed to users in CLI output, even though they were available in the API responses. This made it impossible to paginate through large result sets. Changes: - Created pkg/cmd/pagination_output.go with helper functions: - printPaginationInfo() for text output - marshalListResponseWithPagination() for JSON output - Updated all 8 list commands to display pagination info: - event list - request list - attempt list - transformation list - transformation executions - connection list - source list - destination list - Fixed JSON output to always include pagination metadata (previously returned [] for empty results) - Updated test helper functions to handle new JSON response format - Added comprehensive pagination acceptance tests for: - event list (TestEventListPaginationWorkflow) - request list (TestRequestListPaginationWorkflow) - attempt list (TestAttemptListPaginationWorkflow) - Updated TestEventListJSON to verify pagination metadata Fixes #216 * feat(gateway): add metrics query commands (Phase 5a-E) - Add pkg/hookdeck/metrics.go: 7 API methods, MetricsQueryParams, response parsing - Add gateway metrics command group with shared flags (--start, --end, --granularity, --measures, --dimensions, filters) - Add 7 subcommands: events, requests, attempts, queue-depth, pending, events-by-issue, transformations - events-by-issue takes <issue-id> as positional argument; other commands use optional filter flags - Add test/acceptance/metrics_test.go: help, baseline, common flags, validation, JSON output (~27 tests) - Update REFERENCE.md with metrics use-case table and examples Implements Phase 5a-E metrics plan. API requires date range and measures; events-by-issue requires issue ID. Made-with: Cursor * chore(metrics): tag all metrics subcommands with ShortBeta/LongBeta Use ShortBeta and LongBeta on all 7 metrics subcommands so help text shows [BETA] and feedback link, consistent with other gateway commands. Made-with: Cursor * Update package.json version to 1.9.0-beta.1 * fix(generate-reference): fix bugs in metrics docs generation Fixes in tools/generate-reference/main.go for correct generation of CLI metrics docs (e.g. usage line escaping, table formatting). No CLI binary change; docs generator only. Made-with: Cursor --------- Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
1 parent ccd1c74 commit 2ca70f0

15 files changed

Lines changed: 889 additions & 6 deletions

REFERENCE.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ The Hookdeck CLI provides comprehensive webhook infrastructure management includ
1919
- [Events](#events)
2020
- [Requests](#requests)
2121
- [Attempts](#attempts)
22+
- [Metrics](#metrics)
2223
- [Utilities](#utilities)
2324
<!-- GENERATE_END -->
2425
## Global Options
@@ -1782,6 +1783,24 @@ hookdeck gateway attempt get <attempt-id> [flags]
17821783
hookdeck gateway attempt get atm_abc123
17831784
```
17841785
<!-- GENERATE_END -->
1786+
## Metrics
1787+
1788+
Query Event Gateway metrics (events, requests, attempts, queue depth, pending events, events by issue, transformations). All metrics commands require `--start` and `--end` (ISO 8601 date-time).
1789+
1790+
**Use cases and examples:**
1791+
1792+
| Use case | Example command |
1793+
|----------|-----------------|
1794+
| Event volume and failure rate over time | `hookdeck gateway metrics events --start 2026-02-01T00:00:00Z --end 2026-02-25T00:00:00Z --granularity 1d --measures count,failed_count,error_rate` |
1795+
| Request acceptance vs rejection | `hookdeck gateway metrics requests --start 2026-02-01T00:00:00Z --end 2026-02-25T00:00:00Z --measures count,accepted_count,rejected_count` |
1796+
| Delivery latency (attempts) | `hookdeck gateway metrics attempts --start 2026-02-01T00:00:00Z --end 2026-02-25T00:00:00Z --measures response_latency_avg,response_latency_p95` |
1797+
| Queue backlog per destination | `hookdeck gateway metrics queue-depth --start 2026-02-01T00:00:00Z --end 2026-02-25T00:00:00Z --measures max_depth,max_age --destination-id dest_xxx` |
1798+
| Pending events over time | `hookdeck gateway metrics pending --start 2026-02-01T00:00:00Z --end 2026-02-25T00:00:00Z --granularity 1h --measures count` |
1799+
| Events grouped by issue (debugging) | `hookdeck gateway metrics events-by-issue iss_xxx --start 2026-02-01T00:00:00Z --end 2026-02-25T00:00:00Z --measures count` |
1800+
| Transformation errors | `hookdeck gateway metrics transformations --start 2026-02-01T00:00:00Z --end 2026-02-25T00:00:00Z --measures count,failed_count,error_rate` |
1801+
1802+
**Common flags (all metrics subcommands):** `--start`, `--end` (required), `--granularity` (e.g. 1h, 5m, 1d), `--measures`, `--dimensions`, `--source-id`, `--destination-id`, `--connection-id`, `--status`, `--output` (json).
1803+
17851804
## Utilities
17861805
17871806
<!-- GENERATE:completion|ci:START -->

package-lock.json

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

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "hookdeck-cli",
3-
"version": "1.8.1",
3+
"version": "1.9.0-beta.1",
44
"description": "Hookdeck CLI",
55
"repository": {
66
"type": "git",

pkg/cmd/gateway.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ The gateway command group provides full access to all Event Gateway resources.`,
3939
addEventCmdTo(g.cmd)
4040
addRequestCmdTo(g.cmd)
4141
addAttemptCmdTo(g.cmd)
42+
addMetricsCmdTo(g.cmd)
4243

4344
return g
4445
}

pkg/cmd/metrics.go

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
package cmd
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"strings"
7+
8+
"github.com/spf13/cobra"
9+
10+
"github.com/hookdeck/hookdeck-cli/pkg/hookdeck"
11+
"github.com/hookdeck/hookdeck-cli/pkg/validators"
12+
)
13+
14+
// printMetricsResponse prints data as JSON or a human-readable table.
15+
func printMetricsResponse(data hookdeck.MetricsResponse, output string) error {
16+
if output == "json" {
17+
bytes, err := json.MarshalIndent(data, "", " ")
18+
if err != nil {
19+
return fmt.Errorf("failed to marshal metrics: %w", err)
20+
}
21+
fmt.Println(string(bytes))
22+
return nil
23+
}
24+
if len(data) == 0 {
25+
fmt.Println("No data points.")
26+
return nil
27+
}
28+
for i, pt := range data {
29+
tb := "<none>"
30+
if pt.TimeBucket != nil {
31+
tb = *pt.TimeBucket
32+
}
33+
fmt.Printf("time_bucket: %s\n", tb)
34+
if len(pt.Dimensions) > 0 {
35+
for k, v := range pt.Dimensions {
36+
fmt.Printf(" %s: %v\n", k, v)
37+
}
38+
}
39+
if len(pt.Metrics) > 0 {
40+
for k, v := range pt.Metrics {
41+
fmt.Printf(" %s: %v\n", k, v)
42+
}
43+
}
44+
if i < len(data)-1 {
45+
fmt.Println("---")
46+
}
47+
}
48+
return nil
49+
}
50+
51+
const granularityHelp = `Time bucket size. Format: <number><unit> (e.g. 1h, 5m, 1d).
52+
Units: s (seconds), m (minutes), h (hours), d (days), w (weeks), M (months).`
53+
54+
// metricsCommonFlags holds the common flags for all metrics subcommands.
55+
// Used by addMetricsCommonFlags and to build hookdeck.MetricsQueryParams.
56+
type metricsCommonFlags struct {
57+
start string
58+
end string
59+
granularity string
60+
measures string
61+
dimensions string
62+
sourceID string
63+
destinationID string
64+
connectionID string
65+
status string
66+
issueID string
67+
output string
68+
}
69+
70+
// addMetricsCommonFlags adds common metrics flags to cmd and binds them to f.
71+
// For subcommands that take a required resource id as an argument (e.g. events-by-issue <issue-id>),
72+
// pass skipIssueID true so --issue-id is not added as a flag.
73+
func addMetricsCommonFlags(cmd *cobra.Command, f *metricsCommonFlags) {
74+
addMetricsCommonFlagsEx(cmd, f, false)
75+
}
76+
77+
func addMetricsCommonFlagsEx(cmd *cobra.Command, f *metricsCommonFlags, skipIssueID bool) {
78+
cmd.Flags().StringVar(&f.start, "start", "", "Start of time range (ISO 8601 date-time, required)")
79+
cmd.Flags().StringVar(&f.end, "end", "", "End of time range (ISO 8601 date-time, required)")
80+
cmd.Flags().StringVar(&f.granularity, "granularity", "", granularityHelp)
81+
cmd.Flags().StringVar(&f.measures, "measures", "", "Comma-separated list of measures to return")
82+
cmd.Flags().StringVar(&f.dimensions, "dimensions", "", "Comma-separated list of dimensions")
83+
cmd.Flags().StringVar(&f.sourceID, "source-id", "", "Filter by source ID")
84+
cmd.Flags().StringVar(&f.destinationID, "destination-id", "", "Filter by destination ID")
85+
cmd.Flags().StringVar(&f.connectionID, "connection-id", "", "Filter by connection ID")
86+
cmd.Flags().StringVar(&f.status, "status", "", "Filter by status (e.g. SUCCESSFUL, FAILED)")
87+
if !skipIssueID {
88+
cmd.Flags().StringVar(&f.issueID, "issue-id", "", "Filter by issue ID")
89+
}
90+
cmd.Flags().StringVar(&f.output, "output", "", "Output format (json)")
91+
_ = cmd.MarkFlagRequired("start")
92+
_ = cmd.MarkFlagRequired("end")
93+
}
94+
95+
// metricsParamsFromFlags builds hookdeck.MetricsQueryParams from common flags.
96+
// Measures and dimensions are split from comma-separated strings.
97+
func metricsParamsFromFlags(f *metricsCommonFlags) hookdeck.MetricsQueryParams {
98+
var measures, dimensions []string
99+
if f.measures != "" {
100+
for _, s := range strings.Split(f.measures, ",") {
101+
if t := strings.TrimSpace(s); t != "" {
102+
measures = append(measures, t)
103+
}
104+
}
105+
}
106+
if f.dimensions != "" {
107+
for _, s := range strings.Split(f.dimensions, ",") {
108+
if t := strings.TrimSpace(s); t != "" {
109+
dimensions = append(dimensions, t)
110+
}
111+
}
112+
}
113+
return hookdeck.MetricsQueryParams{
114+
Start: f.start,
115+
End: f.end,
116+
Granularity: f.granularity,
117+
Measures: measures,
118+
Dimensions: dimensions,
119+
SourceID: f.sourceID,
120+
DestinationID: f.destinationID,
121+
ConnectionID: f.connectionID,
122+
Status: f.status,
123+
IssueID: f.issueID,
124+
}
125+
}
126+
127+
type metricsCmd struct {
128+
cmd *cobra.Command
129+
}
130+
131+
func newMetricsCmd() *metricsCmd {
132+
mc := &metricsCmd{}
133+
134+
mc.cmd = &cobra.Command{
135+
Use: "metrics",
136+
Args: validators.NoArgs,
137+
Short: ShortBeta("Query Event Gateway metrics"),
138+
Long: LongBeta(`Query metrics for events, requests, attempts, queue depth, pending events, events by issue, and transformations.
139+
Requires --start and --end (ISO 8601 date-time). Use subcommands to choose the metric type.`),
140+
}
141+
142+
mc.cmd.AddCommand(newMetricsEventsCmd().cmd)
143+
mc.cmd.AddCommand(newMetricsRequestsCmd().cmd)
144+
mc.cmd.AddCommand(newMetricsAttemptsCmd().cmd)
145+
mc.cmd.AddCommand(newMetricsQueueDepthCmd().cmd)
146+
mc.cmd.AddCommand(newMetricsPendingCmd().cmd)
147+
mc.cmd.AddCommand(newMetricsEventsByIssueCmd().cmd)
148+
mc.cmd.AddCommand(newMetricsTransformationsCmd().cmd)
149+
150+
return mc
151+
}
152+
153+
func addMetricsCmdTo(parent *cobra.Command) {
154+
parent.AddCommand(newMetricsCmd().cmd)
155+
}

pkg/cmd/metrics_attempts.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package cmd
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/spf13/cobra"
8+
)
9+
10+
const metricsAttemptsMeasures = "count, successful_count, failed_count, delivered_count, error_rate, response_latency_avg, response_latency_max, response_latency_p95, response_latency_p99, delivery_latency_avg"
11+
12+
type metricsAttemptsCmd struct {
13+
cmd *cobra.Command
14+
flags metricsCommonFlags
15+
}
16+
17+
func newMetricsAttemptsCmd() *metricsAttemptsCmd {
18+
c := &metricsAttemptsCmd{}
19+
c.cmd = &cobra.Command{
20+
Use: "attempts",
21+
Args: cobra.NoArgs,
22+
Short: ShortBeta("Query attempt metrics"),
23+
Long: LongBeta(`Query metrics for delivery attempts (latency, success/failure). Measures: ` + metricsAttemptsMeasures + `.`),
24+
RunE: c.runE,
25+
}
26+
addMetricsCommonFlags(c.cmd, &c.flags)
27+
return c
28+
}
29+
30+
func (c *metricsAttemptsCmd) runE(cmd *cobra.Command, args []string) error {
31+
if err := Config.Profile.ValidateAPIKey(); err != nil {
32+
return err
33+
}
34+
params := metricsParamsFromFlags(&c.flags)
35+
data, err := Config.GetAPIClient().QueryAttemptMetrics(context.Background(), params)
36+
if err != nil {
37+
return fmt.Errorf("query attempt metrics: %w", err)
38+
}
39+
return printMetricsResponse(data, c.flags.output)
40+
}

pkg/cmd/metrics_events.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package cmd
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/spf13/cobra"
8+
)
9+
10+
const metricsEventsMeasures = "count, successful_count, failed_count, scheduled_count, paused_count, error_rate, avg_attempts, scheduled_retry_count"
11+
12+
type metricsEventsCmd struct {
13+
cmd *cobra.Command
14+
flags metricsCommonFlags
15+
}
16+
17+
func newMetricsEventsCmd() *metricsEventsCmd {
18+
c := &metricsEventsCmd{}
19+
c.cmd = &cobra.Command{
20+
Use: "events",
21+
Args: cobra.NoArgs,
22+
Short: ShortBeta("Query event metrics"),
23+
Long: LongBeta(`Query metrics for events (volume, success/failure counts, error rate, etc.). Measures: ` + metricsEventsMeasures + `.`),
24+
RunE: c.runE,
25+
}
26+
addMetricsCommonFlags(c.cmd, &c.flags)
27+
return c
28+
}
29+
30+
func (c *metricsEventsCmd) runE(cmd *cobra.Command, args []string) error {
31+
if err := Config.Profile.ValidateAPIKey(); err != nil {
32+
return err
33+
}
34+
params := metricsParamsFromFlags(&c.flags)
35+
data, err := Config.GetAPIClient().QueryEventMetrics(context.Background(), params)
36+
if err != nil {
37+
return fmt.Errorf("query event metrics: %w", err)
38+
}
39+
return printMetricsResponse(data, c.flags.output)
40+
}

pkg/cmd/metrics_events_by_issue.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package cmd
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/spf13/cobra"
8+
9+
"github.com/hookdeck/hookdeck-cli/pkg/validators"
10+
)
11+
12+
type metricsEventsByIssueCmd struct {
13+
cmd *cobra.Command
14+
flags metricsCommonFlags
15+
}
16+
17+
func newMetricsEventsByIssueCmd() *metricsEventsByIssueCmd {
18+
c := &metricsEventsByIssueCmd{}
19+
c.cmd = &cobra.Command{
20+
Use: "events-by-issue <issue-id>",
21+
Args: validators.ExactArgs(1),
22+
Short: ShortBeta("Query events grouped by issue"),
23+
Long: LongBeta(`Query metrics for events grouped by issue (for debugging). Requires issue ID as argument.`),
24+
RunE: c.runE,
25+
}
26+
addMetricsCommonFlagsEx(c.cmd, &c.flags, true)
27+
return c
28+
}
29+
30+
func (c *metricsEventsByIssueCmd) runE(cmd *cobra.Command, args []string) error {
31+
if err := Config.Profile.ValidateAPIKey(); err != nil {
32+
return err
33+
}
34+
params := metricsParamsFromFlags(&c.flags)
35+
params.IssueID = args[0]
36+
data, err := Config.GetAPIClient().QueryEventsByIssue(context.Background(), params)
37+
if err != nil {
38+
return fmt.Errorf("query events by issue: %w", err)
39+
}
40+
return printMetricsResponse(data, c.flags.output)
41+
}

pkg/cmd/metrics_pending.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package cmd
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/spf13/cobra"
8+
)
9+
10+
type metricsPendingCmd struct {
11+
cmd *cobra.Command
12+
flags metricsCommonFlags
13+
}
14+
15+
func newMetricsPendingCmd() *metricsPendingCmd {
16+
c := &metricsPendingCmd{}
17+
c.cmd = &cobra.Command{
18+
Use: "pending",
19+
Args: cobra.NoArgs,
20+
Short: ShortBeta("Query events pending timeseries"),
21+
Long: LongBeta(`Query events pending over time (timeseries). Measures: count.`),
22+
RunE: c.runE,
23+
}
24+
addMetricsCommonFlags(c.cmd, &c.flags)
25+
return c
26+
}
27+
28+
func (c *metricsPendingCmd) runE(cmd *cobra.Command, args []string) error {
29+
if err := Config.Profile.ValidateAPIKey(); err != nil {
30+
return err
31+
}
32+
params := metricsParamsFromFlags(&c.flags)
33+
data, err := Config.GetAPIClient().QueryEventsPendingTimeseries(context.Background(), params)
34+
if err != nil {
35+
return fmt.Errorf("query events pending: %w", err)
36+
}
37+
return printMetricsResponse(data, c.flags.output)
38+
}

0 commit comments

Comments
 (0)