Skip to content
quarry Artifact [d41e0c6bad]
Login

Artifact [d41e0c6bad]

Artifact d41e0c6bad04f4cf61ba77dd5c8be5a5d324301f3c4a500184402ed4c1bfe347:


package obs

import (
	"net/http"
	"net/url"
	"sync"
	"time"
)

// The user-agent classes a rollup row is split by. Closed set.
const (
	AgentCrawler   = "crawler"   // names itself as a search or AI crawler
	AgentAutomated = "automated" // a script, a library, a sync client, anything not a browser
	AgentBrowser   = "browser"
)

// Unmatched is the pattern of a request no route took.
const Unmatched = "(unmatched)"

// HourKey is what a rollup row is counted by: the hour, the mux
// pattern (never the raw path), and the kind of source. No address is
// in it, so nothing here needs to be dropped after a day.
type HourKey struct {
	Hour     time.Time // UTC, truncated to the hour
	Pattern  string
	SignedIn bool   // the forge verified an account
	Agent    string // AgentCrawler, AgentAutomated or AgentBrowser
	Network  string // from the local ASN table, "" when unknown
	Country  string // the edge's guess, "" when nothing in front says
}

// HourCount is one rollup row: requests and the handler time they took.
type HourCount struct {
	HourKey
	Requests int64
	Elapsed  time.Duration
}

// Rollup counts requests in memory by HourKey. Adding is one map
// update under a lock; the store sees a row only when a closed hour is
// taken, so the hot path never waits on it.
type Rollup struct {
	// Max bounds the keys held; 0 means 20000. Past it a new key loses
	// its network and country, so a flood of distinct networks cannot
	// grow the map without bound.
	Max int
	mu  sync.Mutex
	m   map[HourKey]*HourCount
}

// AgentOf classes a user agent. Only the string and the network speak
// here; who signed in is its own column.
func AgentOf(ua string, asn uint32) string {
	switch r := Classify(Facts{UA: ua, ASN: asn}); {
	case r.Kind == KindCrawler:
		return AgentCrawler
	case r.Kind == KindAutomated:
		return AgentAutomated
	case isBrowser(ua):
		return AgentBrowser
	}
	return AgentAutomated
}

// Add counts one hit.
func (r *Rollup) Add(h Hit) {
	k := HourKey{
		Hour: h.At.UTC().Truncate(time.Hour), Pattern: h.Pattern,
		SignedIn: h.Account != "", Agent: AgentOf(h.UA, h.ASN),
		Network: h.Network, Country: h.Country,
	}
	if k.Pattern == "" {
		k.Pattern = Unmatched
	}
	var took time.Duration
	if !h.Stream { // a stream's time is how long a tab stayed open
		took = h.Elapsed
	}
	r.mu.Lock()
	defer r.mu.Unlock()
	r.bump(k, 1, took)
}

func (r *Rollup) bump(k HourKey, n int64, took time.Duration) {
	if r.m == nil {
		r.m = map[HourKey]*HourCount{}
	}
	c, ok := r.m[k]
	if !ok {
		limit := r.Max
		if limit <= 0 {
			limit = 20000
		}
		if len(r.m) >= limit {
			k.Network, k.Country = "", ""
			c, ok = r.m[k]
		}
		if !ok {
			c = &HourCount{HourKey: k}
			r.m[k] = c
		}
	}
	c.Requests += n
	c.Elapsed += took
}

// Take removes and returns the rows for hours before the given one; a
// zero time takes everything.
func (r *Rollup) Take(before time.Time) []HourCount {
	r.mu.Lock()
	defer r.mu.Unlock()
	var out []HourCount
	for k, c := range r.m {
		if before.IsZero() || k.Hour.Before(before) {
			out = append(out, *c)
			delete(r.m, k)
		}
	}
	return out
}

// Restore puts rows back, for a write that failed.
func (r *Rollup) Restore(rows []HourCount) {
	r.mu.Lock()
	defer r.mu.Unlock()
	for _, c := range rows {
		r.bump(c.HourKey, c.Requests, c.Elapsed)
	}
}

// Pattern records the mux pattern that served the request. The finest
// one wins: a handler inside the mux knows more than a resolver outside
// it, so a later non-empty call replaces an earlier one.
func (n *HitNote) Pattern(p string) {
	if n == nil || p == "" {
		return
	}
	n.mu.Lock()
	n.pattern = p
	n.mu.Unlock()
}

// SetRoutes is how the recorder names the route of a request no handler
// noted — sync, runner downloads, assets. It must not dispatch.
func (r *Recorder) SetRoutes(f func(*http.Request) string) {
	r.mu.Lock()
	r.routes = f
	r.mu.Unlock()
}

// routeOf asks the resolver about the request as it came in, whatever
// the handlers did to the path since.
func (r *Recorder) routeOf(req *http.Request, path string) string {
	r.mu.Lock()
	f := r.routes
	r.mu.Unlock()
	if f == nil {
		return ""
	}
	u := url.URL{Path: path}
	c := *req
	c.URL = &u
	return f(&c)
}

// HourlyQuery selects rollup rows. Pattern "" is every route; a zero
// Since or Until leaves that end open. Until is exclusive.
type HourlyQuery struct {
	Pattern      string
	Since, Until time.Time
}