Add health check for DNS
This commit is contained in:
@ -20,6 +20,11 @@ const (
|
||||
// Values that could replace the placeholder: 127.0.0.1, 10.0.0.1, ...
|
||||
IPPlaceHolder = "[IP]"
|
||||
|
||||
// DNSRCode is a place holder for DNS_RCODE
|
||||
//
|
||||
// Values that could be NOERROR, FORMERR, SERVFAIL, NXDOMAIN, NOTIMP and REFUSED
|
||||
DNSRCode = "[DNS_RCODE]"
|
||||
|
||||
// ResponseTimePlaceHolder is a placeholder for the request response time, in milliseconds.
|
||||
//
|
||||
// Values that could replace the placeholder: 1, 500, 1000, ...
|
||||
@ -143,6 +148,8 @@ func sanitizeAndResolve(list []string, result *Result) []string {
|
||||
element = result.IP
|
||||
case ResponseTimePlaceHolder:
|
||||
element = strconv.Itoa(int(result.Duration.Milliseconds()))
|
||||
case DNSRCode:
|
||||
element = result.DNSRCode
|
||||
case BodyPlaceHolder:
|
||||
element = body
|
||||
case ConnectedPlaceHolder:
|
||||
|
83
core/dns.go
Normal file
83
core/dns.go
Normal file
@ -0,0 +1,83 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrDNSWithNoQueryName
|
||||
ErrDNSWithNoQueryName = errors.New("you must specify query name for DNS")
|
||||
// ErrDNSWithInvalidQueryType
|
||||
ErrDNSWithInvalidQueryType = errors.New("invalid query type")
|
||||
)
|
||||
|
||||
const (
|
||||
dnsPort = 53
|
||||
)
|
||||
|
||||
type DNS struct {
|
||||
// QueryType is the type for the DNS records like A,AAAA, CNAME...
|
||||
QueryType string `yaml:"query-type"`
|
||||
// QueryName is the query for DNS
|
||||
QueryName string `yaml:"query-name"`
|
||||
}
|
||||
|
||||
func (d *DNS) validateAndSetDefault() {
|
||||
if len(d.QueryName) == 0 {
|
||||
panic(ErrDNSWithNoQueryName)
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(d.QueryName, ".") {
|
||||
d.QueryName += "."
|
||||
}
|
||||
if _, ok := dns.StringToType[d.QueryType]; !ok {
|
||||
panic(ErrDNSWithInvalidQueryType)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DNS) query(url string, result *Result) {
|
||||
if !strings.Contains(url, ":") {
|
||||
url = fmt.Sprintf("%s:%d", url, dnsPort)
|
||||
}
|
||||
queryType := dns.StringToType[d.QueryType]
|
||||
c := new(dns.Client)
|
||||
m := new(dns.Msg)
|
||||
m.SetQuestion(d.QueryName, queryType)
|
||||
r, _, err := c.Exchange(m, url)
|
||||
if err != nil {
|
||||
result.Errors = append(result.Errors, err.Error())
|
||||
return
|
||||
}
|
||||
result.Connected = true
|
||||
result.DNSRCode = dns.RcodeToString[r.Rcode]
|
||||
for _, rr := range r.Answer {
|
||||
switch rr.Header().Rrtype {
|
||||
case dns.TypeA:
|
||||
if a, ok := rr.(*dns.A); ok {
|
||||
result.Body = []byte(a.A.String())
|
||||
}
|
||||
case dns.TypeAAAA:
|
||||
if aaaa, ok := rr.(*dns.AAAA); ok {
|
||||
result.Body = []byte(aaaa.AAAA.String())
|
||||
}
|
||||
case dns.TypeCNAME:
|
||||
if cname, ok := rr.(*dns.CNAME); ok {
|
||||
result.Body = []byte(cname.Target)
|
||||
}
|
||||
case dns.TypeMX:
|
||||
if mx, ok := rr.(*dns.MX); ok {
|
||||
result.Body = []byte(mx.Mx)
|
||||
}
|
||||
case dns.TypeNS:
|
||||
if ns, ok := rr.(*dns.NS); ok {
|
||||
result.Body = []byte(ns.Ns)
|
||||
}
|
||||
default:
|
||||
result.Body = []byte("not supported")
|
||||
}
|
||||
}
|
||||
}
|
26
core/dns_test.go
Normal file
26
core/dns_test.go
Normal file
@ -0,0 +1,26 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIntegrationQuery(t *testing.T) {
|
||||
dns := DNS{
|
||||
QueryType: "A",
|
||||
QueryName: "example.com",
|
||||
}
|
||||
result := &Result{}
|
||||
dns.validateAndSetDefault()
|
||||
dns.query("8.8.8.8", result)
|
||||
if len(result.Errors) != 0 {
|
||||
t.Errorf("there should be no error Errors:%v", result.Errors)
|
||||
}
|
||||
|
||||
if result.DNSRCode != "NOERROR" {
|
||||
t.Errorf("DNSRCode '%s' should have been NOERROR", result.DNSRCode)
|
||||
}
|
||||
|
||||
if string(result.Body) != "93.184.216.34" {
|
||||
t.Errorf("expected result %s", "93.184.216.34")
|
||||
}
|
||||
}
|
@ -33,6 +33,9 @@ type Service struct {
|
||||
// URL to send the request to
|
||||
URL string `yaml:"url"`
|
||||
|
||||
// DNS is the configuration of DNS monitoring
|
||||
DNS *DNS `yaml:"dns,omitempty"`
|
||||
|
||||
// Method of the request made to the url of the service
|
||||
Method string `yaml:"method,omitempty"`
|
||||
|
||||
@ -94,6 +97,11 @@ func (service *Service) ValidateAndSetDefaults() {
|
||||
panic(ErrServiceWithNoCondition)
|
||||
}
|
||||
|
||||
if service.DNS != nil {
|
||||
service.DNS.validateAndSetDefault()
|
||||
return
|
||||
}
|
||||
|
||||
// Make sure that the request can be created
|
||||
_, err := http.NewRequest(service.Method, service.URL, bytes.NewBuffer([]byte(service.Body)))
|
||||
if err != nil {
|
||||
@ -104,12 +112,18 @@ func (service *Service) ValidateAndSetDefaults() {
|
||||
// EvaluateHealth sends a request to the service's URL and evaluates the conditions of the service.
|
||||
func (service *Service) EvaluateHealth() *Result {
|
||||
result := &Result{Success: true, Errors: []string{}}
|
||||
service.getIP(result)
|
||||
if len(result.Errors) == 0 {
|
||||
service.call(result)
|
||||
} else {
|
||||
result.Success = false
|
||||
switch {
|
||||
case service.DNS != nil:
|
||||
service.DNS.query(service.URL, result)
|
||||
default:
|
||||
service.getIP(result)
|
||||
if len(result.Errors) == 0 {
|
||||
service.call(result)
|
||||
} else {
|
||||
result.Success = false
|
||||
}
|
||||
}
|
||||
|
||||
for _, condition := range service.Conditions {
|
||||
success := condition.evaluate(result)
|
||||
if !success {
|
||||
@ -151,6 +165,7 @@ func (service *Service) getIP(result *Result) {
|
||||
}
|
||||
|
||||
func (service *Service) call(result *Result) {
|
||||
|
||||
isServiceTCP := strings.HasPrefix(service.URL, "tcp://")
|
||||
var request *http.Request
|
||||
var response *http.Response
|
||||
|
@ -72,6 +72,49 @@ func TestService_ValidateAndSetDefaultsWithNoConditions(t *testing.T) {
|
||||
t.Fatal("Should've panicked because service didn't have at least 1 condition")
|
||||
}
|
||||
|
||||
func TestService_ValidateAndSetDefaultsWithNoDNSQueryName(t *testing.T) {
|
||||
defer func() { recover() }()
|
||||
service := &Service{
|
||||
Name: "",
|
||||
URL: "http://example.com",
|
||||
DNS: &DNS{
|
||||
QueryType: "A",
|
||||
QueryName: "",
|
||||
},
|
||||
}
|
||||
service.ValidateAndSetDefaults()
|
||||
t.Fatal("Should've panicked because service`s dns didn't have a query name, which is a mandatory field for dns")
|
||||
}
|
||||
|
||||
func TestService_ValidateAndSetDefaultsWithInvalidDNSQueryType(t *testing.T) {
|
||||
defer func() { recover() }()
|
||||
service := &Service{
|
||||
Name: "",
|
||||
URL: "http://example.com",
|
||||
DNS: &DNS{
|
||||
QueryType: "B",
|
||||
QueryName: "example.com",
|
||||
},
|
||||
}
|
||||
service.ValidateAndSetDefaults()
|
||||
t.Fatal("Should've panicked because service`s dns query type is invalid, it needs to be a valid query name like A, AAAA, CNAME...")
|
||||
}
|
||||
|
||||
func TestService_ValidateAndSetDefaultsWithDNS(t *testing.T) {
|
||||
service := &Service{
|
||||
Name: "",
|
||||
URL: "http://example.com",
|
||||
DNS: &DNS{
|
||||
QueryType: "A",
|
||||
QueryName: "example.com",
|
||||
},
|
||||
}
|
||||
service.ValidateAndSetDefaults()
|
||||
if service.DNS.QueryName == "example.com." {
|
||||
t.Error("Service.dns.query-name should be formatted with . suffix")
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_GetAlertsTriggered(t *testing.T) {
|
||||
condition := Condition("[STATUS] == 200")
|
||||
service := Service{
|
||||
@ -115,6 +158,30 @@ func TestIntegrationEvaluateHealth(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegrationEvaluateHealthForDNS(t *testing.T) {
|
||||
conditionSuccess := Condition("[DNS_RCODE] == NOERROR")
|
||||
conditionBody := Condition("[BODY] == 93.184.216.34")
|
||||
service := Service{
|
||||
Name: "TwiNNatioN",
|
||||
URL: "8.8.8.8",
|
||||
DNS: &DNS{
|
||||
QueryType: "A",
|
||||
QueryName: "example.com.",
|
||||
},
|
||||
Conditions: []*Condition{&conditionSuccess, &conditionBody},
|
||||
}
|
||||
result := service.EvaluateHealth()
|
||||
if !result.ConditionResults[0].Success {
|
||||
t.Errorf("Conditions '%s' and %s should have been a success", conditionSuccess, conditionBody)
|
||||
}
|
||||
if !result.Connected {
|
||||
t.Error("Because the connection has been established, result.Connected should've been true")
|
||||
}
|
||||
if !result.Success {
|
||||
t.Error("Because all conditions passed, this should have been a success")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegrationEvaluateHealthWithFailure(t *testing.T) {
|
||||
condition := Condition("[STATUS] == 500")
|
||||
service := Service{
|
||||
|
@ -19,6 +19,9 @@ type Result struct {
|
||||
// HTTPStatus is the HTTP response status code
|
||||
HTTPStatus int `json:"status"`
|
||||
|
||||
//
|
||||
DNSRCode string `json:"dnsr_code"`
|
||||
|
||||
// Body is the response body
|
||||
Body []byte `json:"-"`
|
||||
|
||||
|
Reference in New Issue
Block a user