mirror of
https://github.com/zaphar/Heracles.git
synced 2025-07-22 20:19: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.
|
||||
- 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"}'
|
||||
|
10
flake.nix
10
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";
|
||||
};
|
||||
}
|
||||
];
|
||||
|
@ -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<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)]
|
||||
pub struct GraphSpan {
|
||||
@ -47,6 +87,7 @@ pub struct SubPlot {
|
||||
#[derive(Deserialize)]
|
||||
pub struct Graph {
|
||||
pub title: String,
|
||||
pub yaxes: Vec<AxisDefinition>,
|
||||
pub plots: Vec<SubPlot>,
|
||||
pub span: Option<GraphSpan>,
|
||||
pub query_type: QueryType,
|
||||
|
30
src/query.rs
30
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<String>,
|
||||
named_axis: Option<String>,
|
||||
fill: Option<FillTypes>,
|
||||
d3_tick_format: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub enum QueryResult {
|
||||
Series(Vec<(HashMap<String, String>, PlotMeta, Vec<DataPoint>)>),
|
||||
|
@ -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<Arc<Vec<Dashboard>>>;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct GraphPayload {
|
||||
pub yaxes: Vec<AxisDefinition>,
|
||||
pub plots: Vec<QueryResult>,
|
||||
}
|
||||
|
||||
pub async fn graph_query(
|
||||
State(config): Config,
|
||||
Path((dash_idx, graph_idx)): Path<(usize, usize)>,
|
||||
Query(query): Query<HashMap<String, String>>,
|
||||
) -> Json<Vec<QueryResult>> {
|
||||
) -> Json<GraphPayload> {
|
||||
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<Vec<Dashboard>>) -> Router<Config> {
|
||||
|
@ -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) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user