1use 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 pub custom_charts: Vec<ChartType>,
80 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}