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
4 changes: 2 additions & 2 deletions docs/openstatus-docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ Create or update monitors.
> openstatus monitors apply --config custom.yaml -y
> openstatus monitors apply --dry-run

Creates or updates monitors according to the OpenStatus configuration file. Compares your openstatus.yaml with the current state and applies changes.
Creates or updates monitors according to the OpenStatus configuration file. Compares your openstatus.yaml with the current state and applies changes. DEPRECATED: this command is deprecated. Prefer managing your monitors with Terraform via 'openstatus terraform generate'.

Usage:

Expand Down Expand Up @@ -134,7 +134,7 @@ Import all your monitors.
> openstatus monitors import
> openstatus monitors import --output monitors.yaml

Import all your monitors from your workspace to a YAML file; it will also create a lock file to manage your monitors with 'apply'.
Import all your monitors from your workspace to a YAML file; it will also create a lock file to manage your monitors with 'apply'. DEPRECATED: the monitors.yaml config file is deprecated for import. Prefer managing monitors declaratively with 'openstatus monitors apply'.

Usage:

Expand Down
10 changes: 5 additions & 5 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ go 1.25.0
require github.com/urfave/cli/v3 v3.9.0 // direct

require (
buf.build/gen/go/openstatus/api/connectrpc/gosimple v1.20.0-20260724145211-8d1b4f4069ec.1
buf.build/gen/go/openstatus/api/protocolbuffers/go v1.36.11-20260724145211-8d1b4f4069ec.1
buf.build/gen/go/openstatus/api/connectrpc/gosimple v1.20.0-20260826071311-8210bb95208b.1
buf.build/gen/go/openstatus/api/protocolbuffers/go v1.36.12-20260826071311-8210bb95208b.1
connectrpc.com/connect v1.20.0
github.com/briandowns/spinner v1.23.2
github.com/charmbracelet/huh v1.0.0
Expand All @@ -25,13 +25,13 @@ require (
github.com/zclconf/go-cty v1.18.1
golang.org/x/term v0.43.0
golang.org/x/text v0.37.0
google.golang.org/protobuf v1.36.11
google.golang.org/protobuf v1.36.12
sigs.k8s.io/yaml v1.6.0
)

require (
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260415201107-50325440f8f2.1 // indirect
buf.build/gen/go/gnostic/gnostic/protocolbuffers/go v1.36.11-20230414000709-087bc8072ce4.1 // indirect
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20251209175733-2a1774d88802.1 // indirect
buf.build/gen/go/gnostic/gnostic/protocolbuffers/go v1.36.12-20230414000709-087bc8072ce4.1 // indirect
github.com/agext/levenshtein v1.2.3 // indirect
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
github.com/atotto/clipboard v0.1.4 // indirect
Expand Down
20 changes: 10 additions & 10 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260415201107-50325440f8f2.1 h1:s6hzCXtND/ICdGPTMGk7C+/BFlr2Jg5GyH0NKf4XGXg=
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260415201107-50325440f8f2.1/go.mod h1:tvtbpgaVXZX4g6Pn+AnzFycuRK3MOz5HJfEGeEllXYM=
buf.build/gen/go/gnostic/gnostic/protocolbuffers/go v1.36.11-20230414000709-087bc8072ce4.1 h1:t8f+WWZ5WNrZaP5zrpWD8f1tKU7eelJMbIAT9FRX558=
buf.build/gen/go/gnostic/gnostic/protocolbuffers/go v1.36.11-20230414000709-087bc8072ce4.1/go.mod h1:/t9AeRQQp2iNkiGHDLfHLW3SzNpYpNPGRZ+Ih8+SOUs=
buf.build/gen/go/openstatus/api/connectrpc/gosimple v1.20.0-20260724145211-8d1b4f4069ec.1 h1:A3yb5Bf2iuld7SYNcq7I2uXv/zEr58dJFSoQm2HeFOQ=
buf.build/gen/go/openstatus/api/connectrpc/gosimple v1.20.0-20260724145211-8d1b4f4069ec.1/go.mod h1:MmC+HrHeHglSXd6g09VexkguG+ohgxelutvlXZW/syg=
buf.build/gen/go/openstatus/api/protocolbuffers/go v1.36.11-20260724145211-8d1b4f4069ec.1 h1:tERlcNbY6AkmrrDiOT1nlDvEEA+JdSnOQZTDd5BYCb4=
buf.build/gen/go/openstatus/api/protocolbuffers/go v1.36.11-20260724145211-8d1b4f4069ec.1/go.mod h1:GRsD/In1AV3/RC92zWVAjBpj57hJIQm7Rph0rLOYYPg=
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20251209175733-2a1774d88802.1 h1:fllAS5JX4LI/DuJhHTRJeACHEuVU69STRW/lPPS68/M=
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20251209175733-2a1774d88802.1/go.mod h1:TCt1lluMFnctISJXvkIQ4x3ABrPuUKCWKyjKdkJNBpw=
buf.build/gen/go/gnostic/gnostic/protocolbuffers/go v1.36.12-20230414000709-087bc8072ce4.1 h1:vZuF9KSFuaVk8BJ2oIETVL24WFc5ehiY0kdYJMrds+Q=
buf.build/gen/go/gnostic/gnostic/protocolbuffers/go v1.36.12-20230414000709-087bc8072ce4.1/go.mod h1:Y6uGzulgNTMSJafTYzhV2xKPPiUnSoE1AStyVM9mBbI=
buf.build/gen/go/openstatus/api/connectrpc/gosimple v1.20.0-20260826071311-8210bb95208b.1 h1:c8+IWCmwEhaEVaqWcQoYPAi33WPfGDce4taD+mll0kM=
buf.build/gen/go/openstatus/api/connectrpc/gosimple v1.20.0-20260826071311-8210bb95208b.1/go.mod h1:X1+npKnwmGjhmqc/f0DB75aqs6IOfEsvakUTV6oDVJY=
buf.build/gen/go/openstatus/api/protocolbuffers/go v1.36.12-20260826071311-8210bb95208b.1 h1:UByAofqyXvii0fPFzno5TE0JA7p38ZEZ6YrMv2HcLhA=
buf.build/gen/go/openstatus/api/protocolbuffers/go v1.36.12-20260826071311-8210bb95208b.1/go.mod h1:0bDFGY01RucPOrga1Y+2eAfIiDrJVFGxs2bMnIyZOAc=
connectrpc.com/connect v1.20.0 h1:6TNDAB+WeNd2uolWNlYczB5E0KNNaVMNUEx8JEUsPmQ=
connectrpc.com/connect v1.20.0/go.mod h1:A2ygJrukXwWy32vkCAAHNVguZrqZ+jeZ9rGRnGR4dN4=
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
Expand Down Expand Up @@ -169,8 +169,8 @@ golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Expand Down
1 change: 1 addition & 0 deletions internal/config/monitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ type CoordinateKind string
const (
HTTP CoordinateKind = "http"
TCP CoordinateKind = "tcp"
ICMP CoordinateKind = "icmp"
)

type Region string
Expand Down
20 changes: 20 additions & 0 deletions internal/monitors/monitor_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ func CreateMonitor(ctx context.Context, httpClient *http.Client, apiKey string,
return CreateHTTPMonitor(ctx, client, monitor)
case config.TCP:
return CreateTCPMonitor(ctx, client, monitor)
case config.ICMP:
return CreateICMPMonitor(ctx, client, monitor)
default:
return Monitor{}, fmt.Errorf("unsupported monitor kind: %s", monitor.Kind)
}
Expand Down Expand Up @@ -64,6 +66,24 @@ func CreateTCPMonitor(ctx context.Context, client monitorv1connect.MonitorServic
return tcpMonitorToLocal(resp.GetMonitor())
}

// CreateICMPMonitor creates an ICMP monitor using the SDK
func CreateICMPMonitor(ctx context.Context, client monitorv1connect.MonitorServiceClient, monitor config.Monitor) (Monitor, error) {
icmpMonitor, err := configToICMPMonitor(monitor)
if err != nil {
return Monitor{}, err
}
req := &monitorv1.CreateICMPMonitorRequest{
Monitor: icmpMonitor,
}

resp, err := client.CreateICMPMonitor(ctx, req)
if err != nil {
return Monitor{}, fmt.Errorf("failed to create ICMP monitor: %w", err)
}

return icmpMonitorToLocal(resp.GetMonitor())
}

func httpMonitorToLocal(m *monitorv1.HTTPMonitor) (Monitor, error) {
id, err := strconv.Atoi(m.GetId())
if err != nil {
Expand Down
60 changes: 60 additions & 0 deletions internal/monitors/monitor_create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,66 @@ func Test_CreateMonitor(t *testing.T) {
}
})

t.Run("Create ICMP monitor successfully", func(t *testing.T) {
body := `{"monitor":{"id":"789","name":"ICMP Monitor","uri":"8.8.8.8","periodicity":"PERIODICITY_5M","regions":["REGION_FLY_IAD"],"active":true}}`
r := io.NopCloser(bytes.NewReader([]byte(body)))

interceptor := &interceptorHTTPClient{
f: func(req *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusOK,
Body: r,
Header: http.Header{"Content-Type": []string{"application/json"}},
}, nil
},
}

monitor := config.Monitor{
Name: "ICMP Monitor",
Active: true,
Frequency: config.The5M,
Kind: config.ICMP,
Regions: []config.Region{config.Iad},
Request: config.Request{
Host: "8.8.8.8",
},
}

result, err := monitors.CreateMonitor(context.Background(), interceptor.GetHTTPClient(), "test-api-key", monitor)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}

if result.ID != 789 {
t.Errorf("Expected ID 789, got %d", result.ID)
}
if result.Name != "ICMP Monitor" {
t.Errorf("Expected name 'ICMP Monitor', got %s", result.Name)
}
if result.URL != "8.8.8.8" {
t.Errorf("Expected URL '8.8.8.8', got %s", result.URL)
}
if result.JobType != "icmp" {
t.Errorf("Expected jobType 'icmp', got %s", result.JobType)
}
})

