How can sent and received bytes be counted from within a ServeHTTP
function in Go?
The count needs to be relatively accurate. Skipping connection establishment is not ideal, but acceptable. But headers must be included.
It also needs to be fast. Iterating is generally too slow.
The counting itself doesn't need to occur within ServeHTTP
, as long the count for a given connection can be made available to ServeHTTP
.
This must also not break HTTPS or HTTP/2.
Things I've Tried
It's possible to get a rough, slow estimate of received bytes by iterating over the Request
headers. This is far too slow, and the Go standard library removes and combines headers, so it's not accurate either.
I tried writing an intercepting Listener
, which created an internal tls.Listen
or net.Listen
Listener, and whose Accept()
function got a net.Conn
from the internal Listener's Accept()
, and then wrapped that in an intercepting net.Conn
whose Read
and Write
functions call the real net.Conn
and count their reads and writes. It's then possible to make those counts available to the ServeHTTP
function via mutexed shared variables.
The problem is, the intercepting Conn
breaks HTTP/2, because Go's internal libraries cast the net.Conn
as a *tls.Conn
(e.g. https://golang.org/src/net/http/server.go#L1730), and it doesn't appear possible in Go to wrap the object while still making that cast succeed (if it is, it would solve this problem).
Counting sent bytes can be done relatively accurately by counting what is written to the ResponseWriter
. Counting received bytes in the HTTP body is also achievable, via Request.Body
. The critical issue here appears to be quickly and accurately counting request header bytes. Though again, also counting connection establishment bytes would be ideal.
Is this possible? How?
I think it is possible, but I can't say I've done it. However, based on browsing the stdlib implementation of the HTTP server and TLS listener, I don't see why it shouldn't be possible; the key is wrapping the connection before TLS instead of after. This also gets you a more accurate count of bytes on the wire, rather than a count of decrypted bytes.
You've already got an intercepting Listener
, you just need to insert it in the right spot. Rather than passing your Listener
to http.Serve
(or wherever you're inserting it), you want to pass it to tls.NewListener
first, which wraps it in the TLS handler, and then pass the result, which will be a TLS listener (making Go's HTTP/2 support happy) into the HTTP server.
Of course, if you want a count of decrypted bytes rather than wire bytes, you may be SOL - wrapping the net.Conn
just won't get you there. You'll likely have to do the best you can with counting headers & body.