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: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,7 @@ headers = ["Authorization: Bearer token"]
**Global settings** (apply to all targets unless overridden):

- `refresh_interval`, `timeout`, `follow_redirects`, `accept_redirects`, `receive_alert`, `count`
- `body_size_limit`: Response body cap in bytes (default `1048576` = 1 MiB; `0` means no limit)
- `webhook_url`, `webhook_headers`: Default webhook settings
- `only`, `skip`: Target filtering arrays
- `regions`: AWS regions for remote executors
Expand All @@ -299,9 +300,12 @@ headers = ["Authorization: Bearer token"]
- `method`, `headers`, `body`: HTTP request options
- `assert_text`, `should_fail`: Response validation
- `skip_ssl`, `follow_redirects`, `accept_redirects`: Connection options
- `body_size_limit`: Per-target response body cap (set to `0` to disable capping for this target)
- `webhook_url`, `webhook_headers`: Per-target notifications
- `regions`: Target-specific AWS regions

> **Note:** Response bodies are capped at `body_size_limit` bytes when evaluating `assert_text`. If your asserted text appears beyond the cap, the assertion fails and the probe logs a warning (visible in the Recent Logs widget in TUI mode, or on stderr in simple mode). Raise `body_size_limit` or set it to `0` for targets returning large payloads.

## Multi-Region Monitoring

Deploy remote executors as AWS Lambda functions across 13 global regions for distributed monitoring from multiple geographic locations.
Expand Down
2 changes: 2 additions & 0 deletions aws/invocation.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type LambdaRequest struct {
SkipSSL bool `json:"skip_ssl"`
AssertText string `json:"assert_text"`
ShouldFail bool `json:"should_fail"`
BodySizeLimit int64 `json:"body_size_limit,omitempty"`
}

