Repository: prometheus/common
Pull request #462 (2023-03-06 13:08:40)

Add support to use Proxy From Environment
This commit adds a new field `ProxyFromEnvironment` to the `HTTPClientConfig` and `OAuth2` structs to enable Prometheus to use the default proxy from the environment variable (as specified in net/http ProxyFromEnvironment.) This is opt-out by default, so does not change anything for existing users.

This change is necessary because Prometheus previously had insufficient behavior to avoid proxying EC2 metadata requests when `proxy_url` is set in `ec2_sd_configs`.

See https://github.com/prometheus/prometheus/issues/11991


Patch:
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
deleted file mode 100644
index a13b8c5..0000000
--- a/.github/dependabot.yml
+++ /dev/null
@@ -1,6 +0,0 @@
-version: 2
-updates:
-  - package-ecosystem: "gomod"
-    directory: "/"
-    schedule:
-      interval: monthly
diff --git a/.yamllint b/.yamllint
index 1955257..3878a31 100644
--- a/.yamllint
+++ b/.yamllint
@@ -21,4 +21,8 @@ rules:
   line-length: disable
   truthy:
     ignore: |
-      .github/workflows/*.yml
+      .github/workflows/codeql-analysis.yml
+      .github/workflows/funcbench.yml
+      .github/workflows/fuzzing.yml
+      .github/workflows/prombench.yml
+      .github/workflows/golangci-lint.yml
diff --git a/config/http_config.go b/config/http_config.go
index 1bc2d06..7316320 100644
--- a/config/http_config.go
+++ b/config/http_config.go
@@ -31,6 +31,7 @@ import (
 	"time"
 
 	"github.com/mwitkow/go-conntrack"
+	"golang.org/x/net/http/httpproxy"
 	"golang.org/x/net/http2"
 	"golang.org/x/oauth2"
 	"golang.org/x/oauth2/clientcredentials"
@@ -227,11 +228,26 @@ type OAuth2 struct {
 	Scopes           []string          `yaml:"scopes,omitempty" json:"scopes,omitempty"`
 	TokenURL         string            `yaml:"token_url" json:"token_url"`
 	EndpointParams   map[string]string `yaml:"endpoint_params,omitempty" json:"endpoint_params,omitempty"`
+	TLSConfig        TLSConfig         `yaml:"tls_config,omitempty"`
+	ProxyConfig      `yaml:",inline"`
+}
 
-	// HTTP proxy server to use to connect to the targets.
-	ProxyURL URL `yaml:"proxy_url,omitempty" json:"proxy_url,omitempty"`
-	// TLSConfig is used to connect to the token URL.
-	TLSConfig TLSConfig `yaml:"tls_config,omitempty"`
+// UnmarshalYAML implements the yaml.Unmarshaler interface
+func (o *OAuth2) UnmarshalYAML(unmarshal func(interface{}) error) error {
+	type plain OAuth2
+	if err := unmarshal((*plain)(o)); err != nil {
+		return err
+	}
+	return o.ProxyConfig.Validate()
+}
+
+// UnmarshalJSON implements the json.Marshaler interface for URL.
+func (o *OAuth2) UnmarshalJSON(data []byte) error {
+	type plain OAuth2
+	if err := json.Unmarshal(data, (*plain)(o)); err != nil {
+		return err
+	}
+	return o.ProxyConfig.Validate()
 }
 
 // SetDirectory joins any relative file paths with dir.
@@ -281,13 +297,6 @@ type HTTPClientConfig struct {
 	// The bearer token file for the targets. Deprecated in favour of
 	// Authorization.CredentialsFile.
 	BearerTokenFile string `yaml:"bearer_token_file,omitempty" json:"bearer_token_file,omitempty"`
-	// HTTP proxy server to use to connect to the targets.
-	ProxyURL URL `yaml:"proxy_url,omitempty" json:"proxy_url,omitempty"`
-	// ProxyConnectHeader optionally specifies headers to send to
-	// proxies during CONNECT requests. Assume that at least _some_ of
-	// these headers are going to contain secrets and use Secret as the
-	// value type instead of string.
-	ProxyConnectHeader Header `yaml:"proxy_connect_header,omitempty" json:"proxy_connect_header,omitempty"`
 	// TLSConfig to use to connect to the targets.
 	TLSConfig TLSConfig `yaml:"tls_config,omitempty" json:"tls_config,omitempty"`
 	// FollowRedirects specifies whether the client should follow HTTP 3xx redirects.
@@ -298,6 +307,8 @@ type HTTPClientConfig struct {
 	// The omitempty flag is not set, because it would be hidden from the
 	// marshalled configuration when set to false.
 	EnableHTTP2 bool `yaml:"enable_http2" json:"enable_http2"`
+	// Proxy configuration.
+	ProxyConfig `yaml:",inline"`
 }
 
 // SetDirectory joins any relative file paths with dir.
@@ -372,8 +383,8 @@ func (c *HTTPClientConfig) Validate() error {
 			return fmt.Errorf("at most one of oauth2 client_secret & client_secret_file must be configured")
 		}
 	}
-	if len(c.ProxyConnectHeader) > 0 && (c.ProxyURL.URL == nil || c.ProxyURL.String() == "") {
-		return fmt.Errorf("if proxy_connect_header is configured proxy_url must also be configured")
+	if err := c.ProxyConfig.Validate(); err != nil {
+		return err
 	}
 	return nil
 }
@@ -502,8 +513,8 @@ func NewRoundTripperFromConfig(cfg HTTPClientConfig, name string, optFuncs ...HT
 		// The only timeout we care about is the configured scrape timeout.
 		// It is applied on request. So we leave out any timings here.
 		var rt http.RoundTripper = &http.Transport{
-			Proxy:                 http.ProxyURL(cfg.ProxyURL.URL),
-			ProxyConnectHeader:    cfg.ProxyConnectHeader.HTTPHeader(),
+			Proxy:                 cfg.ProxyConfig.Proxy(),
+			ProxyConnectHeader:    cfg.ProxyConfig.GetProxyConnectHeader(),
 			MaxIdleConns:          20000,
 			MaxIdleConnsPerHost:   1000, // see https://github.com/golang/go/issues/13801
 			DisableKeepAlives:     !opts.keepAlivesEnabled,
@@ -724,7 +735,8 @@ func (rt *oauth2RoundTripper) RoundTrip(req *http.Request) (*http.Response, erro
 		tlsTransport := func(tlsConfig *tls.Config) (http.RoundTripper, error) {
 			return &http.Transport{
 				TLSClientConfig:       tlsConfig,
-				Proxy:                 http.ProxyURL(rt.config.ProxyURL.URL),
+				Proxy:                 rt.config.ProxyConfig.Proxy(),
+				ProxyConnectHeader:    rt.config.ProxyConfig.GetProxyConnectHeader(),
 				DisableKeepAlives:     !rt.opts.keepAlivesEnabled,
 				MaxIdleConns:          20,
 				MaxIdleConnsPerHost:   1, // see https://github.com/golang/go/issues/13801
@@ -1072,3 +1084,78 @@ func (c HTTPClientConfig) String() string {
 	}
 	return string(b)
 }
+
+type ProxyConfig struct {
+	// HTTP proxy server to use to connect to the targets.
+	ProxyURL URL `yaml:"proxy_url,omitempty" json:"proxy_url,omitempty"`
+	// NoProxy contains addresses that should not use a proxy.
+	NoProxy string `yaml:"no_proxy,omitempty" json:"no_proxy,omitempty"`
+	// ProxyFromEnvironment makes use of net/http ProxyFromEnvironment function
+	// to determine proxies.
+	ProxyFromEnvironment bool `yaml:"proxy_from_environment,omitempty" json:"proxy_from_environment,omitempty"`
+	// ProxyConnectHeader optionally specifies headers to send to
+	// proxies during CONNECT requests. Assume that at least _some_ of
+	// these headers are going to contain secrets and use Secret as the
+	// value type instead of string.
+	ProxyConnectHeader Header `yaml:"proxy_connect_header,omitempty" json:"proxy_connect_header,omitempty"`
+
+	proxyFunc func(*http.Request) (*url.URL, error)
+}
+
+// UnmarshalYAML implements the yaml.Unmarshaler interface.
+func (c *ProxyConfig) Validate() error {
+	if len(c.ProxyConnectHeader) > 0 && (!c.ProxyFromEnvironment && (c.ProxyURL.URL == nil || c.ProxyURL.String() == "")) {
+		return fmt.Errorf("if proxy_connect_header is configured, proxy_url or proxy_from_environment must also be configured")
+	}
+	if c.ProxyFromEnvironment && c.ProxyURL.URL != nil && c.ProxyURL.String() != "" {
+		return fmt.Errorf("if proxy_from_environment is configured, proxy_url must not be configured")
+	}
+	if c.ProxyFromEnvironment && c.NoProxy != "" {
+		return fmt.Errorf("if proxy_from_environment is configured, no_proxy must not be configured")
+	}
+	if c.ProxyURL.URL == nil && c.NoProxy != "" {
+		return fmt.Errorf("if no_proxy is configured, proxy_url must also be configured")
+	}
+	return nil
+}
+
+// Proxy returns the Proxy URL for a request.
+func (c *ProxyConfig) Proxy() (fn func(*http.Request) (*url.URL, error)) {
+	if c == nil {
+		return nil
+	}
+	defer func() {
+		fn = c.proxyFunc
+	}()
+	if c.proxyFunc != nil {
+		return
+	}
+	if c.ProxyFromEnvironment {
+		proxyFn := httpproxy.FromEnvironment().ProxyFunc()
+		c.proxyFunc = func(req *http.Request) (*url.URL, error) {
+			return proxyFn(req.URL)
+		}
+		return
+	}
+	if c.ProxyURL.URL != nil && c.ProxyURL.URL.String() != "" {
+		if c.NoProxy == "" {
+			c.proxyFunc = http.ProxyURL(c.ProxyURL.URL)
+			return
+		}
+		proxy := &httpproxy.Config{
+			HTTPProxy:  c.ProxyURL.String(),
+			HTTPSProxy: c.ProxyURL.String(),
+			NoProxy:    c.NoProxy,
+		}
+		proxyFn := proxy.ProxyFunc()
+		c.proxyFunc = func(req *http.Request) (*url.URL, error) {
+			return proxyFn(req.URL)
+		}
+	}
+	return
+}
+
+// ProxyConnectHeader() return the Proxy Connext Headers.
+func (c *ProxyConfig) GetProxyConnectHeader() http.Header {
+	return c.ProxyConnectHeader.HTTPHeader()
+}
diff --git a/go.mod b/go.mod
index 84ffe3e..7c012f0 100644
--- a/go.mod
+++ b/go.mod
@@ -3,28 +3,31 @@ module github.com/prometheus/common
 go 1.18
 
 require (
-	github.com/alecthomas/kingpin/v2 v2.3.1
 	github.com/go-kit/log v0.2.1
 	github.com/golang/protobuf v1.5.2
+	github.com/json-iterator/go v1.1.12
 	github.com/julienschmidt/httprouter v1.3.0
 	github.com/matttproud/golang_protobuf_extensions v1.0.4
 	github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f
 	github.com/prometheus/client_golang v1.14.0
 	github.com/prometheus/client_model v0.3.0
 	golang.org/x/net v0.7.0
-	golang.org/x/oauth2 v0.5.0
+	golang.org/x/oauth2 v0.3.0
+	gopkg.in/alecthomas/kingpin.v2 v2.2.6
 	gopkg.in/yaml.v2 v2.4.0
 )
 
 require (
-	github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect
+	github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
+	github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d // indirect
 	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/cespare/xxhash/v2 v2.1.2 // indirect
 	github.com/go-logfmt/logfmt v0.5.1 // indirect
 	github.com/jpillora/backoff v1.0.0 // indirect
+	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+	github.com/modern-go/reflect2 v1.0.2 // indirect
 	github.com/prometheus/procfs v0.8.0 // indirect
 	github.com/stretchr/testify v1.8.0 // indirect
-	github.com/xhit/go-str2duration v1.2.0 // indirect
 	golang.org/x/sys v0.5.0 // indirect
 	golang.org/x/text v0.7.0 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
diff --git a/go.sum b/go.sum
index c5661ec..ec8105f 100644
--- a/go.sum
+++ b/go.sum
@@ -1,8 +1,7 @@
-github.com/alecthomas/kingpin/v2 v2.3.1 h1:ANLJcKmQm4nIaog7xdr/id6FM6zm5hHnfZrvtKPxqGg=
-github.com/alecthomas/kingpin/v2 v2.3.1/go.mod h1:oYL5vtsvEHZGHxU7DMp32Dvx+qL+ptGn6lWaot2vCNE=
-github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
-github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc=
-github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
+github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
+github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E=
+github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
 github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
@@ -22,8 +21,11 @@ github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw
 github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
 github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
 github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
 github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
 github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
@@ -33,6 +35,11 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
 github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
 github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU=
 github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -45,19 +52,17 @@ github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5
 github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
-github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
-github.com/xhit/go-str2duration v1.2.0 h1:BcV5u025cITWxEQKGWr1URRzrcXtu7uk8+luz3Yuhwc=
-github.com/xhit/go-str2duration v1.2.0/go.mod h1:3cPSlfZlUHVlneIVfePFWcJZsuwf+P1v2SRTV4cUmp4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
 golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
 golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
-golang.org/x/oauth2 v0.5.0 h1:HuArIo48skDwlrvM3sEdHXElYslAMsf3KwRkkW4MC4s=
-golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I=
+golang.org/x/oauth2 v0.3.0 h1:6l90koy8/LaBLmLu8jpHeHexzMwEita0zFfYlggy2F8=
+golang.org/x/oauth2 v0.3.0/go.mod h1:rQrIauxkUhJ6CuwEXwymO2/eh4xz2ZWF1nBkcxS+tGk=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
@@ -74,6 +79,8 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0
 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
 google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
+gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
diff --git a/model/time.go b/model/time.go
index 5727452..c909b8a 100644
--- a/model/time.go
+++ b/model/time.go
@@ -18,6 +18,7 @@ import (
 	"errors"
 	"fmt"
 	"math"
+	"regexp"
 	"strconv"
 	"strings"
 	"time"
@@ -182,78 +183,54 @@ func (d *Duration) Type() string {
 	return "duration"
 }
 
-func isdigit(c byte) bool { return c >= '0' && c <= '9' }
-
-// Units are required to go in order from biggest to smallest.
-// This guards against confusion from "1m1d" being 1 minute + 1 day, not 1 month + 1 day.
-var unitMap = map[string]struct {
-	pos  int
-	mult uint64
-}{
-	"ms": {7, uint64(time.Millisecond)},
-	"s":  {6, uint64(time.Second)},
-	"m":  {5, uint64(time.Minute)},
-	"h":  {4, uint64(time.Hour)},
-	"d":  {3, uint64(24 * time.Hour)},
-	"w":  {2, uint64(7 * 24 * time.Hour)},
-	"y":  {1, uint64(365 * 24 * time.Hour)},
-}
+var durationRE = regexp.MustCompile("^(([0-9]+)y)?(([0-9]+)w)?(([0-9]+)d)?(([0-9]+)h)?(([0-9]+)m)?(([0-9]+)s)?(([0-9]+)ms)?$")
 
 // ParseDuration parses a string into a time.Duration, assuming that a year
 // always has 365d, a week always has 7d, and a day always has 24h.
-func ParseDuration(s string) (Duration, error) {
-	switch s {
+func ParseDuration(durationStr string) (Duration, error) {
+	switch durationStr {
 	case "0":
 		// Allow 0 without a unit.
 		return 0, nil
 	case "":
 		return 0, errors.New("empty duration string")
 	}
+	matches := durationRE.FindStringSubmatch(durationStr)
+	if matches == nil {
+		return 0, fmt.Errorf("not a valid duration string: %q", durationStr)
+	}
+	var dur time.Duration
 
-	orig := s
-	var dur uint64
-	lastUnitPos := 0
-
-	for s != "" {
-		if !isdigit(s[0]) {
-			return 0, fmt.Errorf("not a valid duration string: %q", orig)
-		}
-		// Consume [0-9]*
-		i := 0
-		for ; i < len(s) && isdigit(s[i]); i++ {
-		}
-		v, err := strconv.ParseUint(s[:i], 10, 0)
-		if err != nil {
-			return 0, fmt.Errorf("not a valid duration string: %q", orig)
+	// Parse the match at pos `pos` in the regex and use `mult` to turn that
+	// into ms, then add that value to the total parsed duration.
+	var overflowErr error
+	m := func(pos int, mult time.Duration) {
+		if matches[pos] == "" {
+			return
 		}
-		s = s[i:]
+		n, _ := strconv.Atoi(matches[pos])
 
-		// Consume unit.
-		for i = 0; i < len(s) && !isdigit(s[i]); i++ {
-		}
-		if i == 0 {
-			return 0, fmt.Errorf("not a valid duration string: %q", orig)
-		}
-		u := s[:i]
-		s = s[i:]
-		unit, ok := unitMap[u]
-		if !ok {
-			return 0, fmt.Errorf("unknown unit %q in duration %q", u, orig)
-		}
-		if unit.pos <= lastUnitPos { // Units must go in order from biggest to smallest.
-			return 0, fmt.Errorf("not a valid duration string: %q", orig)
-		}
-		lastUnitPos = unit.pos
 		// Check if the provided duration overflows time.Duration (> ~ 290years).
-		if v > 1<<63/unit.mult {
-			return 0, errors.New("duration out of range")
+		if n > int((1<<63-1)/mult/time.Millisecond) {
+			overflowErr = errors.New("duration out of range")
 		}
-		dur += v * unit.mult
-		if dur > 1<<63-1 {
-			return 0, errors.New("duration out of range")
+		d := time.Duration(n) * time.Millisecond
+		dur += d * mult
+
+		if dur < 0 {
+			overflowErr = errors.New("duration out of range")
 		}
 	}
-	return Duration(dur), nil
+
+	m(2, 1000*60*60*24*365) // y
+	m(4, 1000*60*60*24*7)   // w
+	m(6, 1000*60*60*24)     // d
+	m(8, 1000*60*60)        // h
+	m(10, 1000*60)          // m
+	m(12, 1000)             // s
+	m(14, 1)                // ms
+
+	return Duration(dur), overflowErr
 }
 
 func (d Duration) String() string {
diff --git a/model/value_float.go b/model/value_float.go
index 0f615a7..8b59571 100644
--- a/model/value_float.go
+++ b/model/value_float.go
@@ -18,8 +18,15 @@ import (
 	"fmt"
 	"math"
 	"strconv"
+	"unsafe"
+
+	jsoniter "github.com/json-iterator/go"
 )
 
+func init() {
+	jsoniter.RegisterTypeEncoderFunc("model.SamplePair", marshalSamplePairJSON, marshalJSONIsEmpty)
+}
+
 var (
 	// ZeroSamplePair is the pseudo zero-value of SamplePair used to signal a
 	// non-existing sample pair. It is a SamplePair with timestamp Earliest and
@@ -71,16 +78,18 @@ type SamplePair struct {
 	Value     SampleValue
 }
 
+// marshalSamplePairJSON writes `[ts, "val"]`.
+func marshalSamplePairJSON(ptr unsafe.Pointer, stream *jsoniter.Stream) {
+	p := *((*SamplePair)(ptr))
+	stream.WriteArrayStart()
+	MarshalTimestamp(int64(p.Timestamp), stream)
+	stream.WriteMore()
+	MarshalValue(float64(p.Value), stream)
+	stream.WriteArrayEnd()
+}
+
 func (s SamplePair) MarshalJSON() ([]byte, error) {
-	t, err := json.Marshal(s.Timestamp)
-	if err != nil {
-		return nil, err
-	}
-	v, err := json.Marshal(s.Value)
-	if err != nil {
-		return nil, err
-	}
-	return []byte(fmt.Sprintf("[%s,%s]", t, v)), nil
+	return jsoniter.ConfigCompatibleWithStandardLibrary.Marshal(s)
 }
 
 // UnmarshalJSON implements json.Unmarshaler.
diff --git a/model/value_histogram.go b/model/value_histogram.go
index 54bb038..cc221a8 100644
--- a/model/value_histogram.go
+++ b/model/value_histogram.go
@@ -18,8 +18,16 @@ import (
 	"fmt"
 	"strconv"
 	"strings"
+	"unsafe"
+
+	jsoniter "github.com/json-iterator/go"
 )
 
+func init() {
+	jsoniter.RegisterTypeEncoderFunc("model.HistogramBucket", marshalHistogramBucketJSON, marshalJSONIsEmpty)
+	jsoniter.RegisterTypeEncoderFunc("model.SampleHistogramPair", marshalSampleHistogramPairJSON, marshalJSONIsEmpty)
+}
+
 type FloatString float64
 
 func (v FloatString) String() string {
@@ -49,24 +57,10 @@ type HistogramBucket struct {
 	Count      FloatString
 }
 
-func (s HistogramBucket) MarshalJSON() ([]byte, error) {
-	b, err := json.Marshal(s.Boundaries)
-	if err != nil {
-		return nil, err
-	}
-	l, err := json.Marshal(s.Lower)
-	if err != nil {
-		return nil, err
-	}
-	u, err := json.Marshal(s.Upper)
-	if err != nil {
-		return nil, err
-	}
-	c, err := json.Marshal(s.Count)
-	if err != nil {
-		return nil, err
-	}
-	return []byte(fmt.Sprintf("[%s,%s,%s,%s]", b, l, u, c)), nil
+// marshalHistogramBucketJSON writes fmt.Sprintf("[%s,%s,%s,%s]", b.Boundaries, b.Lower, b.Upper, b.Count).
+func marshalHistogramBucketJSON(ptr unsafe.Pointer, stream *jsoniter.Stream) {
+	b := *((*HistogramBucket)(ptr))
+	MarshalHistogramBucket(b, stream)
 }
 
 func (s *HistogramBucket) UnmarshalJSON(buf []byte) error {
@@ -139,19 +133,21 @@ type SampleHistogramPair struct {
 	Histogram *SampleHistogram
 }
 
+// marshalSampleHistogramPairJSON writes `[ts, "val"]`.
+func marshalSampleHistogramPairJSON(ptr unsafe.Pointer, stream *jsoniter.Stream) {
+	p := *((*SampleHistogramPair)(ptr))
+	stream.WriteArrayStart()
+	MarshalTimestamp(int64(p.Timestamp), stream)
+	stream.WriteMore()
+	MarshalHistogram(*p.Histogram, stream)
+	stream.WriteArrayEnd()
+}
+
 func (s SampleHistogramPair) MarshalJSON() ([]byte, error) {
 	if s.Histogram == nil {
 		return nil, fmt.Errorf("histogram is nil")
 	}
-	t, err := json.Marshal(s.Timestamp)
-	if err != nil {
-		return nil, err
-	}
-	v, err := json.Marshal(s.Histogram)
-	if err != nil {
-		return nil, err
-	}
-	return []byte(fmt.Sprintf("[%s,%s]", t, v)), nil
+	return jsoniter.ConfigCompatibleWithStandardLibrary.Marshal(s)
 }
 
 func (s *SampleHistogramPair) UnmarshalJSON(buf []byte) error {
diff --git a/model/value_marshal.go b/model/value_marshal.go
new file mode 100644
index 0000000..df193bc
--- /dev/null
+++ b/model/value_marshal.go
@@ -0,0 +1,131 @@
+// Copyright 2013 The Prometheus Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package model
+
+import (
+	"math"
+	"strconv"
+	"unsafe"
+
+	jsoniter "github.com/json-iterator/go"
+)
+
+func marshalJSONIsEmpty(ptr unsafe.Pointer) bool {
+	return false
+}
+
+// MarshalTimestamp marshals a point timestamp using the passed jsoniter stream.
+func MarshalTimestamp(t int64, stream *jsoniter.Stream) {
+	// Write out the timestamp as a float divided by 1000.
+	// This is ~3x faster than converting to a float.
+	if t < 0 {
+		stream.WriteRaw(`-`)
+		t = -t
+	}
+	stream.WriteInt64(t / 1000)
+	fraction := t % 1000
+	if fraction != 0 {
+		stream.WriteRaw(`.`)
+		if fraction < 100 {
+			stream.WriteRaw(`0`)
+		}
+		if fraction < 10 {
+			stream.WriteRaw(`0`)
+		}
+		stream.WriteInt64(fraction)
+	}
+}
+
+// MarshalValue marshals a point value using the passed jsoniter stream.
+func MarshalValue(v float64, stream *jsoniter.Stream) {
+	stream.WriteRaw(`"`)
+	// Taken from https://github.com/json-iterator/go/blob/master/stream_float.go#L71 as a workaround
+	// to https://github.com/json-iterator/go/issues/365 (jsoniter, to follow json standard, doesn't allow inf/nan).
+	buf := stream.Buffer()
+	abs := math.Abs(v)
+	fmt := byte('f')
+	// Note: Must use float32 comparisons for underlying float32 value to get precise cutoffs right.
+	if abs != 0 {
+		if abs < 1e-6 || abs >= 1e21 {
+			fmt = 'e'
+		}
+	}
+	buf = strconv.AppendFloat(buf, v, fmt, -1, 64)
+	stream.SetBuffer(buf)
+	stream.WriteRaw(`"`)
+}
+
+// MarshalHistogramBucket writes something like: [ 3, "-0.25", "0.25", "3"]
+// See MarshalHistogram to understand what the numbers mean
+func MarshalHistogramBucket(b HistogramBucket, stream *jsoniter.Stream) {
+	stream.WriteArrayStart()
+	stream.WriteInt32(b.Boundaries)
+	stream.WriteMore()
+	MarshalValue(float64(b.Lower), stream)
+	stream.WriteMore()
+	MarshalValue(float64(b.Upper), stream)
+	stream.WriteMore()
+	MarshalValue(float64(b.Count), stream)
+	stream.WriteArrayEnd()
+}
+
+// MarshalHistogram writes something like:
+//
+//	{
+//	    "count": "42",
+//	    "sum": "34593.34",
+//	    "buckets": [
+//	      [ 3, "-0.25", "0.25", "3"],
+//	      [ 0, "0.25", "0.5", "12"],
+//	      [ 0, "0.5", "1", "21"],
+//	      [ 0, "2", "4", "6"]
+//	    ]
+//	}
+//
+// The 1st element in each bucket array determines if the boundaries are
+// inclusive (AKA closed) or exclusive (AKA open):
+//
+//	0: lower exclusive, upper inclusive
+//	1: lower inclusive, upper exclusive
+//	2: both exclusive
+//	3: both inclusive
+//
+// The 2nd and 3rd elements are the lower and upper boundary. The 4th element is
+// the bucket count.
+func MarshalHistogram(h SampleHistogram, stream *jsoniter.Stream) {
+	stream.WriteObjectStart()
+	stream.WriteObjectField(`count`)
+	MarshalValue(float64(h.Count), stream)
+	stream.WriteMore()
+	stream.WriteObjectField(`sum`)
+	MarshalValue(float64(h.Sum), stream)
+
+	bucketFound := false
+	for _, bucket := range h.Buckets {
+		if bucket.Count == 0 {
+			continue // No need to expose empty buckets in JSON.
+		}
+		stream.WriteMore()
+		if !bucketFound {
+			stream.WriteObjectField(`buckets`)
+			stream.WriteArrayStart()
+		}
+		bucketFound = true
+		MarshalHistogramBucket(*bucket, stream)
+	}
+	if bucketFound {
+		stream.WriteArrayEnd()
+	}
+	stream.WriteObjectEnd()
+}
diff --git a/promlog/flag/flag.go b/promlog/flag/flag.go
index c25d818..ec55008 100644
--- a/promlog/flag/flag.go
+++ b/promlog/flag/flag.go
@@ -14,8 +14,8 @@
 package flag
 
 import (
-	kingpin "github.com/alecthomas/kingpin/v2"
 	"github.com/prometheus/common/promlog"
+	kingpin "gopkg.in/alecthomas/kingpin.v2"
 )
 
 // LevelFlagName is the canonical flag name to configure the allowed log level
diff --git a/version/info.go b/version/info.go
index 00caa0b..b3da48a 100644
--- a/version/info.go
+++ b/version/info.go
@@ -53,7 +53,6 @@ func NewCollector(program string) prometheus.Collector {
 				"goversion": GoVersion,
 				"goos":      GoOS,
 				"goarch":    GoArch,
-				"tags":      getTags(),
 			},
 		},
 		func() float64 { return 1 },
@@ -67,7 +66,6 @@ var versionInfoTmpl = `
   build date:       {{.buildDate}}
   go version:       {{.goVersion}}
   platform:         {{.platform}}
-  tags:             {{.tags}}
 `
 
 // Print returns version information.
@@ -81,7 +79,6 @@ func Print(program string) string {
 		"buildDate": BuildDate,
 		"goVersion": GoVersion,
 		"platform":  GoOS + "/" + GoArch,
-		"tags":      getTags(),
 	}
 	t := template.Must(template.New("version").Parse(versionInfoTmpl))
 
@@ -99,5 +96,5 @@ func Info() string {
 
 // BuildContext returns goVersion, platform, buildUser and buildDate information.
 func BuildContext() string {
-	return fmt.Sprintf("(go=%s, platform=%s, user=%s, date=%s, tags=%s)", GoVersion, GoOS+"/"+GoArch, BuildUser, BuildDate, getTags())
+	return fmt.Sprintf("(go=%s, platform=%s, user=%s, date=%s)", GoVersion, GoOS+"/"+GoArch, BuildUser, BuildDate)
 }
diff --git a/version/info_default.go b/version/info_default.go
index 8eb3a0b..2ab0be0 100644
--- a/version/info_default.go
+++ b/version/info_default.go
@@ -19,7 +19,3 @@ package version
 func getRevision() string {
 	return Revision
 }
-
-func getTags() string {
-	return "unknown" // Not available prior to Go 1.18
-}
diff --git a/version/info_go118.go b/version/info_go118.go
index bfc7d41..bed0f49 100644
--- a/version/info_go118.go
+++ b/version/info_go118.go
@@ -19,7 +19,6 @@ package version
 import "runtime/debug"
 
 var computedRevision string
-var computedTags string
 
 func getRevision() string {
 	if Revision != "" {
@@ -28,24 +27,19 @@ func getRevision() string {
 	return computedRevision
 }
 
-func getTags() string {
-	return computedTags
-}
-
 func init() {
-	computedRevision, computedTags = computeRevision()
+	computedRevision = computeRevision()
 }
 
-func computeRevision() (string, string) {
+func computeRevision() string {
 	var (
 		rev      = "unknown"
-		tags     = "unknown"
 		modified bool
 	)
 
 	buildInfo, ok := debug.ReadBuildInfo()
 	if !ok {
-		return rev, tags
+		return rev
 	}
 	for _, v := range buildInfo.Settings {
 		if v.Key == "vcs.revision" {
@@ -56,12 +50,9 @@ func computeRevision() (string, string) {
 				modified = true
 			}
 		}
-		if v.Key == "-tags" {
-			tags = v.Value
-		}
 	}
 	if modified {
-		return rev + "-modified", tags
+		return rev + "-modified"
 	}
-	return rev, tags
+	return rev
 }


Test patch:
diff --git a/config/http_config_test.go b/config/http_config_test.go
index fbe6503..638a133 100644
--- a/config/http_config_test.go
+++ b/config/http_config_test.go
@@ -119,6 +119,18 @@ var invalidHTTPClientConfigs = []struct {
 		httpClientConfigFile: "testdata/http.conf.oauth2-no-token-url.bad.yaml",
 		errMsg:               "oauth2 token_url must be configured",
 	},
+	{
+		httpClientConfigFile: "testdata/http.conf.proxy-from-env.bad.yaml",
+		errMsg:               "if proxy_from_environment is configured, proxy_url must not be configured",
+	},
+	{
+		httpClientConfigFile: "testdata/http.conf.no-proxy.bad.yaml",
+		errMsg:               "if proxy_from_environment is configured, no_proxy must not be configured",
+	},
+	{
+		httpClientConfigFile: "testdata/http.conf.no-proxy-without-proxy-url.bad.yaml",
+		errMsg:               "if no_proxy is configured, proxy_url must also be configured",
+	},
 }
 
 func newTestServer(handler func(w http.ResponseWriter, r *http.Request)) (*httptest.Server, error) {
@@ -1689,3 +1701,140 @@ func loadHTTPConfigJSONFile(filename string) (*HTTPClientConfig, []byte, error)
 	}
 	return cfg, content, nil
 }
+
+func TestProxyConfig_Proxy(t *testing.T) {
+	var proxyServer *httptest.Server
+
+	defer func() {
+		if proxyServer != nil {
+			proxyServer.Close()
+		}
+	}()
+
+	proxyServerHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		fmt.Fprintf(w, "Hello, %s", r.URL.Path)
+	})
+
+	proxyServer = httptest.NewServer(proxyServerHandler)
+
+	testCases := []struct {
+		name             string
+		proxyConfig      string
+		expectedProxyURL string
+		targetURL        string
+		proxyEnv         string
+		noProxyEnv       string
+	}{
+		{
+			name:             "proxy from environment",
+			proxyConfig:      `proxy_from_environment: true`,
+			expectedProxyURL: proxyServer.URL,
+			proxyEnv:         proxyServer.URL,
+			targetURL:        "http://prometheus.io/",
+		},
+		{
+			name:             "proxy_from_environment with no_proxy",
+			proxyConfig:      `proxy_from_environment: true`,
+			expectedProxyURL: "",
+			proxyEnv:         proxyServer.URL,
+			noProxyEnv:       "prometheus.io",
+			targetURL:        "http://prometheus.io/",
+		},
+		{
+			name:             "proxy_from_environment and localhost",
+			proxyConfig:      `proxy_from_environment: true`,
+			expectedProxyURL: "",
+			proxyEnv:         proxyServer.URL,
+			targetURL:        "http://localhost/",
+		},
+		{
+			name:             "valid proxy_url and localhost",
+			proxyConfig:      fmt.Sprintf(`proxy_url: %s`, proxyServer.URL),
+			expectedProxyURL: proxyServer.URL,
+			targetURL:        "http://localhost/",
+		},
+		{
+			name: "valid proxy_url and no_proxy and localhost",
+			proxyConfig: fmt.Sprintf(`proxy_url: %s
+no_proxy: prometheus.io`, proxyServer.URL),
+			expectedProxyURL: "",
+			targetURL:        "http://localhost/",
+		},
+		{
+			name:             "valid proxy_url",
+			proxyConfig:      fmt.Sprintf(`proxy_url: %s`, proxyServer.URL),
+			expectedProxyURL: proxyServer.URL,
+			targetURL:        "http://prometheus.io/",
+		},
+		{
+			name: "valid proxy url and no_proxy",
+			proxyConfig: fmt.Sprintf(`proxy_url: %s
+no_proxy: prometheus.io`, proxyServer.URL),
+			expectedProxyURL: "",
+			targetURL:        "http://prometheus.io/",
+		},
+		{
+			name: "valid proxy url and no_proxies",
+			proxyConfig: fmt.Sprintf(`proxy_url: %s
+no_proxy: promcon.io,prometheus.io,cncf.io`, proxyServer.URL),
+			expectedProxyURL: "",
+			targetURL:        "http://prometheus.io/",
+		},
+		{
+			name: "valid proxy url and no_proxies that do not include target",
+			proxyConfig: fmt.Sprintf(`proxy_url: %s
+no_proxy: promcon.io,cncf.io`, proxyServer.URL),
+			expectedProxyURL: proxyServer.URL,
+			targetURL:        "http://prometheus.io/",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			if proxyServer != nil {
+				defer proxyServer.Close()
+			}
+
+			var proxyConfig ProxyConfig
+
+			err := yaml.Unmarshal([]byte(tc.proxyConfig), &proxyConfig)
+			if err != nil {
+				t.Errorf("failed to unmarshal proxy config: %v", err)
+				return
+			}
+
+			if tc.proxyEnv != "" {
+				currentProxy := os.Getenv("HTTP_PROXY")
+				t.Cleanup(func() { os.Setenv("HTTP_PROXY", currentProxy) })
+				os.Setenv("HTTP_PROXY", tc.proxyEnv)
+			}
+
+			if tc.noProxyEnv != "" {
+				currentProxy := os.Getenv("NO_PROXY")
+				t.Cleanup(func() { os.Setenv("NO_PROXY", currentProxy) })
+				os.Setenv("NO_PROXY", tc.noProxyEnv)
+			}
+
+			req := httptest.NewRequest("GET", tc.targetURL, nil)
+
+			proxyFunc := proxyConfig.Proxy()
+			resultURL, err := proxyFunc(req)
+
+			if err != nil {
+				t.Fatalf("expected no error, but got: %v", err)
+				return
+			}
+			if tc.expectedProxyURL == "" && resultURL != nil {
+				t.Fatalf("expected no result URL, but got: %s", resultURL.String())
+				return
+			}
+			if tc.expectedProxyURL != "" && resultURL == nil {
+				t.Fatalf("expected result URL, but got nil")
+				return
+			}
+			if tc.expectedProxyURL != "" && resultURL.String() != tc.expectedProxyURL {
+				t.Fatalf("expected result URL: %s, but got: %s", tc.expectedProxyURL, resultURL.String())
+			}
+		})
+	}
+}
diff --git a/config/testdata/http.conf.no-proxy-without-proxy-url.bad.yaml b/config/testdata/http.conf.no-proxy-without-proxy-url.bad.yaml
new file mode 100644
index 0000000..9c9ef35
--- /dev/null
+++ b/config/testdata/http.conf.no-proxy-without-proxy-url.bad.yaml
@@ -0,0 +1,1 @@
+no_proxy: 127.0.0.1
diff --git a/config/testdata/http.conf.no-proxy.bad.yaml b/config/testdata/http.conf.no-proxy.bad.yaml
new file mode 100644
index 0000000..3d7a125
--- /dev/null
+++ b/config/testdata/http.conf.no-proxy.bad.yaml
@@ -0,0 +1,2 @@
+proxy_from_environment: true
+no_proxy: 127.0.0.1
diff --git a/config/testdata/http.conf.proxy-from-env.bad.yaml b/config/testdata/http.conf.proxy-from-env.bad.yaml
new file mode 100644
index 0000000..fd177a6
--- /dev/null
+++ b/config/testdata/http.conf.proxy-from-env.bad.yaml
@@ -0,0 +1,2 @@
+proxy_from_environment: true
+proxy_url: foo
diff --git a/model/time_test.go b/model/time_test.go
index b0703fd..5ebebd5 100644
--- a/model/time_test.go
+++ b/model/time_test.go
@@ -367,14 +367,3 @@ func TestTimeJSON(t *testing.T) {
 	}
 
 }
-
-func BenchmarkParseDuration(b *testing.B) {
-	const data = "30s"
-
-	for i := 0; i < b.N; i++ {
-		_, err := ParseDuration(data)
-		if err != nil {
-			b.Fatal(err)
-		}
-	}
-}