From af5d444fdda7a66c8e937ae09afe23fd7e8799af Mon Sep 17 00:00:00 2001 From: Hubert Grochowski Date: Wed, 11 Oct 2023 12:43:50 +0200 Subject: [PATCH] http_proxy: add proxy errors metric Fixes #458 --- http_error.go | 27 +++++++++++++++++---------- http_proxy.go | 2 ++ http_proxy_metrics.go | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 10 deletions(-) create mode 100644 http_proxy_metrics.go diff --git a/http_error.go b/http_error.go index 129d4a23..49fedb78 100644 --- a/http_error.go +++ b/http_error.go @@ -39,11 +39,11 @@ func (hp *HTTPProxy) errorResponse(req *http.Request, err error) *http.Response } var ( - code int - msg string + code int + msg, label string ) for _, h := range handlers { - code, msg = h(req, err) + code, msg, label = h(req, err) if code != 0 { break } @@ -51,8 +51,11 @@ func (hp *HTTPProxy) errorResponse(req *http.Request, err error) *http.Response if code == 0 { code = http.StatusInternalServerError msg = "An unexpected error occurred" + label = "unexpected_error" } + hp.metrics.error(label) + resp := proxyutil.NewResponse(code, bytes.NewBufferString(msg+"\n"), req) resp.Header.Set(ErrorHeader, err.Error()) resp.Header.Set("Content-Type", "text/plain; charset=utf-8") @@ -60,9 +63,9 @@ func (hp *HTTPProxy) errorResponse(req *http.Request, err error) *http.Response return resp } -type errorHandler func(*http.Request, error) (int, string) +type errorHandler func(*http.Request, error) (int, string, string) -func handleNetError(_ *http.Request, err error) (code int, msg string) { +func handleNetError(_ *http.Request, err error) (code int, msg, label string) { var netErr *net.OpError if errors.As(err, &netErr) { if netErr.Timeout() { @@ -72,36 +75,40 @@ func handleNetError(_ *http.Request, err error) (code int, msg string) { code = http.StatusBadGateway msg = "Failed to connect to remote host" } + label = "net_" + netErr.Op } return } -func handleTLSRecordHeader(_ *http.Request, err error) (code int, msg string) { +func handleTLSRecordHeader(_ *http.Request, err error) (code int, msg, label string) { var headerErr *tls.RecordHeaderError if errors.As(err, &headerErr) { code = http.StatusBadGateway msg = "TLS handshake failed" + label = "tls_record_header" } return } -func handleTLSCertificateError(_ *http.Request, err error) (code int, msg string) { +func handleTLSCertificateError(_ *http.Request, err error) (code int, msg, label string) { var certErr *tls.CertificateVerificationError if errors.As(err, &certErr) { code = http.StatusBadGateway msg = "TLS handshake failed" + label = "tls_certificate" } return } -func handleDenyError(req *http.Request, err error) (code int, msg string) { +func handleDenyError(req *http.Request, err error) (code int, msg, label string) { var denyErr denyError if errors.As(err, &denyErr) { code = http.StatusForbidden msg = fmt.Sprintf("proxying is denied to host %q", req.Host) + label = "denied" } return @@ -110,11 +117,11 @@ func handleDenyError(req *http.Request, err error) (code int, msg string) { // There is a difference between sending HTTP and HTTPS requests in the presence of an upstream proxy. // For HTTPS client issues a CONNECT request to the proxy and then sends the original request. // In case the proxy responds with status code 4XX or 5XX to the CONNECT request, the client interprets it as URL error. -func handleStatusText(req *http.Request, err error) (code int, msg string) { +func handleStatusText(req *http.Request, err error) (code int, msg, label string) { if req.URL.Scheme == "https" && err != nil { for i := 400; i < 600; i++ { if err.Error() == http.StatusText(i) { - return i, err.Error() + return i, err.Error(), "https_status_text" } } } diff --git a/http_proxy.go b/http_proxy.go index e8ae135e..d36bcee2 100644 --- a/http_proxy.go +++ b/http_proxy.go @@ -134,6 +134,7 @@ type HTTPProxy struct { creds *CredentialsMatcher transport http.RoundTripper log log.Logger + metrics *httpProxyMetrics proxy *martian.Proxy mitmCACert *x509.Certificate proxyFunc ProxyFunc @@ -200,6 +201,7 @@ func newHTTPProxy(cfg *HTTPProxyConfig, pr PACResolver, cm *CredentialsMatcher, creds: cm, transport: rt, log: log, + metrics: newMetrics(cfg.PromRegistry, cfg.PromNamespace), } if err := hp.configureProxy(); err != nil { diff --git a/http_proxy_metrics.go b/http_proxy_metrics.go new file mode 100644 index 00000000..e4677cae --- /dev/null +++ b/http_proxy_metrics.go @@ -0,0 +1,35 @@ +// Copyright 2023 Sauce Labs Inc. All rights reserved. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +package forwarder + +import ( + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +type httpProxyMetrics struct { + errors *prometheus.CounterVec +} + +func newMetrics(r prometheus.Registerer, namespace string) *httpProxyMetrics { + if r == nil { + r = prometheus.NewRegistry() // This registry will be discarded. + } + f := promauto.With(r) + + return &httpProxyMetrics{ + errors: f.NewCounterVec(prometheus.CounterOpts{ + Name: "proxy_errors_total", + Namespace: namespace, + Help: "Number of proxy errors", + }, []string{"reason"}), + } +} + +func (m *httpProxyMetrics) error(reason string) { + m.errors.WithLabelValues(reason).Inc() +}