mirror of
https://github.com/TwiN/gatus.git
synced 2026-02-04 15:14:43 +00:00
feat(client): Add support for monitoring gRPC endpoints (#1376)
* add grpc * add gRPC to readme
This commit is contained in:
40
README.md
40
README.md
@@ -121,6 +121,7 @@ Have any feedback or questions? [Create a discussion](https://github.com/TwiN/ga
|
|||||||
- [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 a WebSocket endpoint](#monitoring-a-websocket-endpoint)
|
||||||
|
- [Monitoring an endpoint using gRPC](#monitoring-an-endpoint-using-grpc)
|
||||||
- [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 SSH](#monitoring-an-endpoint-using-ssh)
|
- [Monitoring an endpoint using SSH](#monitoring-an-endpoint-using-ssh)
|
||||||
@@ -2956,6 +2957,45 @@ shows whether the connection was successfully established. You can use Go templa
|
|||||||
syntax.
|
syntax.
|
||||||
|
|
||||||
|
|
||||||
|
### Monitoring an endpoint using gRPC
|
||||||
|
You can monitor gRPC services by prefixing `endpoints[].url` with `grpc://` or `grpcs://`.
|
||||||
|
Gatus executes the standard `grpc.health.v1.Health/Check` RPC against the target.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
endpoints:
|
||||||
|
- name: my-grpc
|
||||||
|
url: grpc://localhost:50051
|
||||||
|
interval: 30s
|
||||||
|
conditions:
|
||||||
|
- "[CONNECTED] == true"
|
||||||
|
- "[BODY].status == SERVING" # BODY is read only when referenced
|
||||||
|
client:
|
||||||
|
timeout: 5s
|
||||||
|
```
|
||||||
|
|
||||||
|
For TLS-enabled servers, use `grpcs://` and configure client TLS if necessary:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
endpoints:
|
||||||
|
- name: my-grpcs
|
||||||
|
url: grpcs://example.com:443
|
||||||
|
conditions:
|
||||||
|
- "[CONNECTED] == true"
|
||||||
|
- "[BODY].status == SERVING"
|
||||||
|
client:
|
||||||
|
timeout: 5s
|
||||||
|
insecure: false # set true to skip cert verification (not recommended)
|
||||||
|
tls:
|
||||||
|
certificate-file: /path/to/cert.pem # optional mTLS client cert
|
||||||
|
private-key-file: /path/to/key.pem # optional mTLS client key
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- The health check targets the default service (`service: ""`). Support for a custom service name can be added later if needed.
|
||||||
|
- The response body is exposed as a minimal JSON object like `{"status":"SERVING"}` only when required by conditions or suite store mappings.
|
||||||
|
- Timeouts, custom DNS resolvers and SSH tunnels are honored via the existing [`client` configuration](#client-configuration).
|
||||||
|
|
||||||
|
|
||||||
### 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":
|
||||||
|
|||||||
71
client/grpc.go
Normal file
71
client/grpc.go
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/TwiN/logr"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/credentials"
|
||||||
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
|
health "google.golang.org/grpc/health/grpc_health_v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PerformGRPCHealthCheck dials a gRPC target and performs the standard Health/Check RPC.
|
||||||
|
// Returns whether a connection was established, the serving status string, an error (if any), and the elapsed duration.
|
||||||
|
func PerformGRPCHealthCheck(address string, useTLS bool, cfg *Config) (bool, string, error, time.Duration) {
|
||||||
|
if cfg == nil {
|
||||||
|
cfg = GetDefaultConfig()
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), cfg.Timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
var opts []grpc.DialOption
|
||||||
|
// Transport credentials
|
||||||
|
if useTLS {
|
||||||
|
tlsCfg := &tls.Config{InsecureSkipVerify: cfg.Insecure}
|
||||||
|
if cfg.HasTLSConfig() && cfg.TLS.isValid() == nil {
|
||||||
|
tlsCfg = configureTLS(tlsCfg, *cfg.TLS)
|
||||||
|
}
|
||||||
|
opts = append(opts, grpc.WithTransportCredentials(credentials.NewTLS(tlsCfg)))
|
||||||
|
} else {
|
||||||
|
opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||||
|
}
|
||||||
|
// Custom dialer for DNS resolver or SSH tunnel
|
||||||
|
opts = append(opts, grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) {
|
||||||
|
if cfg.ResolvedTunnel != nil {
|
||||||
|
return cfg.ResolvedTunnel.Dial("tcp", addr)
|
||||||
|
}
|
||||||
|
if cfg.HasCustomDNSResolver() {
|
||||||
|
resolverCfg, err := cfg.parseDNSResolver()
|
||||||
|
if err != nil {
|
||||||
|
// Shouldn't happen because already validated; log and fall back
|
||||||
|
logr.Errorf("[client.PerformGRPCHealthCheck] invalid DNS resolver: %v", err)
|
||||||
|
} else {
|
||||||
|
d := &net.Dialer{Resolver: &net.Resolver{PreferGo: true, Dial: func(ctx context.Context, network, _ string) (net.Conn, error) {
|
||||||
|
d := net.Dialer{}
|
||||||
|
return d.DialContext(ctx, resolverCfg.Protocol, resolverCfg.Host+":"+resolverCfg.Port)
|
||||||
|
}}}
|
||||||
|
return d.DialContext(ctx, "tcp", addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var d net.Dialer
|
||||||
|
return d.DialContext(ctx, "tcp", addr)
|
||||||
|
}))
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
conn, err := grpc.DialContext(ctx, address, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return false, "", err, time.Since(start)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
client := health.NewHealthClient(conn)
|
||||||
|
resp, err := client.Check(ctx, &health.HealthCheckRequest{Service: ""})
|
||||||
|
if err != nil {
|
||||||
|
return false, "", err, time.Since(start)
|
||||||
|
}
|
||||||
|
return true, resp.GetStatus().String(), nil, time.Since(start)
|
||||||
|
}
|
||||||
@@ -50,6 +50,7 @@ const (
|
|||||||
TypeSTARTTLS Type = "STARTTLS"
|
TypeSTARTTLS Type = "STARTTLS"
|
||||||
TypeTLS Type = "TLS"
|
TypeTLS Type = "TLS"
|
||||||
TypeHTTP Type = "HTTP"
|
TypeHTTP Type = "HTTP"
|
||||||
|
TypeGRPC Type = "GRPC"
|
||||||
TypeWS Type = "WEBSOCKET"
|
TypeWS Type = "WEBSOCKET"
|
||||||
TypeSSH Type = "SSH"
|
TypeSSH Type = "SSH"
|
||||||
TypeUNKNOWN Type = "UNKNOWN"
|
TypeUNKNOWN Type = "UNKNOWN"
|
||||||
@@ -177,6 +178,8 @@ func (e *Endpoint) Type() Type {
|
|||||||
return TypeTLS
|
return TypeTLS
|
||||||
case strings.HasPrefix(e.URL, "http://") || strings.HasPrefix(e.URL, "https://"):
|
case strings.HasPrefix(e.URL, "http://") || strings.HasPrefix(e.URL, "https://"):
|
||||||
return TypeHTTP
|
return TypeHTTP
|
||||||
|
case strings.HasPrefix(e.URL, "grpc://") || strings.HasPrefix(e.URL, "grpcs://"):
|
||||||
|
return TypeGRPC
|
||||||
case strings.HasPrefix(e.URL, "ws://") || strings.HasPrefix(e.URL, "wss://"):
|
case strings.HasPrefix(e.URL, "ws://") || strings.HasPrefix(e.URL, "wss://"):
|
||||||
return TypeWS
|
return TypeWS
|
||||||
case strings.HasPrefix(e.URL, "ssh://"):
|
case strings.HasPrefix(e.URL, "ssh://"):
|
||||||
@@ -528,6 +531,19 @@ func (e *Endpoint) call(result *Result) {
|
|||||||
result.Body = output
|
result.Body = output
|
||||||
}
|
}
|
||||||
result.Duration = time.Since(startTime)
|
result.Duration = time.Since(startTime)
|
||||||
|
} else if endpointType == TypeGRPC {
|
||||||
|
useTLS := strings.HasPrefix(e.URL, "grpcs://")
|
||||||
|
address := strings.TrimPrefix(strings.TrimPrefix(e.URL, "grpcs://"), "grpc://")
|
||||||
|
connected, status, err, duration := client.PerformGRPCHealthCheck(address, useTLS, e.ClientConfig)
|
||||||
|
if err != nil {
|
||||||
|
result.AddError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
result.Connected = connected
|
||||||
|
result.Duration = duration
|
||||||
|
if e.needsToReadBody() {
|
||||||
|
result.Body = []byte(fmt.Sprintf("{\"status\":\"%s\"}", status))
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
response, err = client.GetHTTPClient(e.ClientConfig).Do(request)
|
response, err = client.GetHTTPClient(e.ClientConfig).Do(request)
|
||||||
result.Duration = time.Since(startTime)
|
result.Duration = time.Since(startTime)
|
||||||
|
|||||||
1
go.mod
1
go.mod
@@ -33,6 +33,7 @@ require (
|
|||||||
golang.org/x/oauth2 v0.32.0
|
golang.org/x/oauth2 v0.32.0
|
||||||
golang.org/x/sync v0.17.0
|
golang.org/x/sync v0.17.0
|
||||||
google.golang.org/api v0.252.0
|
google.golang.org/api v0.252.0
|
||||||
|
google.golang.org/grpc v1.75.1
|
||||||
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
|
||||||
modernc.org/sqlite v1.39.1
|
modernc.org/sqlite v1.39.1
|
||||||
|
|||||||
Reference in New Issue
Block a user