diff --git a/examples/example_dashboards.yaml b/examples/example_dashboards.yaml index efa86f3..00b0688 100644 --- a/examples/example_dashboards.yaml +++ b/examples/example_dashboards.yaml @@ -3,15 +3,18 @@ graphs: # Each Dashboard can have 1 or more graphs in it. - title: Node cpu # Graphs have titles query_type: Range # The type of graph. Range for timeseries and Scalar for point in time - d3_tick_format: "~s" # Default tick format for the graph y axis + d3_tickformat: "~s" # Default tick format for the graph y axis + yaxes: + - anchor: "y" + # overlaying: "y" + side: left + 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 meta: # metadata for this plot name_format: "`${labels.instance}`" # javascript template literal to format the trace name fill: tozeroy - #d3_tick_format: "~%" # d3 tick format override for this plot's yaxis - #named_axis: "y" # yaxis name to use for this subplots traces span: # The span for this range query end: now # Where the span ends. RFC3339 format with special handling for the now keyword duration: 1d # duration of the span. Uses SI formatting for duration amounts. @@ -23,25 +26,32 @@ step_duration: 1 minute graphs: - title: Node cpu percent - d3_tick_format: "~%" + d3_tickformat: "~%" query_type: Range + yaxes: + - anchor: "y" # This axis is y + tickformat: "~%" + - overlaying: "y" # This axis is y2 but overlays axis y + side: right # show this axis on the right side instead of the left + tickformat: "~%" plots: - source: http://heimdall:9001 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])) meta: - d3_tick_format: "~%" name_format: "`${labels.instance} system`" - named_axis: "y" + yaxis: "y" - source: http://heimdall:9001 query: | sum by (instance)(irate(node_cpu_seconds_total{mode="user",job="nodestats"}[5m])) / sum by (instance)(irate(node_cpu_seconds_total{job="nodestats"}[5m])) meta: - d3_tick_format: "~%" name_format: "`${labels.instance} user`" - named_axis: "y" + yaxis: "y2" - title: Node memory query_type: Scalar + yaxes: + - anchor: "y" + tickformat: "~s" plots: - source: http://heimdall:9001 query: 'node_memory_MemFree_bytes{job="nodestats"}' diff --git a/flake.nix b/flake.nix index 32e617f..7b103f1 100644 --- a/flake.nix +++ b/flake.nix @@ -77,6 +77,11 @@ query_type = "Range"; # yaxis formatting default for this graph d3_tick_format = "~s"; + yaxes = [ + { + tickformat = "~s"; + } + ]; plots = [ { source = "http://heimdall:9001"; @@ -85,9 +90,8 @@ \'\'; meta = { name_function = "''${labels.instance}"; - named_axis = "y"; - # yaxis formatting for this subplot - d3_tick_format = "~s"; + # yaxis to use for this plot + yaxis = "y"; }; } ]; diff --git a/src/dashboard.rs b/src/dashboard.rs index 3d85f58..3e00cb8 100644 --- a/src/dashboard.rs +++ b/src/dashboard.rs @@ -15,12 +15,52 @@ use std::path::Path; use chrono::prelude::*; use chrono::Duration; -use serde::Deserialize; +use serde::{Serialize, Deserialize}; use serde_yaml; use tracing::{debug, error}; use anyhow::Result; -use crate::query::{QueryConn, QueryType, QueryResult, PlotMeta, to_samples}; +use crate::query::{QueryConn, QueryType, QueryResult, to_samples}; + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct PlotMeta { + name_format: Option, + fill: Option, + yaxis: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum FillTypes { + #[serde(rename = "tonexty")] + ToNextY, + #[serde(rename = "tozeroy")] + ToZeroY, + #[serde(rename = "tonextx")] + ToNextX, + #[serde(rename = "tozerox")] + ToZeroX, + #[serde(rename = "toself")] + ToSelf, + #[serde(rename = "tonext")] + ToNext, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum AxisSide { + #[serde(rename = "right")] + Right, + #[serde(rename = "left")] + Left, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct AxisDefinition { + anchor: Option, + overlaying: Option, + side: Option, + #[serde(rename = "tickformat")] + tick_format: Option, +} #[derive(Deserialize, Debug)] pub struct GraphSpan { @@ -47,6 +87,7 @@ pub struct SubPlot { #[derive(Deserialize)] pub struct Graph { pub title: String, + pub yaxes: Vec, pub plots: Vec, pub span: Option, pub query_type: QueryType, diff --git a/src/query.rs b/src/query.rs index b644544..695d94d 100644 --- a/src/query.rs +++ b/src/query.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + // Copyright 2023 Jeremy Wall // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -11,8 +13,6 @@ // 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 prometheus_http_query::{ response::{Data, PromqlResult}, @@ -21,6 +21,8 @@ use prometheus_http_query::{ use serde::{Deserialize, Serialize}; use tracing::debug; +use crate::dashboard::PlotMeta; + #[derive(Deserialize, Clone, Debug)] pub enum QueryType { Range, @@ -117,30 +119,6 @@ pub struct DataPoint { value: f64, } -#[derive(Serialize, Deserialize, Debug, Clone)] -pub enum FillTypes { - #[serde(rename = "tonexty")] - ToNextY, - #[serde(rename = "tozeroy")] - ToZeroY, - #[serde(rename = "tonextx")] - ToNextX, - #[serde(rename = "tozerox")] - ToZeroX, - #[serde(rename = "toself")] - ToSelf, - #[serde(rename = "tonext")] - ToNext, -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct PlotMeta { - name_format: Option, - named_axis: Option, - fill: Option, - d3_tick_format: Option, -} - #[derive(Serialize, Deserialize)] pub enum QueryResult { Series(Vec<(HashMap, PlotMeta, Vec)>), diff --git a/src/routes.rs b/src/routes.rs index a5f4bbf..0720665 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -22,18 +22,25 @@ use axum::{ // https://maud.lambda.xyz/getting-started.html use maud::{html, Markup}; +use serde::{Serialize, Deserialize}; use tracing::debug; -use crate::dashboard::{Dashboard, Graph, GraphSpan, query_data}; -use crate::query::{to_samples, QueryResult}; +use crate::dashboard::{Dashboard, Graph, GraphSpan, AxisDefinition, query_data}; +use crate::query::QueryResult; type Config = State>>; +#[derive(Serialize, Deserialize)] +pub struct GraphPayload { + pub yaxes: Vec, + pub plots: Vec, +} + pub async fn graph_query( State(config): Config, Path((dash_idx, graph_idx)): Path<(usize, usize)>, Query(query): Query>, -) -> Json> { +) -> Json { debug!("Getting data for query"); let dash = config.get(dash_idx).expect("No such dashboard index"); let graph = dash @@ -54,8 +61,8 @@ pub async fn graph_query( None } }; - let data = query_data(graph, dash, query_span).await.expect("Unable to get query results"); - Json(data) + let plots = query_data(graph, dash, query_span).await.expect("Unable to get query results"); + Json(GraphPayload{yaxes: graph.yaxes.clone(), plots}) } pub fn mk_api_routes(config: Arc>) -> Router { diff --git a/static/lib.js b/static/lib.js index b71be79..3b2f16d 100644 --- a/static/lib.js +++ b/static/lib.js @@ -214,7 +214,8 @@ class TimeseriesGraph extends HTMLElement { this.#menuContainer.replaceChildren(...children); } - getLabelsForData(data) { + getLabelsForData(graph) { + const data = graph.plots; for (var subplot of data) { if (subplot.Series) { for (const triple of subplot.Series) { @@ -231,11 +232,25 @@ class TimeseriesGraph extends HTMLElement { } } - async updateGraph(maybeData) { - var data = maybeData; - if (!data) { - data = await this.fetchData(); + yaxisNameGenerator() { + var counter = 1; + return function() { + var name = "yaxis"; + if (counter != 1) { + name = "yaxis" + counter ; + } + counter++; + return name; + }; + } + + async updateGraph(maybeGraph) { + var graph = maybeGraph; + if (!graph) { + graph = await this.fetchData(); } + var data = graph.plots; + var yaxes = graph.yaxes; const config = { legend: { orientation: 'h' @@ -253,11 +268,17 @@ class TimeseriesGraph extends HTMLElement { gridcolor: getCssVariableValue("--accent-color") } }; + var nextYaxis = this.yaxisNameGenerator(); + for (const yaxis of yaxes) { + yaxis.tickformat = yaxis.tickformat || this.#d3TickFormat; + yaxis.gridColor = getCssVariableValue("--accent-color"); + layout[nextYaxis()] = yaxis; + } var traces = []; for (var subplot_idx in data) { const subplot = data[subplot_idx]; const subplotCount = Number(subplot_idx) + 1; - const default_yaxis = "y" + subplotCount + var nextYaxis = this.yaxisNameGenerator(); if (subplot.Series) { // https://plotly.com/javascript/reference/scatter/ loopSeries: for (const triple of subplot.Series) { @@ -269,13 +290,8 @@ class TimeseriesGraph extends HTMLElement { } } const meta = triple[1]; - const yaxis = meta["named_axis"] || default_yaxis; + var yaxis = meta.yaxis || "y"; // https://plotly.com/javascript/reference/layout/yaxis/ - layout["yaxis" + subplotCount] = { - anchor: yaxis, - gridcolor: getCssVariableValue("--accent-color"), - tickformat: meta["d3_tick_format"] || this.#d3TickFormat - }; const series = triple[2]; var trace = { type: "scatter", @@ -285,7 +301,7 @@ class TimeseriesGraph extends HTMLElement { // We always share the x axis for timeseries graphs. xaxis: "x", yaxis: yaxis, - yhoverformat: meta["d3_tick_format"], + //yhoverformat: yaxis.tickformat, }; if (meta.fill) { trace.fill = meta.fill; @@ -300,10 +316,6 @@ class TimeseriesGraph extends HTMLElement { } } else if (subplot.Scalar) { // https://plotly.com/javascript/reference/bar/ - layout["yaxis"] = { - tickformat: this.#d3TickFormat, - gridcolor: getCssVariableValue("--accent-color") - }; loopScalar: for (const triple of subplot.Scalar) { const labels = triple[0]; for (var label in labels) {