mirror of
https://github.com/tailscale/tailscale.git
synced 2025-01-05 23:07:44 +00:00
tsweb: implementing bucketed statistics for started/finished counts
Signed-off-by: Tom DNetto <tom@tailscale.com> Updates: corp#17075
This commit is contained in:
parent
b752bde280
commit
36efc50817
@ -18,6 +18,7 @@
|
||||
"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)
|
||||
}
|
||||
|
@ -11,12 +11,14 @@
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"tailscale.com/tstest"
|
||||
"tailscale.com/util/must"
|
||||
"tailscale.com/util/vizerror"
|
||||
)
|
||||
|
||||
@ -668,3 +670,29 @@ func TestCleanRedirectURL(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBucket(t *testing.T) {
|
||||
tcs := []struct {
|
||||
path string
|
||||
want string
|
||||
}{
|
||||
{"/map", "/map"},
|
||||
{"/key?v=63", "/key"},
|
||||
{"/map/a87e865a9d1c7", "/map/…"},
|
||||
{"/machine/37fc1acb57f256b69b0d76749d814d91c68b241057c6b127fee3df37e4af111e", "/machine/…"},
|
||||
{"/machine/37fc1acb57f256b69b0d76749d814d91c68b241057c6b127fee3df37e4af111e/map", "/machine/…/map"},
|
||||
}
|
||||
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.path, func(t *testing.T) {
|
||||
o := BucketedStatsOptions{}
|
||||
bucket := (&o).bucketForRequest(&http.Request{
|
||||
URL: must.Get(url.Parse(tc.path)),
|
||||
})
|
||||
|
||||
if bucket != tc.want {
|
||||
t.Errorf("bucket for %q was %q, want %q", tc.path, bucket, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user