|
|
|
@ -18,6 +18,7 @@ import (
|
|
|
|
|
"net/url"
|
|
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"regexp"
|
|
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
|
|
|
|
"sync"
|
|
|
|
@ -175,6 +176,52 @@ type ReturnHandler interface {
|
|
|
|
|
ServeHTTPReturn(http.ResponseWriter, *http.Request) error
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// BucketedStatsOptions describes tsweb handler options surrounding
|
|
|
|
|
// the generation of metrics, grouped into buckets.
|
|
|
|
|
type BucketedStatsOptions struct {
|
|
|
|
|
// Bucket returns which bucket the given request is in.
|
|
|
|
|
// If nil, [NormalizedPath] is used to compute the bucket.
|
|
|
|
|
Bucket func(req *http.Request) string
|
|
|
|
|
|
|
|
|
|
// If non-nil, Started maintains a counter of all requests which
|
|
|
|
|
// have begun processing.
|
|
|
|
|
Started *expvar.Map
|
|
|
|
|
|
|
|
|
|
// If non-nil, Finished maintains a counter of all requests which
|
|
|
|
|
// have finished processing (that is, the HTTP handler has returned).
|
|
|
|
|
Finished *expvar.Map
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
hexSequenceRegex = regexp.MustCompile("[a-fA-F0-9]{9,}")
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// NormalizedPath returns the given path with any query parameters
|
|
|
|
|
// removed, and any hex strings of 9 or more characters replaced
|
|
|
|
|
// with an ellipsis.
|
|
|
|
|
func NormalizedPath(p string) string {
|
|
|
|
|
// Fastpath: No hex sequences in there we might have to trim.
|
|
|
|
|
// Avoids allocating.
|
|
|
|
|
if hexSequenceRegex.FindStringIndex(p) == nil {
|
|
|
|
|
b, _, _ := strings.Cut(p, "?")
|
|
|
|
|
return b
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If we got here, there's at least one hex sequences we need to
|
|
|
|
|
// replace with an ellipsis.
|
|
|
|
|
replaced := hexSequenceRegex.ReplaceAllString(p, "…")
|
|
|
|
|
b, _, _ := strings.Cut(replaced, "?")
|
|
|
|
|
return b
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (o *BucketedStatsOptions) bucketForRequest(r *http.Request) string {
|
|
|
|
|
if o.Bucket != nil {
|
|
|
|
|
return o.Bucket(r)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return NormalizedPath(r.URL.Path)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type HandlerOptions struct {
|
|
|
|
|
QuietLoggingIfSuccessful bool // if set, do not log successfully handled HTTP requests (200 and 304 status codes)
|
|
|
|
|
Logf logger.Logf
|
|
|
|
@ -189,6 +236,10 @@ type HandlerOptions struct {
|
|
|
|
|
// The keys are HTTP numeric response codes e.g. 200, 404, ...
|
|
|
|
|
StatusCodeCountersFull *expvar.Map
|
|
|
|
|
|
|
|
|
|
// If non-nil, BucketedStats computes and exposes statistics
|
|
|
|
|
// for each bucket based on the contained parameters.
|
|
|
|
|
BucketedStats *BucketedStatsOptions
|
|
|
|
|
|
|
|
|
|
// OnError is called if the handler returned a HTTPError. This
|
|
|
|
|
// is intended to be used to present pretty error pages if
|
|
|
|
|
// the user agent is determined to be a browser.
|
|
|
|
@ -250,6 +301,14 @@ func (h retHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
RequestID: RequestIDFromContext(r.Context()),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var bucket string
|
|
|
|
|
if bs := h.opts.BucketedStats; bs != nil {
|
|
|
|
|
bucket = bs.bucketForRequest(r)
|
|
|
|
|
if bs.Started != nil {
|
|
|
|
|
bs.Started.Add(bucket, 1)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
lw := &loggingResponseWriter{ResponseWriter: w, logf: h.opts.Logf}
|
|
|
|
|
err := h.rh.ServeHTTPReturn(lw, r)
|
|
|
|
|
|
|
|
|
@ -332,6 +391,10 @@ func (h retHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if bs := h.opts.BucketedStats; bs != nil && bs.Finished != nil {
|
|
|
|
|
bs.Finished.Add(bucket, 1)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !h.opts.QuietLoggingIfSuccessful || (msg.Code != http.StatusOK && msg.Code != http.StatusNotModified) {
|
|
|
|
|
h.opts.Logf("%s", msg)
|
|
|
|
|
}
|
|
|
|
|