mirror of
https://github.com/TwiN/gatus.git
synced 2024-12-14 11:58:04 +00:00
Merge branch 'master' into feature/teams_alert_provider
This commit is contained in:
commit
54d06b8688
13 changed files with 395 additions and 117 deletions
33
.github/workflows/publish-latest.yml
vendored
Normal file
33
.github/workflows/publish-latest.yml
vendored
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
name: publish-latest
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows: ["build"]
|
||||||
|
types: [completed]
|
||||||
|
jobs:
|
||||||
|
publish-latest:
|
||||||
|
name: Publish latest
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||||
|
timeout-minutes: 30
|
||||||
|
steps:
|
||||||
|
- name: Check out code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Get image repository
|
||||||
|
run: echo IMAGE_REPOSITORY=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v1
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
- name: Login to Docker Registry
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
- name: Build and push docker image
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
platforms: linux/amd64
|
||||||
|
pull: true
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
${{ env.IMAGE_REPOSITORY }}:latest
|
|
@ -1,10 +1,10 @@
|
||||||
name: publish
|
name: publish-release
|
||||||
on:
|
on:
|
||||||
release:
|
release:
|
||||||
types: [published]
|
types: [published]
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
publish-release:
|
||||||
name: Publish
|
name: Publish release
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
steps:
|
steps:
|
57
README.md
57
README.md
|
@ -33,6 +33,8 @@ For more details, see [Usage](#usage)
|
||||||
- [Conditions](#conditions)
|
- [Conditions](#conditions)
|
||||||
- [Placeholders](#placeholders)
|
- [Placeholders](#placeholders)
|
||||||
- [Functions](#functions)
|
- [Functions](#functions)
|
||||||
|
- [Storage](#storage)
|
||||||
|
- [Client configuration](#client-configuration)
|
||||||
- [Alerting](#alerting)
|
- [Alerting](#alerting)
|
||||||
- [Configuring Slack alerts](#configuring-slack-alerts)
|
- [Configuring Slack alerts](#configuring-slack-alerts)
|
||||||
- [Configuring Discord alerts](#configuring-discord-alerts)
|
- [Configuring Discord alerts](#configuring-discord-alerts)
|
||||||
|
@ -143,7 +145,6 @@ If you want to test it locally, see [Docker](#docker).
|
||||||
| `services[].group` | Group name. Used to group multiple services together on the dashboard. See [Service groups](#service-groups). | `""` |
|
| `services[].group` | Group name. Used to group multiple services together on the dashboard. See [Service groups](#service-groups). | `""` |
|
||||||
| `services[].url` | URL to send the request to. | Required `""` |
|
| `services[].url` | URL to send the request to. | Required `""` |
|
||||||
| `services[].method` | Request method. | `GET` |
|
| `services[].method` | Request method. | `GET` |
|
||||||
| `services[].insecure` | Whether to skip verifying the server's certificate chain and host name. | `false` |
|
|
||||||
| `services[].conditions` | Conditions used to determine the health of the service. See [Conditions](#conditions). | `[]` |
|
| `services[].conditions` | Conditions used to determine the health of the service. See [Conditions](#conditions). | `[]` |
|
||||||
| `services[].interval` | Duration to wait between every status check. | `60s` |
|
| `services[].interval` | Duration to wait between every status check. | `60s` |
|
||||||
| `services[].graphql` | Whether to wrap the body in a query param (`{"query":"$body"}`). | `false` |
|
| `services[].graphql` | Whether to wrap the body in a query param (`{"query":"$body"}`). | `false` |
|
||||||
|
@ -158,6 +159,7 @@ If you want to test it locally, see [Docker](#docker).
|
||||||
| `services[].alerts[].success-threshold` | Number of successes in a row before an ongoing incident is marked as resolved. | `2` |
|
| `services[].alerts[].success-threshold` | Number of successes in a row before an ongoing incident is marked as resolved. | `2` |
|
||||||
| `services[].alerts[].send-on-resolved` | Whether to send a notification once a triggered alert is marked as resolved. | `false` |
|
| `services[].alerts[].send-on-resolved` | Whether to send a notification once a triggered alert is marked as resolved. | `false` |
|
||||||
| `services[].alerts[].description` | Description of the alert. Will be included in the alert sent. | `""` |
|
| `services[].alerts[].description` | Description of the alert. Will be included in the alert sent. | `""` |
|
||||||
|
| `services[].client` | Client configuration. See [Client configuration](#client-configuration). | `{}` |
|
||||||
| `alerting` | Configuration for alerting. See [Alerting](#alerting). | `{}` |
|
| `alerting` | Configuration for alerting. See [Alerting](#alerting). | `{}` |
|
||||||
| `security` | Security configuration. | `{}` |
|
| `security` | Security configuration. | `{}` |
|
||||||
| `security.basic` | Basic authentication security configuration. | `{}` |
|
| `security.basic` | Basic authentication security configuration. | `{}` |
|
||||||
|
@ -239,6 +241,42 @@ storage:
|
||||||
See [examples/docker-compose-sqlite-storage](examples/docker-compose-sqlite-storage) for an example.
|
See [examples/docker-compose-sqlite-storage](examples/docker-compose-sqlite-storage) for an example.
|
||||||
|
|
||||||
|
|
||||||
|
### Client configuration
|
||||||
|
In order to support a wide range of environments, each monitored service has a unique configuration for
|
||||||
|
the client used to send the request.
|
||||||
|
|
||||||
|
| Parameter | Description | Default |
|
||||||
|
|:-------------------------|:----------------------------------------------------------------------------- |:-------------- |
|
||||||
|
| `client.insecure` | Whether to skip verifying the server's certificate chain and host name. | `false` |
|
||||||
|
| `client.ignore-follow` | Whether to ignore redirects (true) or follow them (false, default). | `false` |
|
||||||
|
| `client.timeout` | Duration before timing out. | `10s` |
|
||||||
|
|
||||||
|
Note that some of these parameters are ignored based on the type of service. For instance, there's no certificate involved
|
||||||
|
in ICMP requests (ping), therefore, setting `client.insecure` to `true` for a service of that type will not do anything.
|
||||||
|
|
||||||
|
This default configuration is as follows:
|
||||||
|
```yaml
|
||||||
|
client:
|
||||||
|
insecure: false
|
||||||
|
ignore-follow: false
|
||||||
|
timeout: 10s
|
||||||
|
```
|
||||||
|
Note that this configuration is only available under `services[]`, `alerting.mattermost` and `alerting.custom`.
|
||||||
|
|
||||||
|
Here's an example with the client configuration under `service[]`:
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
- name: twinnation
|
||||||
|
url: "https://twinnation.org/health"
|
||||||
|
client:
|
||||||
|
insecure: false
|
||||||
|
ignore-follow: false
|
||||||
|
timeout: 10s
|
||||||
|
conditions:
|
||||||
|
- "[STATUS] == 200"
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
### Alerting
|
### Alerting
|
||||||
Gatus supports multiple alerting providers, such as Slack and PagerDuty, and supports different alerts for each
|
Gatus supports multiple alerting providers, such as Slack and PagerDuty, and supports different alerts for each
|
||||||
individual services with configurable descriptions and thresholds.
|
individual services with configurable descriptions and thresholds.
|
||||||
|
@ -261,7 +299,7 @@ ignored.
|
||||||
| `alerting.twilio.to` | Number to send twilio alerts to | Required `""` |
|
| `alerting.twilio.to` | Number to send twilio alerts to | Required `""` |
|
||||||
| `alerting.mattermost` | Configuration for alerts of type `mattermost` | `{}` |
|
| `alerting.mattermost` | Configuration for alerts of type `mattermost` | `{}` |
|
||||||
| `alerting.mattermost.webhook-url` | Mattermost Webhook URL | Required `""` |
|
| `alerting.mattermost.webhook-url` | Mattermost Webhook URL | Required `""` |
|
||||||
| `alerting.mattermost.insecure` | Whether to skip verifying the server's certificate chain and host name | `false` |
|
| `alerting.mattermost.client` | Client configuration. See [Client configuration](#client-configuration). | `{}` |
|
||||||
| `alerting.messagebird` | Settings for alerts of type `messagebird` | `{}` |
|
| `alerting.messagebird` | Settings for alerts of type `messagebird` | `{}` |
|
||||||
| `alerting.messagebird.access-key` | Messagebird access key | Required `""` |
|
| `alerting.messagebird.access-key` | Messagebird access key | Required `""` |
|
||||||
| `alerting.messagebird.originator` | The sender of the message | Required `""` |
|
| `alerting.messagebird.originator` | The sender of the message | Required `""` |
|
||||||
|
@ -274,9 +312,9 @@ ignored.
|
||||||
| `alerting.custom` | Configuration for custom actions on failure or alerts | `{}` |
|
| `alerting.custom` | Configuration for custom actions on failure or alerts | `{}` |
|
||||||
| `alerting.custom.url` | Custom alerting request url | Required `""` |
|
| `alerting.custom.url` | Custom alerting request url | Required `""` |
|
||||||
| `alerting.custom.method` | Request method | `GET` |
|
| `alerting.custom.method` | Request method | `GET` |
|
||||||
| `alerting.custom.insecure` | Whether to skip verifying the server's certificate chain and host name | `false` |
|
|
||||||
| `alerting.custom.body` | Custom alerting request body. | `""` |
|
| `alerting.custom.body` | Custom alerting request body. | `""` |
|
||||||
| `alerting.custom.headers` | Custom alerting request headers | `{}` |
|
| `alerting.custom.headers` | Custom alerting request headers | `{}` |
|
||||||
|
| `alerting.custom.client` | Client configuration. See [Client configuration](#client-configuration). | `{}` |
|
||||||
| `alerting.*.default-alert.enabled` | Whether to enable the alert | N/A |
|
| `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.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.success-threshold` | Number of successes in a row before an ongoing incident is marked as resolved | N/A |
|
||||||
|
@ -397,7 +435,8 @@ services:
|
||||||
alerting:
|
alerting:
|
||||||
mattermost:
|
mattermost:
|
||||||
webhook-url: "http://**********/hooks/**********"
|
webhook-url: "http://**********/hooks/**********"
|
||||||
insecure: true
|
client:
|
||||||
|
insecure: true
|
||||||
|
|
||||||
services:
|
services:
|
||||||
- name: twinnation
|
- name: twinnation
|
||||||
|
@ -517,7 +556,6 @@ alerting:
|
||||||
custom:
|
custom:
|
||||||
url: "https://hooks.slack.com/services/**********/**********/**********"
|
url: "https://hooks.slack.com/services/**********/**********/**********"
|
||||||
method: "POST"
|
method: "POST"
|
||||||
insecure: true
|
|
||||||
body: |
|
body: |
|
||||||
{
|
{
|
||||||
"text": "[ALERT_TRIGGERED_OR_RESOLVED]: [SERVICE_NAME] - [ALERT_DESCRIPTION]"
|
"text": "[ALERT_TRIGGERED_OR_RESOLVED]: [SERVICE_NAME] - [ALERT_DESCRIPTION]"
|
||||||
|
@ -766,7 +804,7 @@ such as 1ms. You'll notice that the response time does not fluctuate - that is b
|
||||||
different goroutines, there's a global lock that prevents multiple services from running at the same time.
|
different goroutines, there's a global lock that prevents multiple services from running at the same time.
|
||||||
|
|
||||||
Unfortunately, there is a drawback. If you have a lot of services, including some that are very slow or prone to time out (the default
|
Unfortunately, there is a drawback. If you have a lot of services, including some that are very slow or prone to time out (the default
|
||||||
time out is 10s for HTTP and 5s for TCP), then it means that for the entire duration of the request, no other services can be evaluated.
|
timeout is 10s), then it means that for the entire duration of the request, no other services can be evaluated.
|
||||||
|
|
||||||
**This does mean that Gatus will be unable to evaluate the health of other services**.
|
**This does mean that Gatus will be unable to evaluate the health of other services**.
|
||||||
The interval does not include the duration of the request itself, which means that if a service has an interval of 30s
|
The interval does not include the duration of the request itself, which means that if a service has an interval of 30s
|
||||||
|
@ -789,11 +827,13 @@ simple health checks used for alerting (PagerDuty/Twilio) to `30s`.
|
||||||
| Protocol | Timeout |
|
| Protocol | Timeout |
|
||||||
|:-------- |:------- |
|
|:-------- |:------- |
|
||||||
| HTTP | 10s
|
| HTTP | 10s
|
||||||
| TCP | 5s
|
| TCP | 10s
|
||||||
|
| ICMP | 10s
|
||||||
|
|
||||||
|
To modify the timeout, see [Client configuration](#client-configuration).
|
||||||
|
|
||||||
|
|
||||||
### Monitoring a TCP service
|
### Monitoring a TCP service
|
||||||
|
|
||||||
By prefixing `services[].url` with `tcp:\\`, you can monitor TCP services at a very basic level:
|
By prefixing `services[].url` with `tcp:\\`, you can monitor TCP services at a very basic level:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
@ -1033,4 +1073,3 @@ No such header is required to query the API.
|
||||||
You can find the full list of sponsors [here](https://github.com/sponsors/TwinProduction).
|
You can find the full list of sponsors [here](https://github.com/sponsors/TwinProduction).
|
||||||
|
|
||||||
[<img src="https://github.com/math280h.png" width="35" />](https://github.com/math280h)
|
[<img src="https://github.com/math280h.png" width="35" />](https://github.com/math280h)
|
||||||
[<img src="https://github.com/mateothegreat.png" width="35" />](https://github.com/mateothegreat)
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -19,18 +20,29 @@ import (
|
||||||
type AlertProvider struct {
|
type AlertProvider struct {
|
||||||
URL string `yaml:"url"`
|
URL string `yaml:"url"`
|
||||||
Method string `yaml:"method,omitempty"`
|
Method string `yaml:"method,omitempty"`
|
||||||
Insecure bool `yaml:"insecure,omitempty"`
|
Insecure bool `yaml:"insecure,omitempty"` // deprecated
|
||||||
Body string `yaml:"body,omitempty"`
|
Body string `yaml:"body,omitempty"`
|
||||||
Headers map[string]string `yaml:"headers,omitempty"`
|
Headers map[string]string `yaml:"headers,omitempty"`
|
||||||
Placeholders map[string]map[string]string `yaml:"placeholders,omitempty"`
|
Placeholders map[string]map[string]string `yaml:"placeholders,omitempty"`
|
||||||
|
|
||||||
|
// ClientConfig is the configuration of the client used to communicate with the provider's target
|
||||||
|
ClientConfig *client.Config `yaml:"client"`
|
||||||
|
|
||||||
// DefaultAlert is the default alert configuration to use for services with an alert of the appropriate type
|
// DefaultAlert is the default alert configuration to use for services with an alert of the appropriate type
|
||||||
DefaultAlert *alert.Alert `yaml:"default-alert"`
|
DefaultAlert *alert.Alert `yaml:"default-alert"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsValid returns whether the provider's configuration is valid
|
// IsValid returns whether the provider's configuration is valid
|
||||||
func (provider *AlertProvider) IsValid() bool {
|
func (provider *AlertProvider) IsValid() bool {
|
||||||
return len(provider.URL) > 0
|
if provider.ClientConfig == nil {
|
||||||
|
provider.ClientConfig = client.GetDefaultConfig()
|
||||||
|
// XXX: remove the next 3 lines in v3.0.0
|
||||||
|
if provider.Insecure {
|
||||||
|
log.Println("WARNING: alerting.*.insecure has been deprecated and will be removed in v3.0.0 in favor of alerting.*.client.insecure")
|
||||||
|
provider.ClientConfig.Insecure = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return len(provider.URL) > 0 && provider.ClientConfig != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToCustomAlertProvider converts the provider into a custom.AlertProvider
|
// ToCustomAlertProvider converts the provider into a custom.AlertProvider
|
||||||
|
@ -103,7 +115,7 @@ func (provider *AlertProvider) Send(serviceName, alertDescription string, resolv
|
||||||
return []byte("{}"), nil
|
return []byte("{}"), nil
|
||||||
}
|
}
|
||||||
request := provider.buildHTTPRequest(serviceName, alertDescription, resolved)
|
request := provider.buildHTTPRequest(serviceName, alertDescription, resolved)
|
||||||
response, err := client.GetHTTPClient(provider.Insecure).Do(request)
|
response, err := client.GetHTTPClient(provider.ClientConfig).Do(request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,17 +2,22 @@ package mattermost
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/TwinProduction/gatus/alerting/alert"
|
"github.com/TwinProduction/gatus/alerting/alert"
|
||||||
"github.com/TwinProduction/gatus/alerting/provider/custom"
|
"github.com/TwinProduction/gatus/alerting/provider/custom"
|
||||||
|
"github.com/TwinProduction/gatus/client"
|
||||||
"github.com/TwinProduction/gatus/core"
|
"github.com/TwinProduction/gatus/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AlertProvider is the configuration necessary for sending an alert using Mattermost
|
// AlertProvider is the configuration necessary for sending an alert using Mattermost
|
||||||
type AlertProvider struct {
|
type AlertProvider struct {
|
||||||
WebhookURL string `yaml:"webhook-url"`
|
WebhookURL string `yaml:"webhook-url"`
|
||||||
Insecure bool `yaml:"insecure,omitempty"`
|
Insecure bool `yaml:"insecure,omitempty"` // deprecated
|
||||||
|
|
||||||
|
// ClientConfig is the configuration of the client used to communicate with the provider's target
|
||||||
|
ClientConfig *client.Config `yaml:"client"`
|
||||||
|
|
||||||
// DefaultAlert is the default alert configuration to use for services with an alert of the appropriate type
|
// DefaultAlert is the default alert configuration to use for services with an alert of the appropriate type
|
||||||
DefaultAlert *alert.Alert `yaml:"default-alert"`
|
DefaultAlert *alert.Alert `yaml:"default-alert"`
|
||||||
|
@ -20,6 +25,14 @@ type AlertProvider struct {
|
||||||
|
|
||||||
// IsValid returns whether the provider's configuration is valid
|
// IsValid returns whether the provider's configuration is valid
|
||||||
func (provider *AlertProvider) IsValid() bool {
|
func (provider *AlertProvider) IsValid() bool {
|
||||||
|
if provider.ClientConfig == nil {
|
||||||
|
provider.ClientConfig = client.GetDefaultConfig()
|
||||||
|
// XXX: remove the next 3 lines in v3.0.0
|
||||||
|
if provider.Insecure {
|
||||||
|
log.Println("WARNING: alerting.mattermost.insecure has been deprecated and will be removed in v3.0.0 in favor of alerting.mattermost.client.insecure")
|
||||||
|
provider.ClientConfig.Insecure = true
|
||||||
|
}
|
||||||
|
}
|
||||||
return len(provider.WebhookURL) > 0
|
return len(provider.WebhookURL) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,9 +58,9 @@ func (provider *AlertProvider) ToCustomAlertProvider(service *core.Service, aler
|
||||||
results += fmt.Sprintf("%s - `%s`\\n", prefix, conditionResult.Condition)
|
results += fmt.Sprintf("%s - `%s`\\n", prefix, conditionResult.Condition)
|
||||||
}
|
}
|
||||||
return &custom.AlertProvider{
|
return &custom.AlertProvider{
|
||||||
URL: provider.WebhookURL,
|
URL: provider.WebhookURL,
|
||||||
Method: http.MethodPost,
|
Method: http.MethodPost,
|
||||||
Insecure: provider.Insecure,
|
ClientConfig: provider.ClientConfig,
|
||||||
Body: fmt.Sprintf(`{
|
Body: fmt.Sprintf(`{
|
||||||
"text": "",
|
"text": "",
|
||||||
"username": "gatus",
|
"username": "gatus",
|
||||||
|
|
|
@ -14,58 +14,14 @@ import (
|
||||||
"github.com/go-ping/ping"
|
"github.com/go-ping/ping"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
secureHTTPClient *http.Client
|
|
||||||
insecureHTTPClient *http.Client
|
|
||||||
|
|
||||||
// pingTimeout is the timeout for the Ping function
|
|
||||||
// This is mainly exposed for testing purposes
|
|
||||||
pingTimeout = 5 * time.Second
|
|
||||||
|
|
||||||
// httpTimeout is the timeout for secureHTTPClient and insecureHTTPClient
|
|
||||||
httpTimeout = 10 * time.Second
|
|
||||||
)
|
|
||||||
|
|
||||||
// GetHTTPClient returns the shared HTTP client
|
// GetHTTPClient returns the shared HTTP client
|
||||||
func GetHTTPClient(insecure bool) *http.Client {
|
func GetHTTPClient(config *Config) *http.Client {
|
||||||
if insecure {
|
return config.GetHTTPClient()
|
||||||
if insecureHTTPClient == nil {
|
|
||||||
insecureHTTPClient = &http.Client{
|
|
||||||
Timeout: httpTimeout,
|
|
||||||
Transport: &http.Transport{
|
|
||||||
MaxIdleConns: 100,
|
|
||||||
MaxIdleConnsPerHost: 20,
|
|
||||||
Proxy: http.ProxyFromEnvironment,
|
|
||||||
TLSClientConfig: &tls.Config{
|
|
||||||
InsecureSkipVerify: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
|
||||||
return http.ErrUseLastResponse // Don't follow redirects
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return insecureHTTPClient
|
|
||||||
}
|
|
||||||
if secureHTTPClient == nil {
|
|
||||||
secureHTTPClient = &http.Client{
|
|
||||||
Timeout: httpTimeout,
|
|
||||||
Transport: &http.Transport{
|
|
||||||
MaxIdleConns: 100,
|
|
||||||
MaxIdleConnsPerHost: 20,
|
|
||||||
Proxy: http.ProxyFromEnvironment,
|
|
||||||
},
|
|
||||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
|
||||||
return http.ErrUseLastResponse // Don't follow redirects
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return secureHTTPClient
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CanCreateTCPConnection checks whether a connection can be established with a TCP service
|
// CanCreateTCPConnection checks whether a connection can be established with a TCP service
|
||||||
func CanCreateTCPConnection(address string) bool {
|
func CanCreateTCPConnection(address string, config *Config) bool {
|
||||||
conn, err := net.DialTimeout("tcp", address, 5*time.Second)
|
conn, err := net.DialTimeout("tcp", address, config.Timeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -74,7 +30,7 @@ func CanCreateTCPConnection(address string) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// CanPerformStartTLS checks whether a connection can be established to an address using the STARTTLS protocol
|
// CanPerformStartTLS checks whether a connection can be established to an address using the STARTTLS protocol
|
||||||
func CanPerformStartTLS(address string, insecure bool) (connected bool, certificate *x509.Certificate, err error) {
|
func CanPerformStartTLS(address string, config *Config) (connected bool, certificate *x509.Certificate, err error) {
|
||||||
hostAndPort := strings.Split(address, ":")
|
hostAndPort := strings.Split(address, ":")
|
||||||
if len(hostAndPort) != 2 {
|
if len(hostAndPort) != 2 {
|
||||||
return false, nil, errors.New("invalid address for starttls, format must be host:port")
|
return false, nil, errors.New("invalid address for starttls, format must be host:port")
|
||||||
|
@ -84,7 +40,7 @@ func CanPerformStartTLS(address string, insecure bool) (connected bool, certific
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = smtpClient.StartTLS(&tls.Config{
|
err = smtpClient.StartTLS(&tls.Config{
|
||||||
InsecureSkipVerify: insecure,
|
InsecureSkipVerify: config.Insecure,
|
||||||
ServerName: hostAndPort[0],
|
ServerName: hostAndPort[0],
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -101,13 +57,13 @@ func CanPerformStartTLS(address string, insecure bool) (connected bool, certific
|
||||||
// Ping checks if an address can be pinged and returns the round-trip time if the address can be pinged
|
// Ping checks if an address can be pinged and returns the round-trip time if the address can be pinged
|
||||||
//
|
//
|
||||||
// Note that this function takes at least 100ms, even if the address is 127.0.0.1
|
// Note that this function takes at least 100ms, even if the address is 127.0.0.1
|
||||||
func Ping(address string) (bool, time.Duration) {
|
func Ping(address string, config *Config) (bool, time.Duration) {
|
||||||
pinger, err := ping.NewPinger(address)
|
pinger, err := ping.NewPinger(address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, 0
|
return false, 0
|
||||||
}
|
}
|
||||||
pinger.Count = 1
|
pinger.Count = 1
|
||||||
pinger.Timeout = pingTimeout
|
pinger.Timeout = config.Timeout
|
||||||
// Set the pinger's privileged mode to true for every operating system except darwin
|
// Set the pinger's privileged mode to true for every operating system except darwin
|
||||||
// https://github.com/TwinProduction/gatus/issues/132
|
// https://github.com/TwinProduction/gatus/issues/132
|
||||||
pinger.SetPrivileged(runtime.GOOS != "darwin")
|
pinger.SetPrivileged(runtime.GOOS != "darwin")
|
||||||
|
|
|
@ -6,43 +6,28 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGetHTTPClient(t *testing.T) {
|
func TestGetHTTPClient(t *testing.T) {
|
||||||
if secureHTTPClient != nil {
|
GetHTTPClient(&Config{
|
||||||
t.Error("secureHTTPClient should've been nil since it hasn't been called a single time yet")
|
Insecure: false,
|
||||||
}
|
IgnoreRedirect: false,
|
||||||
if insecureHTTPClient != nil {
|
Timeout: 0,
|
||||||
t.Error("insecureHTTPClient should've been nil since it hasn't been called a single time yet")
|
httpClient: nil,
|
||||||
}
|
})
|
||||||
_ = GetHTTPClient(false)
|
|
||||||
if secureHTTPClient == nil {
|
|
||||||
t.Error("secureHTTPClient shouldn't have been nil, since it has been called once")
|
|
||||||
}
|
|
||||||
if insecureHTTPClient != nil {
|
|
||||||
t.Error("insecureHTTPClient should've been nil since it hasn't been called a single time yet")
|
|
||||||
}
|
|
||||||
_ = GetHTTPClient(true)
|
|
||||||
if secureHTTPClient == nil {
|
|
||||||
t.Error("secureHTTPClient shouldn't have been nil, since it has been called once")
|
|
||||||
}
|
|
||||||
if insecureHTTPClient == nil {
|
|
||||||
t.Error("insecureHTTPClient shouldn't have been nil, since it has been called once")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPing(t *testing.T) {
|
func TestPing(t *testing.T) {
|
||||||
pingTimeout = 500 * time.Millisecond
|
if success, rtt := Ping("127.0.0.1", &Config{Timeout: 500 * time.Millisecond}); !success {
|
||||||
if success, rtt := Ping("127.0.0.1"); !success {
|
|
||||||
t.Error("expected true")
|
t.Error("expected true")
|
||||||
if rtt == 0 {
|
if rtt == 0 {
|
||||||
t.Error("Round-trip time returned on success should've higher than 0")
|
t.Error("Round-trip time returned on success should've higher than 0")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if success, rtt := Ping("256.256.256.256"); success {
|
if success, rtt := Ping("256.256.256.256", &Config{Timeout: 500 * time.Millisecond}); success {
|
||||||
t.Error("expected false, because the IP is invalid")
|
t.Error("expected false, because the IP is invalid")
|
||||||
if rtt != 0 {
|
if rtt != 0 {
|
||||||
t.Error("Round-trip time returned on failure should've been 0")
|
t.Error("Round-trip time returned on failure should've been 0")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if success, rtt := Ping("192.168.152.153"); success {
|
if success, rtt := Ping("192.168.152.153", &Config{Timeout: 500 * time.Millisecond}); success {
|
||||||
t.Error("expected false, because the IP is valid but the host should be unreachable")
|
t.Error("expected false, because the IP is valid but the host should be unreachable")
|
||||||
if rtt != 0 {
|
if rtt != 0 {
|
||||||
t.Error("Round-trip time returned on failure should've been 0")
|
t.Error("Round-trip time returned on failure should've been 0")
|
||||||
|
@ -88,7 +73,7 @@ func TestCanPerformStartTLS(t *testing.T) {
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
connected, _, err := CanPerformStartTLS(tt.args.address, tt.args.insecure)
|
connected, _, err := CanPerformStartTLS(tt.args.address, &Config{Insecure: tt.args.insecure, Timeout: 5 * time.Second})
|
||||||
if (err != nil) != tt.wantErr {
|
if (err != nil) != tt.wantErr {
|
||||||
t.Errorf("CanPerformStartTLS() err=%v, wantErr=%v", err, tt.wantErr)
|
t.Errorf("CanPerformStartTLS() err=%v, wantErr=%v", err, tt.wantErr)
|
||||||
return
|
return
|
||||||
|
@ -101,7 +86,7 @@ func TestCanPerformStartTLS(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCanCreateTCPConnection(t *testing.T) {
|
func TestCanCreateTCPConnection(t *testing.T) {
|
||||||
if CanCreateTCPConnection("127.0.0.1") {
|
if CanCreateTCPConnection("127.0.0.1", &Config{Timeout: 5 * time.Second}) {
|
||||||
t.Error("should've failed, because there's no port in the address")
|
t.Error("should've failed, because there's no port in the address")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
73
client/config.go
Normal file
73
client/config.go
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultHTTPTimeout = 10 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// DefaultConfig is the default client configuration
|
||||||
|
defaultConfig = Config{
|
||||||
|
Insecure: false,
|
||||||
|
IgnoreRedirect: false,
|
||||||
|
Timeout: defaultHTTPTimeout,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetDefaultConfig returns a copy of the default configuration
|
||||||
|
func GetDefaultConfig() *Config {
|
||||||
|
cfg := defaultConfig
|
||||||
|
return &cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config is the configuration for clients
|
||||||
|
type Config struct {
|
||||||
|
// Insecure determines whether to skip verifying the server's certificate chain and host name
|
||||||
|
Insecure bool `yaml:"insecure"`
|
||||||
|
|
||||||
|
// IgnoreRedirect determines whether to ignore redirects (true) or follow them (false, default)
|
||||||
|
IgnoreRedirect bool `yaml:"ignore-redirect"`
|
||||||
|
|
||||||
|
// Timeout for the client
|
||||||
|
Timeout time.Duration `yaml:"timeout"`
|
||||||
|
|
||||||
|
httpClient *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateAndSetDefaults validates the client configuration and sets the default values if necessary
|
||||||
|
func (c *Config) ValidateAndSetDefaults() {
|
||||||
|
if c.Timeout < time.Millisecond {
|
||||||
|
c.Timeout = 10 * time.Second
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHTTPClient return a HTTP client matching the Config's parameters.
|
||||||
|
func (c *Config) GetHTTPClient() *http.Client {
|
||||||
|
if c.httpClient == nil {
|
||||||
|
c.httpClient = &http.Client{
|
||||||
|
Timeout: c.Timeout,
|
||||||
|
Transport: &http.Transport{
|
||||||
|
MaxIdleConns: 100,
|
||||||
|
MaxIdleConnsPerHost: 20,
|
||||||
|
Proxy: http.ProxyFromEnvironment,
|
||||||
|
TLSClientConfig: &tls.Config{
|
||||||
|
InsecureSkipVerify: c.Insecure,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||||
|
if c.IgnoreRedirect {
|
||||||
|
// Don't follow redirects
|
||||||
|
return http.ErrUseLastResponse
|
||||||
|
}
|
||||||
|
// Follow redirects
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return c.httpClient
|
||||||
|
}
|
37
client/config_test.go
Normal file
37
client/config_test.go
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConfig_GetHTTPClient(t *testing.T) {
|
||||||
|
insecureConfig := &Config{Insecure: true}
|
||||||
|
insecureConfig.ValidateAndSetDefaults()
|
||||||
|
insecureClient := insecureConfig.GetHTTPClient()
|
||||||
|
if !(insecureClient.Transport).(*http.Transport).TLSClientConfig.InsecureSkipVerify {
|
||||||
|
t.Error("expected Config.Insecure set to true to cause the HTTP client to skip certificate verification")
|
||||||
|
}
|
||||||
|
if insecureClient.Timeout != defaultHTTPTimeout {
|
||||||
|
t.Error("expected Config.Timeout to default the HTTP client to a timeout of 10s")
|
||||||
|
}
|
||||||
|
request, _ := http.NewRequest("GET", "", nil)
|
||||||
|
if err := insecureClient.CheckRedirect(request, nil); err != nil {
|
||||||
|
t.Error("expected Config.IgnoreRedirect set to false to cause the HTTP client's CheckRedirect to return nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
secureConfig := &Config{IgnoreRedirect: true, Timeout: 5 * time.Second}
|
||||||
|
secureConfig.ValidateAndSetDefaults()
|
||||||
|
secureClient := secureConfig.GetHTTPClient()
|
||||||
|
if (secureClient.Transport).(*http.Transport).TLSClientConfig.InsecureSkipVerify {
|
||||||
|
t.Error("expected Config.Insecure set to false to cause the HTTP client to not skip certificate verification")
|
||||||
|
}
|
||||||
|
if secureClient.Timeout != 5*time.Second {
|
||||||
|
t.Error("expected Config.Timeout to cause the HTTP client to have a timeout of 5s")
|
||||||
|
}
|
||||||
|
request, _ = http.NewRequest("GET", "", nil)
|
||||||
|
if err := secureClient.CheckRedirect(request, nil); err != http.ErrUseLastResponse {
|
||||||
|
t.Error("expected Config.IgnoreRedirect set to true to cause the HTTP client's CheckRedirect to return http.ErrUseLastResponse")
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,6 +17,7 @@ import (
|
||||||
"github.com/TwinProduction/gatus/alerting/provider/telegram"
|
"github.com/TwinProduction/gatus/alerting/provider/telegram"
|
||||||
"github.com/TwinProduction/gatus/alerting/provider/twilio"
|
"github.com/TwinProduction/gatus/alerting/provider/twilio"
|
||||||
"github.com/TwinProduction/gatus/alerting/provider/teams"
|
"github.com/TwinProduction/gatus/alerting/provider/teams"
|
||||||
|
"github.com/TwinProduction/gatus/client"
|
||||||
"github.com/TwinProduction/gatus/core"
|
"github.com/TwinProduction/gatus/core"
|
||||||
"github.com/TwinProduction/gatus/k8stest"
|
"github.com/TwinProduction/gatus/k8stest"
|
||||||
v1 "k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
|
@ -41,17 +42,31 @@ func TestParseAndValidateConfigBytes(t *testing.T) {
|
||||||
config, err := parseAndValidateConfigBytes([]byte(fmt.Sprintf(`
|
config, err := parseAndValidateConfigBytes([]byte(fmt.Sprintf(`
|
||||||
storage:
|
storage:
|
||||||
file: %s
|
file: %s
|
||||||
|
|
||||||
services:
|
services:
|
||||||
- name: twinnation
|
- name: twinnation
|
||||||
url: https://twinnation.org/health
|
url: https://twinnation.org/health
|
||||||
interval: 15s
|
interval: 15s
|
||||||
conditions:
|
conditions:
|
||||||
- "[STATUS] == 200"
|
- "[STATUS] == 200"
|
||||||
|
|
||||||
- name: github
|
- name: github
|
||||||
url: https://api.github.com/healthz
|
url: https://api.github.com/healthz
|
||||||
|
client:
|
||||||
|
insecure: true
|
||||||
|
ignore-redirect: true
|
||||||
|
timeout: 5s
|
||||||
conditions:
|
conditions:
|
||||||
- "[STATUS] != 400"
|
- "[STATUS] != 400"
|
||||||
- "[STATUS] != 500"
|
- "[STATUS] != 500"
|
||||||
|
|
||||||
|
- name: example
|
||||||
|
url: https://example.com/
|
||||||
|
interval: 30m
|
||||||
|
client:
|
||||||
|
insecure: true
|
||||||
|
conditions:
|
||||||
|
- "[STATUS] == 200"
|
||||||
`, file)))
|
`, file)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error("expected no error, got", err.Error())
|
t.Error("expected no error, got", err.Error())
|
||||||
|
@ -59,33 +74,75 @@ services:
|
||||||
if config == nil {
|
if config == nil {
|
||||||
t.Fatal("Config shouldn't have been nil")
|
t.Fatal("Config shouldn't have been nil")
|
||||||
}
|
}
|
||||||
if len(config.Services) != 2 {
|
if len(config.Services) != 3 {
|
||||||
t.Error("Should have returned two services")
|
t.Error("Should have returned two services")
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.Services[0].URL != "https://twinnation.org/health" {
|
if config.Services[0].URL != "https://twinnation.org/health" {
|
||||||
t.Errorf("URL should have been %s", "https://twinnation.org/health")
|
t.Errorf("URL should have been %s", "https://twinnation.org/health")
|
||||||
}
|
}
|
||||||
if config.Services[1].URL != "https://api.github.com/healthz" {
|
|
||||||
t.Errorf("URL should have been %s", "https://api.github.com/healthz")
|
|
||||||
}
|
|
||||||
if config.Services[0].Method != "GET" {
|
if config.Services[0].Method != "GET" {
|
||||||
t.Errorf("Method should have been %s (default)", "GET")
|
t.Errorf("Method should have been %s (default)", "GET")
|
||||||
}
|
}
|
||||||
if config.Services[1].Method != "GET" {
|
|
||||||
t.Errorf("Method should have been %s (default)", "GET")
|
|
||||||
}
|
|
||||||
if config.Services[0].Interval != 15*time.Second {
|
if config.Services[0].Interval != 15*time.Second {
|
||||||
t.Errorf("Interval should have been %s", 15*time.Second)
|
t.Errorf("Interval should have been %s", 15*time.Second)
|
||||||
}
|
}
|
||||||
if config.Services[1].Interval != 60*time.Second {
|
if config.Services[0].ClientConfig.Insecure != client.GetDefaultConfig().Insecure {
|
||||||
t.Errorf("Interval should have been %s, because it is the default value", 60*time.Second)
|
t.Errorf("ClientConfig.Insecure should have been %v, got %v", true, config.Services[0].ClientConfig.Insecure)
|
||||||
|
}
|
||||||
|
if config.Services[0].ClientConfig.IgnoreRedirect != client.GetDefaultConfig().IgnoreRedirect {
|
||||||
|
t.Errorf("ClientConfig.IgnoreRedirect should have been %v, got %v", true, config.Services[0].ClientConfig.IgnoreRedirect)
|
||||||
|
}
|
||||||
|
if config.Services[0].ClientConfig.Timeout != client.GetDefaultConfig().Timeout {
|
||||||
|
t.Errorf("ClientConfig.Timeout should have been %v, got %v", client.GetDefaultConfig().Timeout, config.Services[0].ClientConfig.Timeout)
|
||||||
}
|
}
|
||||||
if len(config.Services[0].Conditions) != 1 {
|
if len(config.Services[0].Conditions) != 1 {
|
||||||
t.Errorf("There should have been %d conditions", 1)
|
t.Errorf("There should have been %d conditions", 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if config.Services[1].URL != "https://api.github.com/healthz" {
|
||||||
|
t.Errorf("URL should have been %s", "https://api.github.com/healthz")
|
||||||
|
}
|
||||||
|
if config.Services[1].Method != "GET" {
|
||||||
|
t.Errorf("Method should have been %s (default)", "GET")
|
||||||
|
}
|
||||||
|
if config.Services[1].Interval != 60*time.Second {
|
||||||
|
t.Errorf("Interval should have been %s, because it is the default value", 60*time.Second)
|
||||||
|
}
|
||||||
|
if !config.Services[1].ClientConfig.Insecure {
|
||||||
|
t.Errorf("ClientConfig.Insecure should have been %v, got %v", true, config.Services[1].ClientConfig.Insecure)
|
||||||
|
}
|
||||||
|
if !config.Services[1].ClientConfig.IgnoreRedirect {
|
||||||
|
t.Errorf("ClientConfig.IgnoreRedirect should have been %v, got %v", true, config.Services[1].ClientConfig.IgnoreRedirect)
|
||||||
|
}
|
||||||
|
if config.Services[1].ClientConfig.Timeout != 5*time.Second {
|
||||||
|
t.Errorf("ClientConfig.Timeout should have been %v, got %v", 5*time.Second, config.Services[1].ClientConfig.Timeout)
|
||||||
|
}
|
||||||
if len(config.Services[1].Conditions) != 2 {
|
if len(config.Services[1].Conditions) != 2 {
|
||||||
t.Errorf("There should have been %d conditions", 2)
|
t.Errorf("There should have been %d conditions", 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if config.Services[2].URL != "https://example.com/" {
|
||||||
|
t.Errorf("URL should have been %s", "https://example.com/")
|
||||||
|
}
|
||||||
|
if config.Services[2].Method != "GET" {
|
||||||
|
t.Errorf("Method should have been %s (default)", "GET")
|
||||||
|
}
|
||||||
|
if config.Services[2].Interval != 30*time.Minute {
|
||||||
|
t.Errorf("Interval should have been %s, because it is the default value", 30*time.Minute)
|
||||||
|
}
|
||||||
|
if !config.Services[2].ClientConfig.Insecure {
|
||||||
|
t.Errorf("ClientConfig.Insecure should have been %v, got %v", true, config.Services[2].ClientConfig.Insecure)
|
||||||
|
}
|
||||||
|
if config.Services[2].ClientConfig.IgnoreRedirect {
|
||||||
|
t.Errorf("ClientConfig.IgnoreRedirect should have been %v by default, got %v", false, config.Services[2].ClientConfig.IgnoreRedirect)
|
||||||
|
}
|
||||||
|
if config.Services[2].ClientConfig.Timeout != 10*time.Second {
|
||||||
|
t.Errorf("ClientConfig.Timeout should have been %v by default, got %v", 10*time.Second, config.Services[2].ClientConfig.Timeout)
|
||||||
|
}
|
||||||
|
if len(config.Services[2].Conditions) != 1 {
|
||||||
|
t.Errorf("There should have been %d conditions", 1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseAndValidateConfigBytesDefault(t *testing.T) {
|
func TestParseAndValidateConfigBytesDefault(t *testing.T) {
|
||||||
|
@ -105,17 +162,26 @@ services:
|
||||||
if config.Metrics {
|
if config.Metrics {
|
||||||
t.Error("Metrics should've been false by default")
|
t.Error("Metrics should've been false by default")
|
||||||
}
|
}
|
||||||
|
if config.Web.Address != DefaultAddress {
|
||||||
|
t.Errorf("Bind address should have been %s, because it is the default value", DefaultAddress)
|
||||||
|
}
|
||||||
|
if config.Web.Port != DefaultPort {
|
||||||
|
t.Errorf("Port should have been %d, because it is the default value", DefaultPort)
|
||||||
|
}
|
||||||
if config.Services[0].URL != "https://twinnation.org/health" {
|
if config.Services[0].URL != "https://twinnation.org/health" {
|
||||||
t.Errorf("URL should have been %s", "https://twinnation.org/health")
|
t.Errorf("URL should have been %s", "https://twinnation.org/health")
|
||||||
}
|
}
|
||||||
if config.Services[0].Interval != 60*time.Second {
|
if config.Services[0].Interval != 60*time.Second {
|
||||||
t.Errorf("Interval should have been %s, because it is the default value", 60*time.Second)
|
t.Errorf("Interval should have been %s, because it is the default value", 60*time.Second)
|
||||||
}
|
}
|
||||||
if config.Web.Address != DefaultAddress {
|
if config.Services[0].ClientConfig.Insecure != client.GetDefaultConfig().Insecure {
|
||||||
t.Errorf("Bind address should have been %s, because it is the default value", DefaultAddress)
|
t.Errorf("ClientConfig.Insecure should have been %v by default, got %v", true, config.Services[0].ClientConfig.Insecure)
|
||||||
}
|
}
|
||||||
if config.Web.Port != DefaultPort {
|
if config.Services[0].ClientConfig.IgnoreRedirect != client.GetDefaultConfig().IgnoreRedirect {
|
||||||
t.Errorf("Port should have been %d, because it is the default value", DefaultPort)
|
t.Errorf("ClientConfig.IgnoreRedirect should have been %v by default, got %v", true, config.Services[0].ClientConfig.IgnoreRedirect)
|
||||||
|
}
|
||||||
|
if config.Services[0].ClientConfig.Timeout != client.GetDefaultConfig().Timeout {
|
||||||
|
t.Errorf("ClientConfig.Timeout should have been %v by default, got %v", client.GetDefaultConfig().Timeout, config.Services[0].ClientConfig.Timeout)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -144,11 +210,9 @@ services:
|
||||||
if config.Services[0].Interval != 60*time.Second {
|
if config.Services[0].Interval != 60*time.Second {
|
||||||
t.Errorf("Interval should have been %s, because it is the default value", 60*time.Second)
|
t.Errorf("Interval should have been %s, because it is the default value", 60*time.Second)
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.Web.Address != "127.0.0.1" {
|
if config.Web.Address != "127.0.0.1" {
|
||||||
t.Errorf("Bind address should have been %s, because it is specified in config", "127.0.0.1")
|
t.Errorf("Bind address should have been %s, because it is specified in config", "127.0.0.1")
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.Web.Port != DefaultPort {
|
if config.Web.Port != DefaultPort {
|
||||||
t.Errorf("Port should have been %d, because it is the default value", DefaultPort)
|
t.Errorf("Port should have been %d, because it is the default value", DefaultPort)
|
||||||
}
|
}
|
||||||
|
@ -340,6 +404,8 @@ alerting:
|
||||||
integration-key: "00000000000000000000000000000000"
|
integration-key: "00000000000000000000000000000000"
|
||||||
mattermost:
|
mattermost:
|
||||||
webhook-url: "http://example.com"
|
webhook-url: "http://example.com"
|
||||||
|
client:
|
||||||
|
insecure: true
|
||||||
messagebird:
|
messagebird:
|
||||||
access-key: "1"
|
access-key: "1"
|
||||||
originator: "31619191918"
|
originator: "31619191918"
|
||||||
|
@ -940,6 +1006,9 @@ services:
|
||||||
if config.Alerting.Custom.Insecure {
|
if config.Alerting.Custom.Insecure {
|
||||||
t.Fatal("config.Alerting.Custom.Insecure shouldn't have been true")
|
t.Fatal("config.Alerting.Custom.Insecure shouldn't have been true")
|
||||||
}
|
}
|
||||||
|
if config.Alerting.Custom.ClientConfig.Insecure {
|
||||||
|
t.Errorf("ClientConfig.Insecure should have been %v, got %v", false, config.Alerting.Custom.ClientConfig.Insecure)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseAndValidateConfigBytesWithCustomAlertingConfigAndCustomPlaceholderValues(t *testing.T) {
|
func TestParseAndValidateConfigBytesWithCustomAlertingConfigAndCustomPlaceholderValues(t *testing.T) {
|
||||||
|
|
|
@ -30,7 +30,6 @@ var (
|
||||||
Interval: 30 * time.Second,
|
Interval: 30 * time.Second,
|
||||||
Conditions: []*core.Condition{&firstCondition, &secondCondition, &thirdCondition},
|
Conditions: []*core.Condition{&firstCondition, &secondCondition, &thirdCondition},
|
||||||
Alerts: nil,
|
Alerts: nil,
|
||||||
Insecure: false,
|
|
||||||
NumberOfFailuresInARow: 0,
|
NumberOfFailuresInARow: 0,
|
||||||
NumberOfSuccessesInARow: 0,
|
NumberOfSuccessesInARow: 0,
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
@ -78,8 +79,13 @@ type Service struct {
|
||||||
Alerts []*alert.Alert `yaml:"alerts"`
|
Alerts []*alert.Alert `yaml:"alerts"`
|
||||||
|
|
||||||
// Insecure is whether to skip verifying the server's certificate chain and host name
|
// Insecure is whether to skip verifying the server's certificate chain and host name
|
||||||
|
//
|
||||||
|
// deprecated
|
||||||
Insecure bool `yaml:"insecure,omitempty"`
|
Insecure bool `yaml:"insecure,omitempty"`
|
||||||
|
|
||||||
|
// ClientConfig is the configuration of the client used to communicate with the service's target
|
||||||
|
ClientConfig *client.Config `yaml:"client"`
|
||||||
|
|
||||||
// NumberOfFailuresInARow is the number of unsuccessful evaluations in a row
|
// NumberOfFailuresInARow is the number of unsuccessful evaluations in a row
|
||||||
NumberOfFailuresInARow int
|
NumberOfFailuresInARow int
|
||||||
|
|
||||||
|
@ -90,6 +96,16 @@ type Service struct {
|
||||||
// ValidateAndSetDefaults validates the service's configuration and sets the default value of fields that have one
|
// ValidateAndSetDefaults validates the service's configuration and sets the default value of fields that have one
|
||||||
func (service *Service) ValidateAndSetDefaults() error {
|
func (service *Service) ValidateAndSetDefaults() error {
|
||||||
// Set default values
|
// Set default values
|
||||||
|
if service.ClientConfig == nil {
|
||||||
|
service.ClientConfig = client.GetDefaultConfig()
|
||||||
|
// XXX: remove the next 3 lines in v3.0.0
|
||||||
|
if service.Insecure {
|
||||||
|
log.Println("WARNING: services[].insecure has been deprecated and will be removed in v3.0.0 in favor of services[].client.insecure")
|
||||||
|
service.ClientConfig.Insecure = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
service.ClientConfig.ValidateAndSetDefaults()
|
||||||
|
}
|
||||||
if service.Interval == 0 {
|
if service.Interval == 0 {
|
||||||
service.Interval = 1 * time.Minute
|
service.Interval = 1 * time.Minute
|
||||||
}
|
}
|
||||||
|
@ -199,7 +215,7 @@ func (service *Service) call(result *Result) {
|
||||||
service.DNS.query(service.URL, result)
|
service.DNS.query(service.URL, result)
|
||||||
result.Duration = time.Since(startTime)
|
result.Duration = time.Since(startTime)
|
||||||
} else if isServiceStartTLS {
|
} else if isServiceStartTLS {
|
||||||
result.Connected, certificate, err = client.CanPerformStartTLS(strings.TrimPrefix(service.URL, "starttls://"), service.Insecure)
|
result.Connected, certificate, err = client.CanPerformStartTLS(strings.TrimPrefix(service.URL, "starttls://"), service.ClientConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.AddError(err.Error())
|
result.AddError(err.Error())
|
||||||
return
|
return
|
||||||
|
@ -207,12 +223,12 @@ func (service *Service) call(result *Result) {
|
||||||
result.Duration = time.Since(startTime)
|
result.Duration = time.Since(startTime)
|
||||||
result.CertificateExpiration = time.Until(certificate.NotAfter)
|
result.CertificateExpiration = time.Until(certificate.NotAfter)
|
||||||
} else if isServiceTCP {
|
} else if isServiceTCP {
|
||||||
result.Connected = client.CanCreateTCPConnection(strings.TrimPrefix(service.URL, "tcp://"))
|
result.Connected = client.CanCreateTCPConnection(strings.TrimPrefix(service.URL, "tcp://"), service.ClientConfig)
|
||||||
result.Duration = time.Since(startTime)
|
result.Duration = time.Since(startTime)
|
||||||
} else if isServiceICMP {
|
} else if isServiceICMP {
|
||||||
result.Connected, result.Duration = client.Ping(strings.TrimPrefix(service.URL, "icmp://"))
|
result.Connected, result.Duration = client.Ping(strings.TrimPrefix(service.URL, "icmp://"), service.ClientConfig)
|
||||||
} else {
|
} else {
|
||||||
response, err = client.GetHTTPClient(service.Insecure).Do(request)
|
response, err = client.GetHTTPClient(service.ClientConfig).Do(request)
|
||||||
result.Duration = time.Since(startTime)
|
result.Duration = time.Since(startTime)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.AddError(err.Error())
|
result.AddError(err.Error())
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/TwinProduction/gatus/alerting/alert"
|
"github.com/TwinProduction/gatus/alerting/alert"
|
||||||
|
"github.com/TwinProduction/gatus/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestService_ValidateAndSetDefaults(t *testing.T) {
|
func TestService_ValidateAndSetDefaults(t *testing.T) {
|
||||||
|
@ -18,6 +19,19 @@ func TestService_ValidateAndSetDefaults(t *testing.T) {
|
||||||
Alerts: []*alert.Alert{{Type: alert.TypePagerDuty}},
|
Alerts: []*alert.Alert{{Type: alert.TypePagerDuty}},
|
||||||
}
|
}
|
||||||
service.ValidateAndSetDefaults()
|
service.ValidateAndSetDefaults()
|
||||||
|
if service.ClientConfig == nil {
|
||||||
|
t.Error("client configuration should've been set to the default configuration")
|
||||||
|
} else {
|
||||||
|
if service.ClientConfig.Insecure != client.GetDefaultConfig().Insecure {
|
||||||
|
t.Errorf("Default client configuration should've set Insecure to %v, got %v", client.GetDefaultConfig().Insecure, service.ClientConfig.Insecure)
|
||||||
|
}
|
||||||
|
if service.ClientConfig.IgnoreRedirect != client.GetDefaultConfig().IgnoreRedirect {
|
||||||
|
t.Errorf("Default client configuration should've set IgnoreRedirect to %v, got %v", client.GetDefaultConfig().IgnoreRedirect, service.ClientConfig.IgnoreRedirect)
|
||||||
|
}
|
||||||
|
if service.ClientConfig.Timeout != client.GetDefaultConfig().Timeout {
|
||||||
|
t.Errorf("Default client configuration should've set Timeout to %v, got %v", client.GetDefaultConfig().Timeout, service.ClientConfig.Timeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
if service.Method != "GET" {
|
if service.Method != "GET" {
|
||||||
t.Error("Service method should've defaulted to GET")
|
t.Error("Service method should've defaulted to GET")
|
||||||
}
|
}
|
||||||
|
@ -41,6 +55,34 @@ func TestService_ValidateAndSetDefaults(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestService_ValidateAndSetDefaultsWithClientConfig(t *testing.T) {
|
||||||
|
condition := Condition("[STATUS] == 200")
|
||||||
|
service := Service{
|
||||||
|
Name: "twinnation-health",
|
||||||
|
URL: "https://twinnation.org/health",
|
||||||
|
Conditions: []*Condition{&condition},
|
||||||
|
ClientConfig: &client.Config{
|
||||||
|
Insecure: true,
|
||||||
|
IgnoreRedirect: true,
|
||||||
|
Timeout: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
service.ValidateAndSetDefaults()
|
||||||
|
if service.ClientConfig == nil {
|
||||||
|
t.Error("client configuration should've been set to the default configuration")
|
||||||
|
} else {
|
||||||
|
if !service.ClientConfig.Insecure {
|
||||||
|
t.Error("service.ClientConfig.Insecure should've been set to true")
|
||||||
|
}
|
||||||
|
if !service.ClientConfig.IgnoreRedirect {
|
||||||
|
t.Error("service.ClientConfig.IgnoreRedirect should've been set to true")
|
||||||
|
}
|
||||||
|
if service.ClientConfig.Timeout != client.GetDefaultConfig().Timeout {
|
||||||
|
t.Error("service.ClientConfig.Timeout should've been set to 10s, because the timeout value entered is not set or invalid")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestService_ValidateAndSetDefaultsWithNoName(t *testing.T) {
|
func TestService_ValidateAndSetDefaultsWithNoName(t *testing.T) {
|
||||||
defer func() { recover() }()
|
defer func() { recover() }()
|
||||||
condition := Condition("[STATUS] == 200")
|
condition := Condition("[STATUS] == 200")
|
||||||
|
@ -205,6 +247,7 @@ func TestIntegrationEvaluateHealth(t *testing.T) {
|
||||||
URL: "https://twinnation.org/health",
|
URL: "https://twinnation.org/health",
|
||||||
Conditions: []*Condition{&condition, &bodyCondition},
|
Conditions: []*Condition{&condition, &bodyCondition},
|
||||||
}
|
}
|
||||||
|
service.ValidateAndSetDefaults()
|
||||||
result := service.EvaluateHealth()
|
result := service.EvaluateHealth()
|
||||||
if !result.ConditionResults[0].Success {
|
if !result.ConditionResults[0].Success {
|
||||||
t.Errorf("Condition '%s' should have been a success", condition)
|
t.Errorf("Condition '%s' should have been a success", condition)
|
||||||
|
@ -224,6 +267,7 @@ func TestIntegrationEvaluateHealthWithFailure(t *testing.T) {
|
||||||
URL: "https://twinnation.org/health",
|
URL: "https://twinnation.org/health",
|
||||||
Conditions: []*Condition{&condition},
|
Conditions: []*Condition{&condition},
|
||||||
}
|
}
|
||||||
|
service.ValidateAndSetDefaults()
|
||||||
result := service.EvaluateHealth()
|
result := service.EvaluateHealth()
|
||||||
if result.ConditionResults[0].Success {
|
if result.ConditionResults[0].Success {
|
||||||
t.Errorf("Condition '%s' should have been a failure", condition)
|
t.Errorf("Condition '%s' should have been a failure", condition)
|
||||||
|
@ -248,6 +292,7 @@ func TestIntegrationEvaluateHealthForDNS(t *testing.T) {
|
||||||
},
|
},
|
||||||
Conditions: []*Condition{&conditionSuccess, &conditionBody},
|
Conditions: []*Condition{&conditionSuccess, &conditionBody},
|
||||||
}
|
}
|
||||||
|
service.ValidateAndSetDefaults()
|
||||||
result := service.EvaluateHealth()
|
result := service.EvaluateHealth()
|
||||||
if !result.ConditionResults[0].Success {
|
if !result.ConditionResults[0].Success {
|
||||||
t.Errorf("Conditions '%s' and %s should have been a success", conditionSuccess, conditionBody)
|
t.Errorf("Conditions '%s' and %s should have been a success", conditionSuccess, conditionBody)
|
||||||
|
@ -267,6 +312,7 @@ func TestIntegrationEvaluateHealthForICMP(t *testing.T) {
|
||||||
URL: "icmp://127.0.0.1",
|
URL: "icmp://127.0.0.1",
|
||||||
Conditions: []*Condition{&conditionSuccess},
|
Conditions: []*Condition{&conditionSuccess},
|
||||||
}
|
}
|
||||||
|
service.ValidateAndSetDefaults()
|
||||||
result := service.EvaluateHealth()
|
result := service.EvaluateHealth()
|
||||||
if !result.ConditionResults[0].Success {
|
if !result.ConditionResults[0].Success {
|
||||||
t.Errorf("Conditions '%s' should have been a success", conditionSuccess)
|
t.Errorf("Conditions '%s' should have been a success", conditionSuccess)
|
||||||
|
|
Loading…
Reference in a new issue