type LambdaResponse struct {
Expand Down Expand Up @@ -111,6 +112,7 @@ func invokeLambdaInRegion(url string, config net.NetworkConfig, region string, p
SkipSSL: config.SkipSSL,
AssertText: config.AssertText,
ShouldFail: config.ShouldFail,
BodySizeLimit: config.BodySizeLimit,
}

payload, err := json.Marshal(request)
Expand Down
16 changes: 16 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package config
import (
"time"

"github.com/Owloops/updo/net"
"github.com/spf13/viper"
)

Expand All @@ -29,6 +30,7 @@ type Target struct {
WebhookURL string `mapstructure:"webhook_url"`
WebhookHeaders []string `mapstructure:"webhook_headers"`
Regions []string `mapstructure:"regions"`
BodySizeLimit *int64 `mapstructure:"body_size_limit"`
}

// BoolVal returns the value of a *bool, or the fallback if nil.
Expand All @@ -39,6 +41,14 @@ func BoolVal(p *bool, fallback bool) bool {
return fallback
}

// Int64Val returns the value of an *int64, or the fallback if nil.
func Int64Val(p *int64, fallback int64) int64 {
if p != nil {
return *p
}
return fallback
}

type Global struct {
RefreshInterval int `mapstructure:"refresh_interval"`
Timeout int `mapstructure:"timeout"`
Expand All @@ -55,6 +65,7 @@ type Global struct {
WebhookURL string `mapstructure:"webhook_url"`
WebhookHeaders []string `mapstructure:"webhook_headers"`
Regions []string `mapstructure:"regions"`
BodySizeLimit int64 `mapstructure:"body_size_limit"`
}

type Config struct {
Expand All @@ -72,6 +83,7 @@ func LoadConfig(configFile string) (*Config, error) {
viper.SetDefault("global.receive_alert", true)
viper.SetDefault("global.count", 0)
viper.SetDefault("global.method", _defaultMethod)
viper.SetDefault("global.body_size_limit", net.DefaultBodySizeLimit)

if err := viper.ReadInConfig(); err != nil {
return nil, err
Expand Down Expand Up @@ -110,6 +122,10 @@ func LoadConfig(configFile string) (*Config, error) {
v := config.Global.SkipSSL
target.SkipSSL = &v
}
if target.BodySizeLimit == nil {
v := config.Global.BodySizeLimit
target.BodySizeLimit = &v
}
if target.WebhookURL == "" && config.Global.WebhookURL != "" {
target.WebhookURL = config.Global.WebhookURL
}
Expand Down
100 changes: 100 additions & 0 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"os"
"testing"
"time"

"github.com/Owloops/updo/net"
)

func TestLoadConfig(t *testing.T) {
Expand Down Expand Up @@ -151,6 +153,104 @@ name = "Inherit"
}
}

// TestBodySizeLimitInheritance verifies that a target inherits
// global.body_size_limit when unset, overrides when explicitly set,
// and supports 0 as "unlimited".
func TestBodySizeLimitInheritance(t *testing.T) {
configContent := `
[global]
body_size_limit = 2097152

[[targets]]
url = "https://inherit.example.com"
name = "Inherit"

[[targets]]
url = "https://override.example.com"
name = "Override"
body_size_limit = 524288

[[targets]]
url = "https://unlimited.example.com"
name = "Unlimited"
body_size_limit = 0
`

tmpFile, err := os.CreateTemp("", "test-config-bodysize-*.toml")
if err != nil {
t.Fatalf("Failed to create temp file: %v", err)
}
defer func() {
if err := os.Remove(tmpFile.Name()); err != nil {
t.Logf("Failed to remove temp file: %v", err)
}
}()

if _, err := tmpFile.WriteString(configContent); err != nil {
t.Fatalf("Failed to write config: %v", err)
}
if err := tmpFile.Close(); err != nil {
t.Fatalf("Failed to close temp file: %v", err)
}

cfg, err := LoadConfig(tmpFile.Name())
if err != nil {
t.Fatalf("LoadConfig failed: %v", err)
}

if cfg.Global.BodySizeLimit != 2097152 {
t.Errorf("Global.BodySizeLimit = %d, want 2097152", cfg.Global.BodySizeLimit)
}

if got := Int64Val(cfg.Targets[0].BodySizeLimit, -1); got != 2097152 {
t.Errorf("Inherit target: BodySizeLimit = %d, want 2097152 (inherited)", got)
}
if got := Int64Val(cfg.Targets[1].BodySizeLimit, -1); got != 524288 {
t.Errorf("Override target: BodySizeLimit = %d, want 524288", got)
}
if got := Int64Val(cfg.Targets[2].BodySizeLimit, -1); got != 0 {
t.Errorf("Unlimited target: BodySizeLimit = %d, want 0 (explicit unlimited)", got)
}
}

// TestBodySizeLimitDefault verifies that when neither global nor target
// set body_size_limit, the viper default of net.DefaultBodySizeLimit applies.
func TestBodySizeLimitDefault(t *testing.T) {
configContent := `
[[targets]]
url = "https://example.com"
`

tmpFile, err := os.CreateTemp("", "test-config-bodysize-default-*.toml")
if err != nil {
t.Fatalf("Failed to create temp file: %v", err)
}
defer func() {
if err := os.Remove(tmpFile.Name()); err != nil {
t.Logf("Failed to remove temp file: %v", err)
}
}()

if _, err := tmpFile.WriteString(configContent); err != nil {
t.Fatalf("Failed to write config: %v", err)
}
if err := tmpFile.Close(); err != nil {
t.Fatalf("Failed to close temp file: %v", err)
}

cfg, err := LoadConfig(tmpFile.Name())
if err != nil {
t.Fatalf("LoadConfig failed: %v", err)
}

if cfg.Global.BodySizeLimit != net.DefaultBodySizeLimit {
t.Errorf("Global.BodySizeLimit = %d, want %d (default)", cfg.Global.BodySizeLimit, net.DefaultBodySizeLimit)
}
if got := Int64Val(cfg.Targets[0].BodySizeLimit, -1); got != net.DefaultBodySizeLimit {
t.Errorf("Target BodySizeLimit = %d, want %d (default inherited)", got, net.DefaultBodySizeLimit)
}
}

func TestLoadConfigDefaults(t *testing.T) {
configContent := `
[[targets]]
Expand Down
1 change: 1 addition & 0 deletions example-config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ accept_redirects = false
receive_alert = false
count = 0
skip = ["GitHub-API"]
# body_size_limit = 1048576 # Cap response body reads at 1 MiB (default); 0 = no limit
# regions = ["us-east-1", "eu-central-1", "ap-southeast-1"]

[[targets]]
Expand Down
3 changes: 2 additions & 1 deletion lambda/lambda.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ type CheckRequest struct {
SkipSSL bool `json:"skip_ssl"`
AssertText string `json:"assert_text"`
ShouldFail bool `json:"should_fail"`
BodySizeLimit int64 `json:"body_size_limit,omitempty"`
}

type CheckResponse struct {
Expand Down Expand Up @@ -110,7 +111,7 @@ func handleRequest(ctx context.Context, req CheckRequest) (CheckResponse, error)
Headers: req.Headers,
Method: req.Method,
Body: req.Body,
BodySizeLimit: net.DefaultBodySizeLimit,
BodySizeLimit: req.BodySizeLimit,
}

result := net.CheckWebsite(req.URL, netConfig)
Expand Down
2 changes: 1 addition & 1 deletion simple/monitoring.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ func monitorTargetSimple(ctx context.Context, target config.Target, targetIndex
Headers: target.Headers,
Method: target.Method,
Body: target.Body,
BodySizeLimit: net.DefaultBodySizeLimit,
BodySizeLimit: config.Int64Val(target.BodySizeLimit, net.DefaultBodySizeLimit),
}

regions := target.Regions
Expand Down
2 changes: 1 addition & 1 deletion tui/monitoring.go
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ func monitorTargetTUI(ctx context.Context, target config.Target, targetIndex int
Headers: target.Headers,
Method: target.Method,
Body: target.Body,
BodySizeLimit: net.DefaultBodySizeLimit,
BodySizeLimit: config.Int64Val(target.BodySizeLimit, net.DefaultBodySizeLimit),
}

regions := target.Regions
Expand Down
Loading