diff --git a/util/request/roundtrip.go b/util/request/roundtrip.go index 9527176a5..d9538eabe 100644 --- a/util/request/roundtrip.go +++ b/util/request/roundtrip.go @@ -2,12 +2,13 @@ package request import ( "bytes" - "fmt" "net/http" "net/http/httputil" "strings" + "time" "github.com/andig/evcc/util" + "github.com/prometheus/client_golang/prometheus" ) type roundTripper struct { @@ -17,6 +18,43 @@ type roundTripper struct { const max = 2048 * 2 +var ( + reqMetric *prometheus.SummaryVec + cntMetric, errMetric *prometheus.CounterVec +) + +func init() { + labels := []string{"host"} + + reqMetric = prometheus.NewSummaryVec(prometheus.SummaryOpts{ + Namespace: "evcc", + Subsystem: "http", + Name: "request_duration_seconds", + Help: "A summary of HTTP request durations", + Objectives: map[float64]float64{ + 0.5: 0.05, // 50th percentile with a max. absolute error of 0.05 + 0.9: 0.01, // 90th percentile with a max. absolute error of 0.01 + 0.99: 0.001, // 99th percentile with a max. absolute error of 0.001 + }, + }, labels) + + cntMetric = prometheus.NewCounterVec(prometheus.CounterOpts{ + Namespace: "evcc", + Subsystem: "http", + Name: "request_total", + Help: "Total count of HTTP requests", + }, labels) + + errMetric = prometheus.NewCounterVec(prometheus.CounterOpts{ + Namespace: "evcc", + Subsystem: "http", + Name: "request_errors", + Help: "Total count of HTTP request errors", + }, labels) + + prometheus.MustRegister(reqMetric, cntMetric, errMetric) +} + // NewTripper creates a logging roundtrip handler func NewTripper(log *util.Logger, base http.RoundTripper) http.RoundTripper { tripper := &roundTripper{ @@ -35,21 +73,27 @@ func min(a, b int) int { } func (r *roundTripper) RoundTrip(req *http.Request) (*http.Response, error) { - var bld strings.Builder - bld.WriteString(fmt.Sprintf("%s %s", req.Method, req.URL.String())) + r.log.TRACE.Printf("%s %s", req.Method, req.URL.String()) + var bld strings.Builder if body, err := httputil.DumpRequestOut(req, true); err == nil { bld.WriteString("\n") bld.Write(bytes.TrimSpace(body[:min(max, len(body))])) } + startTime := time.Now() resp, err := r.base.RoundTrip(req) + cntMetric.WithLabelValues(req.URL.Hostname()).Add(1) + + if err == nil { + reqMetric.WithLabelValues(req.URL.Hostname()).Observe(time.Since(startTime).Seconds()) - if resp != nil { if body, err := httputil.DumpResponse(resp, true); err == nil { bld.WriteString("\n\n") bld.Write(bytes.TrimSpace(body[:min(max, len(body))])) } + } else { + errMetric.WithLabelValues(req.URL.Hostname()).Add(1) } if bld.Len() > 0 { diff --git a/vehicle/audi.go b/vehicle/audi.go index a9ccaea14..1825938c0 100644 --- a/vehicle/audi.go +++ b/vehicle/audi.go @@ -8,6 +8,7 @@ import ( "github.com/andig/evcc/api" "github.com/andig/evcc/util" + "github.com/andig/evcc/util/request" "github.com/andig/evcc/vehicle/vw" ) @@ -30,8 +31,10 @@ func NewAudiFromConfig(other map[string]interface{}) (api.Vehicle, error) { embed `mapstructure:",squash"` User, Password, VIN string Cache time.Duration + Timeout time.Duration }{ - Cache: interval, + Cache: interval, + Timeout: request.Timeout, } if err := util.DecodeOther(other, &cc); err != nil { @@ -60,6 +63,7 @@ func NewAudiFromConfig(other map[string]interface{}) (api.Vehicle, error) { } api := vw.NewAPI(log, identity, "Audi", "DE") + api.Client.Timeout = cc.Timeout if cc.VIN == "" { cc.VIN, err = findVehicle(api.Vehicles()) diff --git a/vehicle/enyaq.go b/vehicle/enyaq.go index 977d321e5..58982d309 100644 --- a/vehicle/enyaq.go +++ b/vehicle/enyaq.go @@ -8,6 +8,7 @@ import ( "github.com/andig/evcc/api" "github.com/andig/evcc/util" + "github.com/andig/evcc/util/request" "github.com/andig/evcc/vehicle/skoda" "github.com/andig/evcc/vehicle/vw" ) @@ -30,8 +31,10 @@ func NewEnyaqFromConfig(other map[string]interface{}) (api.Vehicle, error) { embed `mapstructure:",squash"` User, Password, VIN string Cache time.Duration + Timeout time.Duration }{ - Cache: interval, + Cache: interval, + Timeout: request.Timeout, } if err := util.DecodeOther(other, &cc); err != nil { @@ -62,6 +65,7 @@ func NewEnyaqFromConfig(other map[string]interface{}) (api.Vehicle, error) { } api := skoda.NewAPI(log, identity) + api.Client.Timeout = cc.Timeout cc.VIN, err = findVehicle(api.Vehicles()) if err == nil { @@ -86,6 +90,7 @@ func NewEnyaqFromConfig(other map[string]interface{}) (api.Vehicle, error) { } api := skoda.NewAPI(log, identity) + api.Client.Timeout = cc.Timeout v.Provider = skoda.NewProvider(api, strings.ToUpper(cc.VIN), cc.Cache) } diff --git a/vehicle/id.go b/vehicle/id.go index 39145dc29..7667de0a4 100644 --- a/vehicle/id.go +++ b/vehicle/id.go @@ -8,6 +8,7 @@ import ( "github.com/andig/evcc/api" "github.com/andig/evcc/util" + "github.com/andig/evcc/util/request" "github.com/andig/evcc/vehicle/id" "github.com/andig/evcc/vehicle/vw" ) @@ -30,8 +31,10 @@ func NewIDFromConfig(other map[string]interface{}) (api.Vehicle, error) { embed `mapstructure:",squash"` User, Password, VIN string Cache time.Duration + Timeout time.Duration }{ - Cache: interval, + Cache: interval, + Timeout: request.Timeout, } if err := util.DecodeOther(other, &cc); err != nil { @@ -58,6 +61,7 @@ func NewIDFromConfig(other map[string]interface{}) (api.Vehicle, error) { } api := id.NewAPI(log, identity) + api.Client.Timeout = cc.Timeout if cc.VIN == "" { cc.VIN, err = findVehicle(api.Vehicles()) diff --git a/vehicle/seat.go b/vehicle/seat.go index 7d3f9d5e4..43cd8977d 100644 --- a/vehicle/seat.go +++ b/vehicle/seat.go @@ -8,6 +8,7 @@ import ( "github.com/andig/evcc/api" "github.com/andig/evcc/util" + "github.com/andig/evcc/util/request" "github.com/andig/evcc/vehicle/vw" ) @@ -30,8 +31,10 @@ func NewSeatFromConfig(other map[string]interface{}) (api.Vehicle, error) { embed `mapstructure:",squash"` User, Password, VIN string Cache time.Duration + Timeout time.Duration }{ - Cache: interval, + Cache: interval, + Timeout: request.Timeout, } if err := util.DecodeOther(other, &cc); err != nil { @@ -58,6 +61,7 @@ func NewSeatFromConfig(other map[string]interface{}) (api.Vehicle, error) { } api := vw.NewAPI(log, identity, "VW", "ES") + api.Client.Timeout = cc.Timeout if cc.VIN == "" { cc.VIN, err = findVehicle(api.Vehicles()) diff --git a/vehicle/skoda.go b/vehicle/skoda.go index 4ad207199..cbd8bdc01 100644 --- a/vehicle/skoda.go +++ b/vehicle/skoda.go @@ -8,6 +8,7 @@ import ( "github.com/andig/evcc/api" "github.com/andig/evcc/util" + "github.com/andig/evcc/util/request" "github.com/andig/evcc/vehicle/vw" ) @@ -30,8 +31,10 @@ func NewSkodaFromConfig(other map[string]interface{}) (api.Vehicle, error) { embed `mapstructure:",squash"` User, Password, VIN string Cache time.Duration + Timeout time.Duration }{ - Cache: interval, + Cache: interval, + Timeout: request.Timeout, } if err := util.DecodeOther(other, &cc); err != nil { @@ -58,6 +61,7 @@ func NewSkodaFromConfig(other map[string]interface{}) (api.Vehicle, error) { } api := vw.NewAPI(log, identity, "VW", "CZ") + api.Client.Timeout = cc.Timeout if cc.VIN == "" { cc.VIN, err = findVehicle(api.Vehicles()) diff --git a/vehicle/vw.go b/vehicle/vw.go index 08370aed8..380edc6b9 100644 --- a/vehicle/vw.go +++ b/vehicle/vw.go @@ -8,6 +8,7 @@ import ( "github.com/andig/evcc/api" "github.com/andig/evcc/util" + "github.com/andig/evcc/util/request" "github.com/andig/evcc/vehicle/vw" ) @@ -30,8 +31,10 @@ func NewVWFromConfig(other map[string]interface{}) (api.Vehicle, error) { embed `mapstructure:",squash"` User, Password, VIN string Cache time.Duration + Timeout time.Duration }{ - Cache: interval, + Cache: interval, + Timeout: request.Timeout, } if err := util.DecodeOther(other, &cc); err != nil { @@ -58,6 +61,7 @@ func NewVWFromConfig(other map[string]interface{}) (api.Vehicle, error) { } api := vw.NewAPI(log, identity, "VW", "DE") + api.Client.Timeout = cc.Timeout if cc.VIN == "" { cc.VIN, err = findVehicle(api.Vehicles()) diff --git a/vehicle/vw/forms.go b/vehicle/vw/forms.go index 7e8b209ff..41613e30b 100644 --- a/vehicle/vw/forms.go +++ b/vehicle/vw/forms.go @@ -22,12 +22,12 @@ func FormValues(reader io.Reader, id string) (FormVars, error) { // only interested in meta tag? if meta := doc.Find("meta[name=_csrf]"); id == "meta" { if meta.Length() != 1 { - return vars, errors.New("unexpected length") + return vars, errors.New("meta not found") } csrf, exists := meta.Attr("content") if !exists { - return vars, errors.New("meta not found") + return vars, errors.New("meta attribute not found") } vars.Inputs["_csrf"] = csrf return vars, nil @@ -35,12 +35,12 @@ func FormValues(reader io.Reader, id string) (FormVars, error) { form := doc.Find(id).First() if form.Length() != 1 { - return vars, errors.New("unexpected length") + return vars, errors.New("form not found") } action, exists := form.Attr("action") if !exists { - return vars, errors.New("attribute not found") + return vars, errors.New("form attribute not found") } vars.Action = action