mirror of
https://github.com/zaphar/Heracles.git
synced 2025-07-23 12:39:50 -04:00
feat: Better y axes definition
This commit is contained in:
parent
0af85229c2
commit
557d704a1b
@ -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"}'
|
||||||
|
10
flake.nix
10
flake.nix
@ -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";
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
@ -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,
|
||||||
|
30
src/query.rs
30
src/query.rs
@ -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>)>),
|
||||||
|
@ -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> {
|
||||||
|
@ -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) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user