mirror of
https://github.com/zaphar/Heracles.git
synced 2025-07-23 12:39:50 -04:00
Compare commits
5 Commits
a36a59b900
...
26782b2bfc
Author | SHA1 | Date | |
---|---|---|---|
26782b2bfc | |||
964a5e10e3 | |||
bec6f69645 | |||
7f30d87d75 | |||
9529271b28 |
@ -6,15 +6,15 @@
|
||||
query: 'sum by (instance)(irate(node_cpu_seconds_total{mode="system",job="nodestats"}[5m])) * 100'
|
||||
query_type: Range
|
||||
span:
|
||||
start: 2024-02-10T00:00:00.00Z
|
||||
duration: 2d
|
||||
step_duration: 1min
|
||||
end: now
|
||||
duration: 1d
|
||||
step_duration: 1h
|
||||
name_label: instance
|
||||
- title: Test Dasbboard 2
|
||||
span:
|
||||
start: 2024-02-10T00:00:00.00Z
|
||||
duration: 2d
|
||||
step_duration: 1min
|
||||
end: 2024-02-10T00:00:00.00Z
|
||||
duration: 2 days
|
||||
step_duration: 1 minute
|
||||
graphs:
|
||||
- title: Node cpu
|
||||
source: http://heimdall:9001
|
||||
|
@ -21,9 +21,9 @@ use tracing::{debug, error};
|
||||
|
||||
use crate::query::{QueryConn, QueryType};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct GraphSpan {
|
||||
pub start: DateTime<Utc>,
|
||||
pub end: String,
|
||||
pub duration: String,
|
||||
pub step_duration: String,
|
||||
}
|
||||
@ -46,8 +46,8 @@ pub struct Graph {
|
||||
pub query_type: QueryType,
|
||||
}
|
||||
|
||||
fn duration_from_string(duration: &str) -> Option<Duration> {
|
||||
match parse_duration::parse(duration) {
|
||||
fn duration_from_string(duration_string: &str) -> Option<Duration> {
|
||||
match parse_duration::parse(duration_string) {
|
||||
Ok(d) => match Duration::from_std(d) {
|
||||
Ok(d) => Some(d),
|
||||
Err(e) => {
|
||||
@ -84,21 +84,33 @@ fn graph_span_to_tuple(span: &Option<GraphSpan>) -> Option<(DateTime<Utc>, Durat
|
||||
return None;
|
||||
}
|
||||
};
|
||||
Some((span.start.clone(), duration, step_duration))
|
||||
let end = if span.end == "now" {
|
||||
Utc::now()
|
||||
} else if let Ok(end) = DateTime::parse_from_rfc3339(&span.end) {
|
||||
end.to_utc()
|
||||
} else {
|
||||
error!(?span.end, "Invalid DateTime using current time.");
|
||||
Utc::now()
|
||||
};
|
||||
Some((end, duration, step_duration))
|
||||
}
|
||||
|
||||
impl Graph {
|
||||
pub fn get_query_connection<'conn, 'graph: 'conn>(&'graph self, graph_span: &'graph Option<GraphSpan>) -> QueryConn<'conn> {
|
||||
pub fn get_query_connection<'conn, 'graph: 'conn>(&'graph self, graph_span: &'graph Option<GraphSpan>, query_span: &'graph Option<GraphSpan>) -> QueryConn<'conn> {
|
||||
debug!(
|
||||
query = self.query,
|
||||
source = self.source,
|
||||
"Getting query connection for graph"
|
||||
);
|
||||
let mut conn = QueryConn::new(&self.source, &self.query, self.query_type.clone());
|
||||
if let Some((start, duration, step_duration)) = graph_span_to_tuple(&self.span) {
|
||||
conn = conn.with_span(start, duration, step_duration);
|
||||
} else if let Some((start, duration, step_duration)) = graph_span_to_tuple(graph_span) {
|
||||
conn = conn.with_span(start, duration, step_duration);
|
||||
// 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);
|
||||
}
|
||||
conn
|
||||
}
|
||||
|
50
src/query.rs
50
src/query.rs
@ -28,7 +28,7 @@ pub enum QueryType {
|
||||
}
|
||||
|
||||
pub struct TimeSpan {
|
||||
pub start: DateTime<Utc>,
|
||||
pub end: DateTime<Utc>,
|
||||
pub duration: chrono::Duration,
|
||||
pub step_seconds: i64,
|
||||
}
|
||||
@ -50,38 +50,45 @@ impl<'conn> QueryConn<'conn> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_span(mut self, start: DateTime<Utc>, duration: chrono::Duration, step: chrono::Duration) -> Self {
|
||||
self.span = Some(TimeSpan { start, duration, step_seconds: step.num_seconds() , });
|
||||
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) -> anyhow::Result<PromqlResult> {
|
||||
debug!("Getting results for query");
|
||||
let client = Client::try_from(self.source)?;
|
||||
let (end, start, step_resolution) = if let Some(TimeSpan {
|
||||
start: st,
|
||||
let (start, end, step_resolution) = if let Some(TimeSpan {
|
||||
end,
|
||||
duration: du,
|
||||
step_seconds,
|
||||
}) = self.span
|
||||
{
|
||||
((st + du).timestamp(), st.timestamp(), step_seconds as f64)
|
||||
let start = end - du;
|
||||
debug!(?start, ?end, step_seconds, "Running Query with range values");
|
||||
(start.timestamp(), end.timestamp(), step_seconds as f64)
|
||||
} else {
|
||||
let end = Utc::now().timestamp();
|
||||
let start = end - (60 * 10);
|
||||
(end, start, 30 as f64)
|
||||
let end = Utc::now();
|
||||
let start = end - chrono::Duration::minutes(10);
|
||||
debug!(?start, ?end, step_seconds=30, "Running Query with range values");
|
||||
(start.timestamp(), end.timestamp(), 30 as f64)
|
||||
};
|
||||
debug!(start, end, step_resolution, "Running Query with range values");
|
||||
//debug!(start, end, step_resolution, "Running Query with range values");
|
||||
match self.query_type {
|
||||
QueryType::Range => Ok(client
|
||||
QueryType::Range => {
|
||||
let results = client
|
||||
.query_range(self.query, start, end, step_resolution)
|
||||
.get()
|
||||
.await?),
|
||||
.await?;
|
||||
//debug!(?results, "range results");
|
||||
Ok(results)
|
||||
},
|
||||
QueryType::Scalar => Ok(client.query(self.query).get().await?),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct DataPoint {
|
||||
timestamp: f64,
|
||||
value: f64,
|
||||
@ -93,6 +100,23 @@ pub enum QueryResult {
|
||||
Scalar(Vec<(HashMap<String, String>, 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, trace)) in v.iter().enumerate() {
|
||||
f.write_fmt(format_args!("; {}: meta {:?} datapoint count = {};", idx, tags, trace.len()))?;
|
||||
}
|
||||
}
|
||||
QueryResult::Scalar(v) => {
|
||||
f.write_fmt(format_args!("{} traces", v.len()))?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_samples(data: Data) -> QueryResult {
|
||||
match data {
|
||||
Data::Matrix(mut range) => QueryResult::Series(
|
||||
|
@ -11,7 +11,7 @@
|
||||
// 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::{sync::Arc, collections::HashMap};
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use axum::{
|
||||
extract::{Path, Query, State},
|
||||
@ -22,8 +22,7 @@ use axum::{
|
||||
|
||||
// https://maud.lambda.xyz/getting-started.html
|
||||
use maud::{html, Markup};
|
||||
use tracing::{debug, error};
|
||||
use chrono::prelude::*;
|
||||
use tracing::debug;
|
||||
|
||||
use crate::dashboard::{Dashboard, Graph, GraphSpan};
|
||||
use crate::query::{to_samples, QueryResult};
|
||||
@ -42,25 +41,23 @@ pub async fn graph_query(
|
||||
.get(graph_idx)
|
||||
.expect(&format!("No such graph in dasboard {}", dash_idx));
|
||||
let query_span = {
|
||||
if query.contains_key("start") && query.contains_key("duration") && query.contains_key("step_duration")
|
||||
if query.contains_key("end")
|
||||
&& query.contains_key("duration")
|
||||
&& query.contains_key("step_duration")
|
||||
{
|
||||
if let Ok(start) = DateTime::parse_from_rfc3339(&query["start"]) {
|
||||
Some(GraphSpan {
|
||||
start: start.to_utc(),
|
||||
duration: query["duration"].clone(),
|
||||
step_duration: query["step_duration"].clone(),
|
||||
})
|
||||
} else {
|
||||
error!(?query, "Invalid date time in start for query string");
|
||||
None
|
||||
}
|
||||
// TODO(jwall): handle the now case.
|
||||
Some(GraphSpan {
|
||||
end: query["end"].clone(),
|
||||
duration: query["duration"].clone(),
|
||||
step_duration: query["step_duration"].clone(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
let data = to_samples(
|
||||
graph
|
||||
.get_query_connection(if query_span.is_some() { &query_span } else { &dash.span })
|
||||
.get_query_connection(&dash.span, &query_span)
|
||||
.get_results()
|
||||
.await
|
||||
.expect("Unable to get query results")
|
||||
@ -113,6 +110,7 @@ pub async fn dash_ui(State(config): State<Config>, Path(dash_idx): Path<usize>)
|
||||
.collect::<Vec<(usize, &Graph)>>();
|
||||
html!(
|
||||
h1 { (dash.title) }
|
||||
span-selector {}
|
||||
@for (idx, graph) in &graph_iter {
|
||||
(graph_component(dash_idx, *idx, *graph))
|
||||
}
|
||||
|
@ -11,6 +11,7 @@
|
||||
// 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.
|
||||
|
||||
class TimeseriesGraph extends HTMLElement {
|
||||
#uri;
|
||||
#width;
|
||||
@ -18,6 +19,9 @@ class TimeseriesGraph extends HTMLElement {
|
||||
#intervalId;
|
||||
#pollSeconds;
|
||||
#label;
|
||||
#end;
|
||||
#duration;
|
||||
#step_duration;
|
||||
#targetNode = null;
|
||||
constructor() {
|
||||
super();
|
||||
@ -27,9 +31,9 @@ class TimeseriesGraph extends HTMLElement {
|
||||
this.#targetNode = this.appendChild(document.createElement("div"));
|
||||
}
|
||||
|
||||
static observedAttributes = ['uri', 'width', 'height', 'poll-seconds'];
|
||||
static observedAttributes = ['uri', 'width', 'height', 'poll-seconds', 'end', 'duration', 'step-duration'];
|
||||
|
||||
attributeChanged(name, _oldValue, newValue) {
|
||||
attributeChangedCallback(name, _oldValue, newValue) {
|
||||
switch (name) {
|
||||
case 'uri':
|
||||
this.#uri = newValue;
|
||||
@ -46,6 +50,15 @@ class TimeseriesGraph extends HTMLElement {
|
||||
case 'label':
|
||||
this.#label = newValue;
|
||||
break;
|
||||
case 'end':
|
||||
this.#end = newValue;
|
||||
break;
|
||||
case 'duration':
|
||||
this.#duration = newValue;
|
||||
break;
|
||||
case 'step-duration':
|
||||
this.#step_duration = newValue;
|
||||
break;
|
||||
default: // do nothing;
|
||||
break;
|
||||
}
|
||||
@ -58,6 +71,9 @@ class TimeseriesGraph extends HTMLElement {
|
||||
this.#height = this.getAttribute('height') || this.#height;
|
||||
this.#pollSeconds = this.getAttribute('poll-seconds') || this.#pollSeconds;
|
||||
this.#label = this.getAttribute('label') || null;
|
||||
this.#end = this.getAttribute('end') || null;
|
||||
this.#duration = this.getAttribute('duration') || null;
|
||||
this.#step_duration = this.getAttribute('step-duration') || null;
|
||||
this.resetInterval()
|
||||
}
|
||||
|
||||
@ -68,7 +84,6 @@ class TimeseriesGraph extends HTMLElement {
|
||||
static elementName = "timeseries-graph";
|
||||
|
||||
getTargetNode() {
|
||||
console.log("targetNode: ", this.#targetNode);
|
||||
return this.#targetNode;
|
||||
}
|
||||
|
||||
@ -93,8 +108,16 @@ class TimeseriesGraph extends HTMLElement {
|
||||
}
|
||||
}
|
||||
|
||||
getUri() {
|
||||
if (this.#end && this.#duration && this.#step_duration) {
|
||||
return this.#uri + "?end=" + this.#end + "&duration=" + this.#duration + "&step_duration=" + this.#step_duration;
|
||||
} else {
|
||||
return this.#uri;
|
||||
}
|
||||
}
|
||||
|
||||
async fetchData() {
|
||||
const response = await fetch(this.#uri);
|
||||
const response = await fetch(this.getUri());
|
||||
const data = await response.json();
|
||||
return data;
|
||||
}
|
||||
@ -122,7 +145,6 @@ class TimeseriesGraph extends HTMLElement {
|
||||
x: [],
|
||||
y: []
|
||||
};
|
||||
console.log("labels: ", labels, this.#label);
|
||||
if (labels[this.#label]) {
|
||||
trace.name = labels[this.#label];
|
||||
};
|
||||
@ -132,12 +154,10 @@ class TimeseriesGraph extends HTMLElement {
|
||||
}
|
||||
traces.push(trace);
|
||||
}
|
||||
console.log("Traces: ", traces);
|
||||
// https://plotly.com/javascript/plotlyjs-function-reference/#plotlyreact
|
||||
Plotly.react(this.getTargetNode(), traces, config, layout);
|
||||
} else if (data.Scalar) {
|
||||
// https://plotly.com/javascript/reference/bar/
|
||||
console.log("scalar data: ", data.Scalar);
|
||||
var traces = [];
|
||||
for (const pair of data.Scalar) {
|
||||
const series = pair[1];
|
||||
@ -147,17 +167,69 @@ class TimeseriesGraph extends HTMLElement {
|
||||
x: [],
|
||||
y: []
|
||||
};
|
||||
console.log("labels: ", labels, this.#label);
|
||||
if (labels[this.#label]) {
|
||||
trace.x.push(labels[this.#label]);
|
||||
};
|
||||
trace.y.push(series.value);
|
||||
traces.push(trace);
|
||||
}
|
||||
console.log("Traces: ", traces);
|
||||
Plotly.react(this.getTargetNode(), traces, config, layout);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TimeseriesGraph.registerElement();
|
||||
|
||||
class SpanSelector extends HTMLElement {
|
||||
#targetNode = null;
|
||||
#endInput = null;
|
||||
#durationInput = null;
|
||||
#stepDurationInput = null;
|
||||
#updateInput = null
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.#targetNode = this.appendChild(document.createElement('div'));
|
||||
|
||||
this.#targetNode.appendChild(document.createElement('span')).innerText = "end: ";
|
||||
this.#endInput = this.#targetNode.appendChild(document.createElement('input'));
|
||||
|
||||
this.#targetNode.appendChild(document.createElement('span')).innerText = "duration: ";
|
||||
this.#durationInput = this.#targetNode.appendChild(document.createElement('input'));
|
||||
|
||||
this.#targetNode.appendChild(document.createElement('span')).innerText = "step duration: ";
|
||||
this.#stepDurationInput = this.#targetNode.appendChild(document.createElement('input'));
|
||||
|
||||
this.#updateInput = this.#targetNode.appendChild(document.createElement('button'));
|
||||
this.#updateInput.innerText = "Update";
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
const self = this;
|
||||
self.#updateInput.onclick = function(_evt) {
|
||||
self.updateGraphs()
|
||||
};
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
this.#updateInput.onclick = undefined;
|
||||
}
|
||||
|
||||
updateGraphs() {
|
||||
for (var node of document.getElementsByTagName(TimeseriesGraph.elementName)) {
|
||||
node.setAttribute('end', this.#endInput.value);
|
||||
node.setAttribute('duration', this.#durationInput.value);
|
||||
node.setAttribute('step-duration', this.#stepDurationInput.value);
|
||||
}
|
||||
}
|
||||
|
||||
static elementName = "span-selector";
|
||||
|
||||
static registerElement() {
|
||||
if (!customElements.get(SpanSelector.elementName)) {
|
||||
customElements.define(SpanSelector.elementName, SpanSelector);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SpanSelector.registerElement();
|
||||
|
Loading…
x
Reference in New Issue
Block a user