dmsc/observability/prometheus.rs
1//! Copyright © 2025-2026 Wenze Wei. All Rights Reserved.
2//!
3//! This file is part of DMSC.
4//! The DMSC project belongs to the Dunimd Team.
5//!
6//! Licensed under the Apache License, Version 2.0 (the "License");
7//! You may not use this file except in compliance with the License.
8//! You may obtain a copy of the License at
9//!
10//! http://www.apache.org/licenses/LICENSE-2.0
11//!
12//! Unless required by applicable law or agreed to in writing, software
13//! distributed under the License is distributed on an "AS IS" BASIS,
14//! WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15//! See the License for the specific language governing permissions and
16//! limitations under the License.
17
18#![allow(non_snake_case)]
19
20//! # Prometheus Exporter
21//!
22//! This module provides a Prometheus exporter implementation for the DMSC framework. It allows
23//! registering and managing Prometheus metrics (counters, gauges, histograms) and generating
24//! Grafana dashboards from these metrics.
25//!
26//! ## Key Components
27//!
28//! - **DMSCPrometheusExporter**: Main exporter class for managing Prometheus metrics
29//!
30//! ## Design Principles
31//!
32//! 1. **Prometheus Integration**: Uses the official prometheus crate for metric collection
33//! 2. **Thread Safety**: Uses Arc and RwLock for safe concurrent access
34//! 3. **Multiple Metric Types**: Supports Counter, Gauge, and Histogram metrics
35//! 4. **Grafana Integration**: Provides methods to generate Grafana dashboards and panels
36//! 5. **Easy to Use**: Simple API for registering and updating metrics
37//! 6. **Text Encoding**: Exports metrics in Prometheus text format
38//! 7. **Registry Management**: Maintains its own Prometheus registry
39//! 8. **Error Handling**: Comprehensive error handling with DMSCResult
40//!
41//! ## Usage
42//!
43//! ```rust
44//! use dmsc::prelude::*;
45//!
46//! fn example() -> DMSCResult<()> {
47//! // Create a new Prometheus exporter
48//! let exporter = DMSCPrometheusExporter::new()?;
49//!
50//! // Register a counter metric
51//! exporter.register_counter("http_requests_total", "Total number of HTTP requests")?;
52//!
53//! // Register a gauge metric
54//! exporter.register_gauge("active_connections", "Number of active connections")?;
55//!
56//! // Register a histogram metric
57//! exporter.register_histogram(
58//! "response_time_seconds",
59//! "Response time in seconds",
60//! vec![0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0]
61//! )?;
62//!
63//! // Update metrics
64//! exporter.increment_counter("http_requests_total", 1.0)?;
65//! exporter.set_gauge("active_connections", 10.0)?;
66//! exporter.observe_histogram("response_time_seconds", 0.123)?;
67//!
68//! // Render metrics in Prometheus format
69//! let metrics_text = exporter.render()?;
70//! println!("Prometheus metrics:\n{}", metrics_text);
71//!
72//! // Generate a Grafana dashboard
73//! let dashboard = exporter.generate_default_dashboard()?;
74//! let dashboard_json = dashboard.to_json()?;
75//! println!("Grafana dashboard JSON:\n{}", dashboard_json);
76//!
77//! Ok(())
78//! }
79//! ```
80
81use std::collections::HashMap;
82use std::sync::{Arc, RwLock};
83#[cfg(feature = "observability")]
84use prometheus::{Counter, Gauge, Histogram, Registry, Encoder, TextEncoder};
85use crate::core::DMSCResult;
86use crate::core::lock::RwLockExtensions;
87
88/// Prometheus exporter for managing metrics and generating Grafana dashboards.
89///
90/// This struct provides methods for registering and updating Prometheus metrics,
91/// as well as generating Grafana dashboards from these metrics.
92#[allow(dead_code)]
93#[derive(Debug, Clone)]
94pub struct DMSCPrometheusExporter {
95 /// Prometheus registry for managing metrics
96 registry: Arc<Registry>,
97 /// Map of registered counter metrics
98 counters: Arc<RwLock<HashMap<String, Counter>>>,
99 /// Map of registered gauge metrics
100 gauges: Arc<RwLock<HashMap<String, Gauge>>>,
101 /// Map of registered histogram metrics
102 histograms: Arc<RwLock<HashMap<String, Histogram>>>,
103}
104
105#[allow(dead_code)]
106impl DMSCPrometheusExporter {
107 /// Creates a new Prometheus exporter instance.
108 ///
109 /// # Returns
110 ///
111 /// A new DMSCPrometheusExporter instance wrapped in DMSCResult
112 pub fn new() -> DMSCResult<Self> {
113 let registry = Arc::new(Registry::new());
114
115 Ok(DMSCPrometheusExporter {
116 registry: registry.clone(),
117 counters: Arc::new(RwLock::new(HashMap::new())),
118 gauges: Arc::new(RwLock::new(HashMap::new())),
119 histograms: Arc::new(RwLock::new(HashMap::new())),
120 })
121 }
122
123 /// Registers a new counter metric.
124 ///
125 /// # Parameters
126 ///
127 /// - `name`: The name of the counter metric
128 /// - `help`: Help text describing the counter
129 ///
130 /// # Returns
131 ///
132 /// DMSCResult indicating success or failure
133 pub fn register_counter(&self, name: &str, help: &str) -> DMSCResult<()> {
134 let counter = Counter::new(name, help)?;
135 self.registry.register(Box::new(counter.clone()))?;
136
137 let mut counters = self.counters.write_safe("counters for register")?;
138 counters.insert(name.to_string(), counter);
139
140 Ok(())
141 }
142
143 /// Increments a counter metric by the specified value.
144 ///
145 /// # Parameters
146 ///
147 /// - `name`: The name of the counter metric
148 /// - `value`: The value to increment by
149 ///
150 /// # Returns
151 ///
152 /// DMSCResult indicating success or failure
153 pub fn increment_counter(&self, name: &str, value: f64) -> DMSCResult<()> {
154 let counters = self.counters.read_safe("counters for increment")?;
155 if let Some(counter) = counters.get(name) {
156 counter.inc_by(value);
157 Ok(())
158 } else {
159 Err(crate::core::DMSCError::Io(format!("Counter {name} not found")))
160 }
161 }
162
163 /// Registers a new gauge metric.
164 ///
165 /// # Parameters
166 ///
167 /// - `name`: The name of the gauge metric
168 /// - `help`: Help text describing the gauge
169 ///
170 /// # Returns
171 ///
172 /// DMSCResult indicating success or failure
173 pub fn register_gauge(&self, name: &str, help: &str) -> DMSCResult<()> {
174 let gauge = Gauge::new(name, help)?;
175 self.registry.register(Box::new(gauge.clone()))?;
176
177 let mut gauges = self.gauges.write_safe("gauges for register")?;
178 gauges.insert(name.to_string(), gauge);
179
180 Ok(())
181 }
182
183 /// Sets a gauge metric to the specified value.
184 ///
185 /// # Parameters
186 ///
187 /// - `name`: The name of the gauge metric
188 /// - `value`: The value to set
189 ///
190 /// # Returns
191 ///
192 /// DMSCResult indicating success or failure
193 pub fn set_gauge(&self, name: &str, value: f64) -> DMSCResult<()> {
194 let gauges = self.gauges.read_safe("gauges for set")?;
195 if let Some(gauge) = gauges.get(name) {
196 gauge.set(value);
197 Ok(())
198 } else {
199 Err(crate::core::DMSCError::Io(format!("Gauge {name} not found")))
200 }
201 }
202
203 /// Registers a new histogram metric.
204 ///
205 /// # Parameters
206 ///
207 /// - `name`: The name of the histogram metric
208 /// - `help`: Help text describing the histogram
209 /// - `buckets`: The histogram buckets
210 ///
211 /// # Returns
212 ///
213 /// DMSCResult indicating success or failure
214 pub fn register_histogram(&self, name: &str, help: &str, buckets: Vec<f64>) -> DMSCResult<()> {
215 let histogram = Histogram::with_opts(prometheus::HistogramOpts::new(name, help).buckets(buckets))?;
216 self.registry.register(Box::new(histogram.clone()))?;
217
218 let mut histograms = self.histograms.write_safe("histograms for register")?;
219 histograms.insert(name.to_string(), histogram);
220
221 Ok(())
222 }
223
224 /// Observes a value in a histogram metric.
225 ///
226 /// # Parameters
227 ///
228 /// - `name`: The name of the histogram metric
229 /// - `value`: The value to observe
230 ///
231 /// # Returns
232 ///
233 /// DMSCResult indicating success or failure
234 pub fn observe_histogram(&self, name: &str, value: f64) -> DMSCResult<()> {
235 let histograms = self.histograms.read_safe("histograms for observe")?;
236 if let Some(histogram) = histograms.get(name) {
237 histogram.observe(value);
238 Ok(())
239 } else {
240 Err(crate::core::DMSCError::Io(format!("Histogram {name} not found")))
241 }
242 }
243
244 /// Renders all metrics in Prometheus text format.
245 ///
246 /// # Returns
247 ///
248 /// A string containing all metrics in Prometheus text format wrapped in DMSCResult
249 pub fn render(&self) -> DMSCResult<String> {
250 let encoder = TextEncoder::new();
251 let metric_families = self.registry.gather();
252 let mut buffer = Vec::new();
253 encoder.encode(&metric_families, &mut buffer)?;
254
255 String::from_utf8(buffer).map_err(|e| crate::core::DMSCError::Serde(format!("Invalid UTF-8 in Prometheus output: {}", e)))
256 }
257
258 /// Adds a counter panel to a Grafana dashboard.
259 ///
260 /// # Parameters
261 ///
262 /// - `dashboard`: The Grafana dashboard to add the panel to
263 /// - `title`: The title of the panel
264 /// - `query`: The Prometheus query for the panel
265 ///
266 /// # Returns
267 ///
268 /// DMSCResult indicating success or failure
269 pub fn add_counter_panel(&self, dashboard: &mut crate::observability::grafana::DMSCGrafanaDashboard, title: &str, query: &str) -> DMSCResult<()> {
270 let mut generator = crate::observability::grafana::DMSCGrafanaDashboardGenerator::new();
271 let mut panel = generator.create_panel(title, "stat", 0, 0, 12, 8);
272 panel.targets.push(generator.create_prometheus_target(query, "A", None));
273 dashboard.panels.push(panel);
274 Ok(())
275 }
276
277 /// Adds a gauge panel to a Grafana dashboard.
278 ///
279 /// # Parameters
280 ///
281 /// - `dashboard`: The Grafana dashboard to add the panel to
282 /// - `title`: The title of the panel
283 /// - `query`: The Prometheus query for the panel
284 ///
285 /// # Returns
286 ///
287 /// DMSCResult indicating success or failure
288 pub fn add_gauge_panel(&self, dashboard: &mut crate::observability::grafana::DMSCGrafanaDashboard, title: &str, query: &str) -> DMSCResult<()> {
289 let mut generator = crate::observability::grafana::DMSCGrafanaDashboardGenerator::new();
290 let mut panel = generator.create_panel(title, "gauge", 12, 0, 12, 8);
291 panel.targets.push(generator.create_prometheus_target(query, "A", None));
292 dashboard.panels.push(panel);
293 Ok(())
294 }
295
296 /// Adds a stat panel to a Grafana dashboard.
297 ///
298 /// # Parameters
299 ///
300 /// - `dashboard`: The Grafana dashboard to add the panel to
301 /// - `title`: The title of the panel
302 /// - `query`: The Prometheus query for the panel
303 ///
304 /// # Returns
305 ///
306 /// DMSCResult indicating success or failure
307 pub fn add_stat_panel(&self, dashboard: &mut crate::observability::grafana::DMSCGrafanaDashboard, title: &str, query: &str) -> DMSCResult<()> {
308 let mut generator = crate::observability::grafana::DMSCGrafanaDashboardGenerator::new();
309 let mut panel = generator.create_panel(title, "stat", 0, 8, 12, 8);
310 panel.targets.push(generator.create_prometheus_target(query, "A", None));
311 dashboard.panels.push(panel);
312 Ok(())
313 }
314
315 /// Generates a Grafana dashboard with default panels.
316 ///
317 /// # Parameters
318 ///
319 /// - `title`: The title of the dashboard
320 ///
321 /// # Returns
322 ///
323 /// A Grafana dashboard with default panels wrapped in DMSCResult
324 pub fn generate_dashboard(&self, title: &str) -> DMSCResult<crate::observability::grafana::DMSCGrafanaDashboard> {
325 let mut dashboard = crate::observability::grafana::DMSCGrafanaDashboard::new(title);
326
327 self.add_counter_panel(&mut dashboard, "Request Count", "dms_requests_total")?;
328 self.add_gauge_panel(&mut dashboard, "Active Connections", "dms_active_connections")?;
329 self.add_stat_panel(&mut dashboard, "Response Time", "dms_response_time_seconds")?;
330
331 Ok(dashboard)
332 }
333
334 /// Generates a default Grafana dashboard with "DMSC Metrics Dashboard" title.
335 ///
336 /// # Returns
337 ///
338 /// A default Grafana dashboard wrapped in DMSCResult
339 pub fn generate_default_dashboard(&self) -> DMSCResult<crate::observability::grafana::DMSCGrafanaDashboard> {
340 self.generate_dashboard("DMSC Metrics Dashboard")
341 }
342}
343