include db for gitea build
This commit is contained in:
1 parent
8de4a2e010
commit
645f923175
33 files changed
+1490
-150
No files matched your search
@@ -7,6 +7,8 @@ package features
|
||||
import (
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"git.tornberg.me/go-otel/internal/uaclass"
|
||||
)
|
||||
|
||||
// Event is the subset of a normalized event used by the detector. Text fields
|
||||
@@ -31,6 +33,14 @@ type Event struct {
|
||||
Route string
|
||||
StatusCode int
|
||||
ExceptionType string
|
||||
|
||||
// Trace-resolved context (see internal/tracectx). All optional: zero
|
||||
// values mean "not resolved", which behaves like the pre-resolution data.
|
||||
ClientIP string // real client behind proxy hops, when known
|
||||
UAClass string // internal/uaclass class of the User-Agent
|
||||
IsDuplicate bool // repeats an ancestor span's failure; not a new request
|
||||
IsRootCause bool // error span with no erroring child
|
||||
HasUpstream bool // an ingress span that had a child in another service
|
||||
}
|
||||
|
||||
// Config controls feature and label windows. A zero Config receives the
|
||||
@@ -94,6 +104,18 @@ type Row struct {
|
||||
TimeoutCount int `json:"timeoutCount"`
|
||||
CancellationCount int `json:"cancellationCount"`
|
||||
|
||||
// Logical-request view: DuplicateErrors are hops repeating an ancestor's
|
||||
// failure, so LogicalErrors = ErrorCount - DuplicateErrors is the number of
|
||||
// failed requests. DistinctClients / TopClientShare separate one noisy
|
||||
// client from broad impact; BotErrorShare is the share of errors whose
|
||||
// User-Agent classifies as a self-declared non-human client.
|
||||
LogicalErrors int `json:"logicalErrors"`
|
||||
DuplicateErrors int `json:"duplicateErrors"`
|
||||
RootCauseErrors int `json:"rootCauseErrors"`
|
||||
DistinctClients int `json:"distinctClients"`
|
||||
TopClientShare float64 `json:"topClientShare"`
|
||||
BotErrorShare float64 `json:"botErrorShare"`
|
||||
|
||||
BaselineErrorRate float64 `json:"baselineErrorRate"`
|
||||
BaselineSamples int `json:"baselineSamples"`
|
||||
FutureEventCount int `json:"futureEventCount"`
|
||||
@@ -189,10 +211,13 @@ type accumulator struct {
|
||||
durations []float64
|
||||
traces map[string]struct{}
|
||||
issues map[string]struct{}
|
||||
clients map[string]int
|
||||
botErrors int
|
||||
classed int
|
||||
}
|
||||
|
||||
func newAccumulator(start, end time.Time, service, release string) *accumulator {
|
||||
return &accumulator{value: Row{WindowStart: start, WindowEnd: end, Service: service, Release: release}, traces: map[string]struct{}{}, issues: map[string]struct{}{}}
|
||||
return &accumulator{value: Row{WindowStart: start, WindowEnd: end, Service: service, Release: release}, traces: map[string]struct{}{}, issues: map[string]struct{}{}, clients: map[string]int{}}
|
||||
}
|
||||
|
||||
func (a *accumulator) add(e Event) {
|
||||
@@ -223,6 +248,20 @@ func (a *accumulator) add(e Event) {
|
||||
return
|
||||
}
|
||||
a.value.ErrorCount++
|
||||
if e.IsDuplicate {
|
||||
a.value.DuplicateErrors++
|
||||
} else if e.ClientIP != "" {
|
||||
a.clients[e.ClientIP]++
|
||||
}
|
||||
if e.IsRootCause {
|
||||
a.value.RootCauseErrors++
|
||||
}
|
||||
if e.UAClass != "" && !e.IsDuplicate {
|
||||
a.classed++
|
||||
if uaclass.IsBot(e.UAClass) {
|
||||
a.botErrors++
|
||||
}
|
||||
}
|
||||
if e.Source == "frontend" {
|
||||
a.value.FrontendErrors++
|
||||
} else {
|
||||
@@ -252,6 +291,17 @@ func issueKey(e Event) string {
|
||||
func (a *accumulator) row() Row {
|
||||
a.value.ErrorRate = rate(a.value.ErrorCount, a.value.TotalEvents)
|
||||
a.value.UniqueTraceCount, a.value.UniqueIssueCount = len(a.traces), len(a.issues)
|
||||
a.value.LogicalErrors = a.value.ErrorCount - a.value.DuplicateErrors
|
||||
a.value.DistinctClients = len(a.clients)
|
||||
top, known := 0, 0
|
||||
for _, n := range a.clients {
|
||||
known += n
|
||||
if n > top {
|
||||
top = n
|
||||
}
|
||||
}
|
||||
a.value.TopClientShare = rate(top, known)
|
||||
a.value.BotErrorShare = rate(a.botErrors, a.classed)
|
||||
if len(a.durations) != 0 {
|
||||
sort.Float64s(a.durations)
|
||||
a.value.P50Duration = percentile(a.durations, .50)
|
||||
|
||||
@@ -54,3 +54,24 @@ func TestBuildWithholdsLabelWithoutCompleteBaseline(t *testing.T) {
|
||||
t.Fatalf("short history must not be labelled: %+v", rows)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildLogicalRequestFeatures(t *testing.T) {
|
||||
at := time.Date(2026, 9, 19, 12, 0, 0, 0, time.UTC)
|
||||
mk := func(client string, dup bool, class string) Event {
|
||||
return Event{Timestamp: at, Service: "nginx", Kind: "span", IsError: true, PatternSignature: "p", ClientIP: client, IsDuplicate: dup, UAClass: class}
|
||||
}
|
||||
rows := Build([]Event{mk("a", false, "ai_crawler"), mk("a", true, "ai_crawler"), mk("a", false, "browser"), mk("b", false, "browser")}, Config{})
|
||||
if len(rows) == 0 {
|
||||
t.Fatal("no rows")
|
||||
}
|
||||
r := rows[0]
|
||||
if r.ErrorCount != 4 || r.DuplicateErrors != 1 || r.LogicalErrors != 3 {
|
||||
t.Fatalf("logical errors wrong: %+v", r)
|
||||
}
|
||||
if r.DistinctClients != 2 || r.TopClientShare < 0.66 || r.TopClientShare > 0.67 {
|
||||
t.Fatalf("client features wrong: %+v", r)
|
||||
}
|
||||
if r.BotErrorShare < 0.33 || r.BotErrorShare > 0.34 {
|
||||
t.Fatalf("bot share wrong: %v", r.BotErrorShare)
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,8 @@ type PatternDelta struct {
|
||||
Count int `json:"count"`
|
||||
BaselineMean float64 `json:"baselineMean"`
|
||||
Excess float64 `json:"excess"`
|
||||
Novel bool `json:"novel"` // never seen in the baseline
|
||||
Novel bool `json:"novel"` // never seen in the baseline
|
||||
Clients int `json:"clients,omitempty"` // distinct resolved client IPs in the current window
|
||||
Example Event `json:"example"`
|
||||
}
|
||||
|
||||
@@ -84,6 +85,7 @@ func DetectSpikes(events []Event, at time.Time, cfg SpikeConfig) []Spike {
|
||||
cur int
|
||||
base []int // per baseline window
|
||||
example Event
|
||||
clients map[string]struct{}
|
||||
}
|
||||
type svc struct {
|
||||
cur int
|
||||
@@ -92,7 +94,10 @@ func DetectSpikes(events []Event, at time.Time, cfg SpikeConfig) []Spike {
|
||||
}
|
||||
services := map[string]*svc{}
|
||||
for _, e := range events {
|
||||
if !e.IsError {
|
||||
// A duplicate is a later hop of a request whose outermost error is
|
||||
// already counted; counting it again multiplies every failure by the
|
||||
// number of services it crossed.
|
||||
if !e.IsError || e.IsDuplicate {
|
||||
continue
|
||||
}
|
||||
ts := e.Timestamp.UTC()
|
||||
@@ -123,6 +128,12 @@ func DetectSpikes(events []Event, at time.Time, cfg SpikeConfig) []Spike {
|
||||
s.cur++
|
||||
p.cur++
|
||||
p.example = e
|
||||
if e.ClientIP != "" {
|
||||
if p.clients == nil {
|
||||
p.clients = map[string]struct{}{}
|
||||
}
|
||||
p.clients[e.ClientIP] = struct{}{}
|
||||
}
|
||||
} else {
|
||||
idx := int(ts.Sub(baseStart) / cfg.Window)
|
||||
if idx >= cfg.BaselineWindows {
|
||||
@@ -154,7 +165,7 @@ func DetectSpikes(events []Event, at time.Time, cfg SpikeConfig) []Spike {
|
||||
}
|
||||
spike.Excess += excess
|
||||
spike.Patterns = append(spike.Patterns, PatternDelta{
|
||||
Signature: sig, Count: p.cur, BaselineMean: mean, Excess: excess, Novel: !seen, Example: p.example,
|
||||
Signature: sig, Count: p.cur, BaselineMean: mean, Excess: excess, Novel: !seen, Example: p.example, Clients: len(p.clients),
|
||||
})
|
||||
}
|
||||
if spike.Excess < float64(cfg.MinExcess) {
|
||||
|
||||
@@ -98,3 +98,26 @@ func TestDetectSpikesWarmupGuard(t *testing.T) {
|
||||
t.Fatalf("guard should be off when DataSince is zero")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectSpikesSkipsDuplicateHopsAndCountsClients(t *testing.T) {
|
||||
at := time.Date(2026, 9, 19, 12, 0, 0, 0, time.UTC)
|
||||
cfg := SpikeConfig{Window: 5 * time.Minute, BaselineWindows: 12}
|
||||
var events []Event
|
||||
for i := 0; i < 8; i++ {
|
||||
ts := at.Add(-time.Minute).Add(time.Duration(i) * time.Second)
|
||||
client := "203.0.113." + string(rune('1'+i%2))
|
||||
events = append(events,
|
||||
Event{Timestamp: ts, Service: "nginx", IsError: true, PatternSignature: "p", ClientIP: client},
|
||||
// two more hops of the same request must not count
|
||||
Event{Timestamp: ts, Service: "nginx", IsError: true, PatternSignature: "p", ClientIP: client, IsDuplicate: true},
|
||||
Event{Timestamp: ts, Service: "nginx", IsError: true, PatternSignature: "p", ClientIP: client, IsDuplicate: true},
|
||||
)
|
||||
}
|
||||
got := DetectSpikes(events, at, cfg)
|
||||
if len(got) != 1 || got[0].ErrorCount != 8 || got[0].Patterns[0].Count != 8 {
|
||||
t.Fatalf("duplicates were counted: %+v", got)
|
||||
}
|
||||
if got[0].Patterns[0].Clients != 2 {
|
||||
t.Fatalf("clients = %d, want 2", got[0].Patterns[0].Clients)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user