Compare commits

...
6 Commits
12 changed files with 269 additions and 80 deletions

No files matched your search

+54 -53
View File
@@ -50,6 +50,7 @@ Have any feedback or questions? [Create a discussion](https://github.com/TwiN/ga
- [Conditions](#conditions)
- [Placeholders](#placeholders)
- [Functions](#functions)
- [Announcements](#announcements)
- [Storage](#storage)
- [Client configuration](#client-configuration)
- [Tunneling](#tunneling)
@@ -95,7 +96,6 @@ Have any feedback or questions? [Create a discussion](https://github.com/TwiN/ga
- [Configuring Zulip alerts](#configuring-zulip-alerts)
- [Configuring custom alerts](#configuring-custom-alerts)
- [Setting a default alert](#setting-a-default-alert)
- [Announcements](#announcements)
- [Maintenance](#maintenance)
- [Security](#security)
- [Basic Authentication](#basic-authentication)
@@ -309,6 +309,7 @@ You can then configure alerts to be triggered when an endpoint is unhealthy once
| `endpoints[].ui.hide-hostname` | Whether to hide the hostname from the results. | `false` |
| `endpoints[].ui.hide-port` | Whether to hide the port from the results. | `false` |
| `endpoints[].ui.hide-url` | Whether to hide the URL from the results. Useful if the URL contains a token. | `false` |
| `endpoints[].ui.hide-errors` | Whether to hide errors from the results. | `false` |
| `endpoints[].ui.dont-resolve-failed-conditions` | Whether to resolve failed conditions for the UI. | `false` |
| `endpoints[].ui.badge.response-time` | List of response time thresholds. Each time a threshold is reached, the badge has a different color. | `[50, 200, 300, 500, 750]` |
| `endpoints[].extra-labels` | Extra labels to add to the metrics. Useful for grouping endpoints together. | `{}` |
@@ -366,7 +367,7 @@ or send an HTTP request:
POST /api/v1/endpoints/{key}/external?success={success}&error={error}&duration={duration}
```
Where:
- `{key}` has the pattern `<GROUP_NAME>_<ENDPOINT_NAME>` in which both variables have ` `, `/`, `_`, `,`, `.`, `#`, `(`, `)`, `+` and `&` replaced by `-`.
- `{key}` has the pattern `<GROUP_NAME>_<ENDPOINT_NAME>` in which both variables have ` `, `/`, `_`, `,`, `.`, `#`, `+` and `&` replaced by `-`.
- Using the example configuration above, the key would be `core_ext-ep-test`.
- `{success}` is a boolean (`true` or `false`) value indicating whether the health check was successful or not.
- `{error}` (optional): a string describing the reason for a failed health check. If {success} is false, this should contain the error message; if the check is successful.
@@ -1300,7 +1301,7 @@ endpoints:
```
#### Configuring ilert alerts
#### Configuring Ilert alerts
| Parameter | Description | Default |
|:-----------------------------------|:-------------------------------------------------------------------------------------------|:--------|
| `alerting.ilert` | Configuration for alerts of type `ilert` | `{}` |
@@ -1852,6 +1853,40 @@ endpoints:
```
#### Configuring SendGrid alerts
> ⚠️ **WARNING**: This alerting provider has not been tested yet. If you've tested it and confirmed that it works, please remove this warning and create a pull request, or comment on [#1223](https://github.com/TwiN/gatus/discussions/1223) with whether the provider works as intended. Thank you for your cooperation.
| Parameter | Description | Default |
|:--------------------------------------|:-------------------------------------------------------------------------------------------|:--------------|
| `alerting.sendgrid` | Configuration for alerts of type `sendgrid` | `{}` |
| `alerting.sendgrid.api-key` | SendGrid API key | Required `""` |
| `alerting.sendgrid.from` | Email address to send from | Required `""` |
| `alerting.sendgrid.to` | Email address(es) to send alerts to (comma-separated for multiple recipients) | Required `""` |
| `alerting.sendgrid.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert) | N/A |
| `alerting.sendgrid.overrides` | List of overrides that may be prioritized over the default configuration | `[]` |
| `alerting.sendgrid.overrides[].group` | Endpoint group for which the configuration will be overridden by this configuration | `""` |
| `alerting.sendgrid.overrides[].*` | See `alerting.sendgrid.*` parameters | `{}` |
```yaml
alerting:
sendgrid:
api-key: "SG.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
from: "alerts@example.com"
to: "admin@example.com,ops@example.com"
endpoints:
- name: website
url: "https://twin.sh/health"
interval: 5m
conditions:
- "[STATUS] == 200"
alerts:
- type: sendgrid
send-on-resolved: true
```
#### Configuring Signal alerts
> ⚠️ **WARNING**: This alerting provider has not been tested yet. If you've tested it and confirmed that it works, please remove this warning and create a pull request, or comment on [#1223](https://github.com/TwiN/gatus/discussions/1223) with whether the provider works as intended. Thank you for your cooperation.
@@ -1918,40 +1953,6 @@ endpoints:
```
#### Configuring SendGrid alerts
> ⚠️ **WARNING**: This alerting provider has not been tested yet. If you've tested it and confirmed that it works, please remove this warning and create a pull request, or comment on [#1223](https://github.com/TwiN/gatus/discussions/1223) with whether the provider works as intended. Thank you for your cooperation.
| Parameter | Description | Default |
|:--------------------------------------|:-------------------------------------------------------------------------------------------|:--------------|
| `alerting.sendgrid` | Configuration for alerts of type `sendgrid` | `{}` |
| `alerting.sendgrid.api-key` | SendGrid API key | Required `""` |
| `alerting.sendgrid.from` | Email address to send from | Required `""` |
| `alerting.sendgrid.to` | Email address(es) to send alerts to (comma-separated for multiple recipients) | Required `""` |
| `alerting.sendgrid.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert) | N/A |
| `alerting.sendgrid.overrides` | List of overrides that may be prioritized over the default configuration | `[]` |
| `alerting.sendgrid.overrides[].group` | Endpoint group for which the configuration will be overridden by this configuration | `""` |
| `alerting.sendgrid.overrides[].*` | See `alerting.sendgrid.*` parameters | `{}` |
```yaml
alerting:
sendgrid:
api-key: "SG.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
from: "alerts@example.com"
to: "admin@example.com,ops@example.com"
endpoints:
- name: website
url: "https://twin.sh/health"
interval: 5m
conditions:
- "[STATUS] == 200"
alerts:
- type: sendgrid
send-on-resolved: true
```
#### Configuring Slack alerts
| Parameter | Description | Default |
|:-----------------------------------|:-------------------------------------------------------------------------------------------|:------------------------------------|
@@ -2298,14 +2299,14 @@ endpoints:
> ⚠️ **WARNING**: This alerting provider has not been tested yet. If you've tested it and confirmed that it works, please remove this warning and create a pull request, or comment on [#1223](https://github.com/TwiN/gatus/discussions/1223) with whether the provider works as intended. Thank you for your cooperation.
| Parameter | Description | Default |
|:-------------------------------|:-------------------------------------------------------------------------------------------|:--------------|
| `alerting.webex` | Configuration for alerts of type `webex` | `{}` |
| `alerting.webex.webhook-url` | Webex Teams webhook URL | Required `""` |
| `alerting.webex.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert) | N/A |
| `alerting.webex.overrides` | List of overrides that may be prioritized over the default configuration | `[]` |
| `alerting.webex.overrides[].group` | Endpoint group for which the configuration will be overridden by this configuration | `""` |
| `alerting.webex.overrides[].*` | See `alerting.webex.*` parameters | `{}` |
| Parameter | Description | Default |
|:-----------------------------------|:-------------------------------------------------------------------------------------------|:--------------|
| `alerting.webex` | Configuration for alerts of type `webex` | `{}` |
| `alerting.webex.webhook-url` | Webex Teams webhook URL | Required `""` |
| `alerting.webex.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert) | N/A |
| `alerting.webex.overrides` | List of overrides that may be prioritized over the default configuration | `[]` |
| `alerting.webex.overrides[].group` | Endpoint group for which the configuration will be overridden by this configuration | `""` |
| `alerting.webex.overrides[].*` | See `alerting.webex.*` parameters | `{}` |
```yaml
alerting:
@@ -2357,7 +2358,7 @@ endpoints:
#### Configuring Zulip alerts
| Parameter | Description | Default |
|:-----------------------------------|:------------------------------------------------------------------------------------|:--------------|
| `alerting.zulip` | Configuration for alerts of type `zulip` | `{}` |
| `alerting.zulip` | Configuration for alerts of type `zulip` | `{}` |
| `alerting.zulip.bot-email` | Bot Email | Required `""` |
| `alerting.zulip.bot-api-key` | Bot API key | Required `""` |
| `alerting.zulip.domain` | Full organization domain (e.g.: yourZulipDomain.zulipchat.com) | Required `""` |
@@ -3297,7 +3298,7 @@ The path to generate a badge is the following:
```
Where:
- `{duration}` is `30d`, `7d`, `24h` or `1h`
- `{key}` has the pattern `<GROUP_NAME>_<ENDPOINT_NAME>` in which both variables have ` `, `/`, `_`, `,`, `.`, `#`, `(`, `)`, `+` and `&` replaced by `-`.
- `{key}` has the pattern `<GROUP_NAME>_<ENDPOINT_NAME>` in which both variables have ` `, `/`, `_`, `,`, `.`, `#`, `+` and `&` replaced by `-`.
For instance, if you want the uptime during the last 24 hours from the endpoint `frontend` in the group `core`,
the URL would look like this:
@@ -3323,7 +3324,7 @@ The path to generate a badge is the following:
/api/v1/endpoints/{key}/health/badge.svg
```
Where:
- `{key}` has the pattern `<GROUP_NAME>_<ENDPOINT_NAME>` in which both variables have ` `, `/`, `_`, `,`, `.`, `#`, `(`, `)`, `+` and `&` replaced by `-`.
- `{key}` has the pattern `<GROUP_NAME>_<ENDPOINT_NAME>` in which both variables have ` `, `/`, `_`, `,`, `.`, `#`, `+` and `&` replaced by `-`.
For instance, if you want the current status of the endpoint `frontend` in the group `core`,
the URL would look like this:
@@ -3340,7 +3341,7 @@ The path to generate a badge is the following:
/api/v1/endpoints/{key}/health/badge.shields
```
Where:
- `{key}` has the pattern `<GROUP_NAME>_<ENDPOINT_NAME>` in which both variables have ` `, `/`, `_`, `,`, `.`, `#`, `(`, `)`, `+` and `&` replaced by `-`.
- `{key}` has the pattern `<GROUP_NAME>_<ENDPOINT_NAME>` in which both variables have ` `, `/`, `_`, `,`, `.`, `#`, `+` and `&` replaced by `-`.
For instance, if you want the current status of the endpoint `frontend` in the group `core`,
the URL would look like this:
@@ -3363,7 +3364,7 @@ The endpoint to generate a badge is the following:
```
Where:
- `{duration}` is `30d`, `7d`, `24h` or `1h`
- `{key}` has the pattern `<GROUP_NAME>_<ENDPOINT_NAME>` in which both variables have ` `, `/`, `_`, `,`, `.`, `#`, `(`, `)`, `+` and `&` replaced by `-`.
- `{key}` has the pattern `<GROUP_NAME>_<ENDPOINT_NAME>` in which both variables have ` `, `/`, `_`, `,`, `.`, `#`, `+` and `&` replaced by `-`.
#### Response time (chart)
![Response time 24h](https://status.twin.sh/api/v1/endpoints/core_blog-external/response-times/24h/chart.svg)
@@ -3376,7 +3377,7 @@ The endpoint to generate a response time chart is the following:
```
Where:
- `{duration}` is `30d`, `7d`, or `24h`
- `{key}` has the pattern `<GROUP_NAME>_<ENDPOINT_NAME>` in which both variables have ` `, `/`, `_`, `,`, `.`, `#`, `(`, `)`, `+` and `&` replaced by `-`.
- `{key}` has the pattern `<GROUP_NAME>_<ENDPOINT_NAME>` in which both variables have ` `, `/`, `_`, `,`, `.`, `#`, `+` and `&` replaced by `-`.
##### How to change the color thresholds of the response time badge
To change the response time badges' threshold, a corresponding configuration can be added to an endpoint.
@@ -3434,7 +3435,7 @@ The path to get raw uptime data for an endpoint is:
```
Where:
- `{duration}` is `30d`, `7d`, `24h` or `1h`
- `{key}` has the pattern `<GROUP_NAME>_<ENDPOINT_NAME>` in which both variables have ` `, `/`, `_`, `,`, `.`, `#`, `(`, `)`, `+` and `&` replaced by `-`.
- `{key}` has the pattern `<GROUP_NAME>_<ENDPOINT_NAME>` in which both variables have ` `, `/`, `_`, `,`, `.`, `#`, `+` and `&` replaced by `-`.
For instance, if you want the raw uptime data for the last 24 hours from the endpoint `frontend` in the group `core`, the URL would look like this:
```
@@ -3448,7 +3449,7 @@ The path to get raw response time data for an endpoint is:
```
Where:
- `{duration}` is `30d`, `7d`, `24h` or `1h`
- `{key}` has the pattern `<GROUP_NAME>_<ENDPOINT_NAME>` in which both variables have ` `, `/`, `_`, `,`, `.`, `#`, `(`, `)`, `+` and `&` replaced by `-`.
- `{key}` has the pattern `<GROUP_NAME>_<ENDPOINT_NAME>` in which both variables have ` `, `/`, `_`, `,`, `.`, `#`, `+` and `&` replaced by `-`.
For instance, if you want the raw response time data for the last 24 hours from the endpoint `frontend` in the group `core`, the URL would look like this:
```
+9 -16
View File
@@ -131,8 +131,8 @@ func TestPing(t *testing.T) {
func TestCanPerformStartTLS(t *testing.T) {
type args struct {
address string
insecure bool
address string
insecure bool
dnsresolver string
}
tests := []struct {
@@ -168,7 +168,7 @@ func TestCanPerformStartTLS(t *testing.T) {
{
name: "dns resolver",
args: args{
address: "smtp.gmail.com:587",
address: "smtp.gmail.com:587",
dnsresolver: "tcp://1.1.1.1:53",
},
wantConnected: true,
@@ -340,7 +340,7 @@ func TestQueryWebSocket(t *testing.T) {
}
func TestTlsRenegotiation(t *testing.T) {
tests := []struct {
scenarios := []struct {
name string
cfg TLSConfig
expectedConfig tls.RenegotiationSupport
@@ -371,12 +371,12 @@ func TestTlsRenegotiation(t *testing.T) {
expectedConfig: tls.RenegotiateNever,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
for _, scenario := range scenarios {
t.Run(scenario.name, func(t *testing.T) {
tls := &tls.Config{}
tlsConfig := configureTLS(tls, test.cfg)
if tlsConfig.Renegotiation != test.expectedConfig {
t.Errorf("expected tls renegotiation to be %v, but got %v", test.expectedConfig, tls.Renegotiation)
tlsConfig := configureTLS(tls, scenario.cfg)
if tlsConfig.Renegotiation != scenario.expectedConfig {
t.Errorf("expected tls renegotiation to be %v, but got %v", scenario.expectedConfig, tls.Renegotiation)
}
})
}
@@ -513,14 +513,11 @@ func TestQueryDNS(t *testing.T) {
func TestCheckSSHBanner(t *testing.T) {
cfg := &Config{Timeout: 3}
t.Run("no-auth-ssh", func(t *testing.T) {
connected, status, err := CheckSSHBanner("tty.sdf.org", cfg)
if err != nil {
t.Errorf("Expected: error != nil, got: %v ", err)
}
if connected == false {
t.Errorf("Expected: connected == true, got: %v", connected)
}
@@ -528,14 +525,11 @@ func TestCheckSSHBanner(t *testing.T) {
t.Errorf("Expected: 0, got: %v", status)
}
})
t.Run("invalid-address", func(t *testing.T) {
connected, status, err := CheckSSHBanner("idontplaytheodds.com", cfg)
if err == nil {
t.Errorf("Expected: error, got: %v ", err)
}
if connected != false {
t.Errorf("Expected: connected == false, got: %v", connected)
}
@@ -543,5 +537,4 @@ func TestCheckSSHBanner(t *testing.T) {
t.Errorf("Expected: 1, got: %v", status)
}
})
}
+4 -1
View File
@@ -353,6 +353,9 @@ func (e *Endpoint) EvaluateHealthWithContext(context *gontext.Gontext) *Result {
}
result.port = ""
}
if processedEndpoint.UIConfig.HideErrors {
result.Errors = nil
}
if processedEndpoint.UIConfig.HideConditions {
result.ConditionResults = nil
}
@@ -498,7 +501,7 @@ func (e *Endpoint) call(result *Result) {
result.Duration = time.Since(startTime)
} else if endpointType == TypeSSH {
// If there's no username/password specified, attempt to validate just the SSH banner
if len(e.SSHConfig.Username) == 0 && len(e.SSHConfig.Password) == 0 {
if e.SSHConfig == nil || (len(e.SSHConfig.Username) == 0 && len(e.SSHConfig.Password) == 0) {
result.Connected, result.HTTPStatus, err = client.CheckSSHBanner(strings.TrimPrefix(e.URL, "ssh://"), e.ClientConfig)
if err != nil {
result.AddError(err.Error())
+165
View File
@@ -1448,3 +1448,168 @@ func TestEndpoint_preprocessWithContext(t *testing.T) {
})
}
}
func TestEndpoint_HideUIFeatures(t *testing.T) {
defer client.InjectHTTPClient(nil)
tests := []struct {
name string
endpoint Endpoint
mockResponse test.MockRoundTripper
checkHostname bool
expectHostname string
checkErrors bool
expectErrors bool
checkConditions bool
expectConditions bool
checkErrorContent string
}{
{
name: "hide-conditions",
endpoint: Endpoint{
Name: "test-endpoint",
URL: "https://example.com/health",
Conditions: []Condition{"[STATUS] == 200", "[BODY].status == UP"},
UIConfig: &ui.Config{HideConditions: true},
},
mockResponse: test.MockRoundTripper(func(r *http.Request) *http.Response {
return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewBufferString(`{"status": "UP"}`))}
}),
checkConditions: true,
expectConditions: false,
},
{
name: "hide-hostname",
endpoint: Endpoint{
Name: "test-endpoint",
URL: "https://example.com/health",
Conditions: []Condition{"[STATUS] == 200"},
UIConfig: &ui.Config{HideHostname: true},
},
mockResponse: test.MockRoundTripper(func(r *http.Request) *http.Response {
return &http.Response{StatusCode: http.StatusOK, Body: http.NoBody}
}),
checkHostname: true,
expectHostname: "",
},
{
name: "hide-url-in-errors",
endpoint: Endpoint{
Name: "test-endpoint",
URL: "https://example.com/health",
Conditions: []Condition{"[CONNECTED] == true"},
UIConfig: &ui.Config{HideURL: true},
ClientConfig: &client.Config{Timeout: time.Millisecond},
},
mockResponse: nil,
checkErrors: true,
expectErrors: true,
checkErrorContent: "<redacted>",
},
{
name: "hide-port-in-errors",
endpoint: Endpoint{
Name: "test-endpoint",
URL: "https://example.com:9999/health",
Conditions: []Condition{"[CONNECTED] == true"},
UIConfig: &ui.Config{HidePort: true},
ClientConfig: &client.Config{Timeout: time.Millisecond},
},
mockResponse: nil,
checkErrors: true,
expectErrors: true,
checkErrorContent: "<redacted>",
},
{
name: "hide-errors",
endpoint: Endpoint{
Name: "test-endpoint",
URL: "https://example.com/health",
Conditions: []Condition{"[CONNECTED] == true"},
UIConfig: &ui.Config{HideErrors: true},
ClientConfig: &client.Config{Timeout: time.Millisecond},
},
mockResponse: nil,
checkErrors: true,
expectErrors: false,
},
{
name: "dont-resolve-failed-conditions",
endpoint: Endpoint{
Name: "test-endpoint",
URL: "https://example.com/health",
Conditions: []Condition{"[STATUS] == 200"},
UIConfig: &ui.Config{DontResolveFailedConditions: true},
},
mockResponse: test.MockRoundTripper(func(r *http.Request) *http.Response {
return &http.Response{StatusCode: http.StatusBadGateway, Body: http.NoBody}
}),
checkConditions: true,
expectConditions: true,
},
{
name: "multiple-hide-features",
endpoint: Endpoint{
Name: "test-endpoint",
URL: "https://example.com/health",
Conditions: []Condition{"[STATUS] == 200"},
UIConfig: &ui.Config{HideConditions: true, HideHostname: true, HideErrors: true},
},
mockResponse: test.MockRoundTripper(func(r *http.Request) *http.Response {
return &http.Response{StatusCode: http.StatusOK, Body: http.NoBody}
}),
checkConditions: true,
expectConditions: false,
checkHostname: true,
expectHostname: "",
checkErrors: true,
expectErrors: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.mockResponse != nil {
mockClient := &http.Client{Transport: tt.mockResponse}
if tt.endpoint.ClientConfig != nil && tt.endpoint.ClientConfig.Timeout > 0 {
mockClient.Timeout = tt.endpoint.ClientConfig.Timeout
}
client.InjectHTTPClient(mockClient)
} else {
client.InjectHTTPClient(nil)
}
err := tt.endpoint.ValidateAndSetDefaults()
if err != nil {
t.Fatalf("ValidateAndSetDefaults failed: %v", err)
}
result := tt.endpoint.EvaluateHealth()
if tt.checkHostname {
if result.Hostname != tt.expectHostname {
t.Errorf("Expected hostname '%s', got '%s'", tt.expectHostname, result.Hostname)
}
}
if tt.checkErrors {
hasErrors := len(result.Errors) > 0
if hasErrors != tt.expectErrors {
t.Errorf("Expected errors=%v, got errors=%v (actual errors: %v)", tt.expectErrors, hasErrors, result.Errors)
}
if tt.checkErrorContent != "" && len(result.Errors) > 0 {
found := false
for _, err := range result.Errors {
if strings.Contains(err, tt.checkErrorContent) {
found = true
break
}
}
if !found {
t.Errorf("Expected error to contain '%s', but got: %v", tt.checkErrorContent, result.Errors)
}
}
}
if tt.checkConditions {
hasConditions := result.ConditionResults != nil && len(result.ConditionResults) > 0
if hasConditions != tt.expectConditions {
t.Errorf("Expected conditions=%v, got conditions=%v (actual: %v)", tt.expectConditions, hasConditions, result.ConditionResults)
}
}
})
}
}
+4
View File
@@ -16,6 +16,9 @@ type Config struct {
// HidePort whether to hide the port in the Result
HidePort bool `yaml:"hide-port"`
// HideErrors whether to hide the errors in the Result
HideErrors bool `yaml:"hide-errors"`
// DontResolveFailedConditions whether to resolve failed conditions in the Result for display in the UI
DontResolveFailedConditions bool `yaml:"dont-resolve-failed-conditions"`
@@ -58,6 +61,7 @@ func GetDefaultConfig() *Config {
HideHostname: false,
HideURL: false,
HidePort: false,
HideErrors: false,
DontResolveFailedConditions: false,
HideConditions: false,
Badge: &Badge{
-2
View File
@@ -15,8 +15,6 @@ func sanitize(s string) string {
s = strings.ReplaceAll(s, ",", "-")
s = strings.ReplaceAll(s, " ", "-")
s = strings.ReplaceAll(s, "#", "-")
s = strings.ReplaceAll(s, "(", "-")
s = strings.ReplaceAll(s, ")", "-")
s = strings.ReplaceAll(s, "+", "-")
s = strings.ReplaceAll(s, "&", "-")
return s
+2 -2
View File
@@ -32,12 +32,12 @@ func TestConvertGroupAndNameToKey(t *testing.T) {
{
GroupName: "API (v1)",
Name: "endpoint",
ExpectedOutput: "api--v1-_endpoint",
ExpectedOutput: "api-(v1)_endpoint",
},
{
GroupName: "website (admin)",
Name: "test",
ExpectedOutput: "website--admin-_test",
ExpectedOutput: "website-(admin)_test",
},
{
GroupName: "search",
+27
View File
@@ -183,6 +183,33 @@ func initializeStorage(cfg *config.Config) {
}
}
}
// Load persisted triggered alerts for suite endpoints
for _, suite := range cfg.Suites {
for _, ep := range suite.Endpoints {
var checksums []string
for _, alert := range ep.Alerts {
if alert.IsEnabled() {
checksums = append(checksums, alert.Checksum())
}
}
numberOfTriggeredAlertsDeleted := store.Get().DeleteAllTriggeredAlertsNotInChecksumsByEndpoint(ep, checksums)
if numberOfTriggeredAlertsDeleted > 0 {
logr.Debugf("[main.initializeStorage] Deleted %d triggered alerts for suite endpoint with key=%s because their configurations have been changed or deleted", numberOfTriggeredAlertsDeleted, ep.Key())
}
for _, alert := range ep.Alerts {
exists, resolveKey, numberOfSuccessesInARow, err := store.Get().GetTriggeredEndpointAlert(ep, alert)
if err != nil {
logr.Errorf("[main.initializeStorage] Failed to get triggered alert for suite endpoint with key=%s: %s", ep.Key(), err.Error())
continue
}
if exists {
alert.Triggered, alert.ResolveKey = true, resolveKey
ep.NumberOfSuccessesInARow, ep.NumberOfFailuresInARow = numberOfSuccessesInARow, alert.FailureThreshold
numberOfPersistedTriggeredAlertsLoaded++
}
}
}
}
if numberOfPersistedTriggeredAlertsLoaded > 0 {
logr.Infof("[main.initializeStorage] Loaded %d persisted triggered alerts", numberOfPersistedTriggeredAlertsLoaded)
}
-1
View File
@@ -64,7 +64,6 @@ func executeEndpoint(ep *endpoint.Endpoint, cfg *config.Config, extraLabels []st
}
}
if !cfg.Maintenance.IsUnderMaintenance() && !inEndpointMaintenanceWindow {
// TODO: Consider moving this after the monitoring lock is unlocked? I mean, how much noise can a single alerting provider cause...
HandleAlerting(ep, result, cfg.Alerting)
} else {
logr.Debug("[watchdog.executeEndpoint] Not handling alerting because currently in the maintenance window")
+2 -2
View File
@@ -50,6 +50,8 @@ func executeSuite(s *suite.Suite, cfg *config.Config, extraLabels []string) {
if cfg.Metrics {
metrics.PublishMetricsForSuite(s, result, extraLabels)
}
// Store result
UpdateSuiteStatus(s, result)
// Handle alerting for suite endpoints
for i, ep := range s.Endpoints {
if i < len(result.EndpointResults) {
@@ -72,8 +74,6 @@ func executeSuite(s *suite.Suite, cfg *config.Config, extraLabels []string) {
}
}
logr.Infof("[watchdog.executeSuite] Completed suite=%s; success=%v; errors=%d; duration=%v; endpoints_executed=%d/%d", s.Name, result.Success, len(result.Errors), result.Duration, len(result.EndpointResults), len(s.Endpoints))
// Store result in database
UpdateSuiteStatus(s, result)
}
// UpdateSuiteStatus persists the suite result in the database
+1 -2
View File
@@ -197,13 +197,12 @@ const buttons = computed(() => {
const fetchConfig = async () => {
try {
const response = await fetch(`${SERVER_URL}/api/v1/config`, { credentials: 'include' })
retrievedConfig.value = true
if (response.status === 200) {
const data = await response.json()
config.value = data
announcements.value = data.announcements || []
}
retrievedConfig.value = true
} catch (error) {
console.error('Failed to fetch config:', error)
retrievedConfig.value = true
+1 -1
View File
@@ -1 +1 @@
(function(){"use strict";var e={438:function(e,t,s){s.d(t,{L:function(){return hr}});var l=s(963),a=s(252),n=s(577),r=s(262),o=s.p+"img/logo.svg",u=s(201),i=s(507),d=s(970),c=s(135),g=s(3),p=s(512),m=s(388);function v(...e){return(0,m.m6)((0,p.W)(e))}const f=["disabled"];var w={__name:"Button",props:{variant:{type:String,default:"default"},size:{type:String,default:"default"},disabled:{type:Boolean,default:!1}},setup(e){const t=(0,g.j)("inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",{variants:{variant:{default:"bg-primary text-primary-foreground hover:bg-primary/90",destructive:"bg-destructive text-destructive-foreground hover:bg-destructive/90",outline:"border border-input bg-background hover:bg-accent hover:text-accent-foreground",secondary:"bg-secondary text-secondary-foreground hover:bg-secondary/80",ghost:"hover:bg-accent hover:text-accent-foreground",link:"text-primary underline-offset-4 hover:underline"},size:{default:"h-10 px-4 py-2",sm:"h-9 rounded-md px-3",lg:"h-11 rounded-md px-8",icon:"h-10 w-10"}},defaultVariants:{variant:"default",size:"default"}});return(s,l)=>((0,a.wg)(),(0,a.iD)("button",{class:(0,n.C_)((0,r.SU)(v)((0,r.SU)(t)({variant:e.variant,size:e.size}),s.$attrs.class??"")),disabled:e.disabled},[(0,a.WI)(s.$slots,"default")],10,f))}};const h=w;var x=h,b={__name:"Card",setup(e){return(e,t)=>((0,a.wg)(),(0,a.iD)("div",{class:(0,n.C_)((0,r.SU)(v)("rounded-lg border bg-card text-card-foreground shadow-sm",e.$attrs.class??""))},[(0,a.WI)(e.$slots,"default")],2))}};const y=b;var k=y,_={__name:"CardHeader",setup(e){return(e,t)=>((0,a.wg)(),(0,a.iD)("div",{class:(0,n.C_)((0,r.SU)(v)("flex flex-col space-y-1.5 p-6",e.$attrs.class??""))},[(0,a.WI)(e.$slots,"default")],2))}};const S=_;var D=S,U={__name:"CardTitle",setup(e){return(e,t)=>((0,a.wg)(),(0,a.iD)("h3",{class:(0,n.C_)((0,r.SU)(v)("text-2xl font-semibold leading-none tracking-tight",e.$attrs.class??""))},[(0,a.WI)(e.$slots,"default")],2))}};const z=U;var W=z,C={__name:"CardContent",setup(e){return(e,t)=>((0,a.wg)(),(0,a.iD)("div",{class:(0,n.C_)((0,r.SU)(v)("p-6 pt-0",e.$attrs.class??""))},[(0,a.WI)(e.$slots,"default")],2))}};const j=C;var H=j;const R={id:"social"};function F(e,t){return(0,a.wg)(),(0,a.iD)("div",R,t[0]||(t[0]=[(0,a._)("a",{href:"https://github.com/TwiN/gatus",target:"_blank",title:"Gatus on GitHub"},[(0,a._)("svg",{xmlns:"http://www.w3.org/2000/svg",width:"32",height:"32",viewBox:"0 0 16 16",class:"hover:scale-110"},[(0,a._)("path",{fill:"gray",d:"M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"})])],-1)]))}var E=s(744);const q={},$=(0,E.Z)(q,[["render",F],["__scopeId","data-v-788af9ce"]]);var T=$;const Z=e=>{let t=(new Date).getTime()-new Date(e).getTime();if(t<500)return"now";if(t>2592e5){let e=(t/864e5).toFixed(0);return e+" day"+("1"!==e?"s":"")+" ago"}if(t>36e5){let e=(t/36e5).toFixed(0);return e+" hour"+("1"!==e?"s":"")+" ago"}if(t>6e4){let e=(t/6e4).toFixed(0);return e+" minute"+("1"!==e?"s":"")+" ago"}let s=(t/1e3).toFixed(0);return s+" second"+("1"!==s?"s":"")+" ago"},L=(e,t)=>{let s=Math.ceil((new Date(e)-new Date(t))/1e3/60);return s+(1===s?" minute":" minutes")},A=e=>{let t=new Date(e),s=t.getFullYear(),l=(t.getMonth()+1<10?"0":"")+(t.getMonth()+1),a=(t.getDate()<10?"0":"")+t.getDate(),n=(t.getHours()<10?"0":"")+t.getHours(),r=(t.getMinutes()<10?"0":"")+t.getMinutes(),o=(t.getSeconds()<10?"0":"")+t.getSeconds();return s+"-"+l+"-"+a+" "+n+":"+r+":"+o},M={key:0,class:"space-y-2"},N={key:0,class:"flex items-center gap-2"},Y={class:"text-xs font-semibold"},I={class:"font-mono text-xs"},P={key:1},O={class:"font-mono text-xs"},K={key:0,class:"mt-1 space-y-0.5"},V={class:"truncate"},B={class:"text-muted-foreground"},G={key:0,class:"text-xs text-muted-foreground"},J={class:"text-xs font-semibold text-muted-foreground uppercase tracking-wider"},X={class:"font-mono text-xs"},Q={key:2},ee={class:"font-mono text-xs space-y-0.5"},te={class:"break-all"},se={key:3},le={class:"font-mono text-xs space-y-0.5"};var ae={__name:"Tooltip",props:{event:{type:[Event,Object],default:null},result:{type:Object,default:null},isPersistent:{type:Boolean,default:!1}},setup(e){const t=(0,u.yj)(),s=e,l=(0,r.iH)(!0),o=(0,r.iH)(0),i=(0,r.iH)(0),d=(0,r.iH)(null),c=(0,r.iH)(null),g=(0,a.Fl)((()=>s.result&Line truncated
(function(){"use strict";var e={438:function(e,t,s){s.d(t,{L:function(){return hr}});var l=s(963),a=s(252),n=s(577),r=s(262),o=s.p+"img/logo.svg",u=s(201),i=s(507),d=s(970),c=s(135),g=s(3),p=s(512),m=s(388);function v(...e){return(0,m.m6)((0,p.W)(e))}const f=["disabled"];var w={__name:"Button",props:{variant:{type:String,default:"default"},size:{type:String,default:"default"},disabled:{type:Boolean,default:!1}},setup(e){const t=(0,g.j)("inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",{variants:{variant:{default:"bg-primary text-primary-foreground hover:bg-primary/90",destructive:"bg-destructive text-destructive-foreground hover:bg-destructive/90",outline:"border border-input bg-background hover:bg-accent hover:text-accent-foreground",secondary:"bg-secondary text-secondary-foreground hover:bg-secondary/80",ghost:"hover:bg-accent hover:text-accent-foreground",link:"text-primary underline-offset-4 hover:underline"},size:{default:"h-10 px-4 py-2",sm:"h-9 rounded-md px-3",lg:"h-11 rounded-md px-8",icon:"h-10 w-10"}},defaultVariants:{variant:"default",size:"default"}});return(s,l)=>((0,a.wg)(),(0,a.iD)("button",{class:(0,n.C_)((0,r.SU)(v)((0,r.SU)(t)({variant:e.variant,size:e.size}),s.$attrs.class??"")),disabled:e.disabled},[(0,a.WI)(s.$slots,"default")],10,f))}};const h=w;var x=h,b={__name:"Card",setup(e){return(e,t)=>((0,a.wg)(),(0,a.iD)("div",{class:(0,n.C_)((0,r.SU)(v)("rounded-lg border bg-card text-card-foreground shadow-sm",e.$attrs.class??""))},[(0,a.WI)(e.$slots,"default")],2))}};const y=b;var k=y,_={__name:"CardHeader",setup(e){return(e,t)=>((0,a.wg)(),(0,a.iD)("div",{class:(0,n.C_)((0,r.SU)(v)("flex flex-col space-y-1.5 p-6",e.$attrs.class??""))},[(0,a.WI)(e.$slots,"default")],2))}};const S=_;var D=S,U={__name:"CardTitle",setup(e){return(e,t)=>((0,a.wg)(),(0,a.iD)("h3",{class:(0,n.C_)((0,r.SU)(v)("text-2xl font-semibold leading-none tracking-tight",e.$attrs.class??""))},[(0,a.WI)(e.$slots,"default")],2))}};const z=U;var W=z,C={__name:"CardContent",setup(e){return(e,t)=>((0,a.wg)(),(0,a.iD)("div",{class:(0,n.C_)((0,r.SU)(v)("p-6 pt-0",e.$attrs.class??""))},[(0,a.WI)(e.$slots,"default")],2))}};const j=C;var H=j;const R={id:"social"};function F(e,t){return(0,a.wg)(),(0,a.iD)("div",R,t[0]||(t[0]=[(0,a._)("a",{href:"https://github.com/TwiN/gatus",target:"_blank",title:"Gatus on GitHub"},[(0,a._)("svg",{xmlns:"http://www.w3.org/2000/svg",width:"32",height:"32",viewBox:"0 0 16 16",class:"hover:scale-110"},[(0,a._)("path",{fill:"gray",d:"M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"})])],-1)]))}var E=s(744);const q={},$=(0,E.Z)(q,[["render",F],["__scopeId","data-v-788af9ce"]]);var T=$;const Z=e=>{let t=(new Date).getTime()-new Date(e).getTime();if(t<500)return"now";if(t>2592e5){let e=(t/864e5).toFixed(0);return e+" day"+("1"!==e?"s":"")+" ago"}if(t>36e5){let e=(t/36e5).toFixed(0);return e+" hour"+("1"!==e?"s":"")+" ago"}if(t>6e4){let e=(t/6e4).toFixed(0);return e+" minute"+("1"!==e?"s":"")+" ago"}let s=(t/1e3).toFixed(0);return s+" second"+("1"!==s?"s":"")+" ago"},L=(e,t)=>{let s=Math.ceil((new Date(e)-new Date(t))/1e3/60);return s+(1===s?" minute":" minutes")},A=e=>{let t=new Date(e),s=t.getFullYear(),l=(t.getMonth()+1<10?"0":"")+(t.getMonth()+1),a=(t.getDate()<10?"0":"")+t.getDate(),n=(t.getHours()<10?"0":"")+t.getHours(),r=(t.getMinutes()<10?"0":"")+t.getMinutes(),o=(t.getSeconds()<10?"0":"")+t.getSeconds();return s+"-"+l+"-"+a+" "+n+":"+r+":"+o},M={key:0,class:"space-y-2"},N={key:0,class:"flex items-center gap-2"},Y={class:"text-xs font-semibold"},I={class:"font-mono text-xs"},P={key:1},O={class:"font-mono text-xs"},K={key:0,class:"mt-1 space-y-0.5"},V={class:"truncate"},B={class:"text-muted-foreground"},G={key:0,class:"text-xs text-muted-foreground"},J={class:"text-xs font-semibold text-muted-foreground uppercase tracking-wider"},X={class:"font-mono text-xs"},Q={key:2},ee={class:"font-mono text-xs space-y-0.5"},te={class:"break-all"},se={key:3},le={class:"font-mono text-xs space-y-0.5"};var ae={__name:"Tooltip",props:{event:{type:[Event,Object],default:null},result:{type:Object,default:null},isPersistent:{type:Boolean,default:!1}},setup(e){const t=(0,u.yj)(),s=e,l=(0,r.iH)(!0),o=(0,r.iH)(0),i=(0,r.iH)(0),d=(0,r.iH)(null),c=(0,r.iH)(null),g=(0,a.Fl)((()=>s.result&Line truncated