func statusCode(code int) string {
	if code >= 100 {
		switch {
		case code < 200:
			return statusCodeWithTable(code-100, statusCode100[:])
		case code < 300:
			return statusCodeWithTable(code-200, statusCode200[:])
		case code < 400:
			return statusCodeWithTable(code-300, statusCode300[:])
		case code < 500:
			return statusCodeWithTable(code-400, statusCode400[:])
		case code < 600:
			return statusCodeWithTable(code-500, statusCode500[:])
		}
	}
	return strconv.Itoa(code)
}
statusCode behaves like strconv.Itoa but uses a lookup table to avoid having
to do a dynamic memory allocation to convert common http status codes to a
string representation.
package httpstats
import (
	"io"
	"math"
	"net/http"
	"net/url"
	"strconv"
	"strings"
	"sync"
	"time"
	"github.com/segmentio/stats/v4"
)
func init() {
	stats.Buckets.Set("http.message:header.size",
		5,
		10,
		20,
		40,
		80,
		math.Inf(+1),
	)
	stats.Buckets.Set("http.message:header.bytes",
		1e2,  
		1e3,  
		1e4,  
		1e5,  
		1e6,  
		math.Inf(+1),
	)
	stats.Buckets.Set("http.message:body.bytes",
		1e2,  
		1e3,  
		1e4,  
		1e5,  
		1e6,  
		1e7,  
		1e8,  
		1e9,  
		math.Inf(+1),
	)
	stats.Buckets.Set("http:rtt.seconds",
		1*time.Millisecond,
		10*time.Millisecond,
		100*time.Millisecond,
		1*time.Second,
		10*time.Second,
		math.Inf(+1),
	)
}
type nullBody struct{}
func (n *nullBody) Close() error { return nil }
func (n *nullBody) Read(b []byte) (int, error) { return 0, io.EOF }
type requestBody struct {
	body    io.ReadCloser
	eng     *stats.Engine
	req     *http.Request
	metrics *metrics
	bytes   int
	op      string
	once    sync.Once
}
func (r *requestBody) Close() (err error) {
	err = r.body.Close()
	r.close()
	return
}
func (r *requestBody) Read(b []byte) (n int, err error) {
	if n, err = r.body.Read(b); n > 0 {
		r.bytes += n
	}
	return
}
func (r *requestBody) close() {
	r.once.Do(r.complete)
}
func (r *requestBody) complete() {
	r.metrics.observeRequest(r.req, r.op, r.bytes)
}
type responseBody struct {
	eng     *stats.Engine
	res     *http.Response
	metrics *metrics
	body    io.ReadCloser
	bytes   int
	op      string
	start   time.Time
	once    sync.Once
}
func (r *responseBody) Close() (err error) {
	err = r.body.Close()
	r.close()
	return
}
func (r *responseBody) Read(b []byte) (n int, err error) {
	if n, err = r.body.Read(b); n > 0 {
		r.bytes += n
	}
	return
}
func (r *responseBody) close() {
	r.once.Do(r.complete)
}
func (r *responseBody) complete() {
	r.metrics.observeResponse(r.res, r.op, r.bytes, time.Now().Sub(r.start))
	r.eng.ReportAt(r.start, r.metrics)
}
type metrics struct {
	http struct {
		err struct {
			count int `metric:"count" type:"counter"`
		} `metric:"error"`
		req struct {
			msg struct {
				count       int `metric:"count"        type:"counter"`
				headerSize  int `metric:"header.size"  type:"histogram"`
				headerBytes int `metric:"header.bytes" type:"histogram"`
				bodyBytes   int `metric:"body.bytes"   type:"histogram"`
			} `metric:"message"`
			operation string `tag:"operation"`
			msgtype   string `tag:"type"`
		}
		res struct {
			rtt time.Duration `metric:"rtt.seconds" type:"histogram"`
			msg struct {
				count       int `metric:"count"        type:"counter"`
				headerSize  int `metric:"header.size"  type:"histogram"`
				headerBytes int `metric:"header.bytes" type:"histogram"`
				bodyBytes   int `metric:"body.bytes"   type:"histogram"`
			} `metric:"message"`
			operation string `tag:"operation"`
			msgtype   string `tag:"type"`
			contentCharset   string `tag:"http_res_content_charset"`
			contentEncoding  string `tag:"http_res_content_endoing"`
			contentType      string `tag:"http_res_content_type"`
			protocol         string `tag:"http_res_protocol"`
			server           string `tag:"http_res_server"`
			statusBucket     string `tag:"http_res_status_bucket"`
			status           string `tag:"http_res_status"`
			transferEncoding string `tag:"http_res_transfer_encoding"`
			upgrade          string `tag:"http_res_upgrade"`
		}
		contentCharset   string `tag:"http_req_content_charset"`
		contentEncoding  string `tag:"http_req_content_endoing"`
		contentType      string `tag:"http_req_content_type"`
		host             string `tag:"http_req_host"`
		method           string `tag:"http_req_method"`
		path             string `tag:"http_req_path"`
		protocol         string `tag:"http_req_protocol"`
		transferEncoding string `tag:"http_req_transfer_encoding"`
	} `metric:"http"`
}
func (m *metrics) observeRequest(req *http.Request, op string, bodyLen int) {
	contentType, charset := contentType(req.Header)
	contentEncoding := contentEncoding(req.Header)
	transferEncoding := transferEncoding(req.TransferEncoding)
	host := requestHost(req)
	m.http.req.msg.count = 1
	m.http.req.msg.headerSize = len(req.Header)
	m.http.req.msg.headerBytes = requestHeaderLength(req)
	m.http.req.msg.bodyBytes = bodyLen
	m.http.req.operation = op
	m.http.req.msgtype = "request"
	m.http.contentCharset = charset
	m.http.contentEncoding = contentEncoding
	m.http.contentType = contentType
	m.http.host = host
	m.http.method = req.Method
	m.http.protocol = req.Proto
	m.http.transferEncoding = transferEncoding
	path := req.URL.Path
	if !strings.HasPrefix(path, "/") {
		path = "/" + path
	}
	m.http.path = path
}
func (m *metrics) observeResponse(res *http.Response, op string, bodyLen int, rtt time.Duration) {
	contentType, charset := contentType(res.Header)
	contentEncoding := contentEncoding(res.Header)
	upgrade := headerValue(res.Header, "Upgrade")
	server := headerValue(res.Header, "Server")
	bucket := responseStatusBucket(res.StatusCode)
	status := statusCode(res.StatusCode)
	transferEncoding := transferEncoding(res.TransferEncoding)
	m.http.res.msg.count = 1
	m.http.res.msg.headerSize = len(res.Header)
	m.http.res.msg.headerBytes = responseHeaderLength(res)
	m.http.res.msg.bodyBytes = bodyLen
	m.http.res.rtt = rtt
	m.http.res.operation = op
	m.http.res.msgtype = "response"
	m.http.res.contentCharset = charset
	m.http.res.contentEncoding = contentEncoding
	m.http.res.contentType = contentType
	m.http.res.protocol = res.Proto
	m.http.res.server = server
	m.http.res.statusBucket = bucket
	m.http.res.status = status
	m.http.res.transferEncoding = transferEncoding
	m.http.res.upgrade = upgrade
}
func (m *metrics) observeError(rtt time.Duration) {
	m.http.err.count = 1
	m.http.res.rtt = rtt
}
func requestHeaderLength(req *http.Request) int {
	n := headerLength(req.Header) +
		urlLength(req.URL) +
		len(" ") +
		len(req.Method) +
		len(" ") +
		len(req.Proto) +
		len("\r\n")
	if _, ok := req.Header["User-Agent"]; !ok {
		n += len("User-Agent: Go-http-client/1.1\r\n")
	}
	n += len("Host: ") + len(req.Host) + len("\r\n")
	return n
}
func responseHeaderLength(res *http.Response) int {
	n := headerLength(res.Header) +
		len(res.Proto) +
		len(" ") +
		intLength(int64(res.StatusCode)) +
		len(" ") +
		len(http.StatusText(res.StatusCode)) +
		len("\r\n")
	if _, ok := res.Header["Connection"]; !ok {
		n += len("Connection: close\r\n")
	}
	return n
}
func headerLength(h http.Header) int {
	n := 0
	for name, values := range h {
		for _, v := range values {
			n += len(name) + len(": ") + len(v) + len("\r\n")
		}
	}
	return n + len("\r\n")
}
func urlLength(u *url.URL) int {
	n := len(u.Host) + len(u.Path)
	if l := len(u.Scheme); l != 0 {
		n += l + len("://")
	}
	if l := len(u.RawQuery); l != 0 {
		n += l + len("?")
	}
	if user := u.User; user != nil {
		n += len(user.Username()) + len("@")
		if p, ok := user.Password(); ok {
			n += len(p) + len(":")
		}
	}
	if len(u.Fragment) != 0 {
		n += len(u.Fragment) + len("#")
	}
	return n
}
func intLength(n int64) int {
	return int(math.Log10(float64(n))) + 1
}
func requestHost(req *http.Request) (host string) {
	if host = req.Host; len(host) == 0 {
		if host = headerValue(req.Header, "Host"); len(host) == 0 {
			host = req.URL.Host
		}
	}
	return
}
func responseStatusBucket(status int) string {
	switch {
	case status < 100 || status >= 600:
		return ""
	case status < 200:
		return "1xx"
	case status < 300:
		return "2xx"
	case status < 400:
		return "3xx"
	case status < 500:
		return "4xx"
	default:
		return "5xx"
	}
}
func contentType(h http.Header) (string, string) {
	return parseContentType(headerValue(h, "Content-Type"))
}
func contentEncoding(h http.Header) string {
	return strings.TrimSpace(headerValue(h, "Content-Encoding"))
}
func transferEncoding(te []string) string {
	switch len(te) {
	case 0:
		return "identity"
	case 1:
		return te[0]
	default:
		return strings.Join(te, ";")
	}
}
func parseContentType(s string) (contentType string, charset string) {
	for i := 0; len(s) != 0; i++ {
		var t string
		if t, s = parseHeaderToken(s); strings.HasPrefix(t, "charset=") {
			charset = t[8:]
		} else if len(contentType) == 0 {
			contentType = t
		}
	}
	return
}
func parseHeaderToken(s string) (token string, next string) {
	if i := strings.IndexByte(s, ';'); i >= 0 {
		token, next = strings.TrimSpace(s[:i]), strings.TrimSpace(s[i+1:])
	} else {
		token = strings.TrimSpace(s)
	}
	return
}
func headerValue(header http.Header, name string) string {
	values := header[name]
	if len(values) == 0 {
		return ""
	}
	return values[0]
}