Repository: prometheus/blackbox_exporter (go)

Issue:
Feature request: DNS probes should return enough metrics to tell servers that don't answer from servers that refuse our query
Right now, DNS probes do not return enough information to tell the difference between a DNS server not responding at all and a DNS server that responds with query refusals (eg status REFUSED). These are potentially fairly different failures, especially if you're querying an external DNS server to verify that it can resolve your domains, and it would be nice if you could tell the difference through the blackbox exporter.

Perhaps the easiest feature addition would be to expose a probe_dns_rcode metric and give it a value of -1 if the probe received no response.

Pull request description:
Add probe_dns_query_succeeded metric
Currently, one may not be able to differentiate between a query refusal
by the target and the target not responding to a query. These two cases
can have very different ramifications.

This patch adds the probe_dns_query_succeeded gauge which is 1 if and
only if the DNS query was executed correctly, i.e., that the target
host has sent a response, and 0 otherwise.

Resolves #474

Signed-off-by: Daniel Teunis <daniel@teunis.cc>

Patch:
diff --git a/prober/dns.go b/prober/dns.go
index 8e8a1b5..6bf2e46 100644
--- a/prober/dns.go
+++ b/prober/dns.go
@@ -142,6 +142,10 @@ func ProbeDNS(ctx context.Context, target string, module config.Module, registry
 		Name: "probe_dns_additional_rrs",
 		Help: "Returns number of entries in the additional resource record list",
 	})
+	probeDNSQuerySucceeded := prometheus.NewGauge(prometheus.GaugeOpts{
+		Name: "probe_dns_query_succeeded",
+		Help: "Displays whether or not the query was executed successfully",
+	})
 
 	for _, lv := range []string{"resolve", "connect", "request"} {
 		probeDNSDurationGaugeVec.WithLabelValues(lv)
@@ -151,6 +155,7 @@ func ProbeDNS(ctx context.Context, target string, module config.Module, registry
 	registry.MustRegister(probeDNSAnswerRRSGauge)
 	registry.MustRegister(probeDNSAuthorityRRSGauge)
 	registry.MustRegister(probeDNSAdditionalRRSGauge)
+	registry.MustRegister(probeDNSQuerySucceeded)
 
 	qc := uint16(dns.ClassINET)
 	if module.DNS.QueryClass != "" {
@@ -274,6 +279,7 @@ func ProbeDNS(ctx context.Context, target string, module config.Module, registry
 	probeDNSAnswerRRSGauge.Set(float64(len(response.Answer)))
 	probeDNSAuthorityRRSGauge.Set(float64(len(response.Ns)))
 	probeDNSAdditionalRRSGauge.Set(float64(len(response.Extra)))
+	probeDNSQuerySucceeded.Set(1)
 
 	if qt == dns.TypeSOA {
 		probeDNSSOAGauge = prometheus.NewGauge(prometheus.GaugeOpts{


Test patch:
diff --git a/prober/dns_test.go b/prober/dns_test.go
index 73385ae..22f702c 100644
--- a/prober/dns_test.go
+++ b/prober/dns_test.go
@@ -179,9 +179,10 @@ func TestRecursiveDNSResponse(t *testing.T) {
 				t.Fatal(err)
 			}
 			expectedResults := map[string]float64{
-				"probe_dns_answer_rrs":     2,
-				"probe_dns_authority_rrs":  0,
-				"probe_dns_additional_rrs": 0,
+				"probe_dns_answer_rrs":      2,
+				"probe_dns_authority_rrs":   0,
+				"probe_dns_additional_rrs":  0,
+				"probe_dns_query_succeeded": 1,
 			}
 			if !test.Probe.Recursion {
 				expectedResults["probe_dns_answer_rrs"] = 0
@@ -382,9 +383,10 @@ func TestAuthoritativeDNSResponse(t *testing.T) {
 				t.Fatal(err)
 			}
 			expectedResults := map[string]float64{
-				"probe_dns_answer_rrs":     1,
-				"probe_dns_authority_rrs":  2,
-				"probe_dns_additional_rrs": 3,
+				"probe_dns_answer_rrs":      1,
+				"probe_dns_authority_rrs":   2,
+				"probe_dns_additional_rrs":  3,
+				"probe_dns_query_succeeded": 1,
 			}
 			if test.Probe.QueryType == "SOA" {
 				expectedResults["probe_dns_serial"] = 1000
@@ -456,10 +458,17 @@ func TestServfailDNSResponse(t *testing.T) {
 				t.Fatal(err)
 			}
 			expectedResults := map[string]float64{
-				"probe_dns_answer_rrs":     0,
-				"probe_dns_authority_rrs":  0,
-				"probe_dns_additional_rrs": 0,
+				"probe_dns_answer_rrs":      0,
+				"probe_dns_authority_rrs":   0,
+				"probe_dns_additional_rrs":  0,
+				"probe_dns_query_succeeded": 1,
 			}
+
+			// Handle case where ProbeDNS fails before executing the query because of an invalid query type
+			if test.Probe.QueryType == "NOT_A_VALID_QUERY_TYPE" {
+				expectedResults["probe_dns_query_succeeded"] = 0
+			}
+
 			checkRegistryResults(expectedResults, mfs, t)
 		}
 	}
@@ -638,9 +647,10 @@ func TestDNSMetrics(t *testing.T) {
 				"request": {},
 			},
 		},
-		"probe_dns_answer_rrs":     nil,
-		"probe_dns_authority_rrs":  nil,
-		"probe_dns_additional_rrs": nil,
+		"probe_dns_answer_rrs":      nil,
+		"probe_dns_authority_rrs":   nil,
+		"probe_dns_additional_rrs":  nil,
+		"probe_dns_query_succeeded": nil,
 	}
 
 	checkMetrics(expectedMetrics, mfs, t)