diff --git a/examples/example_dashboards.yaml b/examples/example_dashboards.yaml index 75728af..ead9cd1 100644 --- a/examples/example_dashboards.yaml +++ b/examples/example_dashboards.yaml @@ -12,7 +12,7 @@ tickformat: "~%" plots: # List of pluts to show on the graph - 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 name_format: "`${labels.instance}`" # javascript template literal to format the trace name fill: tozeroy @@ -39,7 +39,7 @@ - source: http://heimdall:9001 # You can use the FILTERS placeholder to indicate where user selected filters should be placed. query: | - 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])) + 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: name_format: "`${labels.instance} system`" yaxis: "y" diff --git a/src/dashboard.rs b/src/dashboard.rs index cede8d0..e45e9b9 100644 --- a/src/dashboard.rs +++ b/src/dashboard.rs @@ -224,6 +224,7 @@ impl Graph { 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 diff --git a/src/query/prom.rs b/src/query/prom.rs index 8393060..3a48b0e 100644 --- a/src/query/prom.rs +++ b/src/query/prom.rs @@ -24,6 +24,10 @@ use crate::dashboard::PlotMeta; 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)] pub struct PromQueryConn<'conn> { source: &'conn str, @@ -73,6 +77,7 @@ impl<'conn> PromQueryConn<'conn> { 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 { @@ -84,16 +89,26 @@ impl<'conn> PromQueryConn<'conn> { filter_string.push_str(*v); filter_string.push('"'); } - filter_string.push(','); } - if self.query.contains("FILTERS") { - // TODO(jwall): replace the FILTERS placeholder with our filters - self.query.replace("FILTERS", &filter_string) + 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 { - let mut filter_string_curly = String::from("{"); - filter_string_curly.push_str(&filter_string); - // TODO(jwall): Place them after the first `{` - self.query.replace("{", &filter_string_curly) + self.query.to_string() } } diff --git a/src/routes.rs b/src/routes.rs index 4747825..bf04633 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -28,7 +28,7 @@ use tracing::debug; use crate::dashboard::{ loki_query_data, prom_query_data, AxisDefinition, Dashboard, Graph, GraphSpan, Orientation, LogStream, }; -use crate::query::QueryResult; +use crate::query::{self, QueryResult}; type Config = State>>; @@ -91,6 +91,7 @@ pub async fn graph_query( } fn query_to_filterset<'v, 'a: 'v>(query: &'a HashMap) -> Option> { + 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-") { @@ -153,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_data_uri = format!("/api/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!( div { h2 { (graph.title) " - " a href=(graph_embed_uri) { "embed url" } } @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 { - graph-plot uri=(graph_data_uri) id=(graph_id) { } + graph-plot allow-uri-filters=(allow_filters) uri=(graph_data_uri) id=(graph_id) { } } } ) diff --git a/static/lib.js b/static/lib.js index 913474d..5408137 100644 --- a/static/lib.js +++ b/static/lib.js @@ -64,6 +64,8 @@ function getCssVariableValue(variableName) { export class GraphPlot extends HTMLElement { /** @type {?string} */ #uri; + /** @type {?boolean} */ + #allowUriFilters; /** @type {?number} */ #width; /** @type {?number} */ @@ -103,7 +105,7 @@ export class GraphPlot extends HTMLElement { 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. @@ -138,6 +140,9 @@ export class GraphPlot extends HTMLElement { case 'd3-tick-format': this.#d3TickFormat = newValue; break; + case 'allow-uri-filters': + this.#allowUriFilters = Boolean(newValue); + break; default: // do nothing; break; } @@ -153,6 +158,7 @@ export class GraphPlot extends HTMLElement { this.#duration = Number(this.getAttribute('duration')) || null; this.#step_duration = this.getAttribute('step-duration') || null; this.#d3TickFormat = this.getAttribute('d3-tick-format') || this.#d3TickFormat; + this.#allowUriFilters = Boolean(this.getAttribute('allow-uri-filters')); this.reset(); } @@ -211,8 +217,21 @@ export class GraphPlot extends HTMLElement { * @returns {string} */ getUri() { + //var uriParts = [this.#uri]; + var uriParts = []; 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 { return this.#uri; }