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
19 changes: 19 additions & 0 deletions REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ The Hookdeck CLI provides comprehensive webhook infrastructure management includ
- [Events](#events)
- [Requests](#requests)
- [Attempts](#attempts)
- [Metrics](#metrics)
- [Utilities](#utilities)
<!-- GENERATE_END -->
## Global Options
Expand Down Expand Up @@ -1782,6 +1783,24 @@ hookdeck gateway attempt get <attempt-id> [flags]
hookdeck gateway attempt get atm_abc123
```
<!-- GENERATE_END -->
## Metrics

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).

**Use cases and examples:**

| Use case | Example command |
|----------|-----------------|
| 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` |
| 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` |
| 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` |
| 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` |
| Pending events over time | `hookdeck gateway metrics pending --start 2026-02-01T00:00:00Z --end 2026-02-25T00:00:00Z --granularity 1h --measures count` |
| 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` |
| Transformation errors | `hookdeck gateway metrics transformations --start 2026-02-01T00:00:00Z --end 2026-02-25T00:00:00Z --measures count,failed_count,error_rate` |

**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).

## Utilities

<!-- GENERATE:completion|ci:START -->
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "hookdeck-cli",
"version": "1.8.1",
"version": "1.9.0-beta.1",
"description": "Hookdeck CLI",
"repository": {
"type": "git",
Expand Down
1 change: 1 addition & 0 deletions pkg/cmd/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ The gateway command group provides full access to all Event Gateway resources.`,
addEventCmdTo(g.cmd)
addRequestCmdTo(g.cmd)
addAttemptCmdTo(g.cmd)
addMetricsCmdTo(g.cmd)

return g
}
155 changes: 155 additions & 0 deletions pkg/cmd/metrics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package cmd

import (
"encoding/json"
"fmt"
"strings"

"github.com/spf13/cobra"

"github.com/hookdeck/hookdeck-cli/pkg/hookdeck"
"github.com/hookdeck/hookdeck-cli/pkg/validators"
)

// printMetricsResponse prints data as JSON or a human-readable table.
func printMetricsResponse(data hookdeck.MetricsResponse, output string) error {
if output == "json" {
bytes, err := json.MarshalIndent(data, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal metrics: %w", err)
}
fmt.Println(string(bytes))
return nil
}
if len(data) == 0 {
fmt.Println("No data points.")
return nil
}
for i, pt := range data {
tb := "<none>"
if pt.TimeBucket != nil {
tb = *pt.TimeBucket
}
fmt.Printf("time_bucket: %s\n", tb)
if len(pt.Dimensions) > 0 {
for k, v := range pt.Dimensions {
fmt.Printf(" %s: %v\n", k, v)
}
}
if len(pt.Metrics) > 0 {
for k, v := range pt.Metrics {
fmt.Printf(" %s: %v\n", k, v)
}
}
if i < len(data)-1 {
fmt.Println("---")
}
}
return nil
}

const granularityHelp = `Time bucket size. Format: <number><unit> (e.g. 1h, 5m, 1d).
Units: s (seconds), m (minutes), h (hours), d (days), w (weeks), M (months).`

// metricsCommonFlags holds the common flags for all metrics subcommands.
// Used by addMetricsCommonFlags and to build hookdeck.MetricsQueryParams.
type metricsCommonFlags struct {
start string
end string
granularity string
measures string
dimensions string
sourceID string
destinationID string
connectionID string
status string
issueID string
output string
}

// addMetricsCommonFlags adds common metrics flags to cmd and binds them to f.
// For subcommands that take a required resource id as an argument (e.g. events-by-issue <issue-id>),
// pass skipIssueID true so --issue-id is not added as a flag.
func addMetricsCommonFlags(cmd *cobra.Command, f *metricsCommonFlags) {
addMetricsCommonFlagsEx(cmd, f, false)
}

func addMetricsCommonFlagsEx(cmd *cobra.Command, f *metricsCommonFlags, skipIssueID bool) {
cmd.Flags().StringVar(&f.start, "start", "", "Start of time range (ISO 8601 date-time, required)")
cmd.Flags().StringVar(&f.end, "end", "", "End of time range (ISO 8601 date-time, required)")
cmd.Flags().StringVar(&f.granularity, "granularity", "", granularityHelp)
cmd.Flags().StringVar(&f.measures, "measures", "", "Comma-separated list of measures to return")
cmd.Flags().StringVar(&f.dimensions, "dimensions", "", "Comma-separated list of dimensions")
cmd.Flags().StringVar(&f.sourceID, "source-id", "", "Filter by source ID")
cmd.Flags().StringVar(&f.destinationID, "destination-id", "", "Filter by destination ID")
cmd.Flags().StringVar(&f.connectionID, "connection-id", "", "Filter by connection ID")
cmd.Flags().StringVar(&f.status, "status", "", "Filter by status (e.g. SUCCESSFUL, FAILED)")
if !skipIssueID {
cmd.Flags().StringVar(&f.issueID, "issue-id", "", "Filter by issue ID")
}
cmd.Flags().StringVar(&f.output, "output", "", "Output format (json)")
_ = cmd.MarkFlagRequired("start")
_ = cmd.MarkFlagRequired("end")
}

// metricsParamsFromFlags builds hookdeck.MetricsQueryParams from common flags.
// Measures and dimensions are split from comma-separated strings.
func metricsParamsFromFlags(f *metricsCommonFlags) hookdeck.MetricsQueryParams {
var measures, dimensions []string
if f.measures != "" {
for _, s := range strings.Split(f.measures, ",") {
if t := strings.TrimSpace(s); t != "" {
measures = append(measures, t)
}
}
}
if f.dimensions != "" {
for _, s := range strings.Split(f.dimensions, ",") {
if t := strings.TrimSpace(s); t != "" {
dimensions = append(dimensions, t)
}
}
}
return hookdeck.MetricsQueryParams{
Start: f.start,
End: f.end,
Granularity: f.granularity,
Measures: measures,
Dimensions: dimensions,
SourceID: f.sourceID,
DestinationID: f.destinationID,
ConnectionID: f.connectionID,
Status: f.status,
IssueID: f.issueID,
}
}

type metricsCmd struct {
cmd *cobra.Command
}

func newMetricsCmd() *metricsCmd {
mc := &metricsCmd{}

mc.cmd = &cobra.Command{
Use: "metrics",
Args: validators.NoArgs,
Short: ShortBeta("Query Event Gateway metrics"),
Long: LongBeta(`Query metrics for events, requests, attempts, queue depth, pending events, events by issue, and transformations.
Requires --start and --end (ISO 8601 date-time). Use subcommands to choose the metric type.`),
}

mc.cmd.AddCommand(newMetricsEventsCmd().cmd)
mc.cmd.AddCommand(newMetricsRequestsCmd().cmd)
mc.cmd.AddCommand(newMetricsAttemptsCmd().cmd)
mc.cmd.AddCommand(newMetricsQueueDepthCmd().cmd)
mc.cmd.AddCommand(newMetricsPendingCmd().cmd)
mc.cmd.AddCommand(newMetricsEventsByIssueCmd().cmd)
mc.cmd.AddCommand(newMetricsTransformationsCmd().cmd)

return mc
}

func addMetricsCmdTo(parent *cobra.Command) {
parent.AddCommand(newMetricsCmd().cmd)
}
40 changes: 40 additions & 0 deletions pkg/cmd/metrics_attempts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package cmd

import (
"context"
"fmt"

"github.com/spf13/cobra"
)

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"

type metricsAttemptsCmd struct {
cmd *cobra.Command
flags metricsCommonFlags
}

func newMetricsAttemptsCmd() *metricsAttemptsCmd {
c := &metricsAttemptsCmd{}
c.cmd = &cobra.Command{
Use: "attempts",
Args: cobra.NoArgs,
Short: ShortBeta("Query attempt metrics"),
Long: LongBeta(`Query metrics for delivery attempts (latency, success/failure). Measures: ` + metricsAttemptsMeasures + `.`),
RunE: c.runE,
}
addMetricsCommonFlags(c.cmd, &c.flags)
return c
}

func (c *metricsAttemptsCmd) runE(cmd *cobra.Command, args []string) error {
if err := Config.Profile.ValidateAPIKey(); err != nil {
return err
}
params := metricsParamsFromFlags(&c.flags)
data, err := Config.GetAPIClient().QueryAttemptMetrics(context.Background(), params)
if err != nil {
return fmt.Errorf("query attempt metrics: %w", err)
}
return printMetricsResponse(data, c.flags.output)
}
40 changes: 40 additions & 0 deletions pkg/cmd/metrics_events.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package cmd

import (
"context"
"fmt"

"github.com/spf13/cobra"
)

const metricsEventsMeasures = "count, successful_count, failed_count, scheduled_count, paused_count, error_rate, avg_attempts, scheduled_retry_count"

type metricsEventsCmd struct {
cmd *cobra.Command
flags metricsCommonFlags
}

func newMetricsEventsCmd() *metricsEventsCmd {
c := &metricsEventsCmd{}
c.cmd = &cobra.Command{
Use: "events",
Args: cobra.NoArgs,
Short: ShortBeta("Query event metrics"),
Long: LongBeta(`Query metrics for events (volume, success/failure counts, error rate, etc.). Measures: ` + metricsEventsMeasures + `.`),
RunE: c.runE,
}
addMetricsCommonFlags(c.cmd, &c.flags)
return c
}

func (c *metricsEventsCmd) runE(cmd *cobra.Command, args []string) error {
if err := Config.Profile.ValidateAPIKey(); err != nil {
return err
}
params := metricsParamsFromFlags(&c.flags)
data, err := Config.GetAPIClient().QueryEventMetrics(context.Background(), params)
if err != nil {
return fmt.Errorf("query event metrics: %w", err)
}
return printMetricsResponse(data, c.flags.output)
}
41 changes: 41 additions & 0 deletions pkg/cmd/metrics_events_by_issue.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package cmd

import (
"context"
"fmt"

"github.com/spf13/cobra"

"github.com/hookdeck/hookdeck-cli/pkg/validators"
)

type metricsEventsByIssueCmd struct {
cmd *cobra.Command
flags metricsCommonFlags
}

func newMetricsEventsByIssueCmd() *metricsEventsByIssueCmd {
c := &metricsEventsByIssueCmd{}
c.cmd = &cobra.Command{
Use: "events-by-issue <issue-id>",
Args: validators.ExactArgs(1),
Short: ShortBeta("Query events grouped by issue"),
Long: LongBeta(`Query metrics for events grouped by issue (for debugging). Requires issue ID as argument.`),
RunE: c.runE,
}
addMetricsCommonFlagsEx(c.cmd, &c.flags, true)
return c
}

func (c *metricsEventsByIssueCmd) runE(cmd *cobra.Command, args []string) error {
if err := Config.Profile.ValidateAPIKey(); err != nil {
return err
}
params := metricsParamsFromFlags(&c.flags)
params.IssueID = args[0]
data, err := Config.GetAPIClient().QueryEventsByIssue(context.Background(), params)
if err != nil {
return fmt.Errorf("query events by issue: %w", err)
}
return printMetricsResponse(data, c.flags.output)
}
38 changes: 38 additions & 0 deletions pkg/cmd/metrics_pending.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package cmd

import (
"context"
"fmt"

"github.com/spf13/cobra"
)

type metricsPendingCmd struct {
cmd *cobra.Command
flags metricsCommonFlags
}

func newMetricsPendingCmd() *metricsPendingCmd {
c := &metricsPendingCmd{}
c.cmd = &cobra.Command{
Use: "pending",
Args: cobra.NoArgs,
Short: ShortBeta("Query events pending timeseries"),
Long: LongBeta(`Query events pending over time (timeseries). Measures: count.`),
RunE: c.runE,
}
addMetricsCommonFlags(c.cmd, &c.flags)
return c
}

func (c *metricsPendingCmd) runE(cmd *cobra.Command, args []string) error {
if err := Config.Profile.ValidateAPIKey(); err != nil {
return err
}
params := metricsParamsFromFlags(&c.flags)
data, err := Config.GetAPIClient().QueryEventsPendingTimeseries(context.Background(), params)
if err != nil {
return fmt.Errorf("query events pending: %w", err)
}
return printMetricsResponse(data, c.flags.output)
}
Loading