feat: Better y axes definition

This commit is contained in:
Jeremy Wall 2024-02-24 19:53:25 -05:00
parent 0af85229c2
commit 557d704a1b
6 changed files with 113 additions and 61 deletions

View File

@ -3,15 +3,18 @@
graphs: # Each Dashboard can have 1 or more graphs in it. graphs: # Each Dashboard can have 1 or more graphs in it.
- title: Node cpu # Graphs have titles - title: Node cpu # Graphs have titles
query_type: Range # The type of graph. Range for timeseries and Scalar for point in time 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 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{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
#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 span: # The span for this range query
end: now # Where the span ends. RFC3339 format with special handling for the now keyword 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. duration: 1d # duration of the span. Uses SI formatting for duration amounts.
@ -23,25 +26,32 @@
step_duration: 1 minute step_duration: 1 minute
graphs: graphs:
- title: Node cpu percent - title: Node cpu percent
d3_tick_format: "~%" d3_tickformat: "~%"
query_type: Range 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: plots:
- source: http://heimdall:9001 - source: http://heimdall:9001
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{mode="system",job="nodestats"}[5m])) / sum by (instance)(irate(node_cpu_seconds_total{job="nodestats"}[5m]))
meta: meta:
d3_tick_format: "~%"
name_format: "`${labels.instance} system`" name_format: "`${labels.instance} system`"
named_axis: "y" yaxis: "y"
- source: http://heimdall:9001 - source: http://heimdall:9001
query: | 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])) 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: meta:
d3_tick_format: "~%"
name_format: "`${labels.instance} user`" name_format: "`${labels.instance} user`"
named_axis: "y" yaxis: "y2"
- title: Node memory - title: Node memory
query_type: Scalar query_type: Scalar
yaxes:
- anchor: "y"
tickformat: "~s"
plots: plots:
- source: http://heimdall:9001 - source: http://heimdall:9001
query: 'node_memory_MemFree_bytes{job="nodestats"}' query: 'node_memory_MemFree_bytes{job="nodestats"}'

View File

@ -77,6 +77,11 @@
query_type = "Range"; query_type = "Range";
# yaxis formatting default for this graph # yaxis formatting default for this graph
d3_tick_format = "~s"; d3_tick_format = "~s";
yaxes = [
{
tickformat = "~s";
}
];
plots = [ plots = [
{ {
source = "http://heimdall:9001"; source = "http://heimdall:9001";
@ -85,9 +90,8 @@
\'\'; \'\';
meta = { meta = {
name_function = "''${labels.instance}"; name_function = "''${labels.instance}";
named_axis = "y"; # yaxis to use for this plot
# yaxis formatting for this subplot yaxis = "y";
d3_tick_format = "~s";
}; };
} }
]; ];

View File

