metrics_dashboard/
lib.rs

1//! This crate provide simple auto-generate dashboard for [metric-rs](https://crates.io/crates/metrics) crate.
2//! To intergrate to poem webserver, simple include to route like:
3//!
4//! ```rust
5//! use metrics_dashboard::{build_dashboard_route, DashboardOptions, ChartType};
6//! use poem::Route;
7//!
8//! let dashboard_options = DashboardOptions {
9//!     custom_charts: vec![
10//!         ChartType::Line {
11//!             metrics: vec![
12//!                 "demo_live_time".to_string(),
13//!                 "demo_live_time_max".to_string(),
14//!             ],
15//!             desc: "Demo metric line".to_string(),
16//!             unit: "Seconds".to_string(),
17//!         },
18//!     ],
19//!     include_default: true,
20//! };
21//!
22//! let app = Route::new().nest("/dashboard/", build_dashboard_route(dashboard_options));
23//! ```
24//!
25//! After init dashboard route, all of metrics defined metric will be exposed.
26//!
27//! ```rust
28//! use metrics::{describe_counter, counter};
29//!
30//! describe_counter!("demo_metric1", "Demo metric1");
31//! counter!("demo_metric1").increment(1);
32//! ```
33use std::collections::HashMap;
34use std::vec;
35
36pub use metrics;
37
38#[cfg(feature = "system")]
39use metrics_process::register_sysinfo_event;
40use metrics_prometheus::failure::strategy::{self, NoOp};
41use metrics_util::layers::FanoutBuilder;
42pub use middleware::HttpMetricMiddleware;
43use poem::EndpointExt;
44use poem::{
45    handler,
46    web::{Data, Json, Query},
47    Route,
48};
49
50#[cfg(not(feature = "embed"))]
51use poem::endpoint::StaticFilesEndpoint;
52
53#[cfg(feature = "embed")]
54use poem::endpoint::{EmbeddedFileEndpoint, EmbeddedFilesEndpoint};
55#[cfg(feature = "embed")]
56use rust_embed::RustEmbed;
57
58use recorder::{DashboardRecorder, MetricMeta, MetricValue};
59use serde::{Deserialize, Serialize};
60
61#[cfg(feature = "system")]
62pub mod metrics_process;
63mod middleware;
64pub mod recorder;
65
66#[cfg(feature = "embed")]
67#[derive(RustEmbed)]
68#[folder = "public"]
69pub struct Files;
70
71#[derive(Debug, Deserialize)]
72struct MetricQuery {
73    keys: String,
74}
75
76#[derive(Debug, Clone, Default)]
77pub struct DashboardOptions {
78    /// This is custom charts that you want to show in dashboard.
79    pub custom_charts: Vec<ChartType>,
80    /// Whether to include metrics that not mention in the charts options.
81    /// This is useful when you want to include all metrics in the dashboard.
82    pub include_default: bool,
83}
84
85#[derive(Debug, Serialize, Clone)]
86#[serde(tag = "type", content = "meta")]
87pub enum ChartType {
88    Line {
89        metrics: Vec<String>,
90        desc: String,
91        unit: String,
92    },
93    Bar {
94        metrics: Vec<String>,
95        desc: String,
96        unit: String,
97    },
98}
99
100impl ChartType {
101    pub fn metrics(&self) -> &[String] {
102        match self {
103            ChartType::Line { metrics, .. } => metrics,
104            ChartType::Bar { metrics, .. } => metrics,
105        }
106    }
107}
108
109#[handler]
110fn prometheus_metrics(Data(recorder): Data<&metrics_prometheus::Recorder<NoOp>>) -> String {
111    prometheus::TextEncoder::new()
112        .encode_to_string(&recorder.registry().gather())
113        .expect("Should generate")
114}
115
116#[handler]
117fn api_charts(Data(recorder): Data<&DashboardRecorder>) -> Json<Vec<ChartType>> {
118    let option = &recorder.options;
119    let mut res: Vec<ChartType> = vec![];
120    let mut included_metrics = HashMap::new();
121    for chart in option.custom_charts.iter() {
122        res.push(chart.clone());
123        for metric in chart.metrics() {
124            included_metrics.insert(metric.clone(), true);
125        }
126    }
127    if option.include_default {
128        let metrics = recorder.metrics();
129        for meta in metrics.iter() {
130            if included_metrics.contains_key(&meta.key) {
131                continue;
132            }
133            let chart = ChartType::Line {
134                metrics: vec![meta.key.clone()],
135                desc: meta.desc.clone().unwrap_or_else(|| meta.key.clone()),
136                unit: meta.unit.clone().unwrap_or_else(|| "".to_string()),
137            };
138            res.push(chart.clone());
139        }
140    }
141
142    Json(res)
143}
144
145#[handler]
146fn api_metrics(Data(recorder): Data<&DashboardRecorder>) -> Json<Vec<MetricMeta>> {
147    Json(recorder.metrics())
148}
149
150#[handler]
151fn api_metrics_value(
152    Data(recorder): Data<&DashboardRecorder>,
153    Query(query): Query<MetricQuery>,
154) -> Json<Vec<MetricValue>> {
155    let keys = query.keys.split(';').collect::<Vec<&str>>();
156    Json(recorder.metrics_value(keys))
157}
158
159pub fn build_dashboard_route(opts: DashboardOptions) -> Route {
160    build_dashboard_route_with_recorder(opts).1
161}
162
163pub fn build_dashboard_route_with_recorder(opts: DashboardOptions) -> (DashboardRecorder, Route) {
164    let recorder1 = metrics_prometheus::Recorder::builder()
165        .with_failure_strategy(strategy::NoOp)
166        .build();
167
168    let recorder2 = DashboardRecorder::new(opts);
169
170    let recoder_fanout = FanoutBuilder::default()
171        .add_recorder(recorder1.clone())
172        .add_recorder(recorder2.clone())
173        .build();
174
175    metrics::set_global_recorder(recoder_fanout).expect("Should register a recorder successfull");
176    #[cfg(feature = "system")]
177    register_sysinfo_event();
178
179    let route = Route::new()
180        .at("/prometheus", prometheus_metrics.data(recorder1))
181        .at("/api/metrics", api_metrics.data(recorder2.clone()))
182        .at("/api/charts", api_charts.data(recorder2.clone()))
183        .at(
184            "/api/metrics_value",
185            api_metrics_value.data(recorder2.clone()),
186        );
187
188    #[cfg(not(feature = "embed"))]
189    let route = route.nest(
190        "/",
191        StaticFilesEndpoint::new("./public/").index_file("index.html"),
192    );
193
194    #[cfg(feature = "embed")]
195    let route = route.at("/", EmbeddedFileEndpoint::<Files>::new("index.html"));
196    #[cfg(feature = "embed")]
197    let route = route.nest("/", EmbeddedFilesEndpoint::<Files>::new());
198
199    (recorder2, route)
200}
201
202#[allow(unused)]
203pub(crate) fn round_up_f64_2digits(input: f64) -> f64 {
204    (input * 100.0).round() / 100.0
205}