Add health check for DNS

This commit is contained in:
cemturker
2020-11-18 00:55:31 +01:00
parent d1f24dbea4
commit bc914e12b0
298 changed files with 39755 additions and 7 deletions

View File

@ -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
View 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
View 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")
}
}

View File

@ -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

View File

@ -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{

View File

@ -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:"-"`