mirror of
https://github.com/zaphar/Heracles.git
synced 2025-07-23 12:39:50 -04:00
commit
5a76207cca
166
Cargo.lock
generated
166
Cargo.lock
generated
@ -429,12 +429,33 @@ dependencies = [
|
|||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fastrand"
|
||||||
|
version = "2.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fnv"
|
name = "fnv"
|
||||||
version = "1.0.7"
|
version = "1.0.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "foreign-types"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
|
||||||
|
dependencies = [
|
||||||
|
"foreign-types-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "foreign-types-shared"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "form_urlencoded"
|
name = "form_urlencoded"
|
||||||
version = "1.2.1"
|
version = "1.2.1"
|
||||||
@ -579,6 +600,7 @@ dependencies = [
|
|||||||
"maud",
|
"maud",
|
||||||
"parse_duration",
|
"parse_duration",
|
||||||
"prometheus-http-query",
|
"prometheus-http-query",
|
||||||
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_yaml",
|
"serde_yaml",
|
||||||
@ -719,6 +741,19 @@ dependencies = [
|
|||||||
"tokio-rustls",
|
"tokio-rustls",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hyper-tls"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"hyper 0.14.28",
|
||||||
|
"native-tls",
|
||||||
|
"tokio",
|
||||||
|
"tokio-native-tls",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper-util"
|
name = "hyper-util"
|
||||||
version = "0.1.3"
|
version = "0.1.3"
|
||||||
@ -885,6 +920,24 @@ dependencies = [
|
|||||||
"windows-sys 0.48.0",
|
"windows-sys 0.48.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "native-tls"
|
||||||
|
version = "0.2.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e"
|
||||||
|
dependencies = [
|
||||||
|
"lazy_static",
|
||||||
|
"libc",
|
||||||
|
"log",
|
||||||
|
"openssl",
|
||||||
|
"openssl-probe",
|
||||||
|
"openssl-sys",
|
||||||
|
"schannel",
|
||||||
|
"security-framework",
|
||||||
|
"security-framework-sys",
|
||||||
|
"tempfile",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nu-ansi-term"
|
name = "nu-ansi-term"
|
||||||
version = "0.46.0"
|
version = "0.46.0"
|
||||||
@ -1002,6 +1055,50 @@ version = "1.19.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openssl"
|
||||||
|
version = "0.10.63"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "15c9d69dd87a29568d4d017cfe8ec518706046a05184e5aea92d0af890b803c8"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.4.2",
|
||||||
|
"cfg-if",
|
||||||
|
"foreign-types",
|
||||||
|
"libc",
|
||||||
|
"once_cell",
|
||||||
|
"openssl-macros",
|
||||||
|
"openssl-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openssl-macros"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openssl-probe"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openssl-sys"
|
||||||
|
version = "0.9.99"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "22e1bf214306098e4832460f797824c05d25aacdf896f64a985fb0fd992454ae"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"libc",
|
||||||
|
"pkg-config",
|
||||||
|
"vcpkg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "overload"
|
name = "overload"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
@ -1063,6 +1160,12 @@ version = "0.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pkg-config"
|
||||||
|
version = "0.3.29"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2900ede94e305130c13ddd391e0ab7cbaeb783945ae07a279c268cb05109c6cb"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "polling"
|
name = "polling"
|
||||||
version = "3.3.2"
|
version = "3.3.2"
|
||||||
@ -1183,10 +1286,12 @@ dependencies = [
|
|||||||
"http-body 0.4.6",
|
"http-body 0.4.6",
|
||||||
"hyper 0.14.28",
|
"hyper 0.14.28",
|
||||||
"hyper-rustls",
|
"hyper-rustls",
|
||||||
|
"hyper-tls",
|
||||||
"ipnet",
|
"ipnet",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"log",
|
"log",
|
||||||
"mime",
|
"mime",
|
||||||
|
"native-tls",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
@ -1198,6 +1303,7 @@ dependencies = [
|
|||||||
"sync_wrapper",
|
"sync_wrapper",
|
||||||
"system-configuration",
|
"system-configuration",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-native-tls",
|
||||||
"tokio-rustls",
|
"tokio-rustls",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"url",
|
"url",
|
||||||
@ -1284,6 +1390,15 @@ version = "1.0.16"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c"
|
checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "schannel"
|
||||||
|
version = "0.1.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys 0.52.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sct"
|
name = "sct"
|
||||||
version = "0.7.1"
|
version = "0.7.1"
|
||||||
@ -1294,6 +1409,29 @@ dependencies = [
|
|||||||
"untrusted",
|
"untrusted",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "security-framework"
|
||||||
|
version = "2.9.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 1.3.2",
|
||||||
|
"core-foundation",
|
||||||
|
"core-foundation-sys",
|
||||||
|
"libc",
|
||||||
|
"security-framework-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "security-framework-sys"
|
||||||
|
version = "2.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a"
|
||||||
|
dependencies = [
|
||||||
|
"core-foundation-sys",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.196"
|
version = "1.0.196"
|
||||||
@ -1444,6 +1582,18 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tempfile"
|
||||||
|
version = "3.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a365e8cd18e44762ef95d87f284f4b5cd04107fec2ff3052bd6a3e6069669e67"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"fastrand",
|
||||||
|
"rustix",
|
||||||
|
"windows-sys 0.52.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thread_local"
|
name = "thread_local"
|
||||||
version = "1.1.7"
|
version = "1.1.7"
|
||||||
@ -1527,6 +1677,16 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-native-tls"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
|
||||||
|
dependencies = [
|
||||||
|
"native-tls",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-rustls"
|
name = "tokio-rustls"
|
||||||
version = "0.24.1"
|
version = "0.24.1"
|
||||||
@ -1717,6 +1877,12 @@ version = "0.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
|
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "vcpkg"
|
||||||
|
version = "0.2.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "version_check"
|
name = "version_check"
|
||||||
version = "0.9.4"
|
version = "0.9.4"
|
||||||
|
@ -23,3 +23,4 @@ tokio = { version = "1.36.0", features = ["net", "rt", "rt-multi-thread"] }
|
|||||||
tower-http = { version = "0.5.1", features = ["trace"] }
|
tower-http = { version = "0.5.1", features = ["trace"] }
|
||||||
tracing = "0.1.40"
|
tracing = "0.1.40"
|
||||||
tracing-subscriber = "0.3.18"
|
tracing-subscriber = "0.3.18"
|
||||||
|
reqwest = { version = "0.11.24", features = ["rustls-tls"] }
|
||||||
|
@ -58,3 +58,16 @@
|
|||||||
query: 'node_memory_MemFree_bytes{job="nodestats"}'
|
query: 'node_memory_MemFree_bytes{job="nodestats"}'
|
||||||
meta:
|
meta:
|
||||||
name_format: "`${labels.instance}`"
|
name_format: "`${labels.instance}`"
|
||||||
|
- title: Log Test Dashboard 1
|
||||||
|
span:
|
||||||
|
end: now
|
||||||
|
duration: 1h
|
||||||
|
step_duration: 5min
|
||||||
|
logs:
|
||||||
|
- title: Systemd Service Logs
|
||||||
|
query_type: Range
|
||||||
|
yaxes:
|
||||||
|
- anchor: "y" # This axis is y
|
||||||
|
source: http://heimdall:3100
|
||||||
|
query: |
|
||||||
|
{job="systemd-journal"}
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
};
|
};
|
||||||
naersk.url = "github:nix-community/naersk";
|
naersk.url = "github:nix-community/naersk";
|
||||||
flake-compat = {
|
flake-compat = {
|
||||||
url = github:edolstra/flake-compat;
|
url = "github:edolstra/flake-compat";
|
||||||
flake = false;
|
flake = false;
|
||||||
};
|
};
|
||||||
flake-utils.url = "github:numtide/flake-utils";
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
11
jsconfig.json
Normal file
11
jsconfig.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es2022",
|
||||||
|
"noImplicitThis": true,
|
||||||
|
"checkJs": true,
|
||||||
|
"allowJs": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"static/*.js"
|
||||||
|
]
|
||||||
|
}
|
@ -13,14 +13,16 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
use chrono::prelude::*;
|
use chrono::prelude::*;
|
||||||
use chrono::Duration;
|
use chrono::Duration;
|
||||||
use serde::{Serialize, Deserialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_yaml;
|
use serde_yaml;
|
||||||
use tracing::{debug, error};
|
use tracing::{debug, error};
|
||||||
use anyhow::Result;
|
|
||||||
|
|
||||||
use crate::query::{QueryConn, QueryType, QueryResult, to_samples};
|
use crate::query::{
|
||||||
|
loki_to_sample, prom_to_samples, LokiConn, PromQueryConn, QueryResult, QueryType,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
pub struct PlotMeta {
|
pub struct PlotMeta {
|
||||||
@ -73,7 +75,8 @@ pub struct GraphSpan {
|
|||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct Dashboard {
|
pub struct Dashboard {
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub graphs: Vec<Graph>,
|
pub graphs: Option<Vec<Graph>>,
|
||||||
|
pub logs: Option<Vec<LogStream>>,
|
||||||
pub span: Option<GraphSpan>,
|
pub span: Option<GraphSpan>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,6 +95,8 @@ pub enum Orientation {
|
|||||||
Vertical,
|
Vertical,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NOTE(zapher): These two structs look repetitive but we haven't hit the rule of three yet.
|
||||||
|
// If we do then it might be time to restructure them a bit.
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct Graph {
|
pub struct Graph {
|
||||||
pub title: String,
|
pub title: String,
|
||||||
@ -103,21 +108,49 @@ pub struct Graph {
|
|||||||
pub d3_tick_format: Option<String>,
|
pub d3_tick_format: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn query_data(graph: &Graph, dash: &Dashboard, query_span: Option<GraphSpan>) -> Result<Vec<QueryResult>> {
|
#[derive(Deserialize)]
|
||||||
|
pub struct LogStream {
|
||||||
|
pub title: String,
|
||||||
|
pub legend_orientation: Option<Orientation>,
|
||||||
|
pub source: String,
|
||||||
|
pub yaxes: Vec<AxisDefinition>,
|
||||||
|
pub query: String,
|
||||||
|
pub span: Option<GraphSpan>,
|
||||||
|
pub limit: Option<usize>,
|
||||||
|
pub query_type: QueryType,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn prom_query_data(
|
||||||
|
graph: &Graph,
|
||||||
|
dash: &Dashboard,
|
||||||
|
query_span: Option<GraphSpan>,
|
||||||
|
) -> Result<Vec<QueryResult>> {
|
||||||
let connections = graph.get_query_connections(&dash.span, &query_span);
|
let connections = graph.get_query_connections(&dash.span, &query_span);
|
||||||
let mut data = Vec::new();
|
let mut data = Vec::new();
|
||||||
for conn in connections {
|
for conn in connections {
|
||||||
data.push(to_samples(
|
data.push(prom_to_samples(
|
||||||
conn.get_results()
|
conn.get_results().await?.data().clone(),
|
||||||
.await?
|
|
||||||
.data()
|
|
||||||
.clone(),
|
|
||||||
conn.meta,
|
conn.meta,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
Ok(data)
|
Ok(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn loki_query_data(
|
||||||
|
stream: &LogStream,
|
||||||
|
dash: &Dashboard,
|
||||||
|
query_span: Option<GraphSpan>,
|
||||||
|
) -> Result<QueryResult> {
|
||||||
|
let conn = stream.get_query_connection(&dash.span, &query_span);
|
||||||
|
let response = conn.get_results().await?;
|
||||||
|
if response.status == "success" {
|
||||||
|
Ok(loki_to_sample(response.data))
|
||||||
|
} else {
|
||||||
|
// TODO(jwall): Better error handling than this
|
||||||
|
panic!("Loki query status: {}", response.status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn duration_from_string(duration_string: &str) -> Option<Duration> {
|
fn duration_from_string(duration_string: &str) -> Option<Duration> {
|
||||||
match parse_duration::parse(duration_string) {
|
match parse_duration::parse(duration_string) {
|
||||||
Ok(d) => match Duration::from_std(d) {
|
Ok(d) => match Duration::from_std(d) {
|
||||||
@ -172,15 +205,20 @@ impl Graph {
|
|||||||
&'graph self,
|
&'graph self,
|
||||||
graph_span: &'graph Option<GraphSpan>,
|
graph_span: &'graph Option<GraphSpan>,
|
||||||
query_span: &'graph Option<GraphSpan>,
|
query_span: &'graph Option<GraphSpan>,
|
||||||
) -> Vec<QueryConn<'conn>> {
|
) -> Vec<PromQueryConn<'conn>> {
|
||||||
let mut conns = Vec::new();
|
let mut conns = Vec::new();
|
||||||
for plot in self.plots.iter() {
|
for plot in self.plots.iter() {
|
||||||
debug!(
|
debug!(
|
||||||
query = plot.query,
|
query = plot.query,
|
||||||
source = plot.source,
|
source = plot.source,
|
||||||
"Getting query connection for graph"
|
"Getting query connection for graph",
|
||||||
|
);
|
||||||
|
let mut conn = PromQueryConn::new(
|
||||||
|
&plot.source,
|
||||||
|
&plot.query,
|
||||||
|
self.query_type.clone(),
|
||||||
|
plot.meta.clone(),
|
||||||
);
|
);
|
||||||
let mut conn = QueryConn::new(&plot.source, &plot.query, self.query_type.clone(), plot.meta.clone());
|
|
||||||
// Query params take precendence over all other settings. Then graph settings take
|
// Query params take precendence over all other settings. Then graph settings take
|
||||||
// precedences and finally the dashboard settings take precendence
|
// precedences and finally the dashboard settings take precendence
|
||||||
if let Some((end, duration, step_duration)) = graph_span_to_tuple(query_span) {
|
if let Some((end, duration, step_duration)) = graph_span_to_tuple(query_span) {
|
||||||
@ -196,6 +234,34 @@ impl Graph {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl LogStream {
|
||||||
|
pub fn get_query_connection<'conn, 'stream: 'conn>(
|
||||||
|
&'stream self,
|
||||||
|
graph_span: &'stream Option<GraphSpan>,
|
||||||
|
query_span: &'stream Option<GraphSpan>,
|
||||||
|
) -> LokiConn<'conn> {
|
||||||
|
debug!(
|
||||||
|
query = self.query,
|
||||||
|
source = self.source,
|
||||||
|
"Getting query connection for log streams",
|
||||||
|
);
|
||||||
|
let mut conn = LokiConn::new(&self.source, &self.query, self.query_type.clone());
|
||||||
|
// Query params take precendence over all other settings. Then graph settings take
|
||||||
|
// precedences and finally the dashboard settings take precendence
|
||||||
|
if let Some((end, duration, step_duration)) = graph_span_to_tuple(query_span) {
|
||||||
|
conn = conn.with_span(end, duration, step_duration);
|
||||||
|
} else if let Some((end, duration, step_duration)) = graph_span_to_tuple(&self.span) {
|
||||||
|
conn = conn.with_span(end, duration, step_duration);
|
||||||
|
} else if let Some((end, duration, step_duration)) = graph_span_to_tuple(graph_span) {
|
||||||
|
conn = conn.with_span(end, duration, step_duration);
|
||||||
|
}
|
||||||
|
if let Some(limit) = self.limit {
|
||||||
|
conn = conn.with_limit(limit);
|
||||||
|
}
|
||||||
|
conn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn read_dashboard_list(path: &Path) -> anyhow::Result<Vec<Dashboard>> {
|
pub fn read_dashboard_list(path: &Path) -> anyhow::Result<Vec<Dashboard>> {
|
||||||
let f = std::fs::File::open(path)?;
|
let f = std::fs::File::open(path)?;
|
||||||
Ok(serde_yaml::from_reader(f)?)
|
Ok(serde_yaml::from_reader(f)?)
|
||||||
|
26
src/main.rs
26
src/main.rs
@ -11,15 +11,15 @@
|
|||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
use std::path::PathBuf;
|
|
||||||
use anyhow;
|
use anyhow;
|
||||||
use axum::{self, extract::State, routing::*, Router};
|
use axum::{self, extract::State, routing::*, Router};
|
||||||
use clap::{self, Parser, ValueEnum};
|
use clap::{self, Parser, ValueEnum};
|
||||||
use dashboard::{Dashboard, query_data};
|
use dashboard::{prom_query_data, Dashboard};
|
||||||
|
use std::path::PathBuf;
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
use tower_http::trace::TraceLayer;
|
use tower_http::trace::TraceLayer;
|
||||||
use tracing::{error, info};
|
|
||||||
use tracing::Level;
|
use tracing::Level;
|
||||||
|
use tracing::{error, info};
|
||||||
use tracing_subscriber::FmtSubscriber;
|
use tracing_subscriber::FmtSubscriber;
|
||||||
|
|
||||||
mod dashboard;
|
mod dashboard;
|
||||||
@ -49,12 +49,14 @@ struct Cli {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn validate(dash: &Dashboard) -> anyhow::Result<()> {
|
async fn validate(dash: &Dashboard) -> anyhow::Result<()> {
|
||||||
for graph in dash.graphs.iter() {
|
if let Some(ref graphs) = dash.graphs {
|
||||||
let data = query_data(graph, &dash, None).await;
|
for graph in graphs.iter() {
|
||||||
if data.is_err() {
|
let data = prom_query_data(graph, &dash, None).await;
|
||||||
error!(err=?data, "Invalid dashboard query or queries");
|
if data.is_err() {
|
||||||
|
error!(err=?data, "Invalid dashboard query or queries");
|
||||||
|
}
|
||||||
|
let _ = data?;
|
||||||
}
|
}
|
||||||
let _ = data?;
|
|
||||||
}
|
}
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@ -94,12 +96,18 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
"/embed/dash/:dash_idx/graph/:graph_idx",
|
"/embed/dash/:dash_idx/graph/:graph_idx",
|
||||||
get(routes::graph_embed).with_state(State(config.clone())),
|
get(routes::graph_embed).with_state(State(config.clone())),
|
||||||
)
|
)
|
||||||
|
.route(
|
||||||
|
"/embed/dash/:dash_idx/log/:graph_idx",
|
||||||
|
get(routes::log_embed).with_state(State(config.clone())),
|
||||||
|
)
|
||||||
.route("/dash/:dash_idx", get(routes::dashboard_direct))
|
.route("/dash/:dash_idx", get(routes::dashboard_direct))
|
||||||
.route("/", get(routes::index).with_state(State(config.clone())))
|
.route("/", get(routes::index).with_state(State(config.clone())))
|
||||||
.layer(TraceLayer::new_for_http())
|
.layer(TraceLayer::new_for_http())
|
||||||
.with_state(State(config.clone()));
|
.with_state(State(config.clone()));
|
||||||
let socket_addr = args.listen.unwrap_or("127.0.0.1:3000".to_string());
|
let socket_addr = args.listen.unwrap_or("127.0.0.1:3000".to_string());
|
||||||
let listener = TcpListener::bind(socket_addr).await.expect("Unable to bind listener to address");
|
let listener = TcpListener::bind(socket_addr)
|
||||||
|
.await
|
||||||
|
.expect("Unable to bind listener to address");
|
||||||
axum::serve(listener, router).await?;
|
axum::serve(listener, router).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
192
src/query/loki.rs
Normal file
192
src/query/loki.rs
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
// Copyright 2024 Jeremy Wall
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use chrono::prelude::*;
|
||||||
|
use reqwest;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tracing::{debug, error};
|
||||||
|
|
||||||
|
use super::{LogLine, QueryResult, QueryType, TimeSpan};
|
||||||
|
|
||||||
|
// TODO(jwall): Should I allow non stream returns?
|
||||||
|
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||||
|
pub enum ResultType {
|
||||||
|
/// Returned by query endpoints
|
||||||
|
#[serde(rename = "vector")]
|
||||||
|
Vector,
|
||||||
|
/// Returned by query_range endpoints
|
||||||
|
#[serde(rename = "matrix")]
|
||||||
|
Matrix,
|
||||||
|
/// Returned by query and query_range endpoints
|
||||||
|
#[serde(rename = "streams")]
|
||||||
|
Streams,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note that the value and volue types return a pair where the first item is a string but
|
||||||
|
// will in actuality always be an f64 number.
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct LokiResult {
|
||||||
|
#[serde(alias = "metric")]
|
||||||
|
#[serde(alias = "stream")]
|
||||||
|
labels: HashMap<String, String>,
|
||||||
|
/// Calculated Value returned by vector result types
|
||||||
|
value: Option<(String, String)>,
|
||||||
|
/// Stream of Log lines, Returned by matrix and stream result types
|
||||||
|
values: Option<Vec<(String, String)>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct LokiResponse {
|
||||||
|
pub status: String,
|
||||||
|
pub data: LokiData,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct LokiData {
|
||||||
|
#[serde(rename = "resultType")]
|
||||||
|
result_type: ResultType,
|
||||||
|
result: Vec<LokiResult>,
|
||||||
|
//stats: // TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn loki_to_sample(data: LokiData) -> QueryResult {
|
||||||
|
match data.result_type {
|
||||||
|
ResultType::Vector => {
|
||||||
|
let mut values = Vec::with_capacity(data.result.len());
|
||||||
|
for result in data.result {
|
||||||
|
if let Some(value) = result.value {
|
||||||
|
values.push((
|
||||||
|
result.labels,
|
||||||
|
LogLine {
|
||||||
|
timestamp: value.0.parse::<f64>().expect("Invalid f64 type"),
|
||||||
|
line: value.1,
|
||||||
|
},
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
error!(
|
||||||
|
?result,
|
||||||
|
"Invalid LokiResult: No value field when result type is {:?}",
|
||||||
|
data.result_type,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QueryResult::StreamInstant(values)
|
||||||
|
}
|
||||||
|
// Stream types are nanoseconds. // Matrix types are seconds
|
||||||
|
ResultType::Matrix | ResultType::Streams => {
|
||||||
|
let mut values = Vec::with_capacity(data.result.len());
|
||||||
|
let multiple = (if data.result_type == ResultType::Matrix { 1000000 } else { 1 }) as f64;
|
||||||
|
for result in data.result {
|
||||||
|
if let Some(value) = result.values {
|
||||||
|
values.push((
|
||||||
|
result.labels,
|
||||||
|
value
|
||||||
|
.into_iter()
|
||||||
|
.map(|(timestamp, line)| LogLine {
|
||||||
|
timestamp: multiple * timestamp.parse::<f64>().expect("Invalid f64 type"),
|
||||||
|
line,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
error!(
|
||||||
|
?result,
|
||||||
|
"Invalid LokiResult: No values field when result type is {:?}",
|
||||||
|
data.result_type,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QueryResult::Stream(values)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct LokiConn<'conn> {
|
||||||
|
url: &'conn str,
|
||||||
|
query: &'conn str,
|
||||||
|
span: Option<TimeSpan>,
|
||||||
|
query_type: QueryType,
|
||||||
|
limit: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
const SCALAR_API_PATH: &'static str = "/loki/api/v1/query";
|
||||||
|
const RANGE_API_PATH: &'static str = "/loki/api/v1/query_range";
|
||||||
|
|
||||||
|
impl<'conn> LokiConn<'conn> {
|
||||||
|
pub fn new<'a: 'conn>(url: &'a str, query: &'a str, query_type: QueryType) -> Self {
|
||||||
|
Self {
|
||||||
|
url,
|
||||||
|
query,
|
||||||
|
query_type,
|
||||||
|
span: None,
|
||||||
|
limit: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_limit(mut self, limit: usize) -> Self {
|
||||||
|
self.limit = Some(limit);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_span(
|
||||||
|
mut self,
|
||||||
|
end: DateTime<Utc>,
|
||||||
|
duration: chrono::Duration,
|
||||||
|
step: chrono::Duration,
|
||||||
|
) -> Self {
|
||||||
|
self.span = Some(TimeSpan {
|
||||||
|
end,
|
||||||
|
duration,
|
||||||
|
step_seconds: step.num_seconds(),
|
||||||
|
});
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_results(&self) -> Result<LokiResponse> {
|
||||||
|
let url = match self.query_type {
|
||||||
|
QueryType::Scalar => format!("{}{}", self.url, SCALAR_API_PATH),
|
||||||
|
QueryType::Range => format!("{}{}", self.url, RANGE_API_PATH),
|
||||||
|
};
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let mut req = client.get(url).query(&[("query", self.query)]);
|
||||||
|
debug!(?req, "Building loki reqwest client");
|
||||||
|
if self.limit.is_some() {
|
||||||
|
debug!(?req, "adding limit");
|
||||||
|
req = req.query(&[("limit", &self.limit.map(|u| u.to_string()).unwrap())]);
|
||||||
|
}
|
||||||
|
if let QueryType::Range = self.query_type {
|
||||||
|
debug!(?req, "Configuring span query params");
|
||||||
|
let (since, end, step_resolution) = if let Some(span) = &self.span {
|
||||||
|
(
|
||||||
|
span.duration,
|
||||||
|
span.end.timestamp(),
|
||||||
|
span.step_seconds as f64,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
let end = Utc::now();
|
||||||
|
(chrono::Duration::minutes(10), end.timestamp(), 30 as f64)
|
||||||
|
};
|
||||||
|
req = req.query(&[
|
||||||
|
("end", &end.to_string()),
|
||||||
|
("since", &format!("{}s", since.num_seconds())),
|
||||||
|
("step", &step_resolution.to_string()),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
debug!(?req, "Sending request");
|
||||||
|
Ok(req.send().await?.json().await?)
|
||||||
|
}
|
||||||
|
}
|
95
src/query/mod.rs
Normal file
95
src/query/mod.rs
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
// Copyright 2023 Jeremy Wall
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use chrono::prelude::*;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::dashboard::PlotMeta;
|
||||||
|
|
||||||
|
mod loki;
|
||||||
|
mod prom;
|
||||||
|
|
||||||
|
#[derive(Deserialize, Clone, Debug)]
|
||||||
|
pub enum QueryType {
|
||||||
|
Range,
|
||||||
|
Scalar,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct TimeSpan {
|
||||||
|
pub end: DateTime<Utc>,
|
||||||
|
pub duration: chrono::Duration,
|
||||||
|
pub step_seconds: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct DataPoint {
|
||||||
|
timestamp: f64,
|
||||||
|
value: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct LogLine {
|
||||||
|
timestamp: f64,
|
||||||
|
line: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub enum QueryResult {
|
||||||
|
Series(Vec<(HashMap<String, String>, PlotMeta, Vec<DataPoint>)>),
|
||||||
|
Scalar(Vec<(HashMap<String, String>, PlotMeta, DataPoint)>),
|
||||||
|
StreamInstant(Vec<(HashMap<String, String>, LogLine)>),
|
||||||
|
Stream(Vec<(HashMap<String, String>, Vec<LogLine>)>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for QueryResult {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
QueryResult::Series(v) => {
|
||||||
|
f.write_fmt(format_args!("Series trace count = {}", v.len()))?;
|
||||||
|
for (idx, (tags, meta, trace)) in v.iter().enumerate() {
|
||||||
|
f.write_fmt(format_args!(
|
||||||
|
"; {}: tags {:?} meta: {:?} datapoint count = {};",
|
||||||
|
idx,
|
||||||
|
tags,
|
||||||
|
meta,
|
||||||
|
trace.len()
|
||||||
|
))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QueryResult::Scalar(v) => {
|
||||||
|
f.write_fmt(format_args!("{} traces", v.len()))?;
|
||||||
|
}
|
||||||
|
QueryResult::StreamInstant(v) => {
|
||||||
|
f.write_fmt(format_args!("{} traces", v.len()))?;
|
||||||
|
}
|
||||||
|
QueryResult::Stream(v) => {
|
||||||
|
f.write_fmt(format_args!("stream trace count = {}", v.len()))?;
|
||||||
|
for (idx, (tags, trace)) in v.iter().enumerate() {
|
||||||
|
f.write_fmt(format_args!(
|
||||||
|
"; {}: tags {:?} line count = {}",
|
||||||
|
idx,
|
||||||
|
tags,
|
||||||
|
trace.len()
|
||||||
|
))?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub use loki::*;
|
||||||
|
pub use prom::*;
|
@ -1,6 +1,4 @@
|
|||||||
use std::collections::HashMap;
|
// Copyright 2024 Jeremy Wall
|
||||||
|
|
||||||
// Copyright 2023 Jeremy Wall
|
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
@ -13,31 +11,21 @@ use std::collections::HashMap;
|
|||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use chrono::prelude::*;
|
use chrono::prelude::*;
|
||||||
use prometheus_http_query::{
|
use prometheus_http_query::{
|
||||||
response::{Data, PromqlResult},
|
response::{Data, PromqlResult},
|
||||||
Client,
|
Client,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
|
||||||
use crate::dashboard::PlotMeta;
|
use crate::dashboard::PlotMeta;
|
||||||
|
|
||||||
#[derive(Deserialize, Clone, Debug)]
|
use super::{DataPoint, QueryResult, QueryType, TimeSpan};
|
||||||
pub enum QueryType {
|
|
||||||
Range,
|
|
||||||
Scalar,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct TimeSpan {
|
pub struct PromQueryConn<'conn> {
|
||||||
pub end: DateTime<Utc>,
|
|
||||||
pub duration: chrono::Duration,
|
|
||||||
pub step_seconds: i64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct QueryConn<'conn> {
|
|
||||||
source: &'conn str,
|
source: &'conn str,
|
||||||
query: &'conn str,
|
query: &'conn str,
|
||||||
span: Option<TimeSpan>,
|
span: Option<TimeSpan>,
|
||||||
@ -45,8 +33,13 @@ pub struct QueryConn<'conn> {
|
|||||||
pub meta: PlotMeta,
|
pub meta: PlotMeta,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'conn> QueryConn<'conn> {
|
impl<'conn> PromQueryConn<'conn> {
|
||||||
pub fn new<'a: 'conn>(source: &'a str, query: &'a str, query_type: QueryType, meta: PlotMeta) -> Self {
|
pub fn new<'a: 'conn>(
|
||||||
|
source: &'a str,
|
||||||
|
query: &'a str,
|
||||||
|
query_type: QueryType,
|
||||||
|
meta: PlotMeta,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
source,
|
source,
|
||||||
query,
|
query,
|
||||||
@ -113,42 +106,7 @@ impl<'conn> QueryConn<'conn> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
pub fn prom_to_samples(data: Data, meta: PlotMeta) -> QueryResult {
|
||||||
pub struct DataPoint {
|
|
||||||
timestamp: f64,
|
|
||||||
value: f64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
|
||||||
pub enum QueryResult {
|
|
||||||
Series(Vec<(HashMap<String, String>, PlotMeta, Vec<DataPoint>)>),
|
|
||||||
Scalar(Vec<(HashMap<String, String>, PlotMeta, DataPoint)>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Debug for QueryResult {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
QueryResult::Series(v) => {
|
|
||||||
f.write_fmt(format_args!("Series trace count = {}", v.len()))?;
|
|
||||||
for (idx, (tags, meta, trace)) in v.iter().enumerate() {
|
|
||||||
f.write_fmt(format_args!(
|
|
||||||
"; {}: tags {:?} meta: {:?} datapoint count = {};",
|
|
||||||
idx,
|
|
||||||
tags,
|
|
||||||
meta,
|
|
||||||
trace.len()
|
|
||||||
))?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
QueryResult::Scalar(v) => {
|
|
||||||
f.write_fmt(format_args!("{} traces", v.len()))?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_samples(data: Data, meta: PlotMeta) -> QueryResult {
|
|
||||||
match data {
|
match data {
|
||||||
Data::Matrix(mut range) => QueryResult::Series(
|
Data::Matrix(mut range) => QueryResult::Series(
|
||||||
range
|
range
|
150
src/routes.rs
150
src/routes.rs
@ -22,10 +22,12 @@ use axum::{
|
|||||||
|
|
||||||
// https://maud.lambda.xyz/getting-started.html
|
// https://maud.lambda.xyz/getting-started.html
|
||||||
use maud::{html, Markup};
|
use maud::{html, Markup};
|
||||||
use serde::{Serialize, Deserialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
|
||||||
use crate::dashboard::{Dashboard, Graph, GraphSpan, AxisDefinition, Orientation, query_data};
|
use crate::dashboard::{
|
||||||
|
loki_query_data, prom_query_data, AxisDefinition, Dashboard, Graph, GraphSpan, Orientation, LogStream,
|
||||||
|
};
|
||||||
use crate::query::QueryResult;
|
use crate::query::QueryResult;
|
||||||
|
|
||||||
type Config = State<Arc<Vec<Dashboard>>>;
|
type Config = State<Arc<Vec<Dashboard>>>;
|
||||||
@ -37,17 +39,57 @@ pub struct GraphPayload {
|
|||||||
pub plots: Vec<QueryResult>,
|
pub plots: Vec<QueryResult>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(jwall): Should this be a completely different payload?
|
||||||
|
pub async fn loki_query(
|
||||||
|
State(config): Config,
|
||||||
|
Path((dash_idx, loki_idx)): Path<(usize, usize)>,
|
||||||
|
Query(query): Query<HashMap<String, String>>,
|
||||||
|
) -> Json<GraphPayload> {
|
||||||
|
let dash = config
|
||||||
|
.get(dash_idx)
|
||||||
|
.expect(&format!("No such dashboard index {}", dash_idx));
|
||||||
|
let log = dash
|
||||||
|
.logs
|
||||||
|
.as_ref()
|
||||||
|
.expect("No logs in this dashboard")
|
||||||
|
.get(loki_idx)
|
||||||
|
.expect(&format!("No such log query {}", loki_idx));
|
||||||
|
let plots = vec![loki_query_data(log, dash, query_to_graph_span(query))
|
||||||
|
.await
|
||||||
|
.expect("Unable to get log query results")];
|
||||||
|
Json(GraphPayload {
|
||||||
|
legend_orientation: None,
|
||||||
|
yaxes: log.yaxes.clone(),
|
||||||
|
plots,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn graph_query(
|
pub async fn graph_query(
|
||||||
State(config): Config,
|
State(config): Config,
|
||||||
Path((dash_idx, graph_idx)): Path<(usize, usize)>,
|
Path((dash_idx, graph_idx)): Path<(usize, usize)>,
|
||||||
Query(query): Query<HashMap<String, String>>,
|
Query(query): Query<HashMap<String, String>>,
|
||||||
) -> Json<GraphPayload> {
|
) -> Json<GraphPayload> {
|
||||||
debug!("Getting data for query");
|
debug!("Getting data for query");
|
||||||
let dash = config.get(dash_idx).expect("No such dashboard index");
|
let dash = config
|
||||||
|
.get(dash_idx)
|
||||||
|
.expect(&format!("No such dashboard index {}", dash_idx));
|
||||||
let graph = dash
|
let graph = dash
|
||||||
.graphs
|
.graphs
|
||||||
|
.as_ref()
|
||||||
|
.expect("No graphs in this dashboard")
|
||||||
.get(graph_idx)
|
.get(graph_idx)
|
||||||
.expect(&format!("No such graph in dasboard {}", dash_idx));
|
.expect(&format!("No such graph in dasboard {}", dash_idx));
|
||||||
|
let plots = prom_query_data(graph, dash, query_to_graph_span(query))
|
||||||
|
.await
|
||||||
|
.expect("Unable to get query results");
|
||||||
|
Json(GraphPayload {
|
||||||
|
legend_orientation: graph.legend_orientation.clone(),
|
||||||
|
yaxes: graph.yaxes.clone(),
|
||||||
|
plots,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn query_to_graph_span(query: HashMap<String, String>) -> Option<GraphSpan> {
|
||||||
let query_span = {
|
let query_span = {
|
||||||
if query.contains_key("end")
|
if query.contains_key("end")
|
||||||
&& query.contains_key("duration")
|
&& query.contains_key("duration")
|
||||||
@ -62,16 +104,32 @@ pub async fn graph_query(
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let plots = query_data(graph, dash, query_span).await.expect("Unable to get query results");
|
query_span
|
||||||
Json(GraphPayload{legend_orientation: graph.legend_orientation.clone(), yaxes: graph.yaxes.clone(), plots})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mk_api_routes(config: Arc<Vec<Dashboard>>) -> Router<Config> {
|
pub fn mk_api_routes(config: Arc<Vec<Dashboard>>) -> Router<Config> {
|
||||||
// Query routes
|
// Query routes
|
||||||
Router::new().route(
|
Router::new()
|
||||||
"/dash/:dash_idx/graph/:graph_idx",
|
.route(
|
||||||
get(graph_query).with_state(config),
|
"/dash/:dash_idx/graph/:graph_idx",
|
||||||
)
|
get(graph_query).with_state(config.clone()),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/dash/:dash_idx/log/:log_idx",
|
||||||
|
get(loki_query).with_state(config),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn log_component(dash_idx: usize, log_idx: usize, log: &LogStream) -> Markup {
|
||||||
|
let log_id = format!("log-{}-{}", dash_idx, log_idx);
|
||||||
|
let log_data_uri = format!("/api/dash/{}/log/{}", dash_idx, log_idx);
|
||||||
|
let log_embed_uri = format!("/embed/dash/{}/log/{}", dash_idx, log_idx);
|
||||||
|
html! {
|
||||||
|
div {
|
||||||
|
h2 { (log.title) " - " a href=(log_embed_uri) { "embed url" } }
|
||||||
|
graph-plot uri=(log_data_uri) id=(log_id) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn graph_component(dash_idx: usize, graph_idx: usize, graph: &Graph) -> Markup {
|
pub fn graph_component(dash_idx: usize, graph_idx: usize, graph: &Graph) -> Markup {
|
||||||
@ -82,9 +140,9 @@ pub fn graph_component(dash_idx: usize, graph_idx: usize, graph: &Graph) -> Mark
|
|||||||
div {
|
div {
|
||||||
h2 { (graph.title) " - " a href=(graph_embed_uri) { "embed url" } }
|
h2 { (graph.title) " - " a href=(graph_embed_uri) { "embed url" } }
|
||||||
@if graph.d3_tick_format.is_some() {
|
@if graph.d3_tick_format.is_some() {
|
||||||
timeseries-graph uri=(graph_data_uri) id=(graph_id) d3-tick-format=(graph.d3_tick_format.as_ref().unwrap()) { }
|
graph-plot uri=(graph_data_uri) id=(graph_id) d3-tick-format=(graph.d3_tick_format.as_ref().unwrap()) { }
|
||||||
} @else {
|
} @else {
|
||||||
timeseries-graph uri=(graph_data_uri) id=(graph_id) { }
|
graph-plot uri=(graph_data_uri) id=(graph_id) { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -96,31 +154,68 @@ pub async fn graph_ui(
|
|||||||
) -> Markup {
|
) -> Markup {
|
||||||
let graph = config
|
let graph = config
|
||||||
.get(dash_idx)
|
.get(dash_idx)
|
||||||
.expect("No such dashboard")
|
.expect(&format!("No such dashboard {}", dash_idx))
|
||||||
.graphs
|
.graphs
|
||||||
|
.as_ref()
|
||||||
|
.expect("No graphs in this dashboard")
|
||||||
.get(graph_idx)
|
.get(graph_idx)
|
||||||
.expect("No such graph");
|
.expect("No such graph");
|
||||||
graph_component(dash_idx, graph_idx, graph)
|
graph_component(dash_idx, graph_idx, graph)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn log_ui(
|
||||||
|
State(config): State<Config>,
|
||||||
|
Path((dash_idx, log_idx)): Path<(usize, usize)>,
|
||||||
|
) -> Markup {
|
||||||
|
let log = config
|
||||||
|
.get(dash_idx)
|
||||||
|
.expect(&format!("No such dashboard {}", dash_idx))
|
||||||
|
.logs
|
||||||
|
.as_ref()
|
||||||
|
.expect("No graphs in this dashboard")
|
||||||
|
.get(log_idx)
|
||||||
|
.expect("No such graph");
|
||||||
|
log_component(dash_idx, log_idx, log)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn dash_ui(State(config): State<Config>, Path(dash_idx): Path<usize>) -> Markup {
|
pub async fn dash_ui(State(config): State<Config>, Path(dash_idx): Path<usize>) -> Markup {
|
||||||
// TODO(zaphar): Should do better http error reporting here.
|
// TODO(zaphar): Should do better http error reporting here.
|
||||||
dash_elements(config, dash_idx)
|
dash_elements(config, dash_idx)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dash_elements(config: State<Arc<Vec<Dashboard>>>, dash_idx: usize) -> maud::PreEscaped<String> {
|
fn dash_elements(config: State<Arc<Vec<Dashboard>>>, dash_idx: usize) -> maud::PreEscaped<String> {
|
||||||
let dash = config.get(dash_idx).expect("No such dashboard");
|
let dash = config
|
||||||
let graph_iter = dash
|
.get(dash_idx)
|
||||||
|
.expect(&format!("No such dashboard {}", dash_idx));
|
||||||
|
let graph_components = if let Some(graphs) = dash
|
||||||
.graphs
|
.graphs
|
||||||
.iter()
|
.as_ref() {
|
||||||
|
let graph_iter = graphs.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.collect::<Vec<(usize, &Graph)>>();
|
.collect::<Vec<(usize, &Graph)>>();
|
||||||
|
Some(html! {
|
||||||
|
@for (idx, graph) in &graph_iter {
|
||||||
|
(graph_component(dash_idx, *idx, *graph))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let log_components = if let Some(logs) = dash.logs.as_ref() {
|
||||||
|
let log_iter = logs.iter().enumerate().collect::<Vec<(usize, &LogStream)>>();
|
||||||
|
Some(html! {
|
||||||
|
@for (idx, log) in &log_iter {
|
||||||
|
(log_component(dash_idx, *idx, *log))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
html!(
|
html!(
|
||||||
h1 { (dash.title) }
|
h1 { (dash.title) }
|
||||||
span-selector class="row-flex" {}
|
span-selector class="row-flex" {}
|
||||||
@for (idx, graph) in &graph_iter {
|
@if graph_components.is_some() { (graph_components.unwrap()) }
|
||||||
(graph_component(dash_idx, *idx, *graph))
|
@if log_components.is_some() { (log_components.unwrap()) }
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,7 +234,7 @@ pub fn mk_ui_routes(config: Arc<Vec<Dashboard>>) -> Router<Config> {
|
|||||||
fn graph_lib_prelude() -> Markup {
|
fn graph_lib_prelude() -> Markup {
|
||||||
html! {
|
html! {
|
||||||
script src="/js/plotly.js" { }
|
script src="/js/plotly.js" { }
|
||||||
script defer src="/js/lib.js" { }
|
script type="module" defer src="/js/lib.js" { }
|
||||||
link rel="stylesheet" href="/static/site.css" { }
|
link rel="stylesheet" href="/static/site.css" { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -161,6 +256,23 @@ pub async fn graph_embed(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn log_embed(
|
||||||
|
State(config): State<Config>,
|
||||||
|
Path((dash_idx, log_idx)): Path<(usize, usize)>,
|
||||||
|
) -> Markup {
|
||||||
|
html! {
|
||||||
|
html {
|
||||||
|
head {
|
||||||
|
title { ("Heracles - Prometheus Unshackled") }
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
(graph_lib_prelude())
|
||||||
|
(log_ui(State(config.clone()), Path((dash_idx, log_idx))).await)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn index_html(config: Config, dash_idx: Option<usize>) -> Markup {
|
async fn index_html(config: Config, dash_idx: Option<usize>) -> Markup {
|
||||||
html! {
|
html! {
|
||||||
html {
|
html {
|
||||||
|
290
static/lib.js
290
static/lib.js
@ -12,25 +12,145 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef PlotList
|
||||||
|
* @type {object}
|
||||||
|
* @property {Array=} Series
|
||||||
|
* @property {Array=} Scalar
|
||||||
|
* @property {Array<{timestamp: string, line: string}>=} StreamInstant - Timestamps are in seconds
|
||||||
|
* @property {Array<{timestamp: string, line: string}>=} Stream - Timestamps are in nanoseconds
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef QueryData
|
||||||
|
* @type {object}
|
||||||
|
* @property {object} yaxes
|
||||||
|
* @property {?string} legend_orientation
|
||||||
|
* @property {Array<PlotList>} plots
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef HeaderOrCell
|
||||||
|
* @type {object}
|
||||||
|
* @property {array} values
|
||||||
|
* @property {{color: string}=} fill
|
||||||
|
* @property {object=} font
|
||||||
|
* @property {string=} font.family
|
||||||
|
* @property {number=} font.size
|
||||||
|
* @property {string=} font.color
|
||||||
|
* @property {{width: number, color: string}=} line
|
||||||
|
* @property {Array<number>=} columnwidth
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef TableTrace
|
||||||
|
* @type {object}
|
||||||
|
* @property {string=} name
|
||||||
|
* @property {string} type
|
||||||
|
* @property {string=} mode
|
||||||
|
* @property {HeaderOrCell} header
|
||||||
|
* @property {HeaderOrCell} cells - An Array of columns for the table.
|
||||||
|
* @property {string=} xaxis
|
||||||
|
* @property {string=} yaxis
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef GraphTrace
|
||||||
|
* @type {object}
|
||||||
|
* @property {string=} name
|
||||||
|
* @property {string=} fill
|
||||||
|
* @property type {string}
|
||||||
|
* @property {string=} mode
|
||||||
|
* @property {Array} x
|
||||||
|
* @property {Array} y
|
||||||
|
* @property {string=} xaxis
|
||||||
|
* @property {string=} yaxis
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef PlotTrace
|
||||||
|
* @type {(TableTrace|GraphTrace)}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map ansi terminal codes to html color codes.
|
||||||
|
* @param {string} line
|
||||||
|
*/
|
||||||
|
function ansiToHtml(line) {
|
||||||
|
const ansiToHtmlMap = {
|
||||||
|
// Map ANSI color codes to HTML color names or hex values
|
||||||
|
// We don't necessarily handle all the colors but this is enough to start.
|
||||||
|
"30": "black",
|
||||||
|
"31": "red",
|
||||||
|
"32": "green",
|
||||||
|
"33": "yellow",
|
||||||
|
"34": "blue",
|
||||||
|
"35": "magenta",
|
||||||
|
"36": "cyan",
|
||||||
|
"37": "white",
|
||||||
|
"39": "initial"
|
||||||
|
};
|
||||||
|
|
||||||
|
// NOTE(zaphar): Yes this is gross and I should really do a better parser but I'm lazy.
|
||||||
|
// Replace ANSI codes with HTML span elements styled with the corresponding color
|
||||||
|
return line.replace(/\x1b\[([0-9;]*)m/g, (match, p1) => {
|
||||||
|
const parts = p1.split(';'); // ANSI codes can be compounded, e.g., "1;31" for bold red
|
||||||
|
let styles = '';
|
||||||
|
for (let part of parts) {
|
||||||
|
if (ansiToHtmlMap[part]) {
|
||||||
|
// If the code is a color, map it to a CSS color
|
||||||
|
styles += `color: ${ansiToHtmlMap[part]};`;
|
||||||
|
}
|
||||||
|
// TODO(zaphar): Add more conditions here to handle other styles like bold or underline?
|
||||||
|
}
|
||||||
|
return styles ? `<span style="${styles}">` : '</span>';
|
||||||
|
}) + '</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get's a css variable's value from the document.
|
||||||
|
* @param {string} variableName - Name of the variable to get `--var-name`
|
||||||
|
* @returns string
|
||||||
|
*/
|
||||||
function getCssVariableValue(variableName) {
|
function getCssVariableValue(variableName) {
|
||||||
return getComputedStyle(document.documentElement).getPropertyValue(variableName);
|
return getComputedStyle(document.documentElement).getPropertyValue(variableName);
|
||||||
}
|
}
|
||||||
|
|
||||||
class TimeseriesGraph extends HTMLElement {
|
/**
|
||||||
|
* Custom element for showing a plotly graph.
|
||||||
|
*
|
||||||
|
* @extends HTMLElement
|
||||||
|
*/
|
||||||
|
export class GraphPlot extends HTMLElement {
|
||||||
|
/** @type {?string} */
|
||||||
#uri;
|
#uri;
|
||||||
|
/** @type {?number} */
|
||||||
#width;
|
#width;
|
||||||
|
/** @type {?number} */
|
||||||
#height;
|
#height;
|
||||||
|
/** @type {?number} */
|
||||||
#intervalId;
|
#intervalId;
|
||||||
|
/** @type {?number} */
|
||||||
#pollSeconds;
|
#pollSeconds;
|
||||||
|
/** @type {?string} */
|
||||||
#end;
|
#end;
|
||||||
|
/** @type {?number} */
|
||||||
#duration;
|
#duration;
|
||||||
|
/** @type {?string} */
|
||||||
#step_duration;
|
#step_duration;
|
||||||
|
/** @type {?string} */
|
||||||
#d3TickFormat = "~s";
|
#d3TickFormat = "~s";
|
||||||
|
/** @type {?HTMLDivElement} */
|
||||||
#targetNode = null;
|
#targetNode = null;
|
||||||
|
/** @type {?HTMLElement} */
|
||||||
#menuContainer = null;
|
#menuContainer = null;
|
||||||
|
/** @type {Object<string, HTMLSelectElement>} */
|
||||||
#filterSelectElements = {};
|
#filterSelectElements = {};
|
||||||
|
/** @type {Object<string, Array<string>>} */
|
||||||
#filterLabels = {};
|
#filterLabels = {};
|
||||||
|
/** @type {Object<string, Array<string>>} */
|
||||||
#filteredLabelSets = {};
|
#filteredLabelSets = {};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.#width = 800;
|
this.#width = 800;
|
||||||
@ -45,25 +165,32 @@ class TimeseriesGraph extends HTMLElement {
|
|||||||
|
|
||||||
static observedAttributes = ['uri', 'width', 'height', 'poll-seconds', 'end', 'duration', 'step-duration', 'd3-tick-format'];
|
static observedAttributes = ['uri', 'width', 'height', 'poll-seconds', 'end', 'duration', 'step-duration', 'd3-tick-format'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback for attributes changes.
|
||||||
|
*
|
||||||
|
* @param {string} name - The name of the attribute.
|
||||||
|
* @param {?string} _oldValue - The old value for the attribute
|
||||||
|
* @param {?string} newValue - The new value for the attribute
|
||||||
|
*/
|
||||||
attributeChangedCallback(name, _oldValue, newValue) {
|
attributeChangedCallback(name, _oldValue, newValue) {
|
||||||
switch (name) {
|
switch (name) {
|
||||||
case 'uri':
|
case 'uri':
|
||||||
this.#uri = newValue;
|
this.#uri = newValue;
|
||||||
break;
|
break;
|
||||||
case 'width':
|
case 'width':
|
||||||
this.#width = newValue;
|
this.#width = Number(newValue);
|
||||||
break;
|
break;
|
||||||
case 'height':
|
case 'height':
|
||||||
this.#height = newValue;
|
this.#height = Number(newValue);
|
||||||
break;
|
break;
|
||||||
case 'poll-seconds':
|
case 'poll-seconds':
|
||||||
this.#pollSeconds = newValue;
|
this.#pollSeconds = Number(newValue);
|
||||||
break;
|
break;
|
||||||
case 'end':
|
case 'end':
|
||||||
this.#end = newValue;
|
this.#end = newValue;
|
||||||
break;
|
break;
|
||||||
case 'duration':
|
case 'duration':
|
||||||
this.#duration = newValue;
|
this.#duration = Number(newValue);
|
||||||
break;
|
break;
|
||||||
case 'step-duration':
|
case 'step-duration':
|
||||||
this.#step_duration = newValue;
|
this.#step_duration = newValue;
|
||||||
@ -79,14 +206,13 @@ class TimeseriesGraph extends HTMLElement {
|
|||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
this.#uri = this.getAttribute('uri') || this.#uri;
|
this.#uri = this.getAttribute('uri') || this.#uri;
|
||||||
this.#width = this.getAttribute('width') || this.#width;
|
this.#width = Number(this.getAttribute('width') || this.#width);
|
||||||
this.#height = this.getAttribute('height') || this.#height;
|
this.#height = Number(this.getAttribute('height') || this.#height);
|
||||||
this.#pollSeconds = this.getAttribute('poll-seconds') || this.#pollSeconds;
|
this.#pollSeconds = Number(this.getAttribute('poll-seconds') || this.#pollSeconds);
|
||||||
this.#end = this.getAttribute('end') || null;
|
this.#end = this.getAttribute('end') || null;
|
||||||
this.#duration = this.getAttribute('duration') || null;
|
this.#duration = Number(this.getAttribute('duration')) || null;
|
||||||
this.#step_duration = this.getAttribute('step-duration') || null;
|
this.#step_duration = this.getAttribute('step-duration') || null;
|
||||||
this.#d3TickFormat = this.getAttribute('d3-tick-format') || this.#d3TickFormat;
|
this.#d3TickFormat = this.getAttribute('d3-tick-format') || this.#d3TickFormat;
|
||||||
var self = this;
|
|
||||||
this.reset();
|
this.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,12 +220,19 @@ class TimeseriesGraph extends HTMLElement {
|
|||||||
this.stopInterval()
|
this.stopInterval()
|
||||||
}
|
}
|
||||||
|
|
||||||
static elementName = "timeseries-graph";
|
static elementName = "graph-plot";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Get's the target node for placing the plotly graph.
|
||||||
|
*
|
||||||
|
* @returns {?HTMLDivElement}
|
||||||
|
*/
|
||||||
getTargetNode() {
|
getTargetNode() {
|
||||||
return this.#targetNode;
|
return this.#targetNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*/
|
||||||
stopInterval() {
|
stopInterval() {
|
||||||
if (this.#intervalId) {
|
if (this.#intervalId) {
|
||||||
clearInterval(this.#intervalId);
|
clearInterval(this.#intervalId);
|
||||||
@ -107,6 +240,10 @@ class TimeseriesGraph extends HTMLElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the entire graph and then restarts polling.
|
||||||
|
* @param {boolean=} updateOnly
|
||||||
|
*/
|
||||||
reset(updateOnly) {
|
reset(updateOnly) {
|
||||||
var self = this;
|
var self = this;
|
||||||
self.stopInterval()
|
self.stopInterval()
|
||||||
@ -121,12 +258,18 @@ class TimeseriesGraph extends HTMLElement {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Registers the custom element if it doesn't already exist */
|
||||||
static registerElement() {
|
static registerElement() {
|
||||||
if (!customElements.get(TimeseriesGraph.elementName)) {
|
if (!customElements.get(GraphPlot.elementName)) {
|
||||||
customElements.define(TimeseriesGraph.elementName, TimeseriesGraph);
|
customElements.define(GraphPlot.elementName, GraphPlot);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the uri formatted with any query strings if necessary.
|
||||||
|
*
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
getUri() {
|
getUri() {
|
||||||
if (this.#end && this.#duration && this.#step_duration) {
|
if (this.#end && this.#duration && this.#step_duration) {
|
||||||
return this.#uri + "?end=" + this.#end + "&duration=" + this.#duration + "&step_duration=" + this.#step_duration;
|
return this.#uri + "?end=" + this.#end + "&duration=" + this.#duration + "&step_duration=" + this.#step_duration;
|
||||||
@ -135,6 +278,11 @@ class TimeseriesGraph extends HTMLElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the data from an api call.
|
||||||
|
*
|
||||||
|
* @return {Promise<QueryData>}
|
||||||
|
*/
|
||||||
async fetchData() {
|
async fetchData() {
|
||||||
// TODO(zaphar): Can we do some massaging on these
|
// TODO(zaphar): Can we do some massaging on these
|
||||||
// to get the full set of labels and possible values?
|
// to get the full set of labels and possible values?
|
||||||
@ -143,6 +291,12 @@ class TimeseriesGraph extends HTMLElement {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats the name for the plot trace.
|
||||||
|
* @param {{name_format: ?string}} meta
|
||||||
|
* @param {Map<string, string>} labels
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
formatName(meta, labels) {
|
formatName(meta, labels) {
|
||||||
var name = "";
|
var name = "";
|
||||||
const formatter = meta.name_format
|
const formatter = meta.name_format
|
||||||
@ -158,6 +312,9 @@ class TimeseriesGraph extends HTMLElement {
|
|||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Object<string, string>} labels
|
||||||
|
*/
|
||||||
populateFilterData(labels) {
|
populateFilterData(labels) {
|
||||||
for (var key in labels) {
|
for (var key in labels) {
|
||||||
const label = this.#filterLabels[key];
|
const label = this.#filterLabels[key];
|
||||||
@ -171,13 +328,18 @@ class TimeseriesGraph extends HTMLElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} key
|
||||||
|
* @returns {HTMLDivElement}
|
||||||
|
*/
|
||||||
buildSelectElement(key) {
|
buildSelectElement(key) {
|
||||||
// TODO(jwall): Should we have a select all?
|
// TODO(jwall): Should we have a select all?
|
||||||
var id = key + "-select" + Math.random();
|
var id = key + "-select" + Math.random();
|
||||||
const element = document.createElement("div");
|
const element = document.createElement("div");
|
||||||
const select = document.createElement("select");
|
const select = document.createElement("select");
|
||||||
select.setAttribute("name", id);
|
select.setAttribute("name", id);
|
||||||
select.setAttribute("multiple", true);
|
// TODO(jwall): This is how you set boolean attributes. Use the attribute named... :-(
|
||||||
|
select.setAttribute("multiple", "multiple");
|
||||||
const optElement = document.createElement("option");
|
const optElement = document.createElement("option");
|
||||||
const optValue = "Select " + key;
|
const optValue = "Select " + key;
|
||||||
optElement.innerText = optValue;
|
optElement.innerText = optValue;
|
||||||
@ -185,7 +347,7 @@ class TimeseriesGraph extends HTMLElement {
|
|||||||
for (var opt of this.#filterLabels[key]) {
|
for (var opt of this.#filterLabels[key]) {
|
||||||
const optElement = document.createElement("option");
|
const optElement = document.createElement("option");
|
||||||
optElement.setAttribute("value", opt);
|
optElement.setAttribute("value", opt);
|
||||||
optElement.setAttribute("selected", true);
|
optElement.setAttribute("selected", "selected");
|
||||||
optElement.innerText = opt;
|
optElement.innerText = opt;
|
||||||
select.appendChild(optElement);
|
select.appendChild(optElement);
|
||||||
}
|
}
|
||||||
@ -194,7 +356,7 @@ class TimeseriesGraph extends HTMLElement {
|
|||||||
select.onchange = function(evt) {
|
select.onchange = function(evt) {
|
||||||
evt.stopPropagation();
|
evt.stopPropagation();
|
||||||
var filteredValues = [];
|
var filteredValues = [];
|
||||||
for (var opt of evt.target.selectedOptions) {
|
for (var opt of /** @type {HTMLSelectElement} */(evt.target).selectedOptions) {
|
||||||
filteredValues.push(opt.getAttribute("value"));
|
filteredValues.push(opt.getAttribute("value"));
|
||||||
}
|
}
|
||||||
self.#filteredLabelSets[key] = filteredValues;
|
self.#filteredLabelSets[key] = filteredValues;
|
||||||
@ -218,6 +380,9 @@ class TimeseriesGraph extends HTMLElement {
|
|||||||
this.#menuContainer.replaceChildren(...children);
|
this.#menuContainer.replaceChildren(...children);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {QueryData} graph
|
||||||
|
*/
|
||||||
getLabelsForData(graph) {
|
getLabelsForData(graph) {
|
||||||
const data = graph.plots;
|
const data = graph.plots;
|
||||||
for (var subplot of data) {
|
for (var subplot of data) {
|
||||||
@ -233,6 +398,12 @@ class TimeseriesGraph extends HTMLElement {
|
|||||||
this.populateFilterData(labels);
|
this.populateFilterData(labels);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (subplot.Stream) {
|
||||||
|
for (const pair of subplot.Stream) {
|
||||||
|
const labels = pair[0];
|
||||||
|
this.populateFilterData(labels);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -248,6 +419,11 @@ class TimeseriesGraph extends HTMLElement {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the graph with new data.
|
||||||
|
*
|
||||||
|
* @param {?QueryData=} maybeGraph
|
||||||
|
*/
|
||||||
async updateGraph(maybeGraph) {
|
async updateGraph(maybeGraph) {
|
||||||
var graph = maybeGraph;
|
var graph = maybeGraph;
|
||||||
if (!graph) {
|
if (!graph) {
|
||||||
@ -258,13 +434,13 @@ class TimeseriesGraph extends HTMLElement {
|
|||||||
var layout = {
|
var layout = {
|
||||||
displayModeBar: false,
|
displayModeBar: false,
|
||||||
responsive: true,
|
responsive: true,
|
||||||
plot_bgcolor: getCssVariableValue('--paper-background-color').trim(),
|
plot_bgcolor: getCssVariableValue('--plot-background-color').trim(),
|
||||||
paper_bgcolor: getCssVariableValue('--paper-background-color').trim(),
|
paper_bgcolor: getCssVariableValue('--paper-background-color').trim(),
|
||||||
font: {
|
font: {
|
||||||
color: getCssVariableValue('--text-color').trim()
|
color: getCssVariableValue('--text-color').trim()
|
||||||
},
|
},
|
||||||
xaxis: {
|
xaxis: {
|
||||||
gridcolor: getCssVariableValue("--accent-color")
|
gridcolor: getCssVariableValue("--grid-line-color")
|
||||||
},
|
},
|
||||||
legend: {
|
legend: {
|
||||||
orientation: 'v'
|
orientation: 'v'
|
||||||
@ -276,13 +452,12 @@ class TimeseriesGraph extends HTMLElement {
|
|||||||
var nextYaxis = this.yaxisNameGenerator();
|
var nextYaxis = this.yaxisNameGenerator();
|
||||||
for (const yaxis of yaxes) {
|
for (const yaxis of yaxes) {
|
||||||
yaxis.tickformat = yaxis.tickformat || this.#d3TickFormat;
|
yaxis.tickformat = yaxis.tickformat || this.#d3TickFormat;
|
||||||
yaxis.gridColor = getCssVariableValue("--accent-color");
|
yaxis.gridColor = getCssVariableValue("--grid-line-color");
|
||||||
layout[nextYaxis()] = yaxis;
|
layout[nextYaxis()] = yaxis;
|
||||||
}
|
}
|
||||||
var traces = [];
|
var traces = /** @type {Array<PlotTrace>} */ ([]);
|
||||||
for (var subplot_idx in data) {
|
for (var subplot_idx in data) {
|
||||||
const subplot = data[subplot_idx];
|
const subplot = data[subplot_idx];
|
||||||
const subplotCount = Number(subplot_idx) + 1;
|
|
||||||
var nextYaxis = this.yaxisNameGenerator();
|
var nextYaxis = this.yaxisNameGenerator();
|
||||||
if (subplot.Series) {
|
if (subplot.Series) {
|
||||||
// https://plotly.com/javascript/reference/scatter/
|
// https://plotly.com/javascript/reference/scatter/
|
||||||
@ -298,7 +473,7 @@ class TimeseriesGraph extends HTMLElement {
|
|||||||
var yaxis = meta.yaxis || "y";
|
var yaxis = meta.yaxis || "y";
|
||||||
// https://plotly.com/javascript/reference/layout/yaxis/
|
// https://plotly.com/javascript/reference/layout/yaxis/
|
||||||
const series = triple[2];
|
const series = triple[2];
|
||||||
var trace = {
|
const trace = /** @type GraphTrace */({
|
||||||
type: "scatter",
|
type: "scatter",
|
||||||
mode: "lines+text",
|
mode: "lines+text",
|
||||||
x: [],
|
x: [],
|
||||||
@ -307,7 +482,7 @@ class TimeseriesGraph extends HTMLElement {
|
|||||||
xaxis: "x",
|
xaxis: "x",
|
||||||
yaxis: yaxis,
|
yaxis: yaxis,
|
||||||
//yhoverformat: yaxis.tickformat,
|
//yhoverformat: yaxis.tickformat,
|
||||||
};
|
});
|
||||||
if (meta.fill) {
|
if (meta.fill) {
|
||||||
trace.fill = meta.fill;
|
trace.fill = meta.fill;
|
||||||
}
|
}
|
||||||
@ -331,32 +506,88 @@ class TimeseriesGraph extends HTMLElement {
|
|||||||
}
|
}
|
||||||
const meta = triple[1];
|
const meta = triple[1];
|
||||||
const series = triple[2];
|
const series = triple[2];
|
||||||
var trace = {
|
const trace = /** @type GraphTrace */({
|
||||||
type: "bar",
|
type: "bar",
|
||||||
x: [],
|
x: [],
|
||||||
y: [],
|
y: [],
|
||||||
yhoverformat: meta["d3_tick_format"],
|
yhoverformat: meta["d3_tick_format"],
|
||||||
};
|
});
|
||||||
var name = this.formatName(meta, labels);
|
var name = this.formatName(meta, labels);
|
||||||
if (name) { trace.name = name; }
|
if (name) { trace.name = name; }
|
||||||
trace.y.push(series.value);
|
trace.y.push(series.value);
|
||||||
trace.x.push(trace.name);
|
trace.x.push(trace.name);
|
||||||
traces.push(trace);
|
traces.push(trace);
|
||||||
}
|
}
|
||||||
|
} else if (subplot.Stream) {
|
||||||
|
// TODO(jwall): It would be nice if scroll behavior would handle replots better.
|
||||||
|
// TODO(jwall): It's possible that this should actually be a separate custom
|
||||||
|
// element.
|
||||||
|
const trace = /** @type TableTrace */({
|
||||||
|
type: "table",
|
||||||
|
columnwidth: [15, 20, 70],
|
||||||
|
header: {
|
||||||
|
align: "left",
|
||||||
|
values: ["Timestamp","Labels", "Log"],
|
||||||
|
fill: { color: layout.xaxis.paper_bgcolor },
|
||||||
|
font: { color: getCssVariableValue('--text-color').trim() }
|
||||||
|
},
|
||||||
|
cells: {
|
||||||
|
align: "left",
|
||||||
|
values: [],
|
||||||
|
fill: { color: layout.plot_bgcolor }
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const dateColumn = [];
|
||||||
|
const metaColumn = [];
|
||||||
|
const logColumn = [];
|
||||||
|
|
||||||
|
loopStream: for (const pair of subplot.Stream) {
|
||||||
|
const labels = pair[0];
|
||||||
|
var labelList = [];
|
||||||
|
for (var label in labels) {
|
||||||
|
var show = this.#filteredLabelSets[label];
|
||||||
|
if (show && !show.includes(labels[label])) {
|
||||||
|
continue loopStream;
|
||||||
|
}
|
||||||
|
labelList.push(`${label}:${labels[label]}`);
|
||||||
|
}
|
||||||
|
const labelsName = labelList.join("<br>");
|
||||||
|
const lines = pair[1];
|
||||||
|
// TODO(jwall): Headers
|
||||||
|
for (const line of lines) {
|
||||||
|
// For streams the timestamps are in nanoseconds
|
||||||
|
// TODO(zaphar): We should improve the timstamp formatting a bit
|
||||||
|
let timestamp = new Date(line.timestamp / 1000000);
|
||||||
|
dateColumn.push(timestamp.toISOString());
|
||||||
|
metaColumn.push(labelsName);
|
||||||
|
logColumn.push(ansiToHtml(line.line));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
trace.cells.values.push(dateColumn);
|
||||||
|
trace.cells.values.push(metaColumn);
|
||||||
|
trace.cells.values.push(logColumn);
|
||||||
|
traces.push(trace);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// https://plotly.com/javascript/plotlyjs-function-reference/#plotlyreact
|
// https://plotly.com/javascript/plotlyjs-function-reference/#plotlyreact
|
||||||
|
// @ts-ignore
|
||||||
Plotly.react(this.getTargetNode(), traces, layout, null);
|
Plotly.react(this.getTargetNode(), traces, layout, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TimeseriesGraph.registerElement();
|
GraphPlot.registerElement();
|
||||||
|
|
||||||
class SpanSelector extends HTMLElement {
|
/** Custom Element for selecting a timespan for the dashboard. */
|
||||||
|
export class SpanSelector extends HTMLElement {
|
||||||
|
/** @type {HTMLElement} */
|
||||||
#targetNode = null;
|
#targetNode = null;
|
||||||
|
/** @type {HTMLInputElement} */
|
||||||
#endInput = null;
|
#endInput = null;
|
||||||
|
/** @type {HTMLInputElement} */
|
||||||
#durationInput = null;
|
#durationInput = null;
|
||||||
|
/** @type {HTMLInputElement} */
|
||||||
#stepDurationInput = null;
|
#stepDurationInput = null;
|
||||||
|
/** @type {HTMLButtonElement} */
|
||||||
#updateInput = null
|
#updateInput = null
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -388,8 +619,9 @@ class SpanSelector extends HTMLElement {
|
|||||||
this.#updateInput.onclick = undefined;
|
this.#updateInput.onclick = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Updates all the graphs on the dashboard with the new timespan. */
|
||||||
updateGraphs() {
|
updateGraphs() {
|
||||||
for (var node of document.getElementsByTagName(TimeseriesGraph.elementName)) {
|
for (var node of document.getElementsByTagName(GraphPlot.elementName)) {
|
||||||
node.setAttribute('end', this.#endInput.value);
|
node.setAttribute('end', this.#endInput.value);
|
||||||
node.setAttribute('duration', this.#durationInput.value);
|
node.setAttribute('duration', this.#durationInput.value);
|
||||||
node.setAttribute('step-duration', this.#stepDurationInput.value);
|
node.setAttribute('step-duration', this.#stepDurationInput.value);
|
||||||
@ -398,6 +630,7 @@ class SpanSelector extends HTMLElement {
|
|||||||
|
|
||||||
static elementName = "span-selector";
|
static elementName = "span-selector";
|
||||||
|
|
||||||
|
/** Register the element if it doesn't exist */
|
||||||
static registerElement() {
|
static registerElement() {
|
||||||
if (!customElements.get(SpanSelector.elementName)) {
|
if (!customElements.get(SpanSelector.elementName)) {
|
||||||
customElements.define(SpanSelector.elementName, SpanSelector);
|
customElements.define(SpanSelector.elementName, SpanSelector);
|
||||||
@ -406,3 +639,4 @@ class SpanSelector extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SpanSelector.registerElement();
|
SpanSelector.registerElement();
|
||||||
|
|
||||||
|
@ -3,23 +3,12 @@
|
|||||||
--background-color: #FFFFFF; /* Light background */
|
--background-color: #FFFFFF; /* Light background */
|
||||||
--text-color: #333333; /* Dark text for contrast */
|
--text-color: #333333; /* Dark text for contrast */
|
||||||
--paper-background-color: #F0F0F0;
|
--paper-background-color: #F0F0F0;
|
||||||
|
--plot-background-color: #FFFFFF;
|
||||||
--accent-color: #6200EE; /* For buttons and interactive elements */
|
--accent-color: #6200EE; /* For buttons and interactive elements */
|
||||||
|
|
||||||
/* Graph colors */
|
|
||||||
--graph-color-1: #007BFF; /* Blue */
|
|
||||||
--graph-color-2: #28A745; /* Green */
|
|
||||||
--graph-color-3: #DC3545; /* Red */
|
|
||||||
--graph-color-4: #FFC107; /* Yellow */
|
|
||||||
--graph-color-5: #17A2B8; /* Cyan */
|
|
||||||
--graph-color-6: #6C757D; /* Gray */
|
|
||||||
|
|
||||||
/* Axis and grid lines */
|
/* Axis and grid lines */
|
||||||
--axis-color: #CCCCCC;
|
--axis-color: #CCCCCC;
|
||||||
--grid-line-color: #EEEEEE;
|
--grid-line-color: #EEEEEE;
|
||||||
|
|
||||||
/* Tooltip background */
|
|
||||||
--tooltip-background-color: #FFFFFF;
|
|
||||||
--tooltip-text-color: #000000;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
@ -27,24 +16,13 @@
|
|||||||
/* Solarized Dark Base Colors */
|
/* Solarized Dark Base Colors */
|
||||||
--background-color: #002b36; /* base03 */
|
--background-color: #002b36; /* base03 */
|
||||||
--paper-background-color: #003c4a;
|
--paper-background-color: #003c4a;
|
||||||
|
--plot-background-color: rgb(24, 34, 21);
|
||||||
--text-color: #839496; /* base0 */
|
--text-color: #839496; /* base0 */
|
||||||
--accent-color: #268bd2; /* blue */
|
--accent-color: #268bd2; /* blue */
|
||||||
|
|
||||||
/* Graph colors - Solarized Accent Colors */
|
|
||||||
--graph-color-1: #b58900; /* yellow */
|
|
||||||
--graph-color-2: #cb4b16; /* orange */
|
|
||||||
--graph-color-3: #dc322f; /* red */
|
|
||||||
--graph-color-4: #d33682; /* magenta */
|
|
||||||
--graph-color-5: #6c71c4; /* violet */
|
|
||||||
--graph-color-6: #2aa198; /* cyan */
|
|
||||||
|
|
||||||
/* Axis and grid lines */
|
/* Axis and grid lines */
|
||||||
--axis-color: #586e75; /* base01 */
|
--axis-color: #586e75; /* base01 */
|
||||||
--grid-line-color: #073642; /* base02 */
|
--grid-line-color: #073642; /* base02 */
|
||||||
|
|
||||||
/* Tooltip background */
|
|
||||||
--tooltip-background-color: #002b36; /* base03 */
|
|
||||||
--tooltip-text-color: #839496; /* base0 */
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,7 +62,7 @@ body * {
|
|||||||
flex: 0 1 auto;
|
flex: 0 1 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
timeseries-graph {
|
graph-plot {
|
||||||
background-color: var(--paper-background-color);
|
background-color: var(--paper-background-color);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user