@@ -74,61 +74,124 @@ def __init__(
7474 self ._lock = threading .Lock ()
7575 self ._server : Optional [Any ] = None
7676 self ._server_thread : Optional [threading .Thread ] = None
77+
78+ # Track last-seen values for delta calculation
79+ self ._last_seen : Dict [str , Dict [str , int ]] = {}
7780
7881 # Try to import prometheus_client if requested
7982 self ._prom_client = None
8083 if use_prometheus_client and PROMETHEUS_CLIENT_AVAILABLE :
8184 self ._prom_client = prometheus_client
8285 self ._init_prometheus_metrics ()
86+ self ._setup_collector ()
8387
84- def _init_prometheus_metrics (self ) -> None :
85- """Initialize Prometheus metrics using prometheus_client ."""
88+ def _setup_collector (self ) -> None :
89+ """Set up a custom collector to pull metrics from registered functions ."""
8690 if not self ._prom_client :
8791 return
92+
93+ try :
94+ from prometheus_client .core import GaugeMetricFamily , CounterMetricFamily
95+ from prometheus_client import REGISTRY
96+ except (ImportError , AttributeError ):
97+ # If prometheus_client is not properly available, skip collector setup
98+ return
99+
100+ class CachierCollector :
101+ """Custom Prometheus collector that pulls metrics from registered functions."""
102+
103+ def __init__ (self , exporter ):
104+ self .exporter = exporter
105+
106+ def collect (self ):
107+ """Collect metrics from all registered functions."""
108+ with self .exporter ._lock :
109+ # Collect hits
110+ hits = CounterMetricFamily (
111+ 'cachier_cache_hits' ,
112+ 'Total cache hits' ,
113+ labels = ['function' ]
114+ )
115+
116+ # Collect misses
117+ misses = CounterMetricFamily (
118+ 'cachier_cache_misses' ,
119+ 'Total cache misses' ,
120+ labels = ['function' ]
121+ )
122+
123+ # Collect hit rate
124+ hit_rate = GaugeMetricFamily (
125+ 'cachier_cache_hit_rate' ,
126+ 'Cache hit rate percentage' ,
127+ labels = ['function' ]
128+ )
129+
130+ # Collect stale hits
131+ stale_hits = CounterMetricFamily (
132+ 'cachier_stale_hits' ,
133+ 'Total stale cache hits' ,
134+ labels = ['function' ]
135+ )
136+
137+ # Collect recalculations
138+ recalculations = CounterMetricFamily (
139+ 'cachier_recalculations' ,
140+ 'Total cache recalculations' ,
141+ labels = ['function' ]
142+ )
143+
144+ # Collect entry count
145+ entry_count = GaugeMetricFamily (
146+ 'cachier_entry_count' ,
147+ 'Current number of cache entries' ,
148+ labels = ['function' ]
149+ )
150+
151+ # Collect cache size
152+ cache_size = GaugeMetricFamily (
153+ 'cachier_cache_size_bytes' ,
154+ 'Total cache size in bytes' ,
155+ labels = ['function' ]
156+ )
157+
158+ for func_name , func in self .exporter ._registered_functions .items ():
159+ if not hasattr (func , 'metrics' ) or func .metrics is None :
160+ continue
161+
162+ stats = func .metrics .get_stats ()
163+
164+ hits .add_metric ([func_name ], stats .hits )
165+ misses .add_metric ([func_name ], stats .misses )
166+ hit_rate .add_metric ([func_name ], stats .hit_rate )
167+ stale_hits .add_metric ([func_name ], stats .stale_hits )
168+ recalculations .add_metric ([func_name ], stats .recalculations )
169+ entry_count .add_metric ([func_name ], stats .entry_count )
170+ cache_size .add_metric ([func_name ], stats .total_size_bytes )
171+
172+ yield hits
173+ yield misses
174+ yield hit_rate
175+ yield stale_hits
176+ yield recalculations
177+ yield entry_count
178+ yield cache_size
179+
180+ # Register the custom collector
181+ try :
182+ REGISTRY .register (CachierCollector (self ))
183+ except Exception :
184+ # If registration fails, continue without collector
185+ pass
88186
89- # Define Prometheus metrics
90- from prometheus_client import Counter , Gauge , Histogram
91-
92- self ._hits = Counter (
93- "cachier_cache_hits_total" ,
94- "Total number of cache hits" ,
95- ["function" ],
96- )
97- self ._misses = Counter (
98- "cachier_cache_misses_total" ,
99- "Total number of cache misses" ,
100- ["function" ],
101- )
102- self ._hit_rate = Gauge (
103- "cachier_cache_hit_rate" ,
104- "Cache hit rate percentage" ,
105- ["function" ],
106- )
107- self ._latency = Histogram (
108- "cachier_operation_latency_seconds" ,
109- "Cache operation latency in seconds" ,
110- ["function" ],
111- )
112- self ._stale_hits = Counter (
113- "cachier_stale_hits_total" ,
114- "Total number of stale cache hits" ,
115- ["function" ],
116- )
117- self ._recalculations = Counter (
118- "cachier_recalculations_total" ,
119- "Total number of cache recalculations" ,
120- ["function" ],
121- )
122- self ._entry_count = Gauge (
123- "cachier_entry_count" ,
124- "Current number of cache entries" ,
125- ["function" ],
126- )
127- self ._cache_size = Gauge (
128- "cachier_cache_size_bytes" ,
129- "Total cache size in bytes" ,
130- ["function" ],
131- )
187+ def _init_prometheus_metrics (self ) -> None :
188+ """Initialize Prometheus metrics using prometheus_client.
189+
190+ Note: With custom collector, we don't need to pre-define metrics.
191+ The collector will generate them dynamically at scrape time.
192+ """
193+ # Metrics are now handled by the custom collector in _setup_collector()
194+ pass
132195
133196 def register_function (self , func : Callable ) -> None :
134197 """Register a cached function for metrics export.
@@ -156,6 +219,10 @@ def register_function(self, func: Callable) -> None:
156219
157220 def export_metrics (self , func_name : str , metrics : Any ) -> None :
158221 """Export metrics for a specific function to Prometheus.
222+
223+ With custom collector mode, metrics are automatically pulled at scrape time.
224+ This method is kept for backward compatibility but is a no-op when using
225+ prometheus_client with custom collector.
159226
160227 Parameters
161228 ----------
@@ -165,21 +232,9 @@ def export_metrics(self, func_name: str, metrics: Any) -> None:
165232 Metrics snapshot to export
166233
167234 """
168- if not self ._prom_client :
169- return
170-
171- # Update Prometheus metrics
172- self ._hits .labels (function = func_name ).inc (metrics .hits )
173- self ._misses .labels (function = func_name ).inc (metrics .misses )
174- self ._hit_rate .labels (function = func_name ).set (metrics .hit_rate )
175- self ._stale_hits .labels (function = func_name ).inc (metrics .stale_hits )
176- self ._recalculations .labels (function = func_name ).inc (
177- metrics .recalculations
178- )
179- self ._entry_count .labels (function = func_name ).set (metrics .entry_count )
180- self ._cache_size .labels (function = func_name ).set (
181- metrics .total_size_bytes
182- )
235+ # With custom collector, metrics are pulled automatically at scrape time
236+ # No need to manually push metrics
237+ pass
183238
184239 def _generate_text_metrics (self ) -> str :
185240 """Generate Prometheus text format metrics.
0 commit comments