mirror of
https://github.com/zaphar/Heracles.git
synced 2025-07-26 13:59:50 -04:00
Compare commits
4 Commits
02314a3309
...
a0b3956d8a
Author | SHA1 | Date | |
---|---|---|---|
a0b3956d8a | |||
0a2bde6990 | |||
a620cd2b1b | |||
cef7b42fac |
@ -12,7 +12,7 @@
|
|||||||
tickformat: "~%"
|
tickformat: "~%"
|
||||||
plots: # List of pluts to show on the graph
|
plots: # List of pluts to show on the graph
|
||||||
- source: http://heimdall:9001 # Prometheus source uri for this plot
|
- source: http://heimdall:9001 # Prometheus source uri for this plot
|
||||||
query: 'sum by (instance)(irate(node_cpu_seconds_total{job="nodestats"}[5m]))' # The PromQL query for this plot
|
query: 'sum by (instance)(irate(node_cpu_seconds_total{FILTERS, job="nodestats"}[5m]))' # The PromQL query for this plot
|
||||||
meta: # metadata for this plot
|
meta: # metadata for this plot
|
||||||
name_format: "`${labels.instance}`" # javascript template literal to format the trace name
|
name_format: "`${labels.instance}`" # javascript template literal to format the trace name
|
||||||
fill: tozeroy
|
fill: tozeroy
|
||||||
@ -37,8 +37,9 @@
|
|||||||
tickformat: "~%"
|
tickformat: "~%"
|
||||||
plots:
|
plots:
|
||||||
- source: http://heimdall:9001
|
- source: http://heimdall:9001
|
||||||
|
# You can use the FILTERS placeholder to indicate where user selected filters should be placed.
|
||||||
query: |
|
query: |
|
||||||
sum by (instance)(irate(node_cpu_seconds_total{mode="system",job="nodestats"}[5m])) / sum by (instance)(irate(node_cpu_seconds_total{job="nodestats"}[5m]))
|
sum by (instance)(irate(node_cpu_seconds_total{FILTERS mode="system",job="nodestats"}[5m])) / sum by (instance)(irate(node_cpu_seconds_total{FILTERS, job="nodestats"}[5m]))
|
||||||
meta:
|
meta:
|
||||||
name_format: "`${labels.instance} system`"
|
name_format: "`${labels.instance} system`"
|
||||||
yaxis: "y"
|
yaxis: "y"
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
// Copyright 2023 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");
|
||||||
@ -120,12 +121,13 @@ pub struct LogStream {
|
|||||||
pub query_type: QueryType,
|
pub query_type: QueryType,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn prom_query_data(
|
pub async fn prom_query_data<'a>(
|
||||||
graph: &Graph,
|
graph: &Graph,
|
||||||
dash: &Dashboard,
|
dash: &Dashboard,
|
||||||
query_span: Option<GraphSpan>,
|
query_span: Option<GraphSpan>,
|
||||||
|
filters: &Option<HashMap<&'a str, &'a str>>,
|
||||||
) -> Result<Vec<QueryResult>> {
|
) -> Result<Vec<QueryResult>> {
|
||||||
let connections = graph.get_query_connections(&dash.span, &query_span);
|
let connections = graph.get_query_connections(&dash.span, &query_span, filters);
|
||||||
let mut data = Vec::new();
|
let mut data = Vec::new();
|
||||||
for conn in connections {
|
for conn in connections {
|
||||||
data.push(prom_to_samples(
|
data.push(prom_to_samples(
|
||||||
@ -205,12 +207,14 @@ 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>,
|
||||||
|
filters: &'graph Option<HashMap<&'graph str, &'graph str>>,
|
||||||
) -> Vec<PromQueryConn<'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,
|
||||||
|
filters = ?filters,
|
||||||
"Getting query connection for graph",
|
"Getting query connection for graph",
|
||||||
);
|
);
|
||||||
let mut conn = PromQueryConn::new(
|
let mut conn = PromQueryConn::new(
|
||||||
@ -219,6 +223,10 @@ impl Graph {
|
|||||||
self.query_type.clone(),
|
self.query_type.clone(),
|
||||||
plot.meta.clone(),
|
plot.meta.clone(),
|
||||||
);
|
);
|
||||||
|
if let Some(filters) = filters {
|
||||||
|
debug!(?filters, "query connection with filters");
|
||||||
|
conn = conn.with_filters(filters);
|
||||||
|
}
|
||||||
// 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) {
|
||||||
|
@ -53,7 +53,7 @@ struct Cli {
|
|||||||
async fn validate(dash: &Dashboard) -> anyhow::Result<()> {
|
async fn validate(dash: &Dashboard) -> anyhow::Result<()> {
|
||||||
if let Some(ref graphs) = dash.graphs {
|
if let Some(ref graphs) = dash.graphs {
|
||||||
for graph in graphs.iter() {
|
for graph in graphs.iter() {
|
||||||
let data = prom_query_data(graph, &dash, None).await;
|
let data = prom_query_data(graph, &dash, None, &None).await;
|
||||||
if data.is_err() {
|
if data.is_err() {
|
||||||
error!(err=?data, "Invalid dashboard graph query or queries");
|
error!(err=?data, "Invalid dashboard graph query or queries");
|
||||||
}
|
}
|
||||||
|
@ -24,12 +24,17 @@ use crate::dashboard::PlotMeta;
|
|||||||
|
|
||||||
use super::{DataPoint, QueryResult, QueryType, TimeSpan};
|
use super::{DataPoint, QueryResult, QueryType, TimeSpan};
|
||||||
|
|
||||||
|
pub const FILTER_PLACEHOLDER: &'static str = "FILTERS";
|
||||||
|
pub const FILTER_COMMA_PLACEHOLDER: &'static str = ",FILTERS";
|
||||||
|
pub const FILTER_PLACEHOLDER_COMMA: &'static str = "FILTERS,";
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct PromQueryConn<'conn> {
|
pub struct PromQueryConn<'conn> {
|
||||||
source: &'conn str,
|
source: &'conn str,
|
||||||
query: &'conn str,
|
query: &'conn str,
|
||||||
span: Option<TimeSpan>,
|
span: Option<TimeSpan>,
|
||||||
query_type: QueryType,
|
query_type: QueryType,
|
||||||
|
filters: Option<&'conn HashMap<&'conn str, &'conn str>>,
|
||||||
pub meta: PlotMeta,
|
pub meta: PlotMeta,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,9 +51,15 @@ impl<'conn> PromQueryConn<'conn> {
|
|||||||
query_type,
|
query_type,
|
||||||
meta,
|
meta,
|
||||||
span: None,
|
span: None,
|
||||||
|
filters: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_filters(mut self, filters: &'conn HashMap<&'conn str, &'conn str>) -> Self {
|
||||||
|
self.filters = Some(filters);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn with_span(
|
pub fn with_span(
|
||||||
mut self,
|
mut self,
|
||||||
end: DateTime<Utc>,
|
end: DateTime<Utc>,
|
||||||
@ -63,6 +74,44 @@ impl<'conn> PromQueryConn<'conn> {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_query(&self) -> String {
|
||||||
|
let first = true;
|
||||||
|
let mut filter_string = String::new();
|
||||||
|
debug!(filters=?self.filters, orig=?self.query, "Filters from request");
|
||||||
|
if let Some(filters) = self.filters {
|
||||||
|
for (k, v) in filters.iter() {
|
||||||
|
if !first {
|
||||||
|
filter_string.push_str(",");
|
||||||
|
}
|
||||||
|
filter_string.push_str(*k);
|
||||||
|
filter_string.push_str("=~");
|
||||||
|
filter_string.push('"');
|
||||||
|
filter_string.push_str(*v);
|
||||||
|
filter_string.push('"');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if self.query.contains(FILTER_PLACEHOLDER_COMMA) {
|
||||||
|
debug!("Replacing Filter comma placeholder");
|
||||||
|
if !filter_string.is_empty() {
|
||||||
|
filter_string.push(',');
|
||||||
|
}
|
||||||
|
self.query.replace(FILTER_PLACEHOLDER, &filter_string)
|
||||||
|
} else if self.query.contains(FILTER_COMMA_PLACEHOLDER) {
|
||||||
|
debug!("Replacing Filter comma placeholder");
|
||||||
|
if !filter_string.is_empty() {
|
||||||
|
let mut temp: String = ",".into();
|
||||||
|
temp.push_str(&filter_string);
|
||||||
|
filter_string = temp;
|
||||||
|
}
|
||||||
|
self.query.replace(FILTER_PLACEHOLDER, &filter_string)
|
||||||
|
} else if self.query.contains(FILTER_PLACEHOLDER) {
|
||||||
|
debug!("Replacing Filter placeholder");
|
||||||
|
self.query.replace(FILTER_PLACEHOLDER, &filter_string)
|
||||||
|
} else {
|
||||||
|
self.query.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get_results(&self) -> anyhow::Result<PromqlResult> {
|
pub async fn get_results(&self) -> anyhow::Result<PromqlResult> {
|
||||||
debug!("Getting results for query");
|
debug!("Getting results for query");
|
||||||
let client = Client::try_from(self.source)?;
|
let client = Client::try_from(self.source)?;
|
||||||
@ -92,16 +141,18 @@ impl<'conn> PromQueryConn<'conn> {
|
|||||||
(start.timestamp(), end.timestamp(), 30 as f64)
|
(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");
|
||||||
|
let query = self.get_query();
|
||||||
|
debug!(?query, "Using promql query");
|
||||||
match self.query_type {
|
match self.query_type {
|
||||||
QueryType::Range => {
|
QueryType::Range => {
|
||||||
let results = client
|
let results = client
|
||||||
.query_range(self.query, start, end, step_resolution)
|
.query_range(&query, start, end, step_resolution)
|
||||||
.get()
|
.get()
|
||||||
.await?;
|
.await?;
|
||||||
//debug!(?results, "range results");
|
//debug!(?results, "range results");
|
||||||
Ok(results)
|
Ok(results)
|
||||||
}
|
}
|
||||||
QueryType::Scalar => Ok(client.query(self.query).get().await?),
|
QueryType::Scalar => Ok(client.query(&query).get().await?),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,7 @@ use tracing::debug;
|
|||||||
use crate::dashboard::{
|
use crate::dashboard::{
|
||||||
loki_query_data, prom_query_data, AxisDefinition, Dashboard, Graph, GraphSpan, Orientation, LogStream,
|
loki_query_data, prom_query_data, AxisDefinition, Dashboard, Graph, GraphSpan, Orientation, LogStream,
|
||||||
};
|
};
|
||||||
use crate::query::QueryResult;
|
use crate::query::{self, QueryResult};
|
||||||
|
|
||||||
type Config = State<Arc<Vec<Dashboard>>>;
|
type Config = State<Arc<Vec<Dashboard>>>;
|
||||||
|
|
||||||
@ -54,7 +54,7 @@ pub async fn loki_query(
|
|||||||
.expect("No logs in this dashboard")
|
.expect("No logs in this dashboard")
|
||||||
.get(loki_idx)
|
.get(loki_idx)
|
||||||
.expect(&format!("No such log query {}", loki_idx));
|
.expect(&format!("No such log query {}", loki_idx));
|
||||||
let plots = vec![loki_query_data(log, dash, query_to_graph_span(query))
|
let plots = vec![loki_query_data(log, dash, query_to_graph_span(&query))
|
||||||
.await
|
.await
|
||||||
.expect("Unable to get log query results")];
|
.expect("Unable to get log query results")];
|
||||||
Json(GraphPayload {
|
Json(GraphPayload {
|
||||||
@ -79,7 +79,8 @@ pub async fn graph_query(
|
|||||||
.expect("No graphs in this dashboard")
|
.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))
|
let filters = query_to_filterset(&query);
|
||||||
|
let plots = prom_query_data(graph, dash, query_to_graph_span(&query), &filters)
|
||||||
.await
|
.await
|
||||||
.expect("Unable to get query results");
|
.expect("Unable to get query results");
|
||||||
Json(GraphPayload {
|
Json(GraphPayload {
|
||||||
@ -89,7 +90,24 @@ pub async fn graph_query(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn query_to_graph_span(query: HashMap<String, String>) -> Option<GraphSpan> {
|
fn query_to_filterset<'v, 'a: 'v>(query: &'a HashMap<String, String>) -> Option<HashMap<&'v str, &'v str>> {
|
||||||
|
debug!(query_params=?query, "Filtering query params to filter requests");
|
||||||
|
let mut label_set = HashMap::new();
|
||||||
|
for (k, v) in query.iter() {
|
||||||
|
if k.starts_with("filter-") {
|
||||||
|
if let Some(label) = k.strip_prefix("filter-") {
|
||||||
|
label_set.insert(label, v.as_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if label_set.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(label_set)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn query_to_graph_span<'a>(query: &'a 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")
|
||||||
@ -136,13 +154,14 @@ pub fn graph_component(dash_idx: usize, graph_idx: usize, graph: &Graph) -> Mark
|
|||||||
let graph_id = format!("graph-{}-{}", dash_idx, graph_idx);
|
let graph_id = format!("graph-{}-{}", dash_idx, graph_idx);
|
||||||
let graph_data_uri = format!("/api/dash/{}/graph/{}", dash_idx, graph_idx);
|
let graph_data_uri = format!("/api/dash/{}/graph/{}", dash_idx, graph_idx);
|
||||||
let graph_embed_uri = format!("/embed/dash/{}/graph/{}", dash_idx, graph_idx);
|
let graph_embed_uri = format!("/embed/dash/{}/graph/{}", dash_idx, graph_idx);
|
||||||
|
let allow_filters = graph.plots.iter().find(|p| p.query.contains(query::FILTER_PLACEHOLDER)).is_some();
|
||||||
html!(
|
html!(
|
||||||
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() {
|
||||||
graph-plot uri=(graph_data_uri) id=(graph_id) d3-tick-format=(graph.d3_tick_format.as_ref().unwrap()) { }
|
graph-plot allow-uri-filters=(allow_filters) uri=(graph_data_uri) id=(graph_id) d3-tick-format=(graph.d3_tick_format.as_ref().unwrap()) { }
|
||||||
} @else {
|
} @else {
|
||||||
graph-plot uri=(graph_data_uri) id=(graph_id) { }
|
graph-plot allow-uri-filters=(allow_filters) uri=(graph_data_uri) id=(graph_id) { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -64,6 +64,8 @@ function getCssVariableValue(variableName) {
|
|||||||
export class GraphPlot extends HTMLElement {
|
export class GraphPlot extends HTMLElement {
|
||||||
/** @type {?string} */
|
/** @type {?string} */
|
||||||
#uri;
|
#uri;
|
||||||
|
/** @type {?boolean} */
|
||||||
|
#allowUriFilters;
|
||||||
/** @type {?number} */
|
/** @type {?number} */
|
||||||
#width;
|
#width;
|
||||||
/** @type {?number} */
|
/** @type {?number} */
|
||||||
@ -103,7 +105,7 @@ export class GraphPlot extends HTMLElement {
|
|||||||
this.#targetNode = this.appendChild(document.createElement("div"));
|
this.#targetNode = this.appendChild(document.createElement("div"));
|
||||||
}
|
}
|
||||||
|
|
||||||
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', 'allow-uri-filter'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Callback for attributes changes.
|
* Callback for attributes changes.
|
||||||
@ -138,6 +140,9 @@ export class GraphPlot extends HTMLElement {
|
|||||||
case 'd3-tick-format':
|
case 'd3-tick-format':
|
||||||
this.#d3TickFormat = newValue;
|
this.#d3TickFormat = newValue;
|
||||||
break;
|
break;
|
||||||
|
case 'allow-uri-filters':
|
||||||
|
this.#allowUriFilters = Boolean(newValue);
|
||||||
|
break;
|
||||||
default: // do nothing;
|
default: // do nothing;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -153,6 +158,7 @@ export class GraphPlot extends HTMLElement {
|
|||||||
this.#duration = Number(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;
|
||||||
|
this.#allowUriFilters = Boolean(this.getAttribute('allow-uri-filters'));
|
||||||
this.reset();
|
this.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -211,8 +217,21 @@ export class GraphPlot extends HTMLElement {
|
|||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
getUri() {
|
getUri() {
|
||||||
|
//var uriParts = [this.#uri];
|
||||||
|
var uriParts = [];
|
||||||
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;
|
uriParts.push("end=" + this.#end);
|
||||||
|
uriParts.push("duration=" + this.#duration);
|
||||||
|
uriParts.push("step_duration=" + this.#step_duration);
|
||||||
|
}
|
||||||
|
if (this.#allowUriFilters) {
|
||||||
|
for (const filterName in this.#filteredLabelSets) {
|
||||||
|
const filterVals = this.#filteredLabelSets[filterName].join("|");
|
||||||
|
uriParts.push(`filter-${filterName}=${filterVals}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (uriParts) {
|
||||||
|
return this.#uri + "?" + uriParts.join('&');
|
||||||
} else {
|
} else {
|
||||||
return this.#uri;
|
return this.#uri;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user