diff --git a/Cargo.lock b/Cargo.lock index 76a02a92..8c4db055 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4724,9 +4724,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.36.0" +version = "1.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" +checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" dependencies = [ "backtrace", "bytes", diff --git a/Cargo.toml b/Cargo.toml index 20bbbbec..96437478 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,7 +61,7 @@ rust-embed = { version = "8.3.0", features = [ serde = { version = "1.0.197", features = ["derive"] } serde_json = "1.0.115" time = { version = "0.3.34", features = ["local-offset"] } -tokio = { version = "1.36.0", features = ["full"] } +tokio = { version = "1.37.0", features = ["full"] } toml = "0.8.12" tower-http = { version = "0.5.2", features = [ "timeout", diff --git a/crates/kernel/src/prom/mod.rs b/crates/kernel/src/prom/mod.rs index 73a01f27..5da430f3 100644 --- a/crates/kernel/src/prom/mod.rs +++ b/crates/kernel/src/prom/mod.rs @@ -65,6 +65,32 @@ pub struct QueryResponseDataItem { pub values: Vec<(i64, String)>, } +#[derive(Debug, Serialize, Deserialize)] +pub struct LineSeries { + pub total: i64, + pub values: Vec<(i64, i64)>, +} + +impl LineSeries { + pub fn from(res: &QueryResponse, sequence: Vec) -> Self { + let mut times_data = HashMap::::new(); + for item in res.data.result.iter() { + for (t, v) in &item.values { + let value = v.parse::().unwrap_or(0.0) as i64; + times_data.insert(*t, value); + } + } + let mut values = vec![]; + let mut total: i64 = 0; + for t in sequence.iter() { + let value = times_data.get(t).unwrap_or(&0); + values.push((*t * 1000, *value)); // js use milliseconds, t*1000 + total += *value; + } + LineSeries { total, values } + } +} + /// query_range queries range from Prometheus pub async fn query_range(params: QueryRangeParams) -> Result { let prom_env = PROM_ENV diff --git a/crates/worker-server/src/middleware.rs b/crates/worker-server/src/middleware.rs index 05402326..a9109183 100644 --- a/crates/worker-server/src/middleware.rs +++ b/crates/worker-server/src/middleware.rs @@ -1,6 +1,5 @@ -use crate::METRICS_ENABLED; - use super::{DEFAULT_WASM, ENDPOINT_NAME}; +use crate::METRICS_ENABLED; use axum::extract::Request; use axum::http::StatusCode; use axum::middleware::Next; @@ -64,12 +63,23 @@ pub async fn middleware(mut request: Request, next: Next) -> Result, //only for daily or weekly } +#[derive(Debug)] +struct PeriodParams { + start: i64, + end: i64, + step: i64, + step_word: String, + sequence: Vec, // unix timestamp from start to end with step +} + impl RequestsQuery { - pub fn get_period(&self) -> (i64, i64, i64, String) { + fn get_period(&self) -> PeriodParams { if let Some(p) = self.period.as_ref() { if p.eq("weekly") { let end = chrono::Utc::now().timestamp() / 3600 * 3600; let start = end - 604800; // 86400 * 7 - return (start, end, 3600, "1h".to_string()); + let sequence = (0..169).map(|i| start + i * 3600).collect(); + return PeriodParams { + start, + end, + step: 3600, + step_word: "1h".to_string(), + sequence, + }; } } let end = chrono::Utc::now().timestamp() / 600 * 600; - let start = end - 86400; - (start, end, 600, "10m".to_string()) + let start = end - 86400; // oneday 1440/10+1 + let sequence = (0..145).map(|i| start + i * 600).collect(); + PeriodParams { + start, + end, + step: 600, + step_word: "10m".to_string(), + sequence, + } } } @@ -33,12 +56,19 @@ pub async fn flows( Ok("flows".to_string()) } +#[derive(Debug, Serialize, Deserialize)] +pub struct RequestsValues { + pub total: i64, + pub time_series: Vec, + pub values: Vec, +} + /// requests is a handler for GET /traffic/requests pub async fn requests( Extension(user): Extension, Query(q): Query, ) -> Result { - let (start, end, step, step_word) = q.get_period(); + let period = q.get_period(); let acc = q.account.unwrap_or_default(); if acc != user.id.to_string() { return Err(ServerError::forbidden("user id does not match")); @@ -46,25 +76,36 @@ pub async fn requests( let query = if let Some(pid) = q.project { format!( "increase(req_fn_total{{project_id=\"{}\"}}[{}])", - pid, step_word + pid, period.step_word ) } else { format!( "sum(increase(req_fn_total{{user_id=\"{}\"}}[{}]))", - acc, step_word + acc, period.step_word ) }; + let now = tokio::time::Instant::now(); // end time is now ts with latest 10 decade debug!( "query: {}, start:{}, end:{}, step:{}", - query, start, end, step + query, period.start, period.end, period.step ); let params = land_kernel::prom::QueryRangeParams { - query, - step, - start, - end, + query: query.clone(), + step: period.step, + start: period.start, + end: period.end, }; let res = land_kernel::prom::query_range(params).await?; - Ok(Json(res).into_response()) + let values = land_kernel::prom::LineSeries::from(&res, period.sequence); + info!( + "query: {}, start:{}, end:{}, step:{}, values:{}, cost:{}", + query, + period.start, + period.end, + period.step, + values.values.len(), + now.elapsed().as_millis(), + ); + Ok(Json(values)) } diff --git a/land-server/tpls/overview.hbs b/land-server/tpls/overview.hbs index 6a539720..8a729be5 100644 --- a/land-server/tpls/overview.hbs +++ b/land-server/tpls/overview.hbs @@ -66,12 +66,7 @@ -
-
-
Global Traffic
-

Aggregated all projects traffic in recently.

-
-
+ {{> partials/traffic.hbs}} {{> partials/footer.hbs}} diff --git a/land-server/tpls/partials/traffic.hbs b/land-server/tpls/partials/traffic.hbs new file mode 100644 index 00000000..705c8a73 --- /dev/null +++ b/land-server/tpls/partials/traffic.hbs @@ -0,0 +1,24 @@ +
+
+
Global Traffic
+

Aggregated all projects traffic in recently.

+
+
+
+
+
+

Request counts

+

+
+ Loading... +
+
+
+
+
+
222
+
+
+ + \ No newline at end of file diff --git a/land-server/tpls/static/js/traffic.js b/land-server/tpls/static/js/traffic.js new file mode 100644 index 00000000..01303f85 --- /dev/null +++ b/land-server/tpls/static/js/traffic.js @@ -0,0 +1,96 @@ + +(async () => { + let d = document.getElementById("requests-loading-spinner"); + let acc = document.getElementById("traffic").getAttribute("data-x-account"); + let resp = await fetch("/traffic/requests?account=" + acc) + if (!resp.ok) { + // TODO: show error message + return + } + // hide loading spinner + d.classList.add("d-none"); + let series = await resp.json(); + document.getElementById("requests-total").innerText = friendly_bytesize(series.total, false); + let chart = echarts.init(document.getElementById('requests-chart')); + let option = { + title: { + show: false, + }, + tooltip: { + trigger: "axis", + formatter: function (params) { + let t = params[0].axisValueLabel; + let v = params[0].value[1]; + return `${t}
REQUESTS ${v}
` + } + }, + xAxis: { + type: 'time', + axisLabel: { + formatter: function (value, index) { + return unix2hour(value) // js use milliseconds + }, + color: "#AAA", + }, + splitNumber: 3, + }, + yAxis: { + show: false + }, + grid: { + top: 0, + left: 0, + right: 0, + bottom: 20, + }, + series: [ + { + type: 'line', + data: series.values || [], + smooth: true, + symbol: "none", + lineStyle: { + normal: { + color: "#AC3B2A", + width: 2, + } + }, + areaStyle: { + color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ + { + offset: 0, + color: '#AC3B2A' + }, + { + offset: 1, + color: '#FFF' + } + ]) + }, + } + ] + }; + chart.setOption(option); +})() + +/// convert unixtimestamp to hour and minute, HH:MM +function unix2hour(v) { + const dateObj = new Date(v) + const hours = dateObj.getHours() >= 10 ? dateObj.getHours() : '0' + dateObj.getHours() + const minutes = dateObj.getMinutes() < 10 ? dateObj.getMinutes() + '0' : dateObj.getMinutes() + const UnixTimeToDate = hours + ':' + minutes + return UnixTimeToDate +} + +function friendly_bytesize(v, is_bytes) { + let bytes_units = ['iB', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']; + let units = ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']; + let i = 0; + while (v > 1000) { + v /= 1000; + i++; + } + v = v.toFixed(2); + let u = is_bytes ? bytes_units[i] : units[i]; + return `${v}${u}` +} \ No newline at end of file