Skip to content

Commit b28509d

Browse files
appleboyclaude
andcommitted
fix(audit): support late metric registerer initialization
Replace the sync.Once-based counter init with a mutex-protected pattern that allows SetAuditMetricsRegisterer to register a previously created counter. This eliminates the silent misconfiguration when AuditService is constructed before metrics are configured (common in tests). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 85e0319 commit b28509d

1 file changed

Lines changed: 44 additions & 26 deletions

File tree

internal/services/audit.go

Lines changed: 44 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -21,51 +21,69 @@ import (
2121
// Compile-time interface check.
2222
var _ core.AuditLogger = (*AuditService)(nil)
2323

24-
// auditEventsDropped is a singleton counter registered once via sync.Once
25-
// to avoid duplicate-registration panics when multiple AuditService
26-
// instances are created (e.g. in tests).
24+
// auditEventsDropped is a singleton counter protected by a mutex so it can
25+
// be created before metrics are configured and registered later once a
26+
// registerer becomes available.
2727
//
2828
// The counter is only registered with Prometheus when a registerer is
2929
// explicitly provided via SetAuditMetricsRegisterer, so deployments with
3030
// metrics disabled do not leak collectors from the services layer.
3131
var (
3232
auditEventsDropped prometheus.Counter
33-
auditEventsDroppedOnce sync.Once
33+
auditEventsDroppedMu sync.Mutex
34+
auditEventsDroppedRegistered bool
3435
auditEventsDroppedRegisterer prometheus.Registerer
3536
)
3637

38+
// registerAuditDroppedCounterLocked attempts to register the existing counter
39+
// with the configured registerer. The caller must hold auditEventsDroppedMu.
40+
func registerAuditDroppedCounterLocked() {
41+
if auditEventsDropped == nil ||
42+
auditEventsDroppedRegisterer == nil ||
43+
auditEventsDroppedRegistered {
44+
return
45+
}
46+
if err := auditEventsDroppedRegisterer.Register(auditEventsDropped); err != nil {
47+
if existing, ok := err.(prometheus.AlreadyRegisteredError); ok {
48+
if c, ok := existing.ExistingCollector.(prometheus.Counter); ok {
49+
auditEventsDropped = c
50+
auditEventsDroppedRegistered = true
51+
return
52+
}
53+
}
54+
log.Printf("failed to register audit dropped-events counter: %v", err)
55+
return
56+
}
57+
auditEventsDroppedRegistered = true
58+
}
59+
3760
// SetAuditMetricsRegisterer configures the Prometheus registerer used by the
38-
// audit service. It must be called before any AuditService is created in order
39-
// for the dropped-events counter to be registered with Prometheus.
61+
// audit service. If the dropped-events counter was created before metrics
62+
// were configured, setting a non-nil registerer will register the existing
63+
// counter, ensuring late initialization (e.g. in tests) is not silently lost.
4064
func SetAuditMetricsRegisterer(registerer prometheus.Registerer) {
65+
auditEventsDroppedMu.Lock()
66+
defer auditEventsDroppedMu.Unlock()
67+
4168
auditEventsDroppedRegisterer = registerer
69+
registerAuditDroppedCounterLocked()
4270
}
4371

4472
func getAuditEventsDroppedCounter() prometheus.Counter {
45-
auditEventsDroppedOnce.Do(func() {
73+
auditEventsDroppedMu.Lock()
74+
defer auditEventsDroppedMu.Unlock()
75+
76+
if auditEventsDropped == nil {
4677
// Use a single fully-prefixed Name (no Namespace/Subsystem) to
4778
// match the convention used by metrics in internal/metrics.
48-
opts := prometheus.CounterOpts{
79+
auditEventsDropped = prometheus.NewCounter(prometheus.CounterOpts{
4980
Name: "audit_events_dropped_total",
5081
Help: "Total number of audit log events dropped due to a full buffer.",
51-
}
52-
counter := prometheus.NewCounter(opts)
53-
54-
if auditEventsDroppedRegisterer != nil {
55-
if err := auditEventsDroppedRegisterer.Register(counter); err != nil {
56-
if existing, ok := err.(prometheus.AlreadyRegisteredError); ok {
57-
if c, ok := existing.ExistingCollector.(prometheus.Counter); ok {
58-
auditEventsDropped = c
59-
return
60-
}
61-
}
62-
log.Printf("failed to register audit dropped-events counter: %v", err)
63-
}
64-
}
65-
// When no registerer is set, the counter still works in-memory but
66-
// is not exposed via the Prometheus /metrics endpoint.
67-
auditEventsDropped = counter
68-
})
82+
})
83+
}
84+
registerAuditDroppedCounterLocked()
85+
// When no registerer is set, the counter still works in-memory but
86+
// is not exposed via the Prometheus /metrics endpoint.
6987
return auditEventsDropped
7088
}
7189

0 commit comments

Comments
 (0)