Watch
1
0
Fork
You've already forked pkg-proxy
1
mirror of https://github.com/git-pkgs/proxy.git synced 2026-11-09 15:49:16 -05:00
pkg-proxy/internal/metrics/failures.go
2026-10-19 08:35:46 +01:00

113 lines
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
}
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()
}