http2: add Transport.CountErrors

Like the earlier Server.CountErrors. This lets people observe hook up
an http2.Transport to monitoring (expvar/Prometheus/etc) and spot
pattern changes over time.

Change-Id: I12a4d0b3499fb0be75d5a56342ced0719c00654d
Reviewed-on: https://go-review.googlesource.com/c/net/+/350709
Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Damien Neil <dneil@google.com>
Trust: Brad Fitzpatrick <bradfitz@golang.org>
diff --git a/http2/errors.go b/http2/errors.go
index c789fa3..2663e5d 100644
--- a/http2/errors.go
+++ b/http2/errors.go
@@ -53,6 +53,13 @@
 	return fmt.Sprintf("unknown error code 0x%x", uint32(e))
 }
 
+func (e ErrCode) stringToken() string {
+	if s, ok := errCodeName[e]; ok {
+		return s
+	}
+	return fmt.Sprintf("ERR_UNKNOWN_%d", uint32(e))
+}
+
 // ConnectionError is an error that results in the termination of the
 // entire connection.
 type ConnectionError ErrCode
diff --git a/http2/transport.go b/http2/transport.go
index 74c76da..aaaffa8 100644
--- a/http2/transport.go
+++ b/http2/transport.go
@@ -130,6 +130,12 @@
 	// Defaults to 15s.
 	PingTimeout time.Duration
 
+	// CountError, if non-nil, is called on HTTP/2 transport errors.
+	// It's intended to increment a metric for monitoring, such
+	// as an expvar or Prometheus metric.
+	// The errType consists of only ASCII word characters.
+	CountError func(errType string)
+
 	// t1, if non-nil, is the standard library Transport using
 	// this transport. Its settings are used (but not its
 	// RoundTrip method, etc).
@@ -943,6 +949,9 @@
 // closes the client connection immediately. In-flight requests are interrupted.
 func (cc *ClientConn) closeForLostPing() error {
 	err := errors.New("http2: client connection lost")
+	if f := cc.t.CountError; f != nil {
+		f("conn_close_lost_ping")
+	}
 	return cc.closeForError(err)
 }
 
@@ -1830,6 +1839,33 @@
 	cc.mu.Unlock()
 }
 
+// countReadFrameError calls Transport.CountError with a string
+// representing err.
+func (cc *ClientConn) countReadFrameError(err error) {
+	f := cc.t.CountError
+	if f == nil || err == nil {
+		return
+	}
+	if ce, ok := err.(ConnectionError); ok {
+		errCode := ErrCode(ce)
+		f(fmt.Sprintf("read_frame_conn_error_%s", errCode.stringToken()))
+		return
+	}
+	if errors.Is(err, io.EOF) {
+		f("read_frame_eof")
+		return
+	}
+	if errors.Is(err, io.ErrUnexpectedEOF) {
+		f("read_frame_unexpected_eof")
+		return
+	}
+	if errors.Is(err, ErrFrameTooLarge) {
+		f("read_frame_too_large")
+		return
+	}
+	f("read_frame_other")
+}
+
 func (rl *clientConnReadLoop) run() error {
 	cc := rl.cc
 	rl.closeWhenIdle = cc.t.disableKeepAlives() || cc.singleUse
@@ -1860,6 +1896,7 @@
 			}
 			continue
 		} else if err != nil {
+			cc.countReadFrameError(err)
 			return err
 		}
 		if VerboseLogs {
@@ -2352,6 +2389,10 @@
 	if f.ErrCode != 0 {
 		// TODO: deal with GOAWAY more. particularly the error code
 		cc.vlogf("transport got GOAWAY with error code = %v", f.ErrCode)
+		if fn := cc.t.CountError; fn != nil {
+			fn("recv_goaway_" + f.ErrCode.stringToken())
+		}
+
 	}
 	cc.setGoAway(f)
 	return nil
@@ -2466,9 +2507,9 @@
 		if f.ErrCode == ErrCodeProtocol {
 			rl.cc.SetDoNotReuse()
 			serr.Cause = errFromPeer
-			// TODO(bradfitz): increment a varz here, once Transport
-			// takes an optional interface-typed field that expvar.Map.Add
-			// implements.
+		}
+		if fn := cs.cc.t.CountError; fn != nil {
+			fn("recv_rststream_" + f.ErrCode.stringToken())
 		}
 		cs.resetErr = serr
 		close(cs.peerReset)