refactor(metrics): remove unused AI counters

This commit is contained in:
2026-07-21 19:13:15 +07:00
parent 9f2710569b
commit cffde669ec
2 changed files with 20 additions and 27 deletions
+9 -18
View File
@@ -20,8 +20,8 @@ import (
// while still surfacing minute-scale traffic shifts.
const DefaultFlushInterval = 60 * time.Second
// Registry holds named counters across three categories: command
// invocations, errors, and AI calls. Zero-value Registry is ready to use.
// Registry holds named counters across two categories: command invocations
// and errors. Zero-value Registry is ready to use.
//
// Counters use atomic.Int64 so increments don't lock; the per-name map
// itself is guarded by an RWMutex for the rare add path. Names should be
@@ -30,7 +30,6 @@ type Registry struct {
mu sync.RWMutex
commands map[string]*atomic.Int64
errors map[string]*atomic.Int64
ai map[string]*atomic.Int64
}
// New returns an empty Registry. Most callers use the package-level
@@ -39,7 +38,6 @@ func New() *Registry {
return &Registry{
commands: map[string]*atomic.Int64{},
errors: map[string]*atomic.Int64{},
ai: map[string]*atomic.Int64{},
}
}
@@ -56,13 +54,8 @@ func (r *Registry) IncCommand(name string) { r.inc(r.commandsMap(), name) }
// like "ai-429", "kv-unavailable", "telegram-403".
func (r *Registry) IncError(kind string) { r.inc(r.errorsMap(), kind) }
// IncAI bumps the counter for an AI call by model name. Useful for
// tracking the daily-quota path: sum across instances == requests.
func (r *Registry) IncAI(model string) { r.inc(r.aiMap(), model) }
func (r *Registry) commandsMap() map[string]*atomic.Int64 { return r.commands }
func (r *Registry) errorsMap() map[string]*atomic.Int64 { return r.errors }
func (r *Registry) aiMap() map[string]*atomic.Int64 { return r.ai }
// inc bumps the counter for name in m, allocating on first use. Allocates
// only when the name is new, so steady-state increments are mutex-free.
@@ -88,12 +81,11 @@ func (r *Registry) inc(m map[string]*atomic.Int64, name string) {
// snapshot copies and resets the counters atomically per category. The
// returned maps are owned by the caller; the registry's internal state is
// reset to zero for the next interval.
func (r *Registry) snapshot() (cmds, errs, ai map[string]int64) {
func (r *Registry) snapshot() (cmds, errs map[string]int64) {
r.mu.Lock()
defer r.mu.Unlock()
cmds = drain(r.commands)
errs = drain(r.errors)
ai = drain(r.ai)
return
}
@@ -122,20 +114,20 @@ func drain(m map[string]*atomic.Int64) map[string]int64 {
//
// The log line shape:
//
// {"msg":"metrics","commands":{"wordle":3,"loldle":1},"errors":{"ai-429":1},"ai":null}
// {"msg":"metrics","commands":{"wordle":3,"loldle":1},"errors":{"handler-error":1}}
//
// CloudWatch Logs filters on `jsonPayload.msg=metrics` for dashboards.
// Empty categories appear as null (slog's default for nil maps).
func (r *Registry) Flush() {
cmds, errs, ai := r.snapshot()
cmds, errs := r.snapshot()
// Avoid an empty-everything log line — adds noise without signal.
if cmds == nil && errs == nil && ai == nil {
if cmds == nil && errs == nil {
return
}
// slog renders map[string]int64 as a JSON object; tests assert on
// per-key substrings rather than full-line equality so non-deterministic
// hashtable iteration order doesn't make them flaky.
log.Info("metrics", "commands", cmds, "errors", errs, "ai", ai)
log.Info("metrics", "commands", cmds, "errors", errs)
}
// Run starts a goroutine that flushes counters every DefaultFlushInterval
@@ -160,11 +152,10 @@ func (r *Registry) Run(ctx context.Context) {
}
}
// IncCommand / IncError / IncAI on the package-level Default — short
// import-path-free spelling for the common case.
// IncCommand / IncError on the package-level Default — short import-path-free
// spelling for the common case.
func IncCommand(name string) { Default.IncCommand(name) }
func IncError(kind string) { Default.IncError(kind) }
func IncAI(model string) { Default.IncAI(model) }
// Flush flushes the default registry. Used in graceful-shutdown paths.
func Flush() { Default.Flush() }
+11 -9
View File
@@ -27,7 +27,7 @@ func TestRegistry_IncCommand_AccumulatesCounts(t *testing.T) {
r.IncCommand("wordle")
r.IncCommand("loldle")
cmds, _, _ := r.snapshot()
cmds, _ := r.snapshot()
if cmds["wordle"] != 2 {
t.Errorf("wordle = %d, want 2", cmds["wordle"])
}
@@ -40,7 +40,7 @@ func TestRegistry_Snapshot_ResetsCounters(t *testing.T) {
r := New()
r.IncCommand("wordle")
r.snapshot() // first snapshot drains
cmds, _, _ := r.snapshot()
cmds, _ := r.snapshot()
if len(cmds) != 0 {
t.Errorf("after drain, snapshot = %v, want empty", cmds)
}
@@ -52,7 +52,7 @@ func TestRegistry_Flush_EmitsMetricsLine(t *testing.T) {
r := New()
r.IncCommand("wordle")
r.IncError("ai-429")
r.IncError("handler-error")
r.Flush()
output := buf.String()
@@ -62,9 +62,12 @@ func TestRegistry_Flush_EmitsMetricsLine(t *testing.T) {
if !strings.Contains(output, `"wordle":1`) {
t.Errorf("flush output missing wordle counter: %s", output)
}
if !strings.Contains(output, `"ai-429":1`) {
if !strings.Contains(output, `"handler-error":1`) {
t.Errorf("flush output missing error counter: %s", output)
}
if strings.Contains(output, `"ai":`) {
t.Errorf("flush output contains removed AI metrics field: %s", output)
}
}
func TestRegistry_Flush_EmptyIsSilent(t *testing.T) {
@@ -94,7 +97,7 @@ func TestRegistry_ConcurrentInc(t *testing.T) {
}()
}
wg.Wait()
cmds, _, _ := r.snapshot()
cmds, _ := r.snapshot()
if got := cmds["hot"]; got != goroutines*itersEach {
t.Errorf("hot = %d, want %d", got, goroutines*itersEach)
}
@@ -107,11 +110,10 @@ func TestPackageDefault_RoundTrip(t *testing.T) {
IncCommand("ping")
IncError("kv-fail")
IncAI("flash")
cmds, errs, ai := Default.snapshot()
if cmds["ping"] != 1 || errs["kv-fail"] != 1 || ai["flash"] != 1 {
t.Errorf("default registry: cmds=%v errs=%v ai=%v", cmds, errs, ai)
cmds, errs := Default.snapshot()
if cmds["ping"] != 1 || errs["kv-fail"] != 1 {
t.Errorf("default registry: cmds=%v errs=%v", cmds, errs)
}
}