@ -15,12 +15,52 @@ use std::path::Path;
use chrono::prelude::*; use chrono::prelude::*;
use chrono::Duration; use chrono::Duration;
use serde::Deserialize; use serde::{Serialize, Deserialize};
use serde_yaml; use serde_yaml;
use tracing::{debug, error}; use tracing::{debug, error};
use anyhow::Result; 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<String>,
fill: Option<FillTypes>,
yaxis: Option<String>,
}
#[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<String>,
overlaying: Option<String>,
side: Option<AxisSide>,
#[serde(rename = "tickformat")]
tick_format: Option<String>,
}
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct GraphSpan { pub struct GraphSpan {
@ -47,6 +87,7 @@ pub struct SubPlot {
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct Graph { pub struct Graph {
pub title: String, pub title: String,
pub yaxes: Vec<AxisDefinition>,
pub plots: Vec<SubPlot>, pub plots: Vec<SubPlot>,
pub span: Option<GraphSpan>, pub span: Option<GraphSpan>,
pub query_type: QueryType, pub query_type: QueryType,

View File

@ -1,3 +1,5 @@
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");
@ -11,8 +13,6 @@
// 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},
@ -21,6 +21,8 @@ use prometheus_http_query::{
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tracing::debug; use tracing::debug;
use crate::dashboard::PlotMeta;
#[derive(Deserialize, Clone, Debug)] #[derive(Deserialize, Clone, Debug)]
pub enum QueryType { pub enum QueryType {
Range, Range,
@ -117,30 +119,6 @@ pub struct DataPoint {
value: f64, 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<String>,
named_axis: Option<String>,
fill: Option<FillTypes>,
d3_tick_format: Option<String>,
}
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub enum QueryResult { pub enum QueryResult {
Series(Vec<(HashMap<String, String>, PlotMeta, Vec<DataPoint>)>), Series(Vec<(HashMap<String, String>, PlotMeta, Vec<DataPoint>)>),

View File

@ -22,18 +22,25 @@ 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 tracing::debug; use tracing::debug;
use crate::dashboard::{Dashboard, Graph, GraphSpan, query_data}; use crate::dashboard::{Dashboard, Graph, GraphSpan, AxisDefinition, query_data};
use crate::query::{to_samples, QueryResult}; use crate::query::QueryResult;
type Config = State<Arc<Vec<Dashboard>>>; type Config = State<Arc<Vec<Dashboard>>>;
#[derive(Serialize, Deserialize)]
pub struct GraphPayload {
pub yaxes: Vec<AxisDefinition>,
pub plots: Vec<QueryResult>,
}
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<Vec<QueryResult>> { ) -> 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("No such dashboard index");
let graph = dash let graph = dash
@ -54,8 +61,8 @@ pub async fn graph_query(
None None
} }
}; };
let data = query_data(graph, dash, query_span).await.expect("Unable to get query results"); let plots = query_data(graph, dash, query_span).await.expect("Unable to get query results");
Json(data) Json(GraphPayload{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> {

View File

@ -214,7 +214,8 @@ class TimeseriesGraph extends HTMLElement {
this.#menuContainer.replaceChildren(...children); this.#menuContainer.replaceChildren(...children);
} }
getLabelsForData(data) { getLabelsForData(graph) {
const data = graph.plots;
for (var subplot of data) { for (var subplot of data) {
if (subplot.Series) { if (subplot.Series) {
for (const triple of subplot.Series) { for (const triple of subplot.Series) {
@ -231,11 +232,25 @@ class TimeseriesGraph extends HTMLElement {
} }
} }
async updateGraph(maybeData) { yaxisNameGenerator() {
var data = maybeData; var counter = 1;
if (!data) { return function() {
data = await this.fetchData(); 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 = { const config = {
legend: { legend: {
orientation: 'h' orientation: 'h'
@ -253,11 +268,17 @@ class TimeseriesGraph extends HTMLElement {
gridcolor: getCssVariableValue("--accent-color") 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 = []; var traces = [];
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; const subplotCount = Number(subplot_idx) + 1;
const default_yaxis = "y" + subplotCount var nextYaxis = this.yaxisNameGenerator();
if (subplot.Series) { if (subplot.Series) {
// https://plotly.com/javascript/reference/scatter/ // https://plotly.com/javascript/reference/scatter/
loopSeries: for (const triple of subplot.Series) { loopSeries: for (const triple of subplot.Series) {
@ -269,13 +290,8 @@ class TimeseriesGraph extends HTMLElement {
} }
} }
const meta = triple[1]; const meta = triple[1];
const yaxis = meta["named_axis"] || default_yaxis; var yaxis = meta.yaxis || "y";
// https://plotly.com/javascript/reference/layout/yaxis/ // 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]; const series = triple[2];
var trace = { var trace = {
type: "scatter", type: "scatter",
@ -285,7 +301,7 @@ class TimeseriesGraph extends HTMLElement {
// We always share the x axis for timeseries graphs. // We always share the x axis for timeseries graphs.
xaxis: "x", xaxis: "x",
yaxis: yaxis, yaxis: yaxis,
yhoverformat: meta["d3_tick_format"], //yhoverformat: yaxis.tickformat,
}; };
if (meta.fill) { if (meta.fill) {
trace.fill = meta.fill; trace.fill = meta.fill;
@ -300,10 +316,6 @@ class TimeseriesGraph extends HTMLElement {
} }
} else if (subplot.Scalar) { } else if (subplot.Scalar) {
// https://plotly.com/javascript/reference/bar/ // https://plotly.com/javascript/reference/bar/
layout["yaxis"] = {
tickformat: this.#d3TickFormat,
gridcolor: getCssVariableValue("--accent-color")
};
loopScalar: for (const triple of subplot.Scalar) { loopScalar: for (const triple of subplot.Scalar) {
const labels = triple[0]; const labels = triple[0];
for (var label in labels) { for (var label in labels) {