reqstats

package
v1.28.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 8, 2026 License: MIT Imports: 12 Imported by: 0

Documentation

Overview

Package reqstats turns the nginx access feed into a per-site view of request timing: the typical response time and the routes that run well above their own baseline. It consumes the same syslog datagrams that drive idle-suspend, but a widened log format, and keeps a small rolling in-memory window per route. It is framework-agnostic: nothing here knows a framework name, only universal nginx timing signals.

Index

Constants

View Source
const DefaultColdGap = 30 * time.Minute

DefaultColdGap is the fallback idle gap after which a request is treated as a cold start, used when the idle-suspend timeout can't be read. The watcher prefers that configured timeout.

Variables

This section is empty.

Functions

func IsColdStart

func IsColdStart(last time.Time, seen bool, now time.Time, gap time.Duration) bool

IsColdStart reports whether a request at now is a cold start: the site has been seen before and sat idle at least gap since. The first request ever seen for a site isn't a cold start, since there's no prior time to prove it was idle; the watcher seeds the last-seen clock from the durable store on startup so a wake right after a daemon restart still counts against a real prior time.

func IsStaticAsset

func IsStaticAsset(rawURI string) bool

IsStaticAsset reports whether a request URI points at a static asset, judged by the file extension of its last path segment, so the timing view can skip it.

func NormalizeRoute

func NormalizeRoute(method, rawURI string) string

NormalizeRoute turns a method and raw request URI into a stable route key by dropping the query string and collapsing id-like path segments to ":id", so "/users/123" and "/users/456" aggregate as one route. Dropping the query also keeps tokens and other query-string values out of the in-memory window.

func SaveSnapshot

func SaveSnapshot(snap []SiteStats, path string) error

SaveSnapshot persists an already-computed snapshot, so a caller that also needs the snapshot (e.g. to feed slow-route notifications) can build it once and avoid recomputing it inside Save.

func StripQueryFragment

func StripQueryFragment(rawURI string) string

StripQueryFragment returns the path portion of a raw request URI, dropping the query string and fragment, and defaulting to "/". This is the concrete path we keep as a route's openable example, so no query-string values are retained.

Types

type AccessRecord

type AccessRecord struct {
	Host        string
	Status      int
	RequestTime float64 // seconds, as nginx $request_time
	Method      string
	URI         string // raw $request_uri, query string included
}

AccessRecord is one parsed nginx access datagram. The feed emits a pipe-delimited message "$host|$status|$request_time|$request_method|$request_uri" as the final whitespace token, so it survives syslog framing the same way the idle host-only format does.

func ParseAccessRecord

func ParseAccessRecord(datagram []byte) (AccessRecord, bool)

ParseAccessRecord extracts a timing record from one access datagram. It takes the final whitespace token (skipping syslog framing, as the idle parser does) and splits it on '|'. Returns ok=false for the old host-only format, nginx's "-" host placeholder, or a line whose status/time don't parse.

func (AccessRecord) SecondsToMillis

func (r AccessRecord) SecondsToMillis() float64

SecondsToMillis returns the request time in milliseconds.

type Aggregator

type Aggregator struct {
	// contains filtered or unexported fields
}

Aggregator holds rolling per-site, per-route timing windows. Safe for concurrent use: the access-feed reader writes while the UI reads.

func New

func New(resolve SiteResolver) *Aggregator

New returns an aggregator that maps hosts to sites via resolve. A nil resolver drops every record.

func (*Aggregator) Record

func (a *Aggregator) Record(r AccessRecord)

Record ingests one access record, resolving its host to a site and appending the request time to that site's overall and per-route windows. Records for an unknown host, or once a site is at its route cap for a new route, are dropped.

func (*Aggregator) Save

func (a *Aggregator) Save(path string) error

Save writes the current snapshot of every site to path as JSON, via a temp file and rename so a reader never sees a half-written file. The watcher calls this on its tick; lerd-ui reads it with Load.

func (*Aggregator) SiteSnapshot

func (a *Aggregator) SiteSnapshot(site string) (SiteStats, bool)

SiteSnapshot returns the current view for one site, ok=false when the site has no recorded traffic.

func (*Aggregator) Snapshot

func (a *Aggregator) Snapshot() []SiteStats

Snapshot returns a view for every site with recorded traffic.

type Analytics

type Analytics struct {
	Site         string            `json:"site"`
	Samples      int               `json:"samples"`
	ColdStarts   int               `json:"cold_starts"` // excluded from timing, still counted
	MedianMillis float64           `json:"median_millis"`
	P95Millis    float64           `json:"p95_millis"`
	Status       StatusCounts      `json:"status"`
	Distribution []LatencyBucket   `json:"distribution"`
	Throughput   []ThroughputPoint `json:"throughput"`
	Routes       []RouteStat       `json:"routes"`
}

Analytics is the full request-timing view for one site over a window.

type LatencyBucket

type LatencyBucket struct {
	UpperMillis float64 `json:"upper_millis"`
	Count       int     `json:"count"`
}

