1use crate::{Cache, CacheStats, DatabasePool, PoolHealth};
57
58pub struct VectorCache;
60pub struct JwksCache;
61
62#[derive(Debug, Clone, Default)]
64pub struct VectorCacheStats {
65 data: std::collections::HashMap<String, f64>,
66}
67
68impl VectorCacheStats {
69 pub fn get(&self, key: &str) -> Option<&f64> {
70 self.data.get(key)
71 }
72}
73
74#[derive(Debug, Clone, Default)]
75pub struct VectorCacheMetrics {
76 pub hits: u64,
77 pub misses: u64,
78}
79
80impl VectorCacheMetrics {
81 pub fn hit_rate(&self) -> f64 {
82 let total = self.hits + self.misses;
83 if total == 0 {
84 0.0
85 } else {
86 self.hits as f64 / total as f64
87 }
88 }
89}
90
91#[derive(Debug, Clone, Default)]
92pub struct JwksCacheStats {
93 data: std::collections::HashMap<String, f64>,
94}
95
96impl JwksCacheStats {
97 pub fn get(&self, key: &str) -> Option<&f64> {
98 self.data.get(key)
99 }
100}
101
102#[derive(Debug, Clone, Default)]
103pub struct JwksCacheMetrics {
104 pub key_hits: u64,
105 pub key_misses: u64,
106}
107
108impl JwksCacheMetrics {
109 pub fn key_hit_rate(&self) -> f64 {
110 let total = self.key_hits + self.key_misses;
111 if total == 0 {
112 0.0
113 } else {
114 self.key_hits as f64 / total as f64
115 }
116 }
117}
118
119impl VectorCache {
120 pub fn stats(&self) -> VectorCacheStats {
121 VectorCacheStats::default()
122 }
123 pub fn metrics(&self) -> VectorCacheMetrics {
124 VectorCacheMetrics::default()
125 }
126}
127
128impl JwksCache {
129 pub fn stats(&self) -> JwksCacheStats {
130 JwksCacheStats::default()
131 }
132 pub fn metrics(&self) -> JwksCacheMetrics {
133 JwksCacheMetrics::default()
134 }
135}
136use std::collections::HashMap;
137use std::fmt::Write as FmtWrite;
138use std::sync::Arc;
139
140#[derive(Debug, Clone, Copy, PartialEq, Eq)]
142pub enum MetricsFormat {
143 Prometheus,
145 Json,
147 Flat,
149}
150
151#[derive(Debug, Clone, Copy, PartialEq, Eq)]
153pub enum MetricType {
154 Gauge,
155 Counter,
156 Histogram,
157}
158
159impl MetricType {
160 fn as_str(&self) -> &'static str {
161 match self {
162 MetricType::Gauge => "gauge",
163 MetricType::Counter => "counter",
164 MetricType::Histogram => "histogram",
165 }
166 }
167}
168
169#[derive(Debug, Clone)]
171pub struct Metric {
172 pub name: String,
174 pub metric_type: MetricType,
176 pub help: String,
178 pub value: f64,
180 pub labels: HashMap<String, String>,
182}
183
184impl Metric {
185 fn new(
186 name: impl Into<String>,
187 metric_type: MetricType,
188 help: impl Into<String>,
189 value: f64,
190 ) -> Self {
191 Self {
192 name: name.into(),
193 metric_type,
194 help: help.into(),
195 value,
196 labels: HashMap::new(),
197 }
198 }
199
200 fn with_label(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
201 self.labels.insert(key.into(), value.into());
202 self
203 }
204
205 fn to_prometheus(&self) -> String {
207 let mut output = String::new();
208
209 writeln!(&mut output, "# HELP {} {}", self.name, self.help).unwrap();
211 writeln!(
212 &mut output,
213 "# TYPE {} {}",
214 self.name,
215 self.metric_type.as_str()
216 )
217 .unwrap();
218
219 if self.labels.is_empty() {
221 writeln!(&mut output, "{} {}", self.name, self.value).unwrap();
222 } else {
223 let labels: Vec<String> = self
224 .labels
225 .iter()
226 .map(|(k, v)| format!("{k}=\"{v}\""))
227 .collect();
228 writeln!(
229 &mut output,
230 "{}{{{}}} {}",
231 self.name,
232 labels.join(","),
233 self.value
234 )
235 .unwrap();
236 }
237
238 output
239 }
240}
241
242pub struct MetricsExporter {
244 pool: DatabasePool,
245 cache: Option<Arc<Cache>>,
246 vector_cache: Option<Arc<VectorCache>>,
247 jwks_cache: Option<Arc<JwksCache>>,
248}
249
250impl MetricsExporter {
251 pub fn new(pool: DatabasePool) -> Self {
253 Self {
254 pool,
255 cache: None,
256 vector_cache: None,
257 jwks_cache: None,
258 }
259 }
260
261 pub fn with_cache(mut self, cache: Arc<Cache>) -> Self {
263 self.cache = Some(cache);
264 self
265 }
266
267 pub fn with_vector_cache(mut self, vector_cache: Arc<VectorCache>) -> Self {
269 self.vector_cache = Some(vector_cache);
270 self
271 }
272
273 pub fn with_jwks_cache(mut self, jwks_cache: Arc<JwksCache>) -> Self {
275 self.jwks_cache = Some(jwks_cache);
276 self
277 }
278
279 pub fn collect_metrics(&self) -> Vec<Metric> {
281 let mut metrics = Vec::new();
282
283 metrics.extend(self.collect_pool_metrics());
285
286 if let Some(ref cache) = self.cache {
288 metrics.extend(self.collect_cache_metrics(cache));
289 }
290
291 if let Some(ref vector_cache) = self.vector_cache {
293 metrics.extend(self.collect_vector_cache_metrics(vector_cache));
294 }
295
296 if let Some(ref jwks_cache) = self.jwks_cache {
298 metrics.extend(self.collect_jwks_cache_metrics(jwks_cache));
299 }
300
301 metrics
302 }
303
304 fn collect_pool_metrics(&self) -> Vec<Metric> {
306 let stats = self.pool.stats();
307 let health = self.pool.health_status();
308
309 vec![
310 Metric::new(
311 "oxify_storage_pool_size",
312 MetricType::Gauge,
313 "Current size of the connection pool",
314 f64::from(stats.size),
315 ),
316 Metric::new(
317 "oxify_storage_pool_idle",
318 MetricType::Gauge,
319 "Number of idle connections",
320 stats.num_idle as f64,
321 ),
322 Metric::new(
323 "oxify_storage_pool_active",
324 MetricType::Gauge,
325 "Number of active connections",
326 stats.active_connections() as f64,
327 ),
328 Metric::new(
329 "oxify_storage_pool_max",
330 MetricType::Gauge,
331 "Maximum connections allowed",
332 f64::from(stats.max_connections),
333 ),
334 Metric::new(
335 "oxify_storage_pool_utilization",
336 MetricType::Gauge,
337 "Pool utilization (0.0 to 1.0)",
338 stats.utilization(),
339 ),
340 Metric::new(
341 "oxify_storage_pool_health",
342 MetricType::Gauge,
343 "Pool health status (0=critical, 1=degraded, 2=healthy)",
344 match health {
345 PoolHealth::Critical => 0.0,
346 PoolHealth::Degraded => 1.0,
347 PoolHealth::Healthy => 2.0,
348 },
349 ),
350 ]
351 }
352
353 fn collect_cache_metrics(&self, cache: &Cache) -> Vec<Metric> {
355 let stats = cache.stats();
356 let metrics_data = cache.metrics();
357
358 let workflow_stats = stats.get("workflows").cloned().unwrap_or(CacheStats {
359 size: 0,
360 capacity: 0,
361 valid_entries: 0,
362 expired_entries: 0,
363 total_accesses: 0,
364 });
365
366 vec![
367 Metric::new(
368 "oxify_storage_cache_size",
369 MetricType::Gauge,
370 "Current cache size",
371 workflow_stats.size as f64,
372 )
373 .with_label("cache_type", "workflow"),
374 Metric::new(
375 "oxify_storage_cache_hits",
376 MetricType::Counter,
377 "Cache hit count",
378 metrics_data.workflow_hits as f64,
379 )
380 .with_label("cache_type", "workflow"),
381 Metric::new(
382 "oxify_storage_cache_misses",
383 MetricType::Counter,
384 "Cache miss count",
385 metrics_data.workflow_misses as f64,
386 )
387 .with_label("cache_type", "workflow"),
388 Metric::new(
389 "oxify_storage_cache_hit_rate",
390 MetricType::Gauge,
391 "Cache hit rate (0.0 to 1.0)",
392 metrics_data.overall_hit_rate(),
393 )
394 .with_label("cache_type", "workflow"),
395 Metric::new(
396 "oxify_storage_cache_evictions",
397 MetricType::Counter,
398 "Cache eviction count",
399 metrics_data.evictions as f64,
400 )
401 .with_label("cache_type", "workflow"),
402 ]
403 }
404
405 fn collect_vector_cache_metrics(&self, vector_cache: &VectorCache) -> Vec<Metric> {
407 let stats = vector_cache.stats();
408 let metrics_data = vector_cache.metrics();
409
410 vec![
411 Metric::new(
412 "oxify_storage_cache_size",
413 MetricType::Gauge,
414 "Current cache size",
415 stats.get("size").copied().unwrap_or(0.0),
416 )
417 .with_label("cache_type", "vector"),
418 Metric::new(
419 "oxify_storage_cache_hits",
420 MetricType::Counter,
421 "Cache hit count",
422 metrics_data.hits as f64,
423 )
424 .with_label("cache_type", "vector"),
425 Metric::new(
426 "oxify_storage_cache_misses",
427 MetricType::Counter,
428 "Cache miss count",
429 metrics_data.misses as f64,
430 )
431 .with_label("cache_type", "vector"),
432 Metric::new(
433 "oxify_storage_cache_hit_rate",
434 MetricType::Gauge,
435 "Cache hit rate (0.0 to 1.0)",
436 metrics_data.hit_rate(),
437 )
438 .with_label("cache_type", "vector"),
439 ]
440 }
441
442 fn collect_jwks_cache_metrics(&self, jwks_cache: &JwksCache) -> Vec<Metric> {
444 let stats = jwks_cache.stats();
445 let metrics_data = jwks_cache.metrics();
446
447 vec![
448 Metric::new(
449 "oxify_storage_cache_size",
450 MetricType::Gauge,
451 "Current cache size",
452 stats.get("issuers").copied().unwrap_or(0.0),
453 )
454 .with_label("cache_type", "jwks"),
455 Metric::new(
456 "oxify_storage_cache_hits",
457 MetricType::Counter,
458 "Cache hit count",
459 metrics_data.key_hits as f64,
460 )
461 .with_label("cache_type", "jwks"),
462 Metric::new(
463 "oxify_storage_cache_misses",
464 MetricType::Counter,
465 "Cache miss count",
466 metrics_data.key_misses as f64,
467 )
468 .with_label("cache_type", "jwks"),
469 Metric::new(
470 "oxify_storage_cache_hit_rate",
471 MetricType::Gauge,
472 "Cache hit rate (0.0 to 1.0)",
473 metrics_data.key_hit_rate(),
474 )
475 .with_label("cache_type", "jwks"),
476 ]
477 }
478
479 pub fn export(&self, format: MetricsFormat) -> String {
481 let metrics = self.collect_metrics();
482
483 match format {
484 MetricsFormat::Prometheus => {
485 let mut output = String::new();
486 for metric in metrics {
487 output.push_str(&metric.to_prometheus());
488 }
489 output
490 }
491 MetricsFormat::Json => {
492 let json: Vec<serde_json::Value> = metrics
493 .iter()
494 .map(|m| {
495 serde_json::json!({
496 "name": m.name,
497 "type": m.metric_type.as_str(),
498 "help": m.help,
499 "value": m.value,
500 "labels": m.labels,
501 })
502 })
503 .collect();
504 serde_json::to_string_pretty(&json).unwrap()
505 }
506 MetricsFormat::Flat => {
507 let mut output = String::new();
508 for metric in metrics {
509 writeln!(&mut output, "{}: {}", metric.name, metric.value).unwrap();
510 }
511 output
512 }
513 }
514 }
515
516 pub fn export_flat(&self) -> HashMap<String, f64> {
518 let metrics = self.collect_metrics();
519 let mut flat = HashMap::new();
520
521 for metric in metrics {
522 if metric.labels.is_empty() {
523 flat.insert(metric.name, metric.value);
524 } else {
525 let labels: Vec<String> = metric
526 .labels
527 .iter()
528 .map(|(k, v)| format!("{k}_{v}"))
529 .collect();
530 let key = format!("{}_{}", metric.name, labels.join("_"));
531 flat.insert(key, metric.value);
532 }
533 }
534
535 flat
536 }
537}
538
539#[cfg(test)]
540mod tests {
541 use super::*;
542
543 #[test]
544 fn test_metric_creation() {
545 let metric = Metric::new("test_metric", MetricType::Gauge, "Test metric", 42.0)
546 .with_label("env", "test")
547 .with_label("version", "1.0");
548
549 assert_eq!(metric.name, "test_metric");
550 assert_eq!(metric.value, 42.0);
551 assert_eq!(metric.labels.len(), 2);
552 }
553
554 #[test]
555 fn test_prometheus_format_no_labels() {
556 let metric = Metric::new("test_gauge", MetricType::Gauge, "A test gauge", 123.45);
557
558 let output = metric.to_prometheus();
559 assert!(output.contains("# HELP test_gauge A test gauge"));
560 assert!(output.contains("# TYPE test_gauge gauge"));
561 assert!(output.contains("test_gauge 123.45"));
562 }
563
564 #[test]
565 fn test_prometheus_format_with_labels() {
566 let metric = Metric::new("test_counter", MetricType::Counter, "A test counter", 456.0)
567 .with_label("env", "prod")
568 .with_label("region", "us-east");
569
570 let output = metric.to_prometheus();
571 assert!(output.contains("# HELP test_counter A test counter"));
572 assert!(output.contains("# TYPE test_counter counter"));
573 assert!(output.contains("test_counter{"));
574 assert!(output.contains("456"));
575 }
576
577 #[test]
578 fn test_metric_type_as_str() {
579 assert_eq!(MetricType::Gauge.as_str(), "gauge");
580 assert_eq!(MetricType::Counter.as_str(), "counter");
581 assert_eq!(MetricType::Histogram.as_str(), "histogram");
582 }
583}