mirror of
https://github.com/git-pkgs/proxy.git
synced 2026-11-09 15:49:16 -05:00
113 lines
3 KiB
Go
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()
|
|
}
|