prometheus_derive/
lib.rs

1//! # Metrics - Prometheus metrics with automatic registration
2//!
3//! This crate provides easy-to-use Prometheus metrics with automatic registration
4//! It uses procedural macros to generate type-safe
5//! metrics with minimal boilerplate.
6//!
7//! ## Example
8//!
9//! ```rust,ignore
10//! use prometheus_derive::{prometheus_metrics, GLOBAL_REGISTRY};
11//! use prometheus_client::metrics::{counter::Counter, gauge::Gauge};
12//!
13//! prometheus_metrics! {
14//!     /// Total number of HTTP requests received
15//!     #[labels(method = String, status = u16)]
16//!     static HTTP_REQUESTS_TOTAL: Counter;
17//!
18//!     /// Current number of active connections
19//!     static ACTIVE_CONNECTIONS: Gauge;
20//! }
21//!
22//! // Use the metrics - they automatically register when first accessed
23//! HTTP_REQUESTS_TOTAL
24//!     .get_or_create(&HttpRequestsTotalLabels {
25//!         method: "GET".to_string(),
26//!         status: 200,
27//!     })
28//!     .inc();
29//!
30//! ACTIVE_CONNECTIONS.set(42);
31//!
32//! // Export metrics for Prometheus scraping
33//! use prometheus_client::encoding::text::encode;
34//! let mut buffer = String::new();
35//! let registry = GLOBAL_REGISTRY.read().unwrap();
36//! encode(&mut buffer, &registry).unwrap();
37//! println!("{}", buffer);
38//! ```
39
40use prometheus_client::registry::Registry;
41use std::sync::{LazyLock, RwLock};
42
43/// Global registry for all Prometheus metrics.
44///
45/// This registry is shared across all `prometheus_metrics!` blocks in your application.
46/// To register metrics with this registry, use the `register_metrics!` macro or
47/// manually register them using the `register` method.
48pub static GLOBAL_REGISTRY: LazyLock<RwLock<Registry>> =
49    LazyLock::new(|| RwLock::new(Registry::default()));
50
51/// Register a metric with the global registry.
52///
53/// This is typically called automatically when metrics are first accessed.
54pub fn register_metric<M>(name: &str, help: &str, metric: M)
55where
56    M: prometheus_client::encoding::EncodeMetric + Clone + std::fmt::Debug + Send + Sync + 'static,
57{
58    GLOBAL_REGISTRY
59        .write()
60        .unwrap()
61        .register(name, help, metric);
62}
63
64// Re-export the macro and common types for convenience
65pub use prometheus_client;
66pub use prometheus_derive_macros::prometheus_metrics;
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71    use prometheus_client::encoding::text::encode;
72    use prometheus_client::metrics::counter::Counter;
73    use prometheus_client::metrics::gauge::Gauge;
74    use std::sync::LazyLock;
75
76    // Define test metrics manually to avoid circular imports
77    pub static TEST_COUNTER: LazyLock<Counter> = LazyLock::new(|| Counter::default());
78
79    #[derive(Clone, Debug, Hash, PartialEq, Eq, prometheus_client::encoding::EncodeLabelSet)]
80    pub struct TestLabeledCounterLabels {
81        pub method: String,
82    }
83
84    pub static TEST_LABELED_COUNTER: LazyLock<
85        prometheus_client::metrics::family::Family<TestLabeledCounterLabels, Counter>,
86    > = LazyLock::new(|| prometheus_client::metrics::family::Family::default());
87
88    #[test]
89    fn test_metrics_without_registration() {
90        // Create a fresh registry for this test
91        let test_registry = Registry::default();
92        let mut buffer = String::new();
93        encode(&mut buffer, &test_registry).unwrap();
94
95        // This demonstrates the issue - metrics don't appear without registration
96        assert!(
97            !buffer.contains("TEST_COUNTER"),
98            "Metrics should not appear without registration"
99        );
100        assert!(
101            !buffer.contains("TEST_LABELED_COUNTER"),
102            "Metrics should not appear without registration"
103        );
104
105        // Even after using the metrics, they still don't appear without registration
106        TEST_COUNTER.inc();
107        TEST_LABELED_COUNTER
108            .get_or_create(&TestLabeledCounterLabels {
109                method: "GET".to_string(),
110            })
111            .inc();
112
113        // Re-encode the empty registry - still no metrics
114        let mut buffer2 = String::new();
115        encode(&mut buffer2, &test_registry).unwrap();
116        assert!(
117            !buffer2.contains("TEST_COUNTER"),
118            "Metrics should not appear without registration"
119        );
120        assert!(
121            !buffer2.contains("TEST_LABELED_COUNTER"),
122            "Metrics should not appear without registration"
123        );
124    }
125
126    #[test]
127    fn test_global_registry_with_manual_registration() {
128        // Register metrics manually with the global registry
129        register_metric("test_counter_total", "Test counter", TEST_COUNTER.clone());
130        register_metric(
131            "test_labeled_counter_total",
132            "Test counter with labels",
133            TEST_LABELED_COUNTER.clone(),
134        );
135
136        // Use the metrics
137        TEST_COUNTER.inc();
138        TEST_LABELED_COUNTER
139            .get_or_create(&TestLabeledCounterLabels {
140                method: "GET".to_string(),
141            })
142            .inc();
143
144        // Export metrics
145        let registry = GLOBAL_REGISTRY.read().unwrap();
146        let mut buffer = String::new();
147        encode(&mut buffer, &registry).unwrap();
148
149        // Check that both metrics are present
150        assert!(buffer.contains("test_counter_total"));
151        assert!(buffer.contains("test_labeled_counter_total"));
152        assert!(buffer.contains("method=\"GET\""));
153    }
154
155    #[test]
156    fn test_global_registry_with_generated_registration() {
157        // Register metrics using the manual approach (since we can't use generated function in same crate)
158        register_metric(
159            "TEST_COUNTER",
160            "Generated metric: TEST_COUNTER",
161            TEST_COUNTER.clone(),
162        );
163        register_metric(
164            "TEST_LABELED_COUNTER",
165            "Generated metric: TEST_LABELED_COUNTER",
166            TEST_LABELED_COUNTER.clone(),
167        );
168
169        // Use the metrics
170        TEST_COUNTER.inc();
171        TEST_LABELED_COUNTER
172            .get_or_create(&TestLabeledCounterLabels {
173                method: "POST".to_string(),
174            })
175            .inc();
176
177        // Export metrics
178        let registry = GLOBAL_REGISTRY.read().unwrap();
179        let mut buffer = String::new();
180        encode(&mut buffer, &registry).unwrap();
181
182        // Check that both metrics are present
183        assert!(buffer.contains("TEST_COUNTER"));
184        assert!(buffer.contains("TEST_LABELED_COUNTER"));
185        assert!(buffer.contains("method=\"POST\""));
186    }
187
188    #[test]
189    fn test_multi_module_metrics_in_global_registry() {
190        use prometheus_client::metrics::family::Family;
191
192        // Simulate metrics from different modules with auto-registration
193        // Module A metrics
194        static MODULE_A_REQUESTS: LazyLock<Family<ModuleARequestsLabels, Counter>> =
195            LazyLock::new(|| {
196                let family = Family::default();
197                if let Ok(mut registry) = GLOBAL_REGISTRY.write() {
198                    registry.register(
199                        "MODULE_A_REQUESTS",
200                        "HTTP requests from module A",
201                        family.clone(),
202                    );
203                }
204                family
205            });
206        static MODULE_A_MEMORY: LazyLock<Gauge> = LazyLock::new(|| {
207            let gauge = Gauge::default();
208            if let Ok(mut registry) = GLOBAL_REGISTRY.write() {
209                registry.register("MODULE_A_MEMORY", "Memory usage in module A", gauge.clone());
210            }
211            gauge
212        });
213
214        // Module B metrics
215        static MODULE_B_DB_CONNECTIONS: LazyLock<Family<ModuleBDbConnectionsLabels, Counter>> =
216            LazyLock::new(|| {
217                let family = Family::default();
218                if let Ok(mut registry) = GLOBAL_REGISTRY.write() {
219                    registry.register(
220                        "MODULE_B_DB_CONNECTIONS",
221                        "Database connections from module B",
222                        family.clone(),
223                    );
224                }
225                family
226            });
227        static MODULE_B_CACHE_HIT_RATIO: LazyLock<Gauge> = LazyLock::new(|| {
228            let gauge = Gauge::default();
229            if let Ok(mut registry) = GLOBAL_REGISTRY.write() {
230                registry.register(
231                    "MODULE_B_CACHE_HIT_RATIO",
232                    "Cache hit ratio in module B",
233                    gauge.clone(),
234                );
235            }
236            gauge
237        });
238
239        // Define label structs
240        #[derive(
241            Clone, Debug, Hash, PartialEq, Eq, prometheus_client::encoding::EncodeLabelSet,
242        )]
243        struct ModuleARequestsLabels {
244            endpoint: String,
245        }
246
247        #[derive(
248            Clone, Debug, Hash, PartialEq, Eq, prometheus_client::encoding::EncodeLabelSet,
249        )]
250        struct ModuleBDbConnectionsLabels {
251            db_name: String,
252            pool: String,
253        }
254
255        // Use metrics from both "modules" - they auto-register on first access
256        MODULE_A_REQUESTS
257            .get_or_create(&ModuleARequestsLabels {
258                endpoint: "/api/users".to_string(),
259            })
260            .inc();
261        MODULE_A_MEMORY.set(1024);
262
263        MODULE_B_DB_CONNECTIONS
264            .get_or_create(&ModuleBDbConnectionsLabels {
265                db_name: "users".to_string(),
266                pool: "primary".to_string(),
267            })
268            .inc();
269        MODULE_B_CACHE_HIT_RATIO.set(85);
270
271        // Export metrics from global registry
272        let registry = GLOBAL_REGISTRY.read().unwrap();
273        let mut buffer = String::new();
274        encode(&mut buffer, &registry).unwrap();
275
276        // Verify metrics from module A are present
277        assert!(
278            buffer.contains("MODULE_A_REQUESTS"),
279            "Module A requests metric should be in global registry"
280        );
281        assert!(
282            buffer.contains("MODULE_A_MEMORY"),
283            "Module A memory metric should be in global registry"
284        );
285        assert!(
286            buffer.contains("endpoint=\"/api/users\""),
287            "Module A labels should be present"
288        );
289
290        // Verify metrics from module B are present
291        assert!(
292            buffer.contains("MODULE_B_DB_CONNECTIONS"),
293            "Module B DB connections metric should be in global registry"
294        );
295        assert!(
296            buffer.contains("MODULE_B_CACHE_HIT_RATIO"),
297            "Module B cache ratio metric should be in global registry"
298        );
299        assert!(
300            buffer.contains("db_name=\"users\""),
301            "Module B labels should be present"
302        );
303        assert!(
304            buffer.contains("pool=\"primary\""),
305            "Module B pool label should be present"
306        );
307
308        // Verify both modules' metrics coexist
309        let module_a_count = buffer.matches("MODULE_A").count();
310        let module_b_count = buffer.matches("MODULE_B").count();
311        assert!(
312            module_a_count >= 2,
313            "Should have at least 2 module A metrics"
314        );
315        assert!(
316            module_b_count >= 2,
317            "Should have at least 2 module B metrics"
318        );
319
320        println!("Multi-module test successful! Registry contains metrics from both modules:");
321        println!("{}", buffer);
322    }
323
324    #[test]
325    fn test_web_server_pattern() {
326        // Test the exact pattern used in web-server example
327
328        // Main module metrics (simulated)
329        static HTTP_REQUESTS_TOTAL: LazyLock<
330            prometheus_client::metrics::family::Family<HttpRequestsLabels, Counter>,
331        > = LazyLock::new(|| {
332            let family = prometheus_client::metrics::family::Family::default();
333            if let Ok(mut registry) = GLOBAL_REGISTRY.write() {
334                registry.register("HTTP_REQUESTS_TOTAL", "Total HTTP requests", family.clone());
335            }
336            family
337        });
338
339        // Other module metrics (simulated)
340        static USERS_TOTAL: LazyLock<Counter> = LazyLock::new(|| {
341            let counter = Counter::default();
342            if let Ok(mut registry) = GLOBAL_REGISTRY.write() {
343                registry.register("USERS_TOTAL", "Total number of users", counter.clone());
344            }
345            counter
346        });
347
348        #[derive(
349            Clone, Debug, Hash, PartialEq, Eq, prometheus_client::encoding::EncodeLabelSet,
350        )]
351        struct HttpRequestsLabels {
352            method: String,
353            endpoint: String,
354            status: u16,
355        }
356
357        // Simulate web server usage pattern - metrics are used directly without manual registration
358        HTTP_REQUESTS_TOTAL
359            .get_or_create(&HttpRequestsLabels {
360                method: "GET".to_string(),
361                endpoint: "/".to_string(),
362                status: 200,
363            })
364            .inc();
365
366        USERS_TOTAL.inc(); // This is used in track_metrics function
367
368        // Export to verify both metrics appear
369        let registry = GLOBAL_REGISTRY.read().unwrap();
370        let mut buffer = String::new();
371        encode(&mut buffer, &registry).unwrap();
372
373        println!("Web server pattern test - Registry contents:");
374        println!("{}", buffer);
375
376        // Verify both main and other_mod metrics are present
377        assert!(
378            buffer.contains("HTTP_REQUESTS_TOTAL"),
379            "Main module metric should be present"
380        );
381        assert!(
382            buffer.contains("USERS_TOTAL"),
383            "Other module metric should be present"
384        );
385        assert!(
386            buffer.contains("method=\"GET\""),
387            "HTTP request labels should be present"
388        );
389    }
390}