33package cache
44
55import (
6+ "fmt"
67 "math"
78 "sync/atomic"
89 "time"
910
1011 "github.com/ccoveille/go-safecast/v2"
1112 "github.com/maypok86/otter/v2"
1213 "github.com/maypok86/otter/v2/stats"
14+ "github.com/prometheus/client_golang/prometheus"
1315 "github.com/rs/zerolog"
1416)
1517
16- func NewOtterCacheWithMetrics [K KeyString , V any ](name string , config * Config ) (Cache [K , V ], error ) {
17- cache , err := NewOtterCache [K , V ](name , config )
18- if err != nil {
19- return nil , err
20- }
21- // NOTE: this is the difference between `WithMetrics` and not -
22- // the counters are instantiated either way, but they're only registered
23- // in this variant.
24- mustRegisterCache (name , cache )
25- return cache , nil
26- }
18+ const (
19+ promNamespace = "spicedb"
20+ promSubsystem = "cache"
21+ )
2722
2823type valueAndCost [V any ] struct {
2924 value V
3025 cost uint32
3126}
3227
28+ // NewOtterCache creates an Otter-backed cache. It tracks its own metrics (see
29+ // Cache.GetMetrics) but does not export them; use NewOtterCacheWithMetrics to
30+ // also register those metrics with a Prometheus registerer.
3331func NewOtterCache [K KeyString , V any ](name string , config * Config ) (Cache [K , V ], error ) {
32+ return newOtterCache [K , V ](name , config )
33+ }
34+
35+ // NewOtterCacheWithMetrics creates an Otter-backed cache and registers its
36+ // metrics (labeled by name) with the given registerer. The metrics are
37+ // unregistered when the cache is Closed.
38+ func NewOtterCacheWithMetrics [K KeyString , V any ](registerer prometheus.Registerer , name string , config * Config ) (Cache [K , V ], error ) {
39+ cache , err := newOtterCache [K , V ](name , config )
40+ if err != nil {
41+ return nil , err
42+ }
43+ if err := cache .registerMetrics (registerer ); err != nil {
44+ return nil , err
45+ }
46+ return cache , nil
47+ }
48+
49+ func newOtterCache [K KeyString , V any ](name string , config * Config ) (* otterCache [K , V ], error ) {
3450 uintCost , err := safecast.Convert [uint64 ](config .MaxCost )
3551 if err != nil {
3652 return nil , err
@@ -50,10 +66,10 @@ func NewOtterCache[K KeyString, V any](name string, config *Config) (Cache[K, V]
5066
5167 cache , err := otter .New (opts )
5268 return & otterCache [K , V ]{
53- name ,
54- cache ,
55- otterMetrics {atomic.Uint64 {}, counter },
56- config .DefaultTTL ,
69+ name : name ,
70+ cache : cache ,
71+ metrics : otterMetrics {atomic.Uint64 {}, counter },
72+ ttl : config .DefaultTTL ,
5773 }, err
5874}
5975
@@ -62,6 +78,49 @@ type otterCache[K KeyString, V any] struct {
6278 cache * otter.Cache [string , valueAndCost [V ]]
6379 metrics otterMetrics
6480 ttl time.Duration
81+
82+ // registerer and collectors are set when metrics are registered (via
83+ // NewOtterCacheWithMetrics) and used to unregister them on Close.
84+ registerer prometheus.Registerer
85+ collectors []prometheus.Collector
86+ }
87+
88+ // registerMetrics registers this cache's metrics, labeled by its name, with the
89+ // given registerer. The metrics are read from the cache at scrape time. On any
90+ // registration failure, already-registered metrics are rolled back.
91+ func (wtc * otterCache [K , V ]) registerMetrics (registerer prometheus.Registerer ) error {
92+ labels := prometheus.Labels {"cache" : wtc .name }
93+ collectors := []prometheus.Collector {
94+ prometheus .NewCounterFunc (prometheus.CounterOpts {
95+ Namespace : promNamespace , Subsystem : promSubsystem , Name : "hits_total" ,
96+ Help : "Number of cache hits" , ConstLabels : labels ,
97+ }, func () float64 { return float64 (wtc .metrics .Hits ()) }),
98+ prometheus .NewCounterFunc (prometheus.CounterOpts {
99+ Namespace : promNamespace , Subsystem : promSubsystem , Name : "misses_total" ,
100+ Help : "Number of cache misses" , ConstLabels : labels ,
101+ }, func () float64 { return float64 (wtc .metrics .Misses ()) }),
102+ prometheus .NewCounterFunc (prometheus.CounterOpts { //nolint:promlinter // don't add _total
103+ Namespace : promNamespace , Subsystem : promSubsystem , Name : "cost_added_bytes" ,
104+ Help : "Cost of entries added to the cache" , ConstLabels : labels ,
105+ }, func () float64 { return float64 (wtc .metrics .CostAdded ()) }),
106+ prometheus .NewCounterFunc (prometheus.CounterOpts { //nolint:promlinter // don't add _total
107+ Namespace : promNamespace , Subsystem : promSubsystem , Name : "cost_evicted_bytes" ,
108+ Help : "Cost of entries evicted from the cache" , ConstLabels : labels ,
109+ }, func () float64 { return float64 (wtc .metrics .CostEvicted ()) }),
110+ }
111+
112+ for i , c := range collectors {
113+ if err := registerer .Register (c ); err != nil {
114+ for _ , registered := range collectors [:i ] {
115+ registerer .Unregister (registered )
116+ }
117+ return fmt .Errorf ("could not register metrics for cache %q: %w" , wtc .name , err )
118+ }
119+ }
120+
121+ wtc .registerer = registerer
122+ wtc .collectors = collectors
123+ return nil
65124}
66125
67126func (wtc * otterCache [K , V ]) GetTTL () time.Duration {
@@ -101,7 +160,12 @@ func (wtc *otterCache[K, V]) Wait() {}
101160func (wtc * otterCache [K , V ]) Close () {
102161 // Stops the pending goroutine that Otter spins off
103162 wtc .cache .StopAllGoroutines ()
104- unregisterCache (wtc .name )
163+
164+ // Unregister any metrics this cache registered with the registerer.
165+ for _ , c := range wtc .collectors {
166+ wtc .registerer .Unregister (c )
167+ }
168+ wtc .collectors = nil
105169}
106170
107171type otterMetrics struct {
0 commit comments