mirror of
https://github.com/TwiN/gatus.git
synced 2026-02-04 15:14:43 +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:
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/TwiN/gatus/v5/config"
|
||||
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||
"github.com/TwiN/gatus/v5/config/suite"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
@@ -18,6 +19,11 @@ var (
|
||||
resultCertificateExpirationSeconds *prometheus.GaugeVec
|
||||
resultEndpointSuccess *prometheus.GaugeVec
|
||||
|
||||
// Suite metrics
|
||||
suiteResultTotal *prometheus.CounterVec
|
||||
suiteResultDurationSeconds *prometheus.GaugeVec
|
||||
suiteResultSuccess *prometheus.GaugeVec
|
||||
|
||||
// Track if metrics have been initialized to prevent duplicate registration
|
||||
metricsInitialized bool
|
||||
currentRegisterer prometheus.Registerer
|
||||
@@ -49,6 +55,17 @@ func UnregisterPrometheusMetrics() {
|
||||
currentRegisterer.Unregister(resultEndpointSuccess)
|
||||
}
|
||||
|
||||
// Unregister suite metrics
|
||||
if suiteResultTotal != nil {
|
||||
currentRegisterer.Unregister(suiteResultTotal)
|
||||
}
|
||||
if suiteResultDurationSeconds != nil {
|
||||
currentRegisterer.Unregister(suiteResultDurationSeconds)
|
||||
}
|
||||
if suiteResultSuccess != nil {
|
||||
currentRegisterer.Unregister(suiteResultSuccess)
|
||||
}
|
||||
|
||||
metricsInitialized = false
|
||||
currentRegisterer = nil
|
||||
}
|
||||
@@ -109,6 +126,28 @@ func InitializePrometheusMetrics(cfg *config.Config, reg prometheus.Registerer)
|
||||
}, append([]string{"key", "group", "name", "type"}, extraLabels...))
|
||||
reg.MustRegister(resultEndpointSuccess)
|
||||
|
||||
// Suite metrics
|
||||
suiteResultTotal = prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: namespace,
|
||||
Name: "suite_results_total",
|
||||
Help: "Total number of suite executions",
|
||||
}, append([]string{"key", "group", "name", "success"}, extraLabels...))
|
||||
reg.MustRegister(suiteResultTotal)
|
||||
|
||||
suiteResultDurationSeconds = prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: namespace,
|
||||
Name: "suite_results_duration_seconds",
|
||||
Help: "Duration of suite execution in seconds",
|
||||
}, append([]string{"key", "group", "name"}, extraLabels...))
|
||||
reg.MustRegister(suiteResultDurationSeconds)
|
||||
|
||||
suiteResultSuccess = prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: namespace,
|
||||
Name: "suite_results_success",
|
||||
Help: "Whether the suite execution was successful (1) or not (0)",
|
||||
}, append([]string{"key", "group", "name"}, extraLabels...))
|
||||
reg.MustRegister(suiteResultSuccess)
|
||||
|
||||
// Mark as initialized
|
||||
metricsInitialized = true
|
||||
}
|
||||
@@ -116,7 +155,7 @@ func InitializePrometheusMetrics(cfg *config.Config, reg prometheus.Registerer)
|
||||
// PublishMetricsForEndpoint publishes metrics for the given endpoint and its result.
|
||||
// These metrics will be exposed at /metrics if the metrics are enabled
|
||||
func PublishMetricsForEndpoint(ep *endpoint.Endpoint, result *endpoint.Result, extraLabels []string) {
|
||||
labelValues := []string{}
|
||||
var labelValues []string
|
||||
for _, label := range extraLabels {
|
||||
if value, ok := ep.ExtraLabels[label]; ok {
|
||||
labelValues = append(labelValues, value)
|
||||
@@ -124,7 +163,6 @@ func PublishMetricsForEndpoint(ep *endpoint.Endpoint, result *endpoint.Result, e
|
||||
labelValues = append(labelValues, "")
|
||||
}
|
||||
}
|
||||
|
||||
endpointType := ep.Type()
|
||||
resultTotal.WithLabelValues(append([]string{ep.Key(), ep.Group, ep.Name, string(endpointType), strconv.FormatBool(result.Success)}, labelValues...)...).Inc()
|
||||
resultDurationSeconds.WithLabelValues(append([]string{ep.Key(), ep.Group, ep.Name, string(endpointType)}, labelValues...)...).Set(result.Duration.Seconds())
|
||||
@@ -146,3 +184,35 @@ func PublishMetricsForEndpoint(ep *endpoint.Endpoint, result *endpoint.Result, e
|
||||
resultEndpointSuccess.WithLabelValues(append([]string{ep.Key(), ep.Group, ep.Name, string(endpointType)}, labelValues...)...).Set(0)
|
||||
}
|
||||
}
|
||||
|
||||
// PublishMetricsForSuite publishes metrics for the given suite and its result.
|
||||
// These metrics will be exposed at /metrics if the metrics are enabled
|
||||
func PublishMetricsForSuite(s *suite.Suite, result *suite.Result, extraLabels []string) {
|
||||
if !metricsInitialized {
|
||||
return
|
||||
}
|
||||
var labelValues []string
|
||||
// For now, suites don't have ExtraLabels, so we'll use empty values
|
||||
// This maintains consistency with endpoint metrics structure
|
||||
for range extraLabels {
|
||||
labelValues = append(labelValues, "")
|
||||
}
|
||||
// Publish suite execution counter
|
||||
suiteResultTotal.WithLabelValues(
|
||||
append([]string{s.Key(), s.Group, s.Name, strconv.FormatBool(result.Success)}, labelValues...)...,
|
||||
).Inc()
|
||||
// Publish suite duration
|
||||
suiteResultDurationSeconds.WithLabelValues(
|
||||
append([]string{s.Key(), s.Group, s.Name}, labelValues...)...,
|
||||
).Set(result.Duration.Seconds())
|
||||
// Publish suite success status
|
||||
if result.Success {
|
||||
suiteResultSuccess.WithLabelValues(
|
||||
append([]string{s.Key(), s.Group, s.Name}, labelValues...)...,
|
||||
).Set(1)
|
||||
} else {
|
||||
suiteResultSuccess.WithLabelValues(
|
||||
append([]string{s.Key(), s.Group, s.Name}, labelValues...)...,
|
||||
).Set(0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/TwiN/gatus/v5/config"
|
||||
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||
"github.com/TwiN/gatus/v5/config/endpoint/dns"
|
||||
"github.com/TwiN/gatus/v5/config/suite"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/testutil"
|
||||
)
|
||||
@@ -226,3 +227,93 @@ gatus_results_endpoint_success{group="http-ep-group",key="http-ep-group_http-ep-
|
||||
t.Errorf("Expected no errors but got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublishMetricsForSuite(t *testing.T) {
|
||||
reg := prometheus.NewRegistry()
|
||||
InitializePrometheusMetrics(&config.Config{}, reg)
|
||||
|
||||
testSuite := &suite.Suite{
|
||||
Name: "test-suite",
|
||||
Group: "test-group",
|
||||
}
|
||||
// Test successful suite execution
|
||||
successResult := &suite.Result{
|
||||
Success: true,
|
||||
Duration: 5 * time.Second,
|
||||
Name: "test-suite",
|
||||
Group: "test-group",
|
||||
}
|
||||
PublishMetricsForSuite(testSuite, successResult, []string{})
|
||||
|
||||
err := testutil.GatherAndCompare(reg, bytes.NewBufferString(`
|
||||
# HELP gatus_suite_results_duration_seconds Duration of suite execution in seconds
|
||||
# TYPE gatus_suite_results_duration_seconds gauge
|
||||
gatus_suite_results_duration_seconds{group="test-group",key="test-group_test-suite",name="test-suite"} 5
|
||||
# HELP gatus_suite_results_success Whether the suite execution was successful (1) or not (0)
|
||||
# TYPE gatus_suite_results_success gauge
|
||||
gatus_suite_results_success{group="test-group",key="test-group_test-suite",name="test-suite"} 1
|
||||
# HELP gatus_suite_results_total Total number of suite executions
|
||||
# TYPE gatus_suite_results_total counter
|
||||
gatus_suite_results_total{group="test-group",key="test-group_test-suite",name="test-suite",success="true"} 1
|
||||
`), "gatus_suite_results_duration_seconds", "gatus_suite_results_success", "gatus_suite_results_total")
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors but got: %v", err)
|
||||
}
|
||||
|
||||
// Test failed suite execution
|
||||
failureResult := &suite.Result{
|
||||
Success: false,
|
||||
Duration: 10 * time.Second,
|
||||
Name: "test-suite",
|
||||
Group: "test-group",
|
||||
}
|
||||
PublishMetricsForSuite(testSuite, failureResult, []string{})
|
||||
|
||||
err = testutil.GatherAndCompare(reg, bytes.NewBufferString(`
|
||||
# HELP gatus_suite_results_duration_seconds Duration of suite execution in seconds
|
||||
# TYPE gatus_suite_results_duration_seconds gauge
|
||||
gatus_suite_results_duration_seconds{group="test-group",key="test-group_test-suite",name="test-suite"} 10
|
||||
# HELP gatus_suite_results_success Whether the suite execution was successful (1) or not (0)
|
||||
# TYPE gatus_suite_results_success gauge
|
||||
gatus_suite_results_success{group="test-group",key="test-group_test-suite",name="test-suite"} 0
|
||||
# HELP gatus_suite_results_total Total number of suite executions
|
||||
# TYPE gatus_suite_results_total counter
|
||||
gatus_suite_results_total{group="test-group",key="test-group_test-suite",name="test-suite",success="false"} 1
|
||||
gatus_suite_results_total{group="test-group",key="test-group_test-suite",name="test-suite",success="true"} 1
|
||||
`), "gatus_suite_results_duration_seconds", "gatus_suite_results_success", "gatus_suite_results_total")
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors but got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublishMetricsForSuite_NoGroup(t *testing.T) {
|
||||
reg := prometheus.NewRegistry()
|
||||
InitializePrometheusMetrics(&config.Config{}, reg)
|
||||
|
||||
testSuite := &suite.Suite{
|
||||
Name: "no-group-suite",
|
||||
Group: "",
|
||||
}
|
||||
result := &suite.Result{
|
||||
Success: true,
|
||||
Duration: 3 * time.Second,
|
||||
Name: "no-group-suite",
|
||||
Group: "",
|
||||
}
|
||||
PublishMetricsForSuite(testSuite, result, []string{})
|
||||
|
||||
err := testutil.GatherAndCompare(reg, bytes.NewBufferString(`
|
||||
# HELP gatus_suite_results_duration_seconds Duration of suite execution in seconds
|
||||
# TYPE gatus_suite_results_duration_seconds gauge
|
||||
gatus_suite_results_duration_seconds{group="",key="_no-group-suite",name="no-group-suite"} 3
|
||||
# HELP gatus_suite_results_success Whether the suite execution was successful (1) or not (0)
|
||||
# TYPE gatus_suite_results_success gauge
|
||||
gatus_suite_results_success{group="",key="_no-group-suite",name="no-group-suite"} 1
|
||||
# HELP gatus_suite_results_total Total number of suite executions
|
||||
# TYPE gatus_suite_results_total counter
|
||||
gatus_suite_results_total{group="",key="_no-group-suite",name="no-group-suite",success="true"} 1
|
||||
`), "gatus_suite_results_duration_seconds", "gatus_suite_results_success", "gatus_suite_results_total")
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors but got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user