t.Run("Create ICMP monitor fails without host", func(t *testing.T) {
monitor := config.Monitor{
Name: "ICMP Monitor",
Kind: config.ICMP,
Active: true,
Request: config.Request{
Host: "",
},
}

_, err := monitors.CreateMonitor(context.Background(), &http.Client{}, "test-api-key", monitor)
if err == nil {
t.Error("Expected error for missing ICMP host, got nil")
}
})

t.Run("Create monitor fails with non-200 status", func(t *testing.T) {
body := `{"code":"internal","message":"internal error"}`
r := io.NopCloser(bytes.NewReader([]byte(body)))
Expand Down
7 changes: 7 additions & 0 deletions internal/monitors/monitor_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,13 @@ func GetMonitorInfo(ctx context.Context, httpClient *http.Client, apiKey string,
}
regions = monitorConfig.GetTcp().GetRegions()
privateLocationIDs = monitorConfig.GetTcp().GetPrivateLocationIds()
case monitorConfig.HasIcmp():
monitor, err = icmpMonitorToLocal(monitorConfig.GetIcmp())
if err != nil {
return err
}
regions = monitorConfig.GetIcmp().GetRegions()
privateLocationIDs = monitorConfig.GetIcmp().GetPrivateLocationIds()
default:
if monitorConfig.HasDns() {
return fmt.Errorf("DNS monitors are not yet supported in the CLI. Monitor ID: %s", monitorId)
Expand Down
18 changes: 18 additions & 0 deletions internal/monitors/monitor_info_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,24 @@ func Test_getMonitorInfo(t *testing.T) {
}
})

t.Run("Should work with ICMP monitor and degradedAt", func(t *testing.T) {
interceptor := monitorInfoInterceptor([]routeEntry{
{"GetMonitor", `{"monitor":{"icmp":{"id":"4001","name":"Gateway Ping","uri":"8.8.8.8","periodicity":"PERIODICITY_5M","regions":["REGION_FLY_IAD"],"active":true,"public":false,"timeout":10000,"degradedAt":"5000"}}}`},
{"GetMonitorStatus", `{"id":"4001","regions":[{"region":"REGION_FLY_IAD","status":"MONITOR_STATUS_ACTIVE"}]}`},
{"GetMonitorSummary", `{"id":"4001","totalSuccessful":"500","totalDegraded":"0","totalFailed":"0","p50":"20","p75":"30","p90":"40","p95":"50","p99":"60","timeRange":"TIME_RANGE_1D"}`},
})

var bf bytes.Buffer
log.SetOutput(&bf)
t.Cleanup(func() {
log.SetOutput(os.Stdout)
})
err := monitors.GetMonitorInfo(context.Background(), interceptor.GetHTTPClient(), "test", "4001", monitorv1.TimeRange_TIME_RANGE_1D, "1d", nil)
if err != nil {
t.Errorf("Expected no error, got %v", err)
}
})

t.Run("Should gracefully degrade when status RPC fails", func(t *testing.T) {
interceptor := &interceptorHTTPClient{
f: func(req *http.Request) (*http.Response, error) {
Expand Down
23 changes: 23 additions & 0 deletions internal/monitors/monitor_update.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ func UpdateMonitor(ctx context.Context, httpClient *http.Client, apiKey string,
return UpdateHTTPMonitor(ctx, client, id, monitor)
case config.TCP:
return UpdateTCPMonitor(ctx, client, id, monitor)
case config.ICMP:
return UpdateICMPMonitor(ctx, client, id, monitor)
default:
return Monitor{}, fmt.Errorf("unsupported monitor kind: %s", monitor.Kind)
}
Expand Down Expand Up @@ -64,3 +66,24 @@ func UpdateTCPMonitor(ctx context.Context, client monitorv1connect.MonitorServic

return tcpMonitorToLocal(resp.GetMonitor())
}

// UpdateICMPMonitor updates an ICMP monitor using the SDK
func UpdateICMPMonitor(ctx context.Context, client monitorv1connect.MonitorServiceClient, id int, monitor config.Monitor) (Monitor, error) {
icmpMonitor, err := configToICMPMonitor(monitor)
if err != nil {
return Monitor{}, err
}
icmpMonitor.Id = strconv.Itoa(id)

req := &monitorv1.UpdateICMPMonitorRequest{
Id: strconv.Itoa(id),
Monitor: icmpMonitor,
}

resp, err := client.UpdateICMPMonitor(ctx, req)
if err != nil {
return Monitor{}, fmt.Errorf("failed to update ICMP monitor: %w", err)
}

return icmpMonitorToLocal(resp.GetMonitor())
}
41 changes: 41 additions & 0 deletions internal/monitors/monitor_update_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,47 @@ func Test_UpdateMonitor(t *testing.T) {
}
})

t.Run("Update ICMP monitor successfully", func(t *testing.T) {
body := `{"monitor":{"id":"457","name":"Updated ICMP Monitor","uri":"updated.example.com","periodicity":"PERIODICITY_1M","regions":["REGION_FLY_AMS"],"active":true}}`
r := io.NopCloser(bytes.NewReader([]byte(body)))

interceptor := &interceptorHTTPClient{
f: func(req *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusOK,
Body: r,
Header: http.Header{"Content-Type": []string{"application/json"}},
}, nil
},
}

monitor := config.Monitor{
Name: "Updated ICMP Monitor",
Active: true,
Frequency: config.The1M,
Kind: config.ICMP,
Regions: []config.Region{config.Ams},
Request: config.Request{
Host: "updated.example.com",
},
}

result, err := monitors.UpdateMonitor(context.Background(), interceptor.GetHTTPClient(), "test-api-key", 457, monitor)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}

if result.ID != 457 {
t.Errorf("Expected ID 457, got %d", result.ID)
}
if result.JobType != "icmp" {
t.Errorf("Expected jobType 'icmp', got %s", result.JobType)
}
if result.URL != "updated.example.com" {
t.Errorf("Expected URL 'updated.example.com', got %s", result.URL)
}
})

t.Run("Update monitor fails with error response", func(t *testing.T) {
body := `{"code":"not_found","message":"monitor not found"}`
r := io.NopCloser(bytes.NewReader([]byte(body)))
Expand Down
Loading
Loading