Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
447e140479 | ||
|
|
6908199716 | ||
|
|
b12f652553 | ||
|
|
83edca6e80 | ||
|
|
636688b43e | ||
|
|
4fdb55d632 | ||
|
|
a05daeda2e | ||
|
|
0bd0c1fd15 | ||
|
|
eb3ca71c72 | ||
|
|
37325cd78a | ||
|
|
f6e7e346b6 | ||
|
|
b5e742acde |
@@ -4,4 +4,5 @@ Dockerfile
|
||||
.idea
|
||||
.git
|
||||
web/app
|
||||
*.db
|
||||
*.db
|
||||
testdata
|
||||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -28,6 +28,6 @@ jobs:
|
||||
# was configured by the "Set up Go" step (otherwise, it'd use sudo's "go" executable)
|
||||
run: sudo env "PATH=$PATH" "GOROOT=$GOROOT" go test ./... -race -coverprofile=coverage.txt -covermode=atomic
|
||||
- name: Codecov
|
||||
uses: codecov/codecov-action@v3.1.1
|
||||
uses: codecov/codecov-action@v3.1.3
|
||||
with:
|
||||
files: ./coverage.txt
|
||||
|
||||
93
README.md
93
README.md
@@ -67,9 +67,11 @@ Have any feedback or questions? [Create a discussion](https://github.com/TwiN/ga
|
||||
- [Setting a default alert](#setting-a-default-alert)
|
||||
- [Maintenance](#maintenance)
|
||||
- [Security](#security)
|
||||
- [Basic](#basic)
|
||||
- [Basic Authentication](#basic-authentication)
|
||||
- [OIDC](#oidc)
|
||||
- [TLS Encryption](#tls-encryption)
|
||||
- [Metrics](#metrics)
|
||||
- [Connectivity](#connectivity)
|
||||
- [Remote instances (EXPERIMENTAL)](#remote-instances-experimental)
|
||||
- [Deployment](#deployment)
|
||||
- [Docker](#docker)
|
||||
@@ -87,7 +89,7 @@ Have any feedback or questions? [Create a discussion](https://github.com/TwiN/ga
|
||||
- [Monitoring an endpoint using ICMP](#monitoring-an-endpoint-using-icmp)
|
||||
- [Monitoring an endpoint using DNS queries](#monitoring-an-endpoint-using-dns-queries)
|
||||
- [Monitoring an endpoint using STARTTLS](#monitoring-an-endpoint-using-starttls)
|
||||
- [Monitoring an endpoint using TLS](#monitoring-an-endpoint-using-tls)
|
||||
- [Monitoring an endpoint using TLS](#monitoring-an-endpoint-using-tls)>
|
||||
- [Monitoring domain expiration](#monitoring-domain-expiration)
|
||||
- [disable-monitoring-lock](#disable-monitoring-lock)
|
||||
- [Reloading configuration on the fly](#reloading-configuration-on-the-fly)
|
||||
@@ -228,6 +230,8 @@ If you want to test it locally, see [Docker](#docker).
|
||||
| `web` | Web configuration. | `{}` |
|
||||
| `web.address` | Address to listen on. | `0.0.0.0` |
|
||||
| `web.port` | Port to listen on. | `8080` |
|
||||
| `web.tls.certificate-file` | Optional public certificate file for TLS in PEM format. | `` |
|
||||
| `web.tls.private-key-file` | Optional private key file for TLS in PEM format. | `` |
|
||||
| `ui` | UI configuration. | `{}` |
|
||||
| `ui.title` | [Title of the document](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/title). | `Health Dashboard ǀ Gatus` |
|
||||
| `ui.description` | Meta description for the page. | `Gatus is an advanced...`. |
|
||||
@@ -1053,13 +1057,13 @@ As a result, the `[ALERT_TRIGGERED_OR_RESOLVED]` in the body of first example of
|
||||
|
||||
|
||||
#### Setting a default alert
|
||||
| Parameter | Description | Default |
|
||||
|:----------------------------------------------|:------------------------------------------------------------------------------|:--------|
|
||||
| `alerting.*.default-alert.enabled` | Whether to enable the alert | N/A |
|
||||
| `alerting.*.default-alert.failure-threshold` | Number of failures in a row needed before triggering the alert | N/A |
|
||||
| `alerting.*.default-alert.success-threshold` | Number of successes in a row before an ongoing incident is marked as resolved | N/A |
|
||||
| `alerting.*.default-alert.send-on-resolved` | Whether to send a notification once a triggered alert is marked as resolved | N/A |
|
||||
| `alerting.*.default-alert.description` | Description of the alert. Will be included in the alert sent | N/A |
|
||||
| Parameter | Description | Default |
|
||||
|:---------------------------------------------|:------------------------------------------------------------------------------|:--------|
|
||||
| `alerting.*.default-alert.enabled` | Whether to enable the alert | N/A |
|
||||
| `alerting.*.default-alert.failure-threshold` | Number of failures in a row needed before triggering the alert | N/A |
|
||||
| `alerting.*.default-alert.success-threshold` | Number of successes in a row before an ongoing incident is marked as resolved | N/A |
|
||||
| `alerting.*.default-alert.send-on-resolved` | Whether to send a notification once a triggered alert is marked as resolved | N/A |
|
||||
| `alerting.*.default-alert.description` | Description of the alert. Will be included in the alert sent | N/A |
|
||||
|
||||
> ⚠ You must still specify the `type` of the alert in the endpoint configuration even if you set the default alert of a provider.
|
||||
|
||||
@@ -1175,14 +1179,14 @@ maintenance:
|
||||
|
||||
|
||||
### Security
|
||||
| Parameter | Description | Default |
|
||||
|:---------------------------------|:-----------------------------|:--------------|
|
||||
| `security` | Security configuration | `{}` |
|
||||
| `security.basic` | HTTP Basic configuration | `{}` |
|
||||
| `security.oidc` | OpenID Connect configuration | `{}` |
|
||||
| Parameter | Description | Default |
|
||||
|:-----------------|:-----------------------------|:--------|
|
||||
| `security` | Security configuration | `{}` |
|
||||
| `security.basic` | HTTP Basic configuration | `{}` |
|
||||
| `security.oidc` | OpenID Connect configuration | `{}` |
|
||||
|
||||
|
||||
#### Basic
|
||||
#### Basic Authentication
|
||||
| Parameter | Description | Default |
|
||||
|:----------------------------------------|:-----------------------------------------------------------------------------------|:--------------|
|
||||
| `security.basic` | HTTP Basic configuration | `{}` |
|
||||
@@ -1226,6 +1230,17 @@ security:
|
||||
|
||||
Confused? Read [Securing Gatus with OIDC using Auth0](https://twin.sh/articles/56/securing-gatus-with-oidc-using-auth0).
|
||||
|
||||
### TLS Encryption
|
||||
Gatus supports basic encryption with TLS. To enable this, certificate files in PEM format have to be provided.
|
||||
The example below shows an example configuration which makes gatus respond on port 4443 to HTTPS requests.
|
||||
|
||||
```yaml
|
||||
web:
|
||||
port: 4443
|
||||
tls:
|
||||
certificate-file: "server.crt"
|
||||
private-key-file: "server.key"
|
||||
```
|
||||
|
||||
### Metrics
|
||||
To enable metrics, you must set `metrics` to `true`. Doing so will expose Prometheus-friendly metrics at the `/metrics`
|
||||
@@ -1242,6 +1257,28 @@ endpoint on the same port your application is configured to run on (`web.port`).
|
||||
See [examples/docker-compose-grafana-prometheus](.examples/docker-compose-grafana-prometheus) for further documentation as well as an example.
|
||||
|
||||
|
||||
### Connectivity
|
||||
| Parameter | Description | Default |
|
||||
|:--------------------------------|:-------------------------------------------|:--------------|
|
||||
| `connectivity` | Connectivity configuration | `{}` |
|
||||
| `connectivity.checker` | Connectivity checker configuration | Required `{}` |
|
||||
| `connectivity.checker.target` | Host to use for validating connectivity | Required `""` |
|
||||
| `connectivity.checker.interval` | Interval at which to validate connectivity | `1m` |
|
||||
|
||||
While Gatus is used to monitor other services, it is possible for Gatus itself to lose connectivity to the internet.
|
||||
In order to prevent Gatus from reporting endpoints as unhealthy when Gatus itself is unhealthy, you may configure
|
||||
Gatus to periodically check for internet connectivity.
|
||||
|
||||
All endpoint executions are skipped while the connectivity checker deems connectivity to be down.
|
||||
|
||||
```yaml
|
||||
connectivity:
|
||||
checker:
|
||||
target: 1.1.1.1:53
|
||||
interval: 60s
|
||||
```
|
||||
|
||||
|
||||
### Remote instances (EXPERIMENTAL)
|
||||
This feature allows you to retrieve endpoint statuses from a remote Gatus instance.
|
||||
|
||||
@@ -1253,12 +1290,12 @@ This is an experimental feature. It may be removed or updated in a breaking mann
|
||||
there are known issues with this feature. If you'd like to provide some feedback, please write a comment in [#64](https://github.com/TwiN/gatus/issues/64).
|
||||
Use at your own risk.
|
||||
|
||||
| Parameter | Description | Default |
|
||||
|:-----------------------------------|:---------------------------------------------|:---------------|
|
||||
| `remote` | Remote configuration | `{}` |
|
||||
| `remote.instances` | List of remote instances | Required `[]` |
|
||||
| `remote.instances.endpoint-prefix` | String to prefix all endpoint names with | `""` |
|
||||
| `remote.instances.url` | URL from which to retrieve endpoint statuses | Required `""` |
|
||||
| Parameter | Description | Default |
|
||||
|:-----------------------------------|:---------------------------------------------|:--------------|
|
||||
| `remote` | Remote configuration | `{}` |
|
||||
| `remote.instances` | List of remote instances | Required `[]` |
|
||||
| `remote.instances.endpoint-prefix` | String to prefix all endpoint names with | `""` |
|
||||
| `remote.instances.url` | URL from which to retrieve endpoint statuses | Required `""` |
|
||||
|
||||
```yaml
|
||||
remote:
|
||||
@@ -1386,11 +1423,11 @@ simple health checks used for alerting (PagerDuty/Twilio) to `30s`.
|
||||
|
||||
|
||||
### Default timeouts
|
||||
| Endpoint type | Timeout |
|
||||
|:---------------|:--------|
|
||||
| HTTP | 10s |
|
||||
| TCP | 10s |
|
||||
| ICMP | 10s |
|
||||
| Endpoint type | Timeout |
|
||||
|:--------------|:--------|
|
||||
| HTTP | 10s |
|
||||
| TCP | 10s |
|
||||
| ICMP | 10s |
|
||||
|
||||
To modify the timeout, see [Client configuration](#client-configuration).
|
||||
|
||||
@@ -1784,6 +1821,8 @@ No such header is required to query the API.
|
||||
## Sponsors
|
||||
You can find the full list of sponsors [here](https://github.com/sponsors/TwiN).
|
||||
|
||||
_There is currently no sponsors_
|
||||
<!-- _There is currently no sponsors_ -->
|
||||
|
||||
[<img src="https://github.com/8ball030.png" width="50" />](https://github.com/8ball030)
|
||||
|
||||
<!-- [<img src="https://github.com/$user.png" width="50" />](https://github.com/$user) -->
|
||||
|
||||
@@ -191,6 +191,9 @@ func TestCanCreateTCPConnection(t *testing.T) {
|
||||
if CanCreateTCPConnection("127.0.0.1", &Config{Timeout: 5 * time.Second}) {
|
||||
t.Error("should've failed, because there's no port in the address")
|
||||
}
|
||||
if !CanCreateTCPConnection("1.1.1.1:53", &Config{Timeout: 5 * time.Second}) {
|
||||
t.Error("should've succeeded, because that IP should always™ be up")
|
||||
}
|
||||
}
|
||||
|
||||
// This test checks if a HTTP client configured with `configureOAuth2()` automatically
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/TwiN/gatus/v5/alerting"
|
||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||
"github.com/TwiN/gatus/v5/alerting/provider"
|
||||
"github.com/TwiN/gatus/v5/config/connectivity"
|
||||
"github.com/TwiN/gatus/v5/config/maintenance"
|
||||
"github.com/TwiN/gatus/v5/config/remote"
|
||||
"github.com/TwiN/gatus/v5/config/ui"
|
||||
@@ -91,6 +92,9 @@ type Config struct {
|
||||
// WARNING: This is in ALPHA and may change or be completely removed in the future
|
||||
Remote *remote.Config `yaml:"remote,omitempty"`
|
||||
|
||||
// Connectivity is the configuration for connectivity
|
||||
Connectivity *connectivity.Config `yaml:"connectivity,omitempty"`
|
||||
|
||||
configPath string // path to the file or directory from which config was loaded
|
||||
lastFileModTime time.Time // last modification time
|
||||
}
|
||||
@@ -252,10 +256,20 @@ func parseAndValidateConfigBytes(yamlBytes []byte) (config *Config, err error) {
|
||||
if err := validateRemoteConfig(config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := validateConnectivityConfig(config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func validateConnectivityConfig(config *Config) error {
|
||||
if config.Connectivity != nil {
|
||||
return config.Connectivity.ValidateAndSetDefaults()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateRemoteConfig(config *Config) error {
|
||||
if config.Remote != nil {
|
||||
if err := config.Remote.ValidateAndSetDefaults(); err != nil {
|
||||
|
||||
53
config/connectivity/connectivity.go
Normal file
53
config/connectivity/connectivity.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package connectivity
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/TwiN/gatus/v5/client"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidInterval = errors.New("connectivity.checker.interval must be 5s or higher")
|
||||
ErrInvalidDNSTarget = errors.New("connectivity.checker.target must be suffixed with :53")
|
||||
)
|
||||
|
||||
// Config is the configuration for the connectivity checker.
|
||||
type Config struct {
|
||||
Checker *Checker `yaml:"checker,omitempty"`
|
||||
}
|
||||
|
||||
func (c *Config) ValidateAndSetDefaults() error {
|
||||
if c.Checker != nil {
|
||||
if c.Checker.Interval == 0 {
|
||||
c.Checker.Interval = 60 * time.Second
|
||||
} else if c.Checker.Interval < 5*time.Second {
|
||||
return ErrInvalidInterval
|
||||
}
|
||||
if !strings.HasSuffix(c.Checker.Target, ":53") {
|
||||
return ErrInvalidDNSTarget
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Checker is the configuration for making sure Gatus has access to the internet.
|
||||
type Checker struct {
|
||||
Target string `yaml:"target"` // e.g. 1.1.1.1:53
|
||||
Interval time.Duration `yaml:"interval,omitempty"`
|
||||
|
||||
isConnected bool
|
||||
lastCheck time.Time
|
||||
}
|
||||
|
||||
func (c Checker) Check() bool {
|
||||
return client.CanCreateTCPConnection(c.Target, &client.Config{Timeout: 5 * time.Second})
|
||||
}
|
||||
|
||||
func (c *Checker) IsConnected() bool {
|
||||
if now := time.Now(); now.After(c.lastCheck.Add(c.Interval)) {
|
||||
c.lastCheck, c.isConnected = now, c.Check()
|
||||
}
|
||||
return c.isConnected
|
||||
}
|
||||
62
config/connectivity/connectivity_test.go
Normal file
62
config/connectivity/connectivity_test.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package connectivity
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestConfig(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
cfg *Config
|
||||
expectedErr error
|
||||
expectedInterval time.Duration
|
||||
}{
|
||||
{
|
||||
name: "good-config",
|
||||
cfg: &Config{Checker: &Checker{Target: "1.1.1.1:53", Interval: 10 * time.Second}},
|
||||
expectedInterval: 10 * time.Second,
|
||||
},
|
||||
{
|
||||
name: "good-config-with-default-interval",
|
||||
cfg: &Config{Checker: &Checker{Target: "8.8.8.8:53", Interval: 0}},
|
||||
expectedInterval: 60 * time.Second,
|
||||
},
|
||||
{
|
||||
name: "config-with-interval-too-low",
|
||||
cfg: &Config{Checker: &Checker{Target: "1.1.1.1:53", Interval: 4 * time.Second}},
|
||||
expectedErr: ErrInvalidInterval,
|
||||
},
|
||||
{
|
||||
name: "config-with-invalid-target-due-to-missing-port",
|
||||
cfg: &Config{Checker: &Checker{Target: "1.1.1.1", Interval: 15 * time.Second}},
|
||||
expectedErr: ErrInvalidDNSTarget,
|
||||
},
|
||||
{
|
||||
name: "config-with-invalid-target-due-to-invalid-dns-port",
|
||||
cfg: &Config{Checker: &Checker{Target: "1.1.1.1:52", Interval: 15 * time.Second}},
|
||||
expectedErr: ErrInvalidDNSTarget,
|
||||
},
|
||||
}
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.name, func(t *testing.T) {
|
||||
err := scenario.cfg.ValidateAndSetDefaults()
|
||||
if fmt.Sprintf("%s", err) != fmt.Sprintf("%s", scenario.expectedErr) {
|
||||
t.Errorf("expected error %v, got %v", scenario.expectedErr, err)
|
||||
}
|
||||
if err == nil && scenario.expectedErr == nil {
|
||||
if scenario.cfg.Checker.Interval != scenario.expectedInterval {
|
||||
t.Errorf("expected interval %v, got %v", scenario.expectedInterval, scenario.cfg.Checker.Interval)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChecker_IsConnected(t *testing.T) {
|
||||
checker := &Checker{Target: "1.1.1.1:53", Interval: 10 * time.Second}
|
||||
if !checker.IsConnected() {
|
||||
t.Error("expected checker.IsConnected() to be true")
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
)
|
||||
@@ -21,6 +23,19 @@ type Config struct {
|
||||
|
||||
// Port to listen on (default to 8080 specified by DefaultPort)
|
||||
Port int `yaml:"port"`
|
||||
|
||||
// TLS configuration (optional)
|
||||
TLS *TLSConfig `yaml:"tls,omitempty"`
|
||||
}
|
||||
|
||||
type TLSConfig struct {
|
||||
// CertificateFile is the public certificate for TLS in PEM format.
|
||||
CertificateFile string `yaml:"certificate-file,omitempty"`
|
||||
|
||||
// PrivateKeyFile is the private key file for TLS in PEM format.
|
||||
PrivateKeyFile string `yaml:"private-key-file,omitempty"`
|
||||
|
||||
tlsConfig *tls.Config
|
||||
}
|
||||
|
||||
// GetDefaultConfig returns a Config struct with the default values
|
||||
@@ -40,6 +55,12 @@ func (web *Config) ValidateAndSetDefaults() error {
|
||||
} else if web.Port < 0 || web.Port > math.MaxUint16 {
|
||||
return fmt.Errorf("invalid port: value should be between %d and %d", 0, math.MaxUint16)
|
||||
}
|
||||
// Try to load the TLS certificates
|
||||
if web.TLS != nil {
|
||||
if err := web.TLS.loadConfig(); err != nil {
|
||||
return fmt.Errorf("invalid tls config: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -47,3 +68,22 @@ func (web *Config) ValidateAndSetDefaults() error {
|
||||
func (web *Config) SocketAddress() string {
|
||||
return fmt.Sprintf("%s:%d", web.Address, web.Port)
|
||||
}
|
||||
|
||||
func (t *TLSConfig) loadConfig() error {
|
||||
if len(t.CertificateFile) > 0 && len(t.PrivateKeyFile) > 0 {
|
||||
certificate, err := tls.LoadX509KeyPair(t.CertificateFile, t.PrivateKeyFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.tlsConfig = &tls.Config{Certificates: []tls.Certificate{certificate}}
|
||||
return nil
|
||||
}
|
||||
return errors.New("certificate-file and private-key-file must be specified")
|
||||
}
|
||||
|
||||
func (web *Config) TLSConfig() *tls.Config {
|
||||
if web.TLS != nil {
|
||||
return web.TLS.tlsConfig
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -12,6 +12,9 @@ func TestGetDefaultConfig(t *testing.T) {
|
||||
if defaultConfig.Address != DefaultAddress {
|
||||
t.Error("expected default config to have the default address")
|
||||
}
|
||||
if defaultConfig.TLS != nil {
|
||||
t.Error("expected default config to have TLS disabled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfig_ValidateAndSetDefaults(t *testing.T) {
|
||||
@@ -63,3 +66,56 @@ func TestConfig_SocketAddress(t *testing.T) {
|
||||
t.Errorf("expected %s, got %s", "0.0.0.0:8081", web.SocketAddress())
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfig_TLSConfig(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
cfg *Config
|
||||
expectedErr bool
|
||||
}{
|
||||
{
|
||||
name: "good-tls-config",
|
||||
cfg: &Config{TLS: &TLSConfig{CertificateFile: "../../testdata/cert.pem", PrivateKeyFile: "../../testdata/cert.key"}},
|
||||
expectedErr: false,
|
||||
},
|
||||
{
|
||||
name: "missing-crt-file",
|
||||
cfg: &Config{TLS: &TLSConfig{CertificateFile: "doesnotexist", PrivateKeyFile: "../../testdata/cert.key"}},
|
||||
expectedErr: true,
|
||||
},
|
||||
{
|
||||
name: "bad-crt-file",
|
||||
cfg: &Config{TLS: &TLSConfig{CertificateFile: "../../testdata/badcert.pem", PrivateKeyFile: "../../testdata/cert.key"}},
|
||||
expectedErr: true,
|
||||
},
|
||||
{
|
||||
name: "missing-private-key-file",
|
||||
cfg: &Config{TLS: &TLSConfig{CertificateFile: "../../testdata/cert.pem", PrivateKeyFile: "doesnotexist"}},
|
||||
expectedErr: true,
|
||||
},
|
||||
{
|
||||
name: "bad-private-key-file",
|
||||
cfg: &Config{TLS: &TLSConfig{CertificateFile: "../../testdata/cert.pem", PrivateKeyFile: "../../testdata/badcert.key"}},
|
||||
expectedErr: true,
|
||||
},
|
||||
{
|
||||
name: "bad-cert-and-private-key-file",
|
||||
cfg: &Config{TLS: &TLSConfig{CertificateFile: "../../testdata/badcert.pem", PrivateKeyFile: "../../testdata/badcert.key"}},
|
||||
expectedErr: true,
|
||||
},
|
||||
}
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.name, func(t *testing.T) {
|
||||
err := scenario.cfg.ValidateAndSetDefaults()
|
||||
if (err != nil) != scenario.expectedErr {
|
||||
t.Errorf("expected the existence of an error to be %v, got %v", scenario.expectedErr, err)
|
||||
return
|
||||
}
|
||||
if !scenario.expectedErr {
|
||||
if scenario.cfg.TLS.tlsConfig == nil {
|
||||
t.Error("TLS configuration was not correctly loaded although no error was returned")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,8 +24,10 @@ func Handle(cfg *config.Config) {
|
||||
if os.Getenv("ENVIRONMENT") == "dev" {
|
||||
router = handler.DevelopmentCORS(router)
|
||||
}
|
||||
tlsConfig := cfg.Web.TLSConfig()
|
||||
server = &http.Server{
|
||||
Addr: fmt.Sprintf("%s:%d", cfg.Web.Address, cfg.Web.Port),
|
||||
TLSConfig: tlsConfig,
|
||||
Handler: router,
|
||||
ReadTimeout: 15 * time.Second,
|
||||
WriteTimeout: 15 * time.Second,
|
||||
@@ -35,7 +37,11 @@ func Handle(cfg *config.Config) {
|
||||
if os.Getenv("ROUTER_TEST") == "true" {
|
||||
return
|
||||
}
|
||||
log.Println("[controller][Handle]", server.ListenAndServe())
|
||||
if tlsConfig != nil {
|
||||
log.Println("[controller][Handle]", server.ListenAndServeTLS("", ""))
|
||||
} else {
|
||||
log.Println("[controller][Handle]", server.ListenAndServe())
|
||||
}
|
||||
}
|
||||
|
||||
// Shutdown stops the server
|
||||
|
||||
@@ -45,6 +45,48 @@ func TestHandle(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleTLS(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
tls *web.TLSConfig
|
||||
expectedStatusCode int
|
||||
}{
|
||||
{
|
||||
name: "good-tls-config",
|
||||
tls: &web.TLSConfig{CertificateFile: "../testdata/cert.pem", PrivateKeyFile: "../testdata/cert.key"},
|
||||
expectedStatusCode: 200,
|
||||
},
|
||||
}
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.name, func(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
Web: &web.Config{Address: "0.0.0.0", Port: rand.Intn(65534), TLS: scenario.tls},
|
||||
Endpoints: []*core.Endpoint{
|
||||
{Name: "frontend", Group: "core"},
|
||||
{Name: "backend", Group: "core"},
|
||||
},
|
||||
}
|
||||
if err := cfg.Web.ValidateAndSetDefaults(); err != nil {
|
||||
t.Error("expected no error from web (TLS) validation, got", err)
|
||||
}
|
||||
_ = os.Setenv("ROUTER_TEST", "true")
|
||||
_ = os.Setenv("ENVIRONMENT", "dev")
|
||||
defer os.Clearenv()
|
||||
Handle(cfg)
|
||||
defer Shutdown()
|
||||
request, _ := http.NewRequest("GET", "/health", http.NoBody)
|
||||
responseRecorder := httptest.NewRecorder()
|
||||
server.Handler.ServeHTTP(responseRecorder, request)
|
||||
if responseRecorder.Code != scenario.expectedStatusCode {
|
||||
t.Errorf("expected GET /health to return status code %d, got %d", scenario.expectedStatusCode, responseRecorder.Code)
|
||||
}
|
||||
if server == nil {
|
||||
t.Fatal("server should've been set (but because we set ROUTER_TEST, it shouldn't have been started)")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestShutdown(t *testing.T) {
|
||||
// Pretend that we called controller.Handle(), which initializes the server variable
|
||||
server = &http.Server{}
|
||||
|
||||
12
go.mod
12
go.mod
@@ -1,10 +1,10 @@
|
||||
module github.com/TwiN/gatus/v5
|
||||
|
||||
go 1.19
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/TwiN/deepmerge v0.2.0
|
||||
github.com/TwiN/g8 v1.4.0
|
||||
github.com/TwiN/g8/v2 v2.0.0
|
||||
github.com/TwiN/gocache/v2 v2.2.0
|
||||
github.com/TwiN/health v1.6.0
|
||||
github.com/TwiN/whois v1.1.0
|
||||
@@ -18,8 +18,8 @@ require (
|
||||
github.com/prometheus-community/pro-bing v0.1.0
|
||||
github.com/prometheus/client_golang v1.14.0
|
||||
github.com/wcharczuk/go-chart/v2 v2.1.0
|
||||
golang.org/x/crypto v0.7.0
|
||||
golang.org/x/oauth2 v0.5.0
|
||||
golang.org/x/crypto v0.8.0
|
||||
golang.org/x/oauth2 v0.7.0
|
||||
gopkg.in/mail.v2 v2.3.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
modernc.org/sqlite v1.20.4
|
||||
@@ -43,9 +43,9 @@ require (
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
|
||||
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d // indirect
|
||||
golang.org/x/mod v0.7.0 // indirect
|
||||
golang.org/x/net v0.8.0 // indirect
|
||||
golang.org/x/net v0.9.0 // indirect
|
||||
golang.org/x/sync v0.1.0 // indirect
|
||||
golang.org/x/sys v0.6.0 // indirect
|
||||
golang.org/x/sys v0.7.0 // indirect
|
||||
golang.org/x/tools v0.3.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/protobuf v1.28.1 // indirect
|
||||
|
||||
20
go.sum
20
go.sum
@@ -36,8 +36,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/TwiN/deepmerge v0.2.0 h1:8P1tp2qDXllX6V1Ailg2XA074easAcvLMmW9v1jn0aE=
|
||||
github.com/TwiN/deepmerge v0.2.0/go.mod h1:cR9OWsvI13y+FxrbnPLuF6BX2tbYeOjkiI6JWjEGqAE=
|
||||
github.com/TwiN/g8 v1.4.0 h1:RUk5xTtxKCdMo0GGSbBVyjtAAfi2nqVbA9E0C4u5Cxo=
|
||||
github.com/TwiN/g8 v1.4.0/go.mod h1:ECyGJsoIb99klUfvVQoS1StgRLte9yvvPigGrHdy284=
|
||||
github.com/TwiN/g8/v2 v2.0.0 h1:+hwIbRLMhDd2iwHzkZUPp2FkX7yTx8ddYOnS91HkDqQ=
|
||||
github.com/TwiN/g8/v2 v2.0.0/go.mod h1:4sVAF27q8T8ISggRa/Fb0drw7wpB22B6eWd+/+SGMqE=
|
||||
github.com/TwiN/gocache/v2 v2.2.0 h1:M3B36KyH24BntxLrLaUb2kgTdq8DzCnfod0IekLG57w=
|
||||
github.com/TwiN/gocache/v2 v2.2.0/go.mod h1:SnUuBsrwGQeNcDG6vhkOMJnqErZM0JGjgIkuKryokYA=
|
||||
github.com/TwiN/health v1.6.0 h1:L2ks575JhRgQqWWOfKjw9B0ec172hx7GdToqkYUycQM=
|
||||
@@ -261,8 +261,8 @@ golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8U
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
|
||||
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@@ -334,8 +334,8 @@ golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
|
||||
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
|
||||
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -344,8 +344,8 @@ golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4Iltr
|
||||
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
|
||||
golang.org/x/oauth2 v0.3.0/go.mod h1:rQrIauxkUhJ6CuwEXwymO2/eh4xz2ZWF1nBkcxS+tGk=
|
||||
golang.org/x/oauth2 v0.5.0 h1:HuArIo48skDwlrvM3sEdHXElYslAMsf3KwRkkW4MC4s=
|
||||
golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I=
|
||||
golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g=
|
||||
golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -400,8 +400,8 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
|
||||
"github.com/TwiN/g8"
|
||||
g8 "github.com/TwiN/g8/v2"
|
||||
"github.com/gorilla/mux"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
3
testdata/badcert.key
vendored
Normal file
3
testdata/badcert.key
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
wat
|
||||
-----END PRIVATE KEY-----
|
||||
3
testdata/badcert.pem
vendored
Normal file
3
testdata/badcert.pem
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
wat
|
||||
-----END CERTIFICATE-----
|
||||
5
testdata/cert.key
vendored
Normal file
5
testdata/cert.key
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgJh67FWpz8wrN1mM/
|
||||
CebkZN0zF83691ZVD83XlbNLRUqhRANCAAScfyPxScqz+Z/yNtAID/FOORy9J6LM
|
||||
DUAJevGDvAZCMp/nh+Ps3nLrMoRlykcux3mq+N8HPlJ8R3eetB4S1tHY
|
||||
-----END PRIVATE KEY-----
|
||||
10
testdata/cert.pem
vendored
Normal file
10
testdata/cert.pem
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIBaDCCAQ2gAwIBAgICBNIwCgYIKoZIzj0EAwIwFTETMBEGA1UEChMKR2F0dXMg
|
||||
dGVzdDAgFw0yMzA0MjIxODUwMDVaGA8yMjk3MDIwNDE4NTAwNVowFTETMBEGA1UE
|
||||
ChMKR2F0dXMgdGVzdDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJx/I/FJyrP5
|
||||
n/I20AgP8U45HL0noswNQAl68YO8BkIyn+eH4+zecusyhGXKRy7Hear43wc+UnxH
|
||||
d560HhLW0dijSzBJMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcD
|
||||
ATAMBgNVHRMBAf8EAjAAMBQGA1UdEQQNMAuCCWxvY2FsaG9zdDAKBggqhkjOPQQD
|
||||
AgNJADBGAiEA/SdthKOoNw3azSHuPid7XJsXYB8DisIC9LBwcb/QTMECIQCAB36Y
|
||||
OI15ao+J/RUz2sXdPXCAN8hlohi6OnmZmJB32g==
|
||||
-----END CERTIFICATE-----
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/TwiN/gatus/v5/alerting"
|
||||
"github.com/TwiN/gatus/v5/config"
|
||||
"github.com/TwiN/gatus/v5/config/connectivity"
|
||||
"github.com/TwiN/gatus/v5/config/maintenance"
|
||||
"github.com/TwiN/gatus/v5/core"
|
||||
"github.com/TwiN/gatus/v5/metrics"
|
||||
@@ -30,15 +31,15 @@ func Monitor(cfg *config.Config) {
|
||||
if endpoint.IsEnabled() {
|
||||
// To prevent multiple requests from running at the same time, we'll wait for a little before each iteration
|
||||
time.Sleep(777 * time.Millisecond)
|
||||
go monitor(endpoint, cfg.Alerting, cfg.Maintenance, cfg.DisableMonitoringLock, cfg.Metrics, cfg.Debug, ctx)
|
||||
go monitor(endpoint, cfg.Alerting, cfg.Maintenance, cfg.Connectivity, cfg.DisableMonitoringLock, cfg.Metrics, cfg.Debug, ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// monitor a single endpoint in a loop
|
||||
func monitor(endpoint *core.Endpoint, alertingConfig *alerting.Config, maintenanceConfig *maintenance.Config, disableMonitoringLock, enabledMetrics, debug bool, ctx context.Context) {
|
||||
func monitor(endpoint *core.Endpoint, alertingConfig *alerting.Config, maintenanceConfig *maintenance.Config, connectivityConfig *connectivity.Config, disableMonitoringLock, enabledMetrics, debug bool, ctx context.Context) {
|
||||
// Run it immediately on start
|
||||
execute(endpoint, alertingConfig, maintenanceConfig, disableMonitoringLock, enabledMetrics, debug)
|
||||
execute(endpoint, alertingConfig, maintenanceConfig, connectivityConfig, disableMonitoringLock, enabledMetrics, debug)
|
||||
// Loop for the next executions
|
||||
for {
|
||||
select {
|
||||
@@ -46,16 +47,22 @@ func monitor(endpoint *core.Endpoint, alertingConfig *alerting.Config, maintenan
|
||||
log.Printf("[watchdog][monitor] Canceling current execution of group=%s; endpoint=%s", endpoint.Group, endpoint.Name)
|
||||
return
|
||||
case <-time.After(endpoint.Interval):
|
||||
execute(endpoint, alertingConfig, maintenanceConfig, disableMonitoringLock, enabledMetrics, debug)
|
||||
execute(endpoint, alertingConfig, maintenanceConfig, connectivityConfig, disableMonitoringLock, enabledMetrics, debug)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func execute(endpoint *core.Endpoint, alertingConfig *alerting.Config, maintenanceConfig *maintenance.Config, disableMonitoringLock, enabledMetrics, debug bool) {
|
||||
func execute(endpoint *core.Endpoint, alertingConfig *alerting.Config, maintenanceConfig *maintenance.Config, connectivityConfig *connectivity.Config, disableMonitoringLock, enabledMetrics, debug bool) {
|
||||
if !disableMonitoringLock {
|
||||
// By placing the lock here, we prevent multiple endpoints from being monitored at the exact same time, which
|
||||
// could cause performance issues and return inaccurate results
|
||||
monitoringMutex.Lock()
|
||||
defer monitoringMutex.Unlock()
|
||||
}
|
||||
// If there's a connectivity checker configured, check if Gatus has internet connectivity
|
||||
if connectivityConfig != nil && connectivityConfig.Checker != nil && !connectivityConfig.Checker.IsConnected() {
|
||||
log.Println("[watchdog][execute] No connectivity; skipping execution")
|
||||
return
|
||||
}
|
||||
if debug {
|
||||
log.Printf("[watchdog][execute] Monitoring group=%s; endpoint=%s", endpoint.Group, endpoint.Name)
|
||||
@@ -79,9 +86,6 @@ func execute(endpoint *core.Endpoint, alertingConfig *alerting.Config, maintenan
|
||||
if debug {
|
||||
log.Printf("[watchdog][execute] Waiting for interval=%s before monitoring group=%s endpoint=%s again", endpoint.Interval, endpoint.Group, endpoint.Name)
|
||||
}
|
||||
if !disableMonitoringLock {
|
||||
monitoringMutex.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateEndpointStatuses updates the slice of endpoint statuses
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
<template>
|
||||
<Loading v-if="!retrievedData" class="h-64 w-64 px-4 my-24"/>
|
||||
<slot v-else>
|
||||
<slot>
|
||||
<Endpoints
|
||||
v-show="retrievedData"
|
||||
:endpointStatuses="endpointStatuses"
|
||||
:showStatusOnHover="true"
|
||||
@showTooltip="showTooltip"
|
||||
@toggleShowAverageResponseTime="toggleShowAverageResponseTime"
|
||||
:showAverageResponseTime="showAverageResponseTime"
|
||||
/>
|
||||
<Pagination @page="changePage"/>
|
||||
<Pagination v-show="retrievedData" @page="changePage"/>
|
||||
</slot>
|
||||
<Settings @refreshData="fetchData"/>
|
||||
</template>
|
||||
@@ -31,6 +32,7 @@ export default {
|
||||
emits: ['showTooltip', 'toggleShowAverageResponseTime'],
|
||||
methods: {
|
||||
fetchData() {
|
||||
this.retrievedData = false;
|
||||
fetch(`${SERVER_URL}/api/v1/endpoints/statuses?page=${this.currentPage}`, {credentials: 'include'})
|
||||
.then(response => {
|
||||
this.retrievedData = true;
|
||||
|
||||
@@ -1 +1 @@
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><script>window.config = {logo: "{{ .Logo }}", header: "{{ .Header }}", link: "{{ .Link }}", buttons: []};{{- range .Buttons}}window.config.buttons.push({name:"{{ .Name }}",link:"{{ .Link }}"});{{end}}</script><title>{{ .Title }}</title><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png"><link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"/><link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"/><link rel="manifest" href="/manifest.json"><link rel="shortcut icon" href="/favicon.ico"/><meta name="description" content="{{ .Description }}"/><meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"><meta name="apple-mobile-web-app-title" content="{{ .Title }}"><meta name="application-name" content="{{ .Title }}"><meta name="theme-color" content="#f7f9fb"><script defer="defer" src="/js/chunk-vendors.js"></script><script defer="defer" src="/js/app.js"></script><link href="/css/app.css" rel="stylesheet"></head><body class="dark:bg-gray-900"><noscript><strong>Enable JavaScript to view this page.</strong></noscript><div id="app"></div></body></html>
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><script>window.config = {logo: "{{ .Logo }}", header: "{{ .Header }}", link: "{{ .Link }}", buttons: []};{{- range .Buttons}}window.config.buttons.push({name:"{{ .Name }}",link:"{{ .Link }}"});{{end}}</script><title>{{ .Title }}</title><meta http-equiv="X-UA-Compatible" content="IE=edge"/><meta name="viewport" content="width=device-width,initial-scale=1"/><link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png"/><link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"/><link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"/><link rel="manifest" href="/manifest.json"/><link rel="shortcut icon" href="/favicon.ico"/><meta name="description" content="{{ .Description }}"/><meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"/><meta name="apple-mobile-web-app-title" content="{{ .Title }}"/><meta name="application-name" content="{{ .Title }}"/><meta name="theme-color" content="#f7f9fb"/><script defer="defer" src="/js/chunk-vendors.js"></script><script defer="defer" src="/js/app.js"></script><link href="/css/app.css" rel="stylesheet"></head><body class="dark:bg-gray-900"><noscript><strong>Enable JavaScript to view this page.</strong></noscript><div id="app"></div></body></html>
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user