-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathstats.go
More file actions
181 lines (167 loc) · 4.63 KB
/
Copy pathstats.go
File metadata and controls
181 lines (167 loc) · 4.63 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
package main
import (
"sort"
"strings"
"sync"
"time"
)
// authStat is what the executor observed for one credential. It never stores the key.
type authStat struct {
Kind string
BaseURL string
Requests int64
Errors int64
LastStatus int
LastError string
LastUsed time.Time
// absentSince marks the first snapshot in which the host no longer listed this
// credential; the row is dropped once the grace window passes.
absentSince time.Time
}
// Counter rows survive a short absence so the host's auth-list cache and a momentary
// reload do not wipe them, then disappear so a deleted credential leaves no phantom row.
const (
statsAbsentGrace = 2 * time.Minute
statsRetention = 24 * time.Hour
statsMaxRows = 512
)
var stats = struct {
sync.Mutex
byAuth map[string]*authStat
total int64
errors int64
}{byAuth: map[string]*authStat{}}
// recordRequest counts one request against a credential and remembers where it went.
// Requests without a credential id only move the global counters: the panel lists
// credentials, and an empty id would show up as a phantom row.
func recordRequest(authID string, apiKey string) {
id := normalizeCredentialID(authID)
stats.Lock()
defer stats.Unlock()
stats.total++
if id == "" {
return
}
entry := statEntryLocked(stats.byAuth, id)
entry.Kind = credentialKind(apiKey)
entry.BaseURL = baseURLForCredential(apiKey)
entry.Requests++
entry.LastUsed = time.Now()
}
// recordResult stores the upstream outcome of the most recent request.
func recordResult(authID string, status int, errMsg string) {
id := normalizeCredentialID(authID)
failed := status >= 400 || errMsg != ""
stats.Lock()
defer stats.Unlock()
if failed {
stats.errors++
}
if id == "" {
return
}
entry := statEntryLocked(stats.byAuth, id)
entry.LastStatus = status
if failed {
entry.Errors++
}
entry.LastError = errMsg
entry.LastUsed = time.Now()
}
// statEntryLocked returns the counter row for a credential, creating it on first use.
func statEntryLocked(byAuth map[string]*authStat, id string) *authStat {
entry := byAuth[id]
if entry == nil {
entry = &authStat{}
byAuth[id] = entry
}
return entry
}
// dropStats forgets one credential immediately; used when the host says it is gone.
func dropStats(authID string) {
id := normalizeCredentialID(authID)
if id == "" {
return
}
stats.Lock()
defer stats.Unlock()
delete(stats.byAuth, id)
}
// statsFor returns the counters of one credential.
func statsFor(authID string) authStat {
stats.Lock()
defer stats.Unlock()
if entry := stats.byAuth[normalizeCredentialID(authID)]; entry != nil {
return *entry
}
return authStat{}
}
// statsTotals returns the global request and error counters.
func statsTotals() (int64, int64) {
stats.Lock()
defer stats.Unlock()
return stats.total, stats.errors
}
// statsAuthIDs lists every credential the executor has served, sorted for stable output.
func statsAuthIDs() []string {
stats.Lock()
defer stats.Unlock()
return statKeysLocked()
}
// pruneStats drops rows the host no longer lists — after a short grace so a cached
// listing or a reload does not erase live counters — and keeps the map bounded.
func pruneStats(present map[string]struct{}) {
stats.Lock()
defer stats.Unlock()
now := time.Now()
for id, entry := range stats.byAuth {
if _, known := present[id]; known {
entry.absentSince = time.Time{}
continue
}
if entry.absentSince.IsZero() {
entry.absentSince = now
continue
}
if now.Sub(entry.absentSince) > statsAbsentGrace || now.Sub(entry.LastUsed) > statsRetention {
delete(stats.byAuth, id)
}
}
if len(stats.byAuth) <= statsMaxRows {
return
}
ids := statKeysLocked()
sort.Slice(ids, func(i, j int) bool { return stats.byAuth[ids[i]].LastUsed.Before(stats.byAuth[ids[j]].LastUsed) })
for _, id := range ids[:len(ids)-statsMaxRows] {
delete(stats.byAuth, id)
}
}
func statKeysLocked() []string {
ids := make([]string, 0, len(stats.byAuth))
for id := range stats.byAuth {
if id == "" {
continue
}
ids = append(ids, id)
}
sort.Strings(ids)
return ids
}
// credentialKind classifies a MiMo key without exposing it. Only the documented
// prefixes are trusted; anything else is reported as unknown so the panel can show it
// instead of silently pretending the key is a pay-as-you-go credential.
func credentialKind(apiKey string) string {
key := strings.TrimSpace(apiKey)
switch {
case key == "":
return ""
case strings.HasPrefix(key, tokenPlanTeamPrefix):
return "token-plan-team"
case strings.HasPrefix(key, tokenPlanKeyPrefix):
return "token-plan"
case strings.HasPrefix(key, payAsYouGoKeyPrefix):
return "pay-as-you-go"
default:
return "unknown"
}
}