mirror of
https://github.com/TwiN/gatus.git
synced 2024-11-21 23:43:27 +01:00
feat: support monitoring WebSocket endpoints (#511)
* feat: support monitoring WebSocket endpoints WebSocket endpoints are automatically identified by the URL protocol specification: `wss://` or `ws://`. The request body is used as the "message" written to the server, and the answer is stored in the `[BODY]`. Optionally, the user can set the `jsonrpc` flag to automatically wrap the request body in a JSON RPC 2.0 method call. * core:websocket: close connection after using it * test: add tests related to WebSocket support - test we can identify the endpoint type for WebSockets based on the URL supplied: `wss://` (with SSL/TLS) and `ws://` (plain text). - test we can generate a JsonRPC 2.0 message via the new endpoint flag `JsonRPC`. * core:endpoint: fix name of jsonrpc parameter See https://en.wikipedia.org/wiki/JSON-RPC#Version_2.0 * core:websocket: fix dangling open connection on error Move the `defer ws.Close()` to after opening the connection, so the socket is closed also in case of errors. * remove jsonrpc flag * core:websocket: fix nil pointer dereference The connection should only be closed if successfully opened. * Move websocket function to client * update go.mod * Fix build errors * Fix errors * Update client/client.go --------- Co-authored-by: TwiN <twin@linux.com>
This commit is contained in:
parent
34313bec7e
commit
5f69351b6b
113
README.md
113
README.md
@ -87,6 +87,7 @@ Have any feedback or questions? [Create a discussion](https://github.com/TwiN/ga
|
|||||||
- [Monitoring a TCP endpoint](#monitoring-a-tcp-endpoint)
|
- [Monitoring a TCP endpoint](#monitoring-a-tcp-endpoint)
|
||||||
- [Monitoring a UDP endpoint](#monitoring-a-udp-endpoint)
|
- [Monitoring a UDP endpoint](#monitoring-a-udp-endpoint)
|
||||||
- [Monitoring a SCTP endpoint](#monitoring-a-sctp-endpoint)
|
- [Monitoring a SCTP endpoint](#monitoring-a-sctp-endpoint)
|
||||||
|
- [Monitoring a WebSocket endpoint](#monitoring-a-websocket-endpoint)
|
||||||
- [Monitoring an endpoint using ICMP](#monitoring-an-endpoint-using-icmp)
|
- [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 DNS queries](#monitoring-an-endpoint-using-dns-queries)
|
||||||
- [Monitoring an endpoint using STARTTLS](#monitoring-an-endpoint-using-starttls)
|
- [Monitoring an endpoint using STARTTLS](#monitoring-an-endpoint-using-starttls)
|
||||||
@ -196,54 +197,54 @@ If you want to test it locally, see [Docker](#docker).
|
|||||||
## Configuration
|
## Configuration
|
||||||
| Parameter | Description | Default |
|
| Parameter | Description | Default |
|
||||||
|:------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------|:---------------------------|
|
|:------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------|:---------------------------|
|
||||||
| `debug` | Whether to enable debug logs. | `false` |
|
| `debug` | Whether to enable debug logs. | `false` |
|
||||||
| `metrics` | Whether to expose metrics at /metrics. | `false` |
|
| `metrics` | Whether to expose metrics at /metrics. | `false` |
|
||||||
| `storage` | [Storage configuration](#storage) | `{}` |
|
| `storage` | [Storage configuration](#storage) | `{}` |
|
||||||
| `endpoints` | List of endpoints to monitor. | Required `[]` |
|
| `endpoints` | List of endpoints to monitor. | Required `[]` |
|
||||||
| `endpoints[].enabled` | Whether to monitor the endpoint. | `true` |
|
| `endpoints[].enabled` | Whether to monitor the endpoint. | `true` |
|
||||||
| `endpoints[].name` | Name of the endpoint. Can be anything. | Required `""` |
|
| `endpoints[].name` | Name of the endpoint. Can be anything. | Required `""` |
|
||||||
| `endpoints[].group` | Group name. Used to group multiple endpoints together on the dashboard. <br />See [Endpoint groups](#endpoint-groups). | `""` |
|
| `endpoints[].group` | Group name. Used to group multiple endpoints together on the dashboard. <br />See [Endpoint groups](#endpoint-groups). | `""` |
|
||||||
| `endpoints[].url` | URL to send the request to. | Required `""` |
|
| `endpoints[].url` | URL to send the request to. | Required `""` |
|
||||||
| `endpoints[].method` | Request method. | `GET` |
|
| `endpoints[].method` | Request method. | `GET` |
|
||||||
| `endpoints[].conditions` | Conditions used to determine the health of the endpoint. <br />See [Conditions](#conditions). | `[]` |
|
| `endpoints[].conditions` | Conditions used to determine the health of the endpoint. <br />See [Conditions](#conditions). | `[]` |
|
||||||
| `endpoints[].interval` | Duration to wait between every status check. | `60s` |
|
| `endpoints[].interval` | Duration to wait between every status check. | `60s` |
|
||||||
| `endpoints[].graphql` | Whether to wrap the body in a query param (`{"query":"$body"}`). | `false` |
|
| `endpoints[].graphql` | Whether to wrap the body in a query param (`{"query":"$body"}`). | `false` |
|
||||||
| `endpoints[].body` | Request body. | `""` |
|
| `endpoints[].body` | Request body. | `""` |
|
||||||
| `endpoints[].headers` | Request headers. | `{}` |
|
| `endpoints[].headers` | Request headers. | `{}` |
|
||||||
| `endpoints[].dns` | Configuration for an endpoint of type DNS. <br />See [Monitoring an endpoint using DNS queries](#monitoring-an-endpoint-using-dns-queries). | `""` |
|
| `endpoints[].dns` | Configuration for an endpoint of type DNS. <br />See [Monitoring an endpoint using DNS queries](#monitoring-an-endpoint-using-dns-queries). | `""` |
|
||||||
| `endpoints[].dns.query-type` | Query type (e.g. MX) | `""` |
|
| `endpoints[].dns.query-type` | Query type (e.g. MX) | `""` |
|
||||||
| `endpoints[].dns.query-name` | Query name (e.g. example.com) | `""` |
|
| `endpoints[].dns.query-name` | Query name (e.g. example.com) | `""` |
|
||||||
| `endpoints[].alerts[].type` | Type of alert. <br />See [Alerting](#alerting) for all valid types. | Required `""` |
|
| `endpoints[].alerts[].type` | Type of alert. <br />See [Alerting](#alerting) for all valid types. | Required `""` |
|
||||||
| `endpoints[].alerts[].enabled` | Whether to enable the alert. | `true` |
|
| `endpoints[].alerts[].enabled` | Whether to enable the alert. | `true` |
|
||||||
| `endpoints[].alerts[].failure-threshold` | Number of failures in a row needed before triggering the alert. | `3` |
|
| `endpoints[].alerts[].failure-threshold` | Number of failures in a row needed before triggering the alert. | `3` |
|
||||||
| `endpoints[].alerts[].success-threshold` | Number of successes in a row before an ongoing incident is marked as resolved. | `2` |
|
| `endpoints[].alerts[].success-threshold` | Number of successes in a row before an ongoing incident is marked as resolved. | `2` |
|
||||||
| `endpoints[].alerts[].send-on-resolved` | Whether to send a notification once a triggered alert is marked as resolved. | `false` |
|
| `endpoints[].alerts[].send-on-resolved` | Whether to send a notification once a triggered alert is marked as resolved. | `false` |
|
||||||
| `endpoints[].alerts[].description` | Description of the alert. Will be included in the alert sent. | `""` |
|
| `endpoints[].alerts[].description` | Description of the alert. Will be included in the alert sent. | `""` |
|
||||||
| `endpoints[].client` | [Client configuration](#client-configuration). | `{}` |
|
| `endpoints[].client` | [Client configuration](#client-configuration). | `{}` |
|
||||||
| `endpoints[].ui` | UI configuration at the endpoint level. | `{}` |
|
| `endpoints[].ui` | UI configuration at the endpoint level. | `{}` |
|
||||||
| `endpoints[].ui.hide-hostname` | Whether to hide the hostname in the result. | `false` |
|
| `endpoints[].ui.hide-hostname` | Whether to hide the hostname in the result. | `false` |
|
||||||
| `endpoints[].ui.hide-url` | Whether to ensure the URL is not displayed in the results. Useful if the URL contains a token. | `false` |
|
| `endpoints[].ui.hide-url` | Whether to ensure the URL is not displayed in the results. Useful if the URL contains a token. | `false` |
|
||||||
| `endpoints[].ui.dont-resolve-failed-conditions` | Whether to resolve failed conditions for the UI. | `false` |
|
| `endpoints[].ui.dont-resolve-failed-conditions` | Whether to resolve failed conditions for the UI. | `false` |
|
||||||
| `endpoints[].ui.badge.reponse-time` | List of response time thresholds. Each time a threshold is reached, the badge has a different color. | `[50, 200, 300, 500, 750]` |
|
| `endpoints[].ui.badge.reponse-time` | List of response time thresholds. Each time a threshold is reached, the badge has a different color. | `[50, 200, 300, 500, 750]` |
|
||||||
| `alerting` | [Alerting configuration](#alerting). | `{}` |
|
| `alerting` | [Alerting configuration](#alerting). | `{}` |
|
||||||
| `security` | [Security configuration](#security). | `{}` |
|
| `security` | [Security configuration](#security). | `{}` |
|
||||||
| `disable-monitoring-lock` | Whether to [disable the monitoring lock](#disable-monitoring-lock). | `false` |
|
| `disable-monitoring-lock` | Whether to [disable the monitoring lock](#disable-monitoring-lock). | `false` |
|
||||||
| `skip-invalid-config-update` | Whether to ignore invalid configuration update. <br />See [Reloading configuration on the fly](#reloading-configuration-on-the-fly). | `false` |
|
| `skip-invalid-config-update` | Whether to ignore invalid configuration update. <br />See [Reloading configuration on the fly](#reloading-configuration-on-the-fly). | `false` |
|
||||||
| `web` | Web configuration. | `{}` |
|
| `web` | Web configuration. | `{}` |
|
||||||
| `web.address` | Address to listen on. | `0.0.0.0` |
|
| `web.address` | Address to listen on. | `0.0.0.0` |
|
||||||
| `web.port` | Port to listen on. | `8080` |
|
| `web.port` | Port to listen on. | `8080` |
|
||||||
| `web.tls.certificate-file` | Optional public certificate file for TLS in PEM format. | `` |
|
| `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. | `` |
|
| `web.tls.private-key-file` | Optional private key file for TLS in PEM format. | `` |
|
||||||
| `ui` | UI configuration. | `{}` |
|
| `ui` | UI configuration. | `{}` |
|
||||||
| `ui.title` | [Title of the document](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/title). | `Health Dashboard ǀ Gatus` |
|
| `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...`. |
|
| `ui.description` | Meta description for the page. | `Gatus is an advanced...`. |
|
||||||
| `ui.header` | Header at the top of the dashboard. | `Health Status` |
|
| `ui.header` | Header at the top of the dashboard. | `Health Status` |
|
||||||
| `ui.logo` | URL to the logo to display. | `""` |
|
| `ui.logo` | URL to the logo to display. | `""` |
|
||||||
| `ui.link` | Link to open when the logo is clicked. | `""` |
|
| `ui.link` | Link to open when the logo is clicked. | `""` |
|
||||||
| `ui.buttons` | List of buttons to display below the header. | `[]` |
|
| `ui.buttons` | List of buttons to display below the header. | `[]` |
|
||||||
| `ui.buttons[].name` | Text to display on the button. | Required `""` |
|
| `ui.buttons[].name` | Text to display on the button. | Required `""` |
|
||||||
| `ui.buttons[].link` | Link to open when the button is clicked. | Required `""` |
|
| `ui.buttons[].link` | Link to open when the button is clicked. | Required `""` |
|
||||||
| `maintenance` | [Maintenance configuration](#maintenance). | `{}` |
|
| `maintenance` | [Maintenance configuration](#maintenance). | `{}` |
|
||||||
|
|
||||||
|
|
||||||
### Conditions
|
### Conditions
|
||||||
@ -1531,6 +1532,22 @@ Placeholders `[STATUS]` and `[BODY]` as well as the fields `endpoints[].body`, `
|
|||||||
|
|
||||||
This works for SCTP based application.
|
This works for SCTP based application.
|
||||||
|
|
||||||
|
### Monitoring a WebSocket endpoint
|
||||||
|
By prefixing `endpoints[].url` with `ws://` or `wss://`, you can monitor WebSocket endpoints at a very basic level:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
endpoints:
|
||||||
|
- name: example
|
||||||
|
url: "wss://example.com/"
|
||||||
|
body: "status"
|
||||||
|
conditions:
|
||||||
|
- "[CONNECTED] == true"
|
||||||
|
- "[BODY].result >= 0"
|
||||||
|
```
|
||||||
|
|
||||||
|
The `[BODY]` placeholder contains the output of the query, and `[CONNECTED]`
|
||||||
|
shows whether the connected was successfully established.
|
||||||
|
|
||||||
### Monitoring an endpoint using ICMP
|
### Monitoring an endpoint using ICMP
|
||||||
By prefixing `endpoints[].url` with `icmp:\\`, you can monitor endpoints at a very basic level using ICMP, or more
|
By prefixing `endpoints[].url` with `icmp:\\`, you can monitor endpoints at a very basic level using ICMP, or more
|
||||||
commonly known as "ping" or "echo":
|
commonly known as "ping" or "echo":
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"golang.org/x/net/websocket"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/smtp"
|
"net/smtp"
|
||||||
@ -184,6 +185,37 @@ func Ping(address string, config *Config) (bool, time.Duration) {
|
|||||||
return true, 0
|
return true, 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Open a websocket connection, write `body` and return a message from the server
|
||||||
|
func QueryWebSocket(address string, config *Config, body string) (bool, []byte, error) {
|
||||||
|
const (
|
||||||
|
Origin = "http://localhost/"
|
||||||
|
MaximumMessageSize = 1024 // in bytes
|
||||||
|
)
|
||||||
|
|
||||||
|
wsConfig, err := websocket.NewConfig(address, Origin)
|
||||||
|
if err != nil {
|
||||||
|
return false, nil, fmt.Errorf("error configuring websocket connection: %w", err)
|
||||||
|
}
|
||||||
|
// Dial URL
|
||||||
|
ws, err := websocket.DialConfig(wsConfig)
|
||||||
|
if err != nil {
|
||||||
|
return false, nil, fmt.Errorf("error dialing websocket: %w", err)
|
||||||
|
}
|
||||||
|
defer ws.Close()
|
||||||
|
connected := true
|
||||||
|
// Write message
|
||||||
|
if _, err := ws.Write([]byte(body)); err != nil {
|
||||||
|
return false, nil, fmt.Errorf("error writing websocket body: %w", err)
|
||||||
|
}
|
||||||
|
// Read message
|
||||||
|
var n int
|
||||||
|
msg := make([]byte, MaximumMessageSize)
|
||||||
|
if n, err = ws.Read(msg); err != nil {
|
||||||
|
return false, nil, fmt.Errorf("error reading websocket message: %w", err)
|
||||||
|
}
|
||||||
|
return connected, msg[:n], nil
|
||||||
|
}
|
||||||
|
|
||||||
// InjectHTTPClient is used to inject a custom HTTP client for testing purposes
|
// InjectHTTPClient is used to inject a custom HTTP client for testing purposes
|
||||||
func InjectHTTPClient(httpClient *http.Client) {
|
func InjectHTTPClient(httpClient *http.Client) {
|
||||||
injectedHTTPClient = httpClient
|
injectedHTTPClient = httpClient
|
||||||
|
@ -42,6 +42,7 @@ const (
|
|||||||
EndpointTypeSTARTTLS EndpointType = "STARTTLS"
|
EndpointTypeSTARTTLS EndpointType = "STARTTLS"
|
||||||
EndpointTypeTLS EndpointType = "TLS"
|
EndpointTypeTLS EndpointType = "TLS"
|
||||||
EndpointTypeHTTP EndpointType = "HTTP"
|
EndpointTypeHTTP EndpointType = "HTTP"
|
||||||
|
EndpointTypeWS EndpointType = "WEBSOCKET"
|
||||||
EndpointTypeUNKNOWN EndpointType = "UNKNOWN"
|
EndpointTypeUNKNOWN EndpointType = "UNKNOWN"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -149,6 +150,8 @@ func (endpoint Endpoint) Type() EndpointType {
|
|||||||
return EndpointTypeTLS
|
return EndpointTypeTLS
|
||||||
case strings.HasPrefix(endpoint.URL, "http://") || strings.HasPrefix(endpoint.URL, "https://"):
|
case strings.HasPrefix(endpoint.URL, "http://") || strings.HasPrefix(endpoint.URL, "https://"):
|
||||||
return EndpointTypeHTTP
|
return EndpointTypeHTTP
|
||||||
|
case strings.HasPrefix(endpoint.URL, "ws://") || strings.HasPrefix(endpoint.URL, "wss://"):
|
||||||
|
return EndpointTypeWS
|
||||||
default:
|
default:
|
||||||
return EndpointTypeUNKNOWN
|
return EndpointTypeUNKNOWN
|
||||||
}
|
}
|
||||||
@ -340,6 +343,9 @@ func (endpoint *Endpoint) call(result *Result) {
|
|||||||
result.Duration = time.Since(startTime)
|
result.Duration = time.Since(startTime)
|
||||||
} else if endpointType == EndpointTypeICMP {
|
} else if endpointType == EndpointTypeICMP {
|
||||||
result.Connected, result.Duration = client.Ping(strings.TrimPrefix(endpoint.URL, "icmp://"), endpoint.ClientConfig)
|
result.Connected, result.Duration = client.Ping(strings.TrimPrefix(endpoint.URL, "icmp://"), endpoint.ClientConfig)
|
||||||
|
} else if endpointType == EndpointTypeWS {
|
||||||
|
result.Connected, result.Body, err = client.QueryWebSocket(endpoint.URL, endpoint.ClientConfig, endpoint.Body)
|
||||||
|
result.Duration = time.Since(startTime)
|
||||||
} else {
|
} else {
|
||||||
response, err = client.GetHTTPClient(endpoint.ClientConfig).Do(request)
|
response, err = client.GetHTTPClient(endpoint.ClientConfig).Do(request)
|
||||||
result.Duration = time.Since(startTime)
|
result.Duration = time.Since(startTime)
|
||||||
|
@ -313,6 +313,18 @@ func TestEndpoint_Type(t *testing.T) {
|
|||||||
},
|
},
|
||||||
want: EndpointTypeHTTP,
|
want: EndpointTypeHTTP,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
args: args{
|
||||||
|
URL: "wss://example.com/",
|
||||||
|
},
|
||||||
|
want: EndpointTypeWS,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
args: args{
|
||||||
|
URL: "ws://example.com/",
|
||||||
|
},
|
||||||
|
want: EndpointTypeWS,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
args: args{
|
args: args{
|
||||||
URL: "invalid://example.org",
|
URL: "invalid://example.org",
|
||||||
|
2
go.mod
2
go.mod
@ -20,6 +20,7 @@ require (
|
|||||||
github.com/valyala/fasthttp v1.48.0
|
github.com/valyala/fasthttp v1.48.0
|
||||||
github.com/wcharczuk/go-chart/v2 v2.1.0
|
github.com/wcharczuk/go-chart/v2 v2.1.0
|
||||||
golang.org/x/crypto v0.11.0
|
golang.org/x/crypto v0.11.0
|
||||||
|
golang.org/x/net v0.11.0
|
||||||
golang.org/x/oauth2 v0.8.0
|
golang.org/x/oauth2 v0.8.0
|
||||||
gopkg.in/mail.v2 v2.3.1
|
gopkg.in/mail.v2 v2.3.1
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
@ -57,7 +58,6 @@ require (
|
|||||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||||
golang.org/x/image v0.5.0 // indirect
|
golang.org/x/image v0.5.0 // indirect
|
||||||
golang.org/x/mod v0.9.0 // indirect
|
golang.org/x/mod v0.9.0 // indirect
|
||||||
golang.org/x/net v0.11.0 // indirect
|
|
||||||
golang.org/x/sync v0.3.0 // indirect
|
golang.org/x/sync v0.3.0 // indirect
|
||||||
golang.org/x/sys v0.10.0 // indirect
|
golang.org/x/sys v0.10.0 // indirect
|
||||||
golang.org/x/tools v0.4.0 // indirect
|
golang.org/x/tools v0.4.0 // indirect
|
||||||
|
Loading…
Reference in New Issue
Block a user