Repository: prometheus/blackbox_exporter
Pull request #944 (2022-07-11 15:30:20)

HTTP: Add Skip Resolve Phase With Proxy option
Signed-off-by: Julien Pivotto <roidelapluie@o11y.eu>


Patch:
diff --git a/CONFIGURATION.md b/CONFIGURATION.md
index 35e8446..1143c89 100644
--- a/CONFIGURATION.md
+++ b/CONFIGURATION.md
@@ -114,6 +114,9 @@ The other placeholders are specified separately.
   # HTTP proxy server to use to connect to the targets.
   [ proxy_url: <string> ]
 
+  # Skip DNS resolution and URL change when an HTTP proxy (proxy_url) is set.
+  [ skip_resolve_phase_with_proxy: <boolean> | default = false ]
+
   # OAuth 2.0 configuration to use to connect to the targets.
   oauth2:
       [ <oauth2> ]
@@ -128,7 +131,6 @@ The other placeholders are specified separately.
   # The body of the HTTP request used in probe.
   body: [ <string> ]
 
-
 ```
 
 #### <http_header_match_spec>
diff --git a/config/config.go b/config/config.go
index 8094b29..7a23824 100644
--- a/config/config.go
+++ b/config/config.go
@@ -210,6 +210,7 @@ type HTTPProbe struct {
 	ValidHTTPVersions            []string                `yaml:"valid_http_versions,omitempty"`
 	IPProtocol                   string                  `yaml:"preferred_ip_protocol,omitempty"`
 	IPProtocolFallback           bool                    `yaml:"ip_protocol_fallback,omitempty"`
+	SkipResolvePhaseWithProxy    bool                    `yaml:"skip_resolve_phase_with_proxy,omitempty"`
 	NoFollowRedirects            *bool                   `yaml:"no_follow_redirects,omitempty"`
 	FailIfSSL                    bool                    `yaml:"fail_if_ssl,omitempty"`
 	FailIfNotSSL                 bool                    `yaml:"fail_if_not_ssl,omitempty"`
diff --git a/prober/http.go b/prober/http.go
index 99d38a2..802bdc9 100644
--- a/prober/http.go
+++ b/prober/http.go
@@ -333,11 +333,15 @@ func ProbeHTTP(ctx context.Context, target string, module config.Module, registr
 	targetHost := targetURL.Hostname()
 	targetPort := targetURL.Port()
 
-	ip, lookupTime, err := chooseProtocol(ctx, module.HTTP.IPProtocol, module.HTTP.IPProtocolFallback, targetHost, registry, logger)
-	durationGaugeVec.WithLabelValues("resolve").Add(lookupTime)
-	if err != nil {
-		level.Error(logger).Log("msg", "Error resolving address", "err", err)
-		return false
+	var ip *net.IPAddr
+	if !module.HTTP.SkipResolvePhaseWithProxy || module.HTTP.HTTPClientConfig.ProxyURL.URL == nil {
+		var lookupTime float64
+		ip, lookupTime, err = chooseProtocol(ctx, module.HTTP.IPProtocol, module.HTTP.IPProtocolFallback, targetHost, registry, logger)
+		durationGaugeVec.WithLabelValues("resolve").Add(lookupTime)
+		if err != nil {
+			level.Error(logger).Log("msg", "Error resolving address", "err", err)
+			return false
+		}
 	}
 
 	// Do not move the following variable to global scope. The cases.Caser returned by
@@ -404,16 +408,18 @@ func ProbeHTTP(ctx context.Context, target string, module config.Module, registr
 		httpConfig.Method = "GET"
 	}
 
-	// Replace the host field in the URL with the IP we resolved.
 	origHost := targetURL.Host
-	if targetPort == "" {
-		if strings.Contains(ip.String(), ":") {
-			targetURL.Host = "[" + ip.String() + "]"
+	if ip != nil {
+		// Replace the host field in the URL with the IP we resolved.
+		if targetPort == "" {
+			if strings.Contains(ip.String(), ":") {
+				targetURL.Host = "[" + ip.String() + "]"
+			} else {
+				targetURL.Host = ip.String()
+			}
 		} else {
-			targetURL.Host = ip.String()
+			targetURL.Host = net.JoinHostPort(ip.String(), targetPort)
 		}
-	} else {
-		targetURL.Host = net.JoinHostPort(ip.String(), targetPort)
 	}
 
 	var body io.Reader


Test patch:
diff --git a/prober/http_test.go b/prober/http_test.go
index cd27761..078475c 100644
--- a/prober/http_test.go
+++ b/prober/http_test.go
@@ -25,6 +25,7 @@ import (
 	"io/ioutil"
 	"net/http"
 	"net/http/httptest"
+	"net/url"
 	"os"
 	"strconv"
 	"strings"
@@ -1338,3 +1339,72 @@ func TestCookieJar(t *testing.T) {
 		t.Fatalf("Redirect test failed unexpectedly, got %s", body)
 	}
 }
+
+func TestSkipResolvePhase(t *testing.T) {
+	if testing.Short() {
+		t.Skip("skipping network dependent test")
+	}
+	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		w.WriteHeader(http.StatusNoContent)
+	}))
+	defer ts.Close()
+
+	t.Run("Without Proxy", func(t *testing.T) {
+		registry := prometheus.NewRegistry()
+		testCTX, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+		defer cancel()
+		result := ProbeHTTP(testCTX, ts.URL,
+			config.Module{Timeout: time.Second, HTTP: config.HTTPProbe{IPProtocolFallback: true, HTTPClientConfig: pconfig.DefaultHTTPClientConfig, SkipResolvePhaseWithProxy: true}}, registry, log.NewNopLogger())
+		if !result {
+			t.Fatalf("Probe unsuccessful")
+		}
+		mfs, err := registry.Gather()
+		if err != nil {
+			t.Fatal(err)
+		}
+		expectedMetrics := map[string]map[string]map[string]struct{}{
+			"probe_http_duration_seconds": {
+				"phase": {
+					"connect":    {},
+					"processing": {},
+					"resolve":    {},
+					"transfer":   {},
+					"tls":        {},
+				},
+			},
+		}
+
+		checkMetrics(expectedMetrics, mfs, t)
+	})
+	t.Run("With Proxy", func(t *testing.T) {
+		registry := prometheus.NewRegistry()
+		testCTX, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+		defer cancel()
+		httpCfg := pconfig.DefaultHTTPClientConfig
+		u, err := url.Parse("http://127.0.0.1:3128")
+		if err != nil {
+			t.Fatalf(err.Error())
+		}
+		httpCfg.ProxyURL = pconfig.URL{
+			URL: u,
+		}
+		ProbeHTTP(testCTX, ts.URL,
+			config.Module{Timeout: time.Second, HTTP: config.HTTPProbe{IPProtocolFallback: true, HTTPClientConfig: httpCfg, SkipResolvePhaseWithProxy: true}}, registry, log.NewNopLogger())
+		mfs, err := registry.Gather()
+		if err != nil {
+			t.Fatal(err)
+		}
+		expectedMetrics := map[string]map[string]map[string]struct{}{
+			"probe_http_duration_seconds": {
+				"phase": {
+					"connect":    {},
+					"processing": {},
+					"transfer":   {},
+					"tls":        {},
+				},
+			},
+		}
+
+		checkMetrics(expectedMetrics, mfs, t)
+	})
+}