textfile_metrics/lib.rs
1// Copyright (c) Ted Kaplan. All Rights Reserved.
2// SPDX-License-Identifier: MIT
3
4//! Non-blocking Prometheus textfile metrics writer.
5//!
6//! This crate provides a production-ready metrics system that writes to
7//! Prometheus textfile format with a non-blocking, thread-safe design. Metrics
8//! are snapshot under lock, then written to disk after releasing the lock.
9//!
10//! # Features
11//!
12//! - **Non-blocking I/O pattern**: Takes snapshot under lock, drops lock, then
13//! writes
14//! - **Thread-safe**: Uses `DashMap` for lock-free metric updates
15//! - **Lazy configuration**: Reads `METRICS_TEXTFILE_PATH` env var (default:
16//! `/var/lib/node_exporter/textfile_collector/`)
17//! - **Prometheus format**: Writes valid `.prom` files
18//! - **Type-safe helpers**: `Counter` and `Gauge` abstractions with label
19//! support
20//!
21//! # Quick Start
22//!
23//! ```no_run
24//! use textfile_metrics::MetricsWriter;
25//!
26//! #[tokio::main]
27//! async fn main() -> anyhow::Result<()> {
28//! let metrics = MetricsWriter::new()?;
29//!
30//! // Increment a counter with labels
31//! metrics.counter("requests_total", vec![
32//! ("method".to_string(), "GET".to_string()),
33//! ("status".to_string(), "200".to_string()),
34//! ], 1.0)?;
35//!
36//! // Set a gauge value
37//! metrics.gauge("temperature_celsius", vec![
38//! ("location".to_string(), "warehouse_a".to_string()),
39//! ], 23.5)?;
40//!
41//! // Flush metrics to disk
42//! metrics.flush().await?;
43//!
44//! Ok(())
45//! }
46//! ```
47//!
48//! # Environment Variables
49//!
50//! - `METRICS_TEXTFILE_PATH`: Directory to write metrics files (default:
51//! `/var/lib/node_exporter/textfile_collector/`)
52//! - `METRICS_DEBUG`: Enable debug logging (optional)
53
54#![forbid(unsafe_code)]
55#![warn(missing_docs, unused_imports, dead_code)]
56
57mod errors;
58mod labels;
59mod metric;
60mod writer;
61
62pub use errors::{MetricsError, Result};
63pub use labels::Labels;
64pub use metric::{Counter, Gauge, MetricType};
65pub use writer::MetricsWriter;
66
67/// Pre-initialized global metrics writer.
68///
69/// # Example
70///
71/// ```ignore
72/// use textfile_metrics::GLOBAL_METRICS;
73///
74/// GLOBAL_METRICS.counter("requests_total", vec![], 1).ok();
75/// ```
76pub fn get_global_metrics() -> &'static tokio::sync::OnceCell<MetricsWriter> {
77 static GLOBAL: tokio::sync::OnceCell<MetricsWriter> = tokio::sync::OnceCell::const_new();
78 &GLOBAL
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84
85 #[test]
86 fn test_labels_formatting() {
87 let labels = Labels::from(vec![
88 ("method".to_string(), "GET".to_string()),
89 ("status".to_string(), "200".to_string()),
90 ]);
91
92 let formatted = labels.to_string();
93 assert!(formatted.contains("method=\"GET\""));
94 assert!(formatted.contains("status=\"200\""));
95 }
96
97 #[tokio::test]
98 async fn test_metrics_writer_creation() {
99 let writer = MetricsWriter::new();
100 assert!(writer.is_ok());
101 }
102
103 #[tokio::test]
104 async fn test_counter_increment() -> anyhow::Result<()> {
105 let metrics = MetricsWriter::new()?;
106 metrics.counter("test_counter", Vec::<(String, String)>::new(), 5.0)?;
107 metrics.counter("test_counter", Vec::<(String, String)>::new(), 3.0)?;
108 Ok(())
109 }
110
111 #[tokio::test]
112 async fn test_gauge_set() -> anyhow::Result<()> {
113 let metrics = MetricsWriter::new()?;
114 metrics.gauge("test_gauge", Vec::<(String, String)>::new(), 42.5)?;
115 Ok(())
116 }
117
118 #[test]
119 fn test_labels_sorting() {
120 let labels = Labels::from(vec![
121 ("z_label".to_string(), "last".to_string()),
122 ("a_label".to_string(), "first".to_string()),
123 ]);
124
125 let formatted = labels.to_string();
126 let a_pos = formatted.find("a_label").unwrap();
127 let z_pos = formatted.find("z_label").unwrap();
128 assert!(a_pos < z_pos, "Labels should be sorted");
129 }
130}