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:
109
config/config.go
109
config/config.go
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user