1
0
mirror of https://github.com/TwiN/gatus.git synced 2026-02-15 12:05:07 +00:00

feat(suite): Implement Suites (#1239)

* feat(suite): Implement Suites

Fixes #1230

* Update docs

* Fix variable alignment

* Prevent always-run endpoint from running if a context placeholder fails to resolve in the URL

* Return errors when a context placeholder path fails to resolve

* Add a couple of unit tests

* Add a couple of unit tests

* fix(ui): Update group count properly

Fixes #1233

* refactor: Pass down entire config instead of several sub-configs

* fix: Change default suite interval and timeout

* fix: Deprecate disable-monitoring-lock in favor of concurrency

* fix: Make sure there are no duplicate keys

* Refactor some code

* Update watchdog/watchdog.go

* Update web/app/src/components/StepDetailsModal.vue

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* chore: Remove useless log

* fix: Set default concurrency to 3 instead of 5

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
TwiN
2025-09-05 15:39:12 -04:00
committed by GitHub
parent 10cabb9dde
commit d668a14703
74 changed files with 7513 additions and 652 deletions

View File

@@ -17,8 +17,10 @@ import (
"github.com/TwiN/gatus/v5/config/announcement"
"github.com/TwiN/gatus/v5/config/connectivity"
"github.com/TwiN/gatus/v5/config/endpoint"
"github.com/TwiN/gatus/v5/config/key"
"github.com/TwiN/gatus/v5/config/maintenance"
"github.com/TwiN/gatus/v5/config/remote"
"github.com/TwiN/gatus/v5/config/suite"
"github.com/TwiN/gatus/v5/config/ui"
"github.com/TwiN/gatus/v5/config/web"
"github.com/TwiN/gatus/v5/security"
@@ -35,6 +37,9 @@ const (
// DefaultFallbackConfigurationFilePath is the default fallback path that will be used to search for the
// configuration file if DefaultConfigurationFilePath didn't work
DefaultFallbackConfigurationFilePath = "config/config.yml"
// DefaultConcurrency is the default number of endpoints/suites that can be monitored concurrently
DefaultConcurrency = 3
)
var (
@@ -67,8 +72,14 @@ type Config struct {
// DisableMonitoringLock Whether to disable the monitoring lock
// The monitoring lock is what prevents multiple endpoints from being processed at the same time.
// Disabling this may lead to inaccurate response times
//
// Deprecated: Use Concurrency instead TODO: REMOVE THIS IN v6.0.0
DisableMonitoringLock bool `yaml:"disable-monitoring-lock,omitempty"`
// Concurrency is the maximum number of endpoints/suites that can be monitored concurrently
// Defaults to DefaultConcurrency. Set to 0 for unlimited concurrency.
Concurrency int `yaml:"concurrency,omitempty"`
// Security is the configuration for securing access to Gatus
Security *security.Config `yaml:"security,omitempty"`
@@ -81,6 +92,9 @@ type Config struct {
// ExternalEndpoints is the list of all external endpoints
ExternalEndpoints []*endpoint.ExternalEndpoint `yaml:"external-endpoints,omitempty"`
// Suites is the list of suites to monitor
Suites []*suite.Suite `yaml:"suites,omitempty"`
// Storage is the configuration for how the data is stored
Storage *storage.Config `yaml:"storage,omitempty"`
@@ -309,6 +323,13 @@ func parseAndValidateConfigBytes(yamlBytes []byte) (config *Config, err error) {
if err := validateAnnouncementsConfig(config); err != nil {
return nil, err
}
if err := validateSuitesConfig(config); err != nil {
return nil, err
}
if err := validateUniqueKeys(config); err != nil {
return nil, err
}
validateAndSetConcurrencyDefaults(config)
// Cross-config changes
config.UI.MaximumNumberOfResults = config.Storage.MaximumNumberOfResults
}
@@ -405,7 +426,7 @@ func validateEndpointsConfig(config *Config) error {
logr.Infof("[config.validateEndpointsConfig] Validated %d endpoints", len(config.Endpoints))
// Validate external endpoints
for _, ee := range config.ExternalEndpoints {
logr.Debugf("[config.validateEndpointsConfig] Validating external endpoint '%s'", ee.Name)
logr.Debugf("[config.validateEndpointsConfig] Validating external endpoint '%s'", ee.Key())
if endpointKey := ee.Key(); duplicateValidationMap[endpointKey] {
return fmt.Errorf("invalid external endpoint %s: name and group combination must be unique", ee.Key())
} else {
@@ -419,6 +440,78 @@ func validateEndpointsConfig(config *Config) error {
return nil
}
func validateSuitesConfig(config *Config) error {
if config.Suites == nil || len(config.Suites) == 0 {
logr.Info("[config.validateSuitesConfig] No suites configured")
return nil
}
suiteNames := make(map[string]bool)
for _, suite := range config.Suites {
// Check for duplicate suite names
if suiteNames[suite.Name] {
return fmt.Errorf("duplicate suite name: %s", suite.Key())
}
suiteNames[suite.Name] = true
// Validate the suite configuration
if err := suite.ValidateAndSetDefaults(); err != nil {
return fmt.Errorf("invalid suite '%s': %w", suite.Key(), err)
}
// Check that endpoints referenced in Store mappings use valid placeholders
for _, suiteEndpoint := range suite.Endpoints {
if suiteEndpoint.Store != nil {
for contextKey, placeholder := range suiteEndpoint.Store {
// Basic validation that the context key is a valid identifier
if len(contextKey) == 0 {
return fmt.Errorf("suite '%s' endpoint '%s' has empty context key in store mapping", suite.Key(), suiteEndpoint.Key())
}
if len(placeholder) == 0 {
return fmt.Errorf("suite '%s' endpoint '%s' has empty placeholder in store mapping for key '%s'", suite.Key(), suiteEndpoint.Key(), contextKey)
}
}
}
}
}
logr.Infof("[config.validateSuitesConfig] Validated %d suite(s)", len(config.Suites))
return nil
}
func validateUniqueKeys(config *Config) error {
keyMap := make(map[string]string) // key -> description for error messages
// Check all endpoints
for _, ep := range config.Endpoints {
epKey := ep.Key()
if existing, exists := keyMap[epKey]; exists {
return fmt.Errorf("duplicate key '%s': endpoint '%s' conflicts with %s", epKey, ep.Key(), existing)
}
keyMap[epKey] = fmt.Sprintf("endpoint '%s'", ep.Key())
}
// Check all external endpoints
for _, ee := range config.ExternalEndpoints {
eeKey := ee.Key()
if existing, exists := keyMap[eeKey]; exists {
return fmt.Errorf("duplicate key '%s': external endpoint '%s' conflicts with %s", eeKey, ee.Key(), existing)
}
keyMap[eeKey] = fmt.Sprintf("external endpoint '%s'", ee.Key())
}
// Check all suites
for _, suite := range config.Suites {
suiteKey := suite.Key()
if existing, exists := keyMap[suiteKey]; exists {
return fmt.Errorf("duplicate key '%s': suite '%s' conflicts with %s", suiteKey, suite.Key(), existing)
}
keyMap[suiteKey] = fmt.Sprintf("suite '%s'", suite.Key())
// Check endpoints within suites (they generate keys using suite group + endpoint name)
for _, ep := range suite.Endpoints {
epKey := key.ConvertGroupAndNameToKey(suite.Group, ep.Name)
if existing, exists := keyMap[epKey]; exists {
return fmt.Errorf("duplicate key '%s': endpoint '%s' in suite '%s' conflicts with %s", epKey, epKey, suite.Key(), existing)
}
keyMap[epKey] = fmt.Sprintf("endpoint '%s' in suite '%s'", epKey, suite.Key())
}
}
return nil
}
func validateSecurityConfig(config *Config) error {
if config.Security != nil {
if config.Security.IsValid() {
@@ -531,3 +624,17 @@ func validateAlertingConfig(alertingConfig *alerting.Config, endpoints []*endpoi
}
logr.Infof("[config.validateAlertingConfig] configuredProviders=%s; ignoredProviders=%s", validProviders, invalidProviders)
}
func validateAndSetConcurrencyDefaults(config *Config) {
if config.DisableMonitoringLock {
config.Concurrency = 0
logr.Warn("WARNING: The 'disable-monitoring-lock' configuration has been deprecated and will be removed in v6.0.0")
logr.Warn("WARNING: Please set 'concurrency: 0' instead")
logr.Debug("[config.validateAndSetConcurrencyDefaults] DisableMonitoringLock is true, setting unlimited (0) concurrency")
} else if config.Concurrency <= 0 && !config.DisableMonitoringLock {
config.Concurrency = DefaultConcurrency
logr.Debugf("[config.validateAndSetConcurrencyDefaults] Setting default concurrency to %d", config.Concurrency)
} else {
logr.Debugf("[config.validateAndSetConcurrencyDefaults] Using configured concurrency of %d", config.Concurrency)
}
}