feat: Add body to TCP, UDP, and TLS endpoints and templating (#1134)
* feat(endpoints): Add body to TCP, UDP, and TLS endpoints and templating * Changed the template to be more consistent with the rest of the application and added additional substritutions. * Changed getModifiedBody to getParsedBody and fixed connected response * Apply suggestion from @TwiN * Apply suggestion from @TwiN * Apply suggestion from @TwiN * Apply suggestion from @TwiN * Apply suggestion from @TwiN * Apply suggestion from @TwiN * Apply suggestion from @TwiN * Apply suggestion from @TwiN * Apply suggestion from @TwiN * Apply suggestion from @TwiN * Apply suggestion from @TwiN * Apply suggestion from @TwiN * Update client/client.go --------- Co-authored-by: TwiN <twin@linux.com>
This commit is contained in:
22
README.md
22
README.md
@@ -288,6 +288,13 @@ You can then configure alerts to be triggered when an endpoint is unhealthy once
|
|||||||
| `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.response-time` | List of response time thresholds. Each time a threshold is reached, the badge has a different color. | `[50, 200, 300, 500, 750]` |
|
| `endpoints[].ui.badge.response-time` | List of response time thresholds. Each time a threshold is reached, the badge has a different color. | `[50, 200, 300, 500, 750]` |
|
||||||
|
|
||||||
|
You may use the following placeholders in the body (`endpoints[].body`):
|
||||||
|
- `[ENDPOINT_NAME]` (resolved from `endpoints[].name`)
|
||||||
|
- `[ENDPOINT_GROUP]` (resolved from `endpoints[].group`)
|
||||||
|
- `[ENDPOINT_URL]` (resolved from `endpoints[].url`)
|
||||||
|
- `[LOCAL_ADDRESS]` (resolves to the local IP and port like `192.0.2.1:25` or `[2001:db8::1]:80`)
|
||||||
|
- `[RANDOM_STRING_N]` (resolves to a random string of numbers and letters of length N)
|
||||||
|
|
||||||
|
|
||||||
### External Endpoints
|
### External Endpoints
|
||||||
Unlike regular endpoints, external endpoints are not monitored by Gatus, but they are instead pushed programmatically.
|
Unlike regular endpoints, external endpoints are not monitored by Gatus, but they are instead pushed programmatically.
|
||||||
@@ -2125,8 +2132,9 @@ endpoints:
|
|||||||
conditions:
|
conditions:
|
||||||
- "[CONNECTED] == true"
|
- "[CONNECTED] == true"
|
||||||
```
|
```
|
||||||
|
If `endpoints[].body` is set then it is sent and the first 1024 bytes of the response will be in `[BODY]`.
|
||||||
|
|
||||||
Placeholders `[STATUS]` and `[BODY]` as well as the fields `endpoints[].body`, `endpoints[].headers`,
|
Placeholder `[STATUS]` as well as the fields `endpoints[].headers`,
|
||||||
`endpoints[].method` and `endpoints[].graphql` are not supported for TCP endpoints.
|
`endpoints[].method` and `endpoints[].graphql` are not supported for TCP endpoints.
|
||||||
|
|
||||||
This works for applications such as databases (Postgres, MySQL, etc.) and caches (Redis, Memcached, etc.).
|
This works for applications such as databases (Postgres, MySQL, etc.) and caches (Redis, Memcached, etc.).
|
||||||
@@ -2146,7 +2154,9 @@ endpoints:
|
|||||||
- "[CONNECTED] == true"
|
- "[CONNECTED] == true"
|
||||||
```
|
```
|
||||||
|
|
||||||
Placeholders `[STATUS]` and `[BODY]` as well as the fields `endpoints[].body`, `endpoints[].headers`,
|
If `endpoints[].body` is set then it is sent and the first 1024 bytes of the response will be in `[BODY]`.
|
||||||
|
|
||||||
|
Placeholder `[STATUS]` as well as the fields `endpoints[].headers`,
|
||||||
`endpoints[].method` and `endpoints[].graphql` are not supported for UDP endpoints.
|
`endpoints[].method` and `endpoints[].graphql` are not supported for UDP endpoints.
|
||||||
|
|
||||||
This works for UDP based application.
|
This works for UDP based application.
|
||||||
@@ -2181,7 +2191,8 @@ endpoints:
|
|||||||
```
|
```
|
||||||
|
|
||||||
The `[BODY]` placeholder contains the output of the query, and `[CONNECTED]`
|
The `[BODY]` placeholder contains the output of the query, and `[CONNECTED]`
|
||||||
shows whether the connection was successfully established.
|
shows whether the connection was successfully established. You can use Go template
|
||||||
|
syntax. The functions LocalAddr and RandomString with a length can be used.
|
||||||
|
|
||||||
|
|
||||||
### Monitoring an endpoint using ICMP
|
### Monitoring an endpoint using ICMP
|
||||||
@@ -2293,6 +2304,11 @@ endpoints:
|
|||||||
- "[CERTIFICATE_EXPIRATION] > 48h"
|
- "[CERTIFICATE_EXPIRATION] > 48h"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If `endpoints[].body` is set then it is sent and the first 1024 bytes of the response will be in `[BODY]`.
|
||||||
|
|
||||||
|
Placeholder `[STATUS]` as well as the fields `endpoints[].headers`,
|
||||||
|
`endpoints[].method` and `endpoints[].graphql` are not supported for TLS endpoints.
|
||||||
|
|
||||||
|
|
||||||
### Monitoring domain expiration
|
### Monitoring domain expiration
|
||||||
You can monitor the expiration of a domain with all endpoint types except for DNS by using the `[DOMAIN_EXPIRATION]`
|
You can monitor the expiration of a domain with all endpoint types except for DNS by using the `[DOMAIN_EXPIRATION]`
|
||||||
|
|||||||
@@ -76,24 +76,37 @@ func GetDomainExpiration(hostname string) (domainExpiration time.Duration, err e
|
|||||||
return domainExpiration, nil
|
return domainExpiration, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CanCreateTCPConnection checks whether a connection can be established with a TCP endpoint
|
// parseLocalAddressPlaceholder returns a string with the local address replaced
|
||||||
func CanCreateTCPConnection(address string, config *Config) bool {
|
func parseLocalAddressPlaceholder(item string, localAddr net.Addr) string {
|
||||||
conn, err := net.DialTimeout("tcp", address, config.Timeout)
|
item = strings.ReplaceAll(item, "[LOCAL_ADDRESS]", localAddr.String())
|
||||||
if err != nil {
|
return item
|
||||||
return false
|
|
||||||
}
|
|
||||||
_ = conn.Close()
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CanCreateUDPConnection checks whether a connection can be established with a UDP endpoint
|
// CanCreateNetworkConnection checks whether a connection can be established with a TCP or UDP endpoint
|
||||||
func CanCreateUDPConnection(address string, config *Config) bool {
|
func CanCreateNetworkConnection(netType string, address string, body string, config *Config) (bool, []byte) {
|
||||||
conn, err := net.DialTimeout("udp", address, config.Timeout)
|
const (
|
||||||
|
MaximumMessageSize = 1024 // in bytes
|
||||||
|
)
|
||||||
|
connection, err := net.DialTimeout(netType, address, config.Timeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false, nil
|
||||||
}
|
}
|
||||||
_ = conn.Close()
|
defer connection.Close()
|
||||||
return true
|
if body != "" {
|
||||||
|
body = parseLocalAddressPlaceholder(body, connection.LocalAddr())
|
||||||
|
connection.SetDeadline(time.Now().Add(config.Timeout))
|
||||||
|
_, err = connection.Write([]byte(body))
|
||||||
|
if err != nil {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
buf := make([]byte, MaximumMessageSize)
|
||||||
|
n, err := connection.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return true, buf[:n]
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CanCreateSCTPConnection checks whether a connection can be established with a SCTP endpoint
|
// CanCreateSCTPConnection checks whether a connection can be established with a SCTP endpoint
|
||||||
@@ -152,7 +165,10 @@ func CanPerformStartTLS(address string, config *Config) (connected bool, certifi
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CanPerformTLS checks whether a connection can be established to an address using the TLS protocol
|
// CanPerformTLS checks whether a connection can be established to an address using the TLS protocol
|
||||||
func CanPerformTLS(address string, config *Config) (connected bool, certificate *x509.Certificate, err error) {
|
func CanPerformTLS(address string, body string, config *Config) (connected bool, response []byte, certificate *x509.Certificate, err error) {
|
||||||
|
const (
|
||||||
|
MaximumMessageSize = 1024 // in bytes
|
||||||
|
)
|
||||||
connection, err := tls.DialWithDialer(&net.Dialer{Timeout: config.Timeout}, "tcp", address, &tls.Config{
|
connection, err := tls.DialWithDialer(&net.Dialer{Timeout: config.Timeout}, "tcp", address, &tls.Config{
|
||||||
InsecureSkipVerify: config.Insecure,
|
InsecureSkipVerify: config.Insecure,
|
||||||
})
|
})
|
||||||
@@ -166,9 +182,27 @@ func CanPerformTLS(address string, config *Config) (connected bool, certificate
|
|||||||
// Reference: https://pkg.go.dev/crypto/tls#PeerCertificates
|
// Reference: https://pkg.go.dev/crypto/tls#PeerCertificates
|
||||||
if len(verifiedChains) == 0 || len(verifiedChains[0]) == 0 {
|
if len(verifiedChains) == 0 || len(verifiedChains[0]) == 0 {
|
||||||
peerCertificates := connection.ConnectionState().PeerCertificates
|
peerCertificates := connection.ConnectionState().PeerCertificates
|
||||||
return true, peerCertificates[0], nil
|
certificate = peerCertificates[0]
|
||||||
|
} else {
|
||||||
|
certificate = verifiedChains[0][0]
|
||||||
}
|
}
|
||||||
return true, verifiedChains[0][0], nil
|
connected = true
|
||||||
|
if body != "" {
|
||||||
|
body = parseLocalAddressPlaceholder(body, connection.LocalAddr())
|
||||||
|
connection.SetDeadline(time.Now().Add(config.Timeout))
|
||||||
|
_, err = connection.Write([]byte(body))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
buf := make([]byte, MaximumMessageSize)
|
||||||
|
var n int
|
||||||
|
n, err = connection.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response = buf[:n]
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// CanCreateSSHConnection checks whether a connection can be established and a command can be executed to an address
|
// CanCreateSSHConnection checks whether a connection can be established and a command can be executed to an address
|
||||||
@@ -234,6 +268,7 @@ func ExecuteSSHCommand(sshClient *ssh.Client, body string, config *Config) (bool
|
|||||||
}
|
}
|
||||||
defer sshClient.Close()
|
defer sshClient.Close()
|
||||||
var b Body
|
var b Body
|
||||||
|
body = parseLocalAddressPlaceholder(body, sshClient.Conn.LocalAddr())
|
||||||
if err := json.Unmarshal([]byte(body), &b); err != nil {
|
if err := json.Unmarshal([]byte(body), &b); err != nil {
|
||||||
return false, 0, err
|
return false, 0, err
|
||||||
}
|
}
|
||||||
@@ -304,6 +339,7 @@ func QueryWebSocket(address, body string, config *Config) (bool, []byte, error)
|
|||||||
return false, nil, fmt.Errorf("error dialing websocket: %w", err)
|
return false, nil, fmt.Errorf("error dialing websocket: %w", err)
|
||||||
}
|
}
|
||||||
defer ws.Close()
|
defer ws.Close()
|
||||||
|
body = parseLocalAddressPlaceholder(body, ws.LocalAddr())
|
||||||
// Write message
|
// Write message
|
||||||
if _, err := ws.Write([]byte(body)); err != nil {
|
if _, err := ws.Write([]byte(body)); err != nil {
|
||||||
return false, nil, fmt.Errorf("error writing websocket body: %w", err)
|
return false, nil, fmt.Errorf("error writing websocket body: %w", err)
|
||||||
|
|||||||
@@ -223,7 +223,7 @@ func TestCanPerformTLS(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) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
connected, _, err := CanPerformTLS(tt.args.address, &Config{Insecure: tt.args.insecure, Timeout: 5 * time.Second})
|
connected, _, _, err := CanPerformTLS(tt.args.address, "", &Config{Insecure: tt.args.insecure, Timeout: 5 * time.Second})
|
||||||
if (err != nil) != tt.wantErr {
|
if (err != nil) != tt.wantErr {
|
||||||
t.Errorf("CanPerformTLS() err=%v, wantErr=%v", err, tt.wantErr)
|
t.Errorf("CanPerformTLS() err=%v, wantErr=%v", err, tt.wantErr)
|
||||||
return
|
return
|
||||||
@@ -235,11 +235,13 @@ func TestCanPerformTLS(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCanCreateTCPConnection(t *testing.T) {
|
func TestCanCreateConnection(t *testing.T) {
|
||||||
if CanCreateTCPConnection("127.0.0.1", &Config{Timeout: 5 * time.Second}) {
|
connected, _ := CanCreateNetworkConnection("tcp", "127.0.0.1", "", &Config{Timeout: 5 * time.Second})
|
||||||
|
if connected {
|
||||||
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")
|
||||||
}
|
}
|
||||||
if !CanCreateTCPConnection("1.1.1.1:53", &Config{Timeout: 5 * time.Second}) {
|
connected, _ = CanCreateNetworkConnection("tcp", "1.1.1.1:53", "", &Config{Timeout: 5 * time.Second})
|
||||||
|
if !connected {
|
||||||
t.Error("should've succeeded, because that IP should always™ be up")
|
t.Error("should've succeeded, because that IP should always™ be up")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,8 @@ type Checker struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Checker) Check() bool {
|
func (c *Checker) Check() bool {
|
||||||
return client.CanCreateTCPConnection(c.Target, &client.Config{Timeout: 5 * time.Second})
|
connected, _ := client.CanCreateNetworkConnection("tcp", c.Target, "", &client.Config{Timeout: 5 * time.Second})
|
||||||
|
return connected
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Checker) IsConnected() bool {
|
func (c *Checker) IsConnected() bool {
|
||||||
|
|||||||
@@ -7,9 +7,12 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"math/rand"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -229,7 +232,7 @@ func (e *Endpoint) ValidateAndSetDefaults() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Make sure that the request can be created
|
// Make sure that the request can be created
|
||||||
_, err := http.NewRequest(e.Method, e.URL, bytes.NewBuffer([]byte(e.Body)))
|
_, err := http.NewRequest(e.Method, e.URL, bytes.NewBuffer([]byte(e.getParsedBody())))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -326,6 +329,26 @@ func (e *Endpoint) EvaluateHealth() *Result {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *Endpoint) getParsedBody() string {
|
||||||
|
body := e.Body
|
||||||
|
body = strings.ReplaceAll(body, "[ENDPOINT_NAME]", e.Name)
|
||||||
|
body = strings.ReplaceAll(body, "[ENDPOINT_GROUP]", e.Group)
|
||||||
|
body = strings.ReplaceAll(body, "[ENDPOINT_URL]", e.URL)
|
||||||
|
randRegex, err := regexp.Compile(`\[RANDOM_STRING_\d+\]`)
|
||||||
|
if err == nil {
|
||||||
|
body = randRegex.ReplaceAllStringFunc(body, func(match string) string {
|
||||||
|
n, _ := strconv.Atoi(match[15 : len(match)-1])
|
||||||
|
const availableCharacterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||||
|
b := make([]byte, n)
|
||||||
|
for i := range b {
|
||||||
|
b[i] = availableCharacterBytes[rand.Intn(len(availableCharacterBytes))]
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
|
||||||
func (e *Endpoint) getIP(result *Result) {
|
func (e *Endpoint) getIP(result *Result) {
|
||||||
if ips, err := net.LookupIP(result.Hostname); err != nil {
|
if ips, err := net.LookupIP(result.Hostname); err != nil {
|
||||||
result.AddError(err.Error())
|
result.AddError(err.Error())
|
||||||
@@ -356,7 +379,7 @@ func (e *Endpoint) call(result *Result) {
|
|||||||
if endpointType == TypeSTARTTLS {
|
if endpointType == TypeSTARTTLS {
|
||||||
result.Connected, certificate, err = client.CanPerformStartTLS(strings.TrimPrefix(e.URL, "starttls://"), e.ClientConfig)
|
result.Connected, certificate, err = client.CanPerformStartTLS(strings.TrimPrefix(e.URL, "starttls://"), e.ClientConfig)
|
||||||
} else {
|
} else {
|
||||||
result.Connected, certificate, err = client.CanPerformTLS(strings.TrimPrefix(e.URL, "tls://"), e.ClientConfig)
|
result.Connected, result.Body, certificate, err = client.CanPerformTLS(strings.TrimPrefix(e.URL, "tls://"), e.getParsedBody(), e.ClientConfig)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.AddError(err.Error())
|
result.AddError(err.Error())
|
||||||
@@ -365,10 +388,10 @@ func (e *Endpoint) 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 endpointType == TypeTCP {
|
} else if endpointType == TypeTCP {
|
||||||
result.Connected = client.CanCreateTCPConnection(strings.TrimPrefix(e.URL, "tcp://"), e.ClientConfig)
|
result.Connected, result.Body = client.CanCreateNetworkConnection("tcp", strings.TrimPrefix(e.URL, "tcp://"), e.getParsedBody(), e.ClientConfig)
|
||||||
result.Duration = time.Since(startTime)
|
result.Duration = time.Since(startTime)
|
||||||
} else if endpointType == TypeUDP {
|
} else if endpointType == TypeUDP {
|
||||||
result.Connected = client.CanCreateUDPConnection(strings.TrimPrefix(e.URL, "udp://"), e.ClientConfig)
|
result.Connected, result.Body = client.CanCreateNetworkConnection("udp", strings.TrimPrefix(e.URL, "udp://"), e.getParsedBody(), e.ClientConfig)
|
||||||
result.Duration = time.Since(startTime)
|
result.Duration = time.Since(startTime)
|
||||||
} else if endpointType == TypeSCTP {
|
} else if endpointType == TypeSCTP {
|
||||||
result.Connected = client.CanCreateSCTPConnection(strings.TrimPrefix(e.URL, "sctp://"), e.ClientConfig)
|
result.Connected = client.CanCreateSCTPConnection(strings.TrimPrefix(e.URL, "sctp://"), e.ClientConfig)
|
||||||
@@ -376,7 +399,7 @@ func (e *Endpoint) call(result *Result) {
|
|||||||
} else if endpointType == TypeICMP {
|
} else if endpointType == TypeICMP {
|
||||||
result.Connected, result.Duration = client.Ping(strings.TrimPrefix(e.URL, "icmp://"), e.ClientConfig)
|
result.Connected, result.Duration = client.Ping(strings.TrimPrefix(e.URL, "icmp://"), e.ClientConfig)
|
||||||
} else if endpointType == TypeWS {
|
} else if endpointType == TypeWS {
|
||||||
result.Connected, result.Body, err = client.QueryWebSocket(e.URL, e.Body, e.ClientConfig)
|
result.Connected, result.Body, err = client.QueryWebSocket(e.URL, e.getParsedBody(), e.ClientConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.AddError(err.Error())
|
result.AddError(err.Error())
|
||||||
return
|
return
|
||||||
@@ -401,7 +424,7 @@ func (e *Endpoint) call(result *Result) {
|
|||||||
result.AddError(err.Error())
|
result.AddError(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
result.Success, result.HTTPStatus, err = client.ExecuteSSHCommand(cli, e.Body, e.ClientConfig)
|
result.Success, result.HTTPStatus, err = client.ExecuteSSHCommand(cli, e.getParsedBody(), e.ClientConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.AddError(err.Error())
|
result.AddError(err.Error())
|
||||||
return
|
return
|
||||||
@@ -435,12 +458,12 @@ func (e *Endpoint) buildHTTPRequest() *http.Request {
|
|||||||
var bodyBuffer *bytes.Buffer
|
var bodyBuffer *bytes.Buffer
|
||||||
if e.GraphQL {
|
if e.GraphQL {
|
||||||
graphQlBody := map[string]string{
|
graphQlBody := map[string]string{
|
||||||
"query": e.Body,
|
"query": e.getParsedBody(),
|
||||||
}
|
}
|
||||||
body, _ := json.Marshal(graphQlBody)
|
body, _ := json.Marshal(graphQlBody)
|
||||||
bodyBuffer = bytes.NewBuffer(body)
|
bodyBuffer = bytes.NewBuffer(body)
|
||||||
} else {
|
} else {
|
||||||
bodyBuffer = bytes.NewBuffer([]byte(e.Body))
|
bodyBuffer = bytes.NewBuffer([]byte(e.getParsedBody()))
|
||||||
}
|
}
|
||||||
request, _ := http.NewRequest(e.Method, e.URL, bodyBuffer)
|
request, _ := http.NewRequest(e.Method, e.URL, bodyBuffer)
|
||||||
for k, v := range e.Headers {
|
for k, v := range e.Headers {
|
||||||
|
|||||||
Reference in New Issue
Block a user