LatencyBucket is one bar of the response-time histogram. UpperMillis is the exclusive upper bound; 0 marks the open-ended top bucket.

type Record

type Record struct {
	At     time.Time
	Site   string
	Route  string
	Method string
	Status int
	Millis float64
	URI    string
	// Cold marks the first request after the site had gone idle: a cold start
	// (suspended workers waking, cold caches) whose inflated time would skew the
	// timing view, so it's kept out of the percentiles but still counted.
	Cold bool
}

Record is one persisted request. Route is the normalized template (e.g. "GET /products/:id"); URI is the concrete path last seen, so a route stays openable and the recent list shows real paths.

func RecordFrom

func RecordFrom(r AccessRecord, site string, at time.Time) Record

RecordFrom builds a store record from a parsed access record for a resolved site, normalizing the route the same way the live aggregator does so the two views agree on route identity.

type RouteStat

type RouteStat struct {
	Route     string  `json:"route"`
	Method    string  `json:"method"`
	Example   string  `json:"example"` // a concrete path last seen for this route, openable in a browser
	P50Millis float64 `json:"p50_millis"`
	P95Millis float64 `json:"p95_millis"`
	// RecentP95Millis is the p95 over only the route's most recent samples, so a
	// route that was slow but has since been fixed reads as fast again even while
	// its old slow samples still sit in the window's overall p95.
	RecentP95Millis float64 `json:"recent_p95_millis"`
	Multiplier      float64 `json:"multiplier"`
	Samples         int     `json:"samples"`
}

RouteStat is one flagged route in a site snapshot. The representative time is the route's p95, not its median: a route that is only sometimes slow (a mix of fast redirects and slow renders under one path) hides in the median but shows in the tail, which is the "this request takes a second" the user actually feels.

type SiteResolver

type SiteResolver func(host string) (site string, ok bool)

SiteResolver maps a request host to the owning site name, returning ok=false for a host that belongs to no registered site. Mirrors the idle resolver so the watcher can pass the same function.

type SiteStats

type SiteStats struct {
	Site         string      `json:"site"`
	MedianMillis float64     `json:"median_millis"`
	Samples      int         `json:"samples"`
	Slow         []RouteStat `json:"slow"`
	UpdatedAt    time.Time   `json:"updated_at"`
}

SiteStats is the per-site view the UI renders: the typical response time and the routes running well above their own baseline, slowest first.

func Load

func Load(path string) []SiteStats

Load reads a snapshot previously written by Save. A missing or unreadable file yields an empty slice, so a UI that starts before the watcher just shows nothing rather than erroring.

func LoadSite

func LoadSite(path, site string) (SiteStats, bool)

LoadSite returns the snapshot for one site from the persisted file, ok=false when the site has no recorded traffic.

type StatusCounts

type StatusCounts struct {
	C2xx int `json:"c2xx"`
	C3xx int `json:"c3xx"`
	C4xx int `json:"c4xx"`
	C5xx int `json:"c5xx"`
}

StatusCounts is the response-status breakdown, used for the error rate.

type Store

type Store struct {
	// contains filtered or unexported fields
}

Store is the durable SQLite record of requests. The watcher (the only process on the nginx access feed) writes to it; lerd-ui opens it read-only to build the request-timing analytics view over any window. Pure-Go driver, so the CGO-free build is unaffected.

func OpenStore

func OpenStore(path string) (*Store, error)

OpenStore opens (creating if needed) the SQLite store at path with WAL enabled so the watcher can write while lerd-ui reads across processes.

func (*Store) Close

func (s *Store) Close() error

Close releases the database handle.

func (*Store) Insert

func (s *Store) Insert(recs []Record) error

Insert writes a batch of records in one transaction, so the watcher can buffer a tick's worth of requests and flush them together rather than per request.

func (*Store) LastSeenBySite

func (s *Store) LastSeenBySite() (map[string]time.Time, error)

LastSeenBySite returns the most recent request time for every site in the store, so the watcher can seed its cold-start clock on startup. Without it a daemon restart forgets the last request and judges the next wake as warm, letting the cold boot's inflated time dominate the route p95.

func (*Store) Prune

func (s *Store) Prune(before time.Time) (int64, error)

Prune deletes rows older than before, returning how many were removed, so the watcher can bound the store to a retention window.

func (*Store) Recent

func (s *Store) Recent(site string, limit int) ([]Record, error)

Recent returns the newest limit requests for a site, newest first.

func (*Store) SiteAnalytics

func (s *Store) SiteAnalytics(site string, since, until time.Time) (Analytics, error)

SiteAnalytics aggregates every request for a site in [since, until) into the full analytics view. Percentiles are computed in Go from the window's rows, reusing the same helpers as the live aggregator, since SQLite has no native percentile and local request volumes are small.

type ThroughputPoint

type ThroughputPoint struct {
	AtMillis int64 `json:"at_millis"`
	Count    int   `json:"count"`
}

ThroughputPoint is the request count in one minute bucket, keyed by the bucket's start as unix milliseconds.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL