Watch
1
0
Fork
You've already forked pkg-proxy
1
mirror of https://github.com/git-pkgs/proxy.git synced 2026-11-09 23:59:16 -05:00
pkg-proxy/internal/metrics/failures.go
Simon Chrzanowski b131db394d Isolate the recent failure history in tests
The history is process-wide, and tests cleared it only after running, and
not all of them. Records from earlier tests or an earlier -count repetition
reached later assertions: TestRecordUpstreamOperations left a record for
TestRecentFailuresNewestFirst, and the server tests never cleared it.

- Export ResetRecentFailures so tests outside the metrics package can use it
- Clear the history before and after every test that records or reads
  failures directly, in metrics, server and handler
- Assert exactly one failure in the handler tests instead of looking only
  at the newest
2026-10-12 18:44:12 +02:00

122 lines
3.3 KiB
Go

package metrics
import (
"net/url"
"regexp"
"sync"
"time"
)
// recentFailuresPerSeries bounds how many failures are kept for each series of
// a failure counter. The series set is already bounded by the counter's
// labels, so this caps the whole store.
const recentFailuresPerSeries = 20
// FailureDetail is what a failure counter's call site knows beyond the
// counter's labels: which artifact failed, where it came from or was going,
// and why. Every field is optional.
//
// None of it becomes a metric label. Package names and URLs are unbounded, so
// they are kept in a small in-memory store for /ui/analytics instead.
type FailureDetail struct {
PURL string
Filename string
URL string
Path string
Err error
}
// Failure is one recorded failure, with credentials already stripped from the
// URL and the error text.
type Failure struct {
At time.Time
PURL string
Filename string
URL string
Path string
Error string
}
// recentFailures holds the latest failures per counter series. It lives in
// process memory only, so like the counters themselves it starts empty when
// the proxy starts.
var recentFailures = struct {
sync.Mutex
series map[string][]Failure
}{series: make(map[string][]Failure)}
// RecentFailures returns the latest failures recorded for one series of a
// failure counter, newest first, or nil when the series has none.
func RecentFailures(name string, labels map[string]string) []Failure {
recentFailures.Lock()
defer recentFailures.Unlock()
stored := recentFailures.series[failureKey(name, labels)]
if len(stored) == 0 {
return nil
}
return append([]Failure(nil), stored...)
}
func recordFailure(name string, labels map[string]string, detail FailureDetail) {
failure := Failure{
At: time.Now(),
PURL: detail.PURL,
Filename: detail.Filename,
URL: redactURL(detail.URL),
Path: detail.Path,
}
if detail.Err != nil {
failure.Error = redactURLsIn(detail.Err.Error())
}
key := failureKey(name, labels)
recentFailures.Lock()
defer recentFailures.Unlock()
stored := append([]Failure{failure}, recentFailures.series[key]...)
if len(stored) > recentFailuresPerSeries {
stored = stored[:recentFailuresPerSeries]
}
recentFailures.series[key] = stored
}
// ResetRecentFailures empties the recent failure history. The history is
// process-wide, so tests that record or read failures call it to start from a
// known state and to leave none behind.
func ResetRecentFailures() {
recentFailures.Lock()
defer recentFailures.Unlock()
recentFailures.series = make(map[string][]Failure)
}
func failureKey(name string, labels map[string]string) string {
return name + "{" + labelKey(labels) + "}"
}
var urlInText = regexp.MustCompile(`[a-zA-Z][a-zA-Z0-9+.-]*://[^\s"'<>]+`)
// redactURLsIn strips credentials from every URL embedded in text, such as the
// one net/http quotes in a failed request's error.
func redactURLsIn(text string) string {
return urlInText.ReplaceAllStringFunc(text, redactURL)
}
// redactURL drops the userinfo, query and fragment, which is where registry
// tokens and signed-URL signatures live.
func redactURL(raw string) string {
if raw == "" {
return ""
}
u, err := url.Parse(raw)
if err != nil {
return "(unparseable URL)"
}
u.User = nil
u.RawQuery = ""
u.ForceQuery = false
u.Fragment = ""
u.RawFragment = ""
return u.String()
}