mirror of
https://github.com/logos-blockchain/logos-execution-zone.git
synced 2026-08-05 20:43:14 +00:00
feat(tools): add dashboard generator
This commit is contained in:
parent
8afd2cea66
commit
008eb247e9
31
.github/workflows/ci.yml
vendored
31
.github/workflows/ci.yml
vendored
@ -353,3 +353,34 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo "✅ Artifacts are up to date"
|
echo "✅ Artifacts are up to date"
|
||||||
|
|
||||||
|
# The dashboard generator is plain Rust (no Docker), so this runs directly in
|
||||||
|
# the CI image container rather than through `run-in-ci-image`.
|
||||||
|
dashboards:
|
||||||
|
needs: ci-image
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: ${{ needs.ci-image.outputs.image }}
|
||||||
|
credentials:
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
timeout-minutes: 30
|
||||||
|
|
||||||
|
name: dashboards
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v5
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.pull_request.head.sha || github.head_ref }}
|
||||||
|
|
||||||
|
- name: Regenerate dashboards
|
||||||
|
run: just regenerate-dashboards
|
||||||
|
|
||||||
|
- name: Check if dashboards match repository
|
||||||
|
run: |
|
||||||
|
git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||||||
|
if ! git diff --exit-code monitoring/grafana/dashboards/; then
|
||||||
|
echo "❌ Dashboards in the repository are out of date!"
|
||||||
|
echo "Please run 'just regenerate-dashboards' and commit the changes."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "✅ Dashboards are up to date"
|
||||||
|
|||||||
19
Cargo.lock
generated
19
Cargo.lock
generated
@ -2292,6 +2292,15 @@ dependencies = [
|
|||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dashboard_gen"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"json-pretty-compact",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "data-encoding"
|
name = "data-encoding"
|
||||||
version = "2.11.0"
|
version = "2.11.0"
|
||||||
@ -4596,6 +4605,16 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "json-pretty-compact"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "62a9c1f06b173b0da0ccc8cae00599d7b3ceda6e76d68be9b6bc2c941adafe0e"
|
||||||
|
dependencies = [
|
||||||
|
"serde_json",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jsonrpsee"
|
name = "jsonrpsee"
|
||||||
version = "0.26.0"
|
version = "0.26.0"
|
||||||
|
|||||||
@ -67,6 +67,7 @@ members = [
|
|||||||
"tools/crypto_primitives_bench",
|
"tools/crypto_primitives_bench",
|
||||||
"tools/integration_bench",
|
"tools/integration_bench",
|
||||||
"tools/cross_zone_chat",
|
"tools/cross_zone_chat",
|
||||||
|
"tools/dashboard_gen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
|
|||||||
7
Justfile
7
Justfile
@ -53,6 +53,13 @@ regenerate-test-fixture:
|
|||||||
@echo "🧪 Regenerating test fixture"
|
@echo "🧪 Regenerating test fixture"
|
||||||
RISC0_DEV_MODE=1 cargo run -p test_fixtures --bin regenerate_test_fixture
|
RISC0_DEV_MODE=1 cargo run -p test_fixtures --bin regenerate_test_fixture
|
||||||
|
|
||||||
|
# Regenerate the committed Grafana dashboards from the Rust generator
|
||||||
|
# (tools/dashboard_gen) and commit the result. CI checks these are up to date.
|
||||||
|
regenerate-dashboards:
|
||||||
|
@echo "📊 Regenerating Grafana dashboards"
|
||||||
|
@cargo build -q -p dashboard_gen
|
||||||
|
@cargo run -q -p dashboard_gen > monitoring/grafana/dashboards/sequencer.json
|
||||||
|
|
||||||
# Run criterion benches: fast crypto primitives, then the slow PPE verify (real proving setup).
|
# Run criterion benches: fast crypto primitives, then the slow PPE verify (real proving setup).
|
||||||
bench:
|
bench:
|
||||||
@echo "📊 Running criterion benches"
|
@echo "📊 Running criterion benches"
|
||||||
|
|||||||
@ -1,24 +1,20 @@
|
|||||||
{
|
{
|
||||||
"annotations": { "list": [] },
|
"annotations": { "list": [ ] },
|
||||||
"editable": true,
|
"editable": true,
|
||||||
"graphTooltip": 1,
|
"graphTooltip": 1,
|
||||||
"panels": [
|
"panels": [
|
||||||
{
|
{
|
||||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||||
"fieldConfig": {
|
"fieldConfig": {
|
||||||
"defaults": {
|
"defaults": { "color": { "mode": "fixed", "fixedColor": "blue" }, "unit": "short", "decimals": 0 },
|
||||||
"color": { "mode": "fixed", "fixedColor": "blue" },
|
"overrides": [ ]
|
||||||
"unit": "short",
|
|
||||||
"decimals": 0
|
|
||||||
},
|
|
||||||
"overrides": []
|
|
||||||
},
|
},
|
||||||
"gridPos": { "h": 7, "w": 6, "x": 0, "y": 0 },
|
"gridPos": { "h": 7, "w": 6, "x": 0, "y": 0 },
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"options": {
|
"options": {
|
||||||
"colorMode": "value",
|
"colorMode": "value",
|
||||||
"graphMode": "area",
|
"graphMode": "area",
|
||||||
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }
|
"reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }
|
||||||
},
|
},
|
||||||
"targets": [
|
"targets": [
|
||||||
{
|
{
|
||||||
@ -34,16 +30,13 @@
|
|||||||
{
|
{
|
||||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||||
"fieldConfig": {
|
"fieldConfig": {
|
||||||
"defaults": {
|
"defaults": { "custom": { "drawStyle": "line", "lineWidth": 1, "fillOpacity": 10 }, "unit": "short" },
|
||||||
"custom": { "drawStyle": "line", "lineWidth": 1, "fillOpacity": 10 },
|
"overrides": [ ]
|
||||||
"unit": "short"
|
|
||||||
},
|
|
||||||
"overrides": []
|
|
||||||
},
|
},
|
||||||
"gridPos": { "h": 7, "w": 18, "x": 6, "y": 0 },
|
"gridPos": { "h": 7, "w": 18, "x": 6, "y": 0 },
|
||||||
"id": 2,
|
"id": 2,
|
||||||
"options": {
|
"options": {
|
||||||
"legend": { "displayMode": "list", "placement": "bottom", "calcs": ["last", "max"] },
|
"legend": { "displayMode": "list", "placement": "bottom", "calcs": [ "last", "max" ] },
|
||||||
"tooltip": { "mode": "single" }
|
"tooltip": { "mode": "single" }
|
||||||
},
|
},
|
||||||
"targets": [
|
"targets": [
|
||||||
@ -60,15 +53,12 @@
|
|||||||
{
|
{
|
||||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||||
"fieldConfig": {
|
"fieldConfig": {
|
||||||
"defaults": {
|
"defaults": { "custom": { "drawStyle": "line", "lineWidth": 1, "fillOpacity": 10 }, "unit": "s" },
|
||||||
"custom": { "drawStyle": "line", "lineWidth": 1, "fillOpacity": 10 },
|
|
||||||
"unit": "s"
|
|
||||||
},
|
|
||||||
"overrides": [
|
"overrides": [
|
||||||
{
|
{
|
||||||
"matcher": { "id": "byName", "options": "avg" },
|
"matcher": { "id": "byName", "options": "avg" },
|
||||||
"properties": [
|
"properties": [
|
||||||
{ "id": "custom.lineStyle", "value": { "dash": [8, 4], "fill": "dash" } },
|
{ "id": "custom.lineStyle", "value": { "dash": [ 8, 4 ], "fill": "dash" } },
|
||||||
{ "id": "color", "value": { "mode": "fixed", "fixedColor": "text" } }
|
{ "id": "color", "value": { "mode": "fixed", "fixedColor": "text" } }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -77,7 +67,7 @@
|
|||||||
"gridPos": { "h": 9, "w": 24, "x": 0, "y": 7 },
|
"gridPos": { "h": 9, "w": 24, "x": 0, "y": 7 },
|
||||||
"id": 3,
|
"id": 3,
|
||||||
"options": {
|
"options": {
|
||||||
"legend": { "displayMode": "table", "placement": "bottom", "calcs": ["last", "max"] },
|
"legend": { "displayMode": "table", "placement": "bottom", "calcs": [ "last", "max" ] },
|
||||||
"tooltip": { "mode": "multi", "sort": "desc" }
|
"tooltip": { "mode": "multi", "sort": "desc" }
|
||||||
},
|
},
|
||||||
"targets": [
|
"targets": [
|
||||||
@ -118,16 +108,13 @@
|
|||||||
{
|
{
|
||||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||||
"fieldConfig": {
|
"fieldConfig": {
|
||||||
"defaults": {
|
"defaults": { "custom": { "drawStyle": "line", "lineWidth": 1, "fillOpacity": 10 }, "unit": "s" },
|
||||||
"custom": { "drawStyle": "line", "lineWidth": 1, "fillOpacity": 10 },
|
"overrides": [ ]
|
||||||
"unit": "s"
|
|
||||||
},
|
|
||||||
"overrides": []
|
|
||||||
},
|
},
|
||||||
"gridPos": { "h": 9, "w": 12, "x": 0, "y": 16 },
|
"gridPos": { "h": 9, "w": 12, "x": 0, "y": 16 },
|
||||||
"id": 4,
|
"id": 4,
|
||||||
"options": {
|
"options": {
|
||||||
"legend": { "displayMode": "table", "placement": "bottom", "calcs": ["last", "max"] },
|
"legend": { "displayMode": "table", "placement": "bottom", "calcs": [ "last", "max" ] },
|
||||||
"tooltip": { "mode": "multi", "sort": "desc" }
|
"tooltip": { "mode": "multi", "sort": "desc" }
|
||||||
},
|
},
|
||||||
"targets": [
|
"targets": [
|
||||||
@ -166,12 +153,12 @@
|
|||||||
"custom": { "drawStyle": "line", "lineWidth": 1, "fillOpacity": 10, "spanNulls": true },
|
"custom": { "drawStyle": "line", "lineWidth": 1, "fillOpacity": 10, "spanNulls": true },
|
||||||
"unit": "short"
|
"unit": "short"
|
||||||
},
|
},
|
||||||
"overrides": []
|
"overrides": [ ]
|
||||||
},
|
},
|
||||||
"gridPos": { "h": 9, "w": 12, "x": 12, "y": 16 },
|
"gridPos": { "h": 9, "w": 12, "x": 12, "y": 16 },
|
||||||
"id": 5,
|
"id": 5,
|
||||||
"options": {
|
"options": {
|
||||||
"legend": { "displayMode": "list", "placement": "bottom", "calcs": ["last", "max"] },
|
"legend": { "displayMode": "list", "placement": "bottom", "calcs": [ "last", "max" ] },
|
||||||
"tooltip": { "mode": "single" }
|
"tooltip": { "mode": "single" }
|
||||||
},
|
},
|
||||||
"targets": [
|
"targets": [
|
||||||
@ -188,15 +175,12 @@
|
|||||||
{
|
{
|
||||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||||
"fieldConfig": {
|
"fieldConfig": {
|
||||||
"defaults": {
|
"defaults": { "custom": { "drawStyle": "line", "lineWidth": 1, "fillOpacity": 10 }, "unit": "short" },
|
||||||
"custom": { "drawStyle": "line", "lineWidth": 1, "fillOpacity": 10 },
|
|
||||||
"unit": "short"
|
|
||||||
},
|
|
||||||
"overrides": [
|
"overrides": [
|
||||||
{
|
{
|
||||||
"matcher": { "id": "byName", "options": "avg" },
|
"matcher": { "id": "byName", "options": "avg" },
|
||||||
"properties": [
|
"properties": [
|
||||||
{ "id": "custom.lineStyle", "value": { "dash": [8, 4], "fill": "dash" } },
|
{ "id": "custom.lineStyle", "value": { "dash": [ 8, 4 ], "fill": "dash" } },
|
||||||
{ "id": "color", "value": { "mode": "fixed", "fixedColor": "text" } }
|
{ "id": "color", "value": { "mode": "fixed", "fixedColor": "text" } }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -205,7 +189,7 @@
|
|||||||
"gridPos": { "h": 9, "w": 12, "x": 0, "y": 25 },
|
"gridPos": { "h": 9, "w": 12, "x": 0, "y": 25 },
|
||||||
"id": 6,
|
"id": 6,
|
||||||
"options": {
|
"options": {
|
||||||
"legend": { "displayMode": "table", "placement": "bottom", "calcs": ["last", "max"] },
|
"legend": { "displayMode": "table", "placement": "bottom", "calcs": [ "last", "max" ] },
|
||||||
"tooltip": { "mode": "multi", "sort": "desc" }
|
"tooltip": { "mode": "multi", "sort": "desc" }
|
||||||
},
|
},
|
||||||
"targets": [
|
"targets": [
|
||||||
@ -246,25 +230,22 @@
|
|||||||
{
|
{
|
||||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||||
"fieldConfig": {
|
"fieldConfig": {
|
||||||
"defaults": {
|
"defaults": { "custom": { "drawStyle": "line", "lineWidth": 1, "fillOpacity": 10 }, "unit": "short" },
|
||||||
"custom": { "drawStyle": "line", "lineWidth": 1, "fillOpacity": 10 },
|
|
||||||
"unit": "short"
|
|
||||||
},
|
|
||||||
"overrides": [
|
"overrides": [
|
||||||
{
|
{
|
||||||
"matcher": { "id": "byName", "options": "failed" },
|
"matcher": { "id": "byName", "options": "failed" },
|
||||||
"properties": [{ "id": "color", "value": { "mode": "fixed", "fixedColor": "red" } }]
|
"properties": [ { "id": "color", "value": { "mode": "fixed", "fixedColor": "red" } } ]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"matcher": { "id": "byName", "options": "submitted" },
|
"matcher": { "id": "byName", "options": "submitted" },
|
||||||
"properties": [{ "id": "color", "value": { "mode": "fixed", "fixedColor": "green" } }]
|
"properties": [ { "id": "color", "value": { "mode": "fixed", "fixedColor": "green" } } ]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"gridPos": { "h": 9, "w": 12, "x": 12, "y": 25 },
|
"gridPos": { "h": 9, "w": 12, "x": 12, "y": 25 },
|
||||||
"id": 7,
|
"id": 7,
|
||||||
"options": {
|
"options": {
|
||||||
"legend": { "displayMode": "table", "placement": "bottom", "calcs": ["last", "max"] },
|
"legend": { "displayMode": "table", "placement": "bottom", "calcs": [ "last", "max" ] },
|
||||||
"tooltip": { "mode": "multi", "sort": "desc" }
|
"tooltip": { "mode": "multi", "sort": "desc" }
|
||||||
},
|
},
|
||||||
"targets": [
|
"targets": [
|
||||||
@ -287,8 +268,8 @@
|
|||||||
],
|
],
|
||||||
"refresh": "5s",
|
"refresh": "5s",
|
||||||
"schemaVersion": 39,
|
"schemaVersion": 39,
|
||||||
"tags": ["sequencer"],
|
"tags": [ "sequencer" ],
|
||||||
"templating": { "list": [] },
|
"templating": { "list": [ ] },
|
||||||
"time": { "from": "now-15m", "to": "now" },
|
"time": { "from": "now-15m", "to": "now" },
|
||||||
"timezone": "",
|
"timezone": "",
|
||||||
"title": "Sequencer",
|
"title": "Sequencer",
|
||||||
|
|||||||
13
tools/dashboard_gen/Cargo.toml
Normal file
13
tools/dashboard_gen/Cargo.toml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
[package]
|
||||||
|
name = "dashboard_gen"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
license = { workspace = true }
|
||||||
|
|
||||||
|
# NOTE: experiment — intentionally NOT inheriting `[lints] workspace = true`.
|
||||||
|
# If we keep this crate, turn workspace lints on and fix the fallout.
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde = { workspace = true, features = ["derive", "alloc"] }
|
||||||
|
serde_json.workspace = true
|
||||||
|
json-pretty-compact = "0.1.2"
|
||||||
397
tools/dashboard_gen/src/lib.rs
Normal file
397
tools/dashboard_gen/src/lib.rs
Normal file
@ -0,0 +1,397 @@
|
|||||||
|
//! A tiny, hand-rolled Grafana dashboard builder.
|
||||||
|
//!
|
||||||
|
//! This is a deliberately small subset of what the (Rust-less) Grafana
|
||||||
|
//! Foundation SDK does: model only the panel types and options we actually use,
|
||||||
|
//! expose a fluent builder, and render to the same dashboard JSON Grafana
|
||||||
|
//! provisions. The payoff over hand-written JSON:
|
||||||
|
//!
|
||||||
|
//! * metric names live in Rust `const`s, so a rename is a compile error here;
|
||||||
|
//! * repetitive structure (percentile targets, grid layout, legend/tooltip defaults) collapses into
|
||||||
|
//! a single call instead of copy-pasted JSON.
|
||||||
|
//!
|
||||||
|
//! Build a [`Dashboard`] and serialize it directly.
|
||||||
|
|
||||||
|
use schema::{
|
||||||
|
Calc, Color, Custom, Datasource, Defaults, DrawStyle, EmptyList, FieldConfig, Fill, GraphMode,
|
||||||
|
GridPos, Legend, LegendDisplay, LineStyle, Matcher, MatcherKind, Options, OverrideProperty,
|
||||||
|
PanelModel, PanelType, Placement, PropertyId, PropertyValue, ReduceOptions, SortOrder,
|
||||||
|
StatColorMode, StatOptions, TimeRange, TimeSeriesOptions, Tooltip, TooltipMode,
|
||||||
|
};
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
mod schema;
|
||||||
|
|
||||||
|
/// Datasource uid every panel/target points at. Dashboards stay portable across
|
||||||
|
/// environments because they reference the datasource by this stable uid rather
|
||||||
|
/// than by a per-environment URL.
|
||||||
|
pub const DATASOURCE_UID: &str = "prometheus";
|
||||||
|
|
||||||
|
fn default_unit() -> String {
|
||||||
|
"short".to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ref_letter(index: usize) -> String {
|
||||||
|
((b'A' + index as u8) as char).to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A single Prometheus query within a panel.
|
||||||
|
#[derive(Clone, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Target {
|
||||||
|
datasource: Datasource,
|
||||||
|
expr: String,
|
||||||
|
#[serde(rename = "legendFormat")]
|
||||||
|
legend: String,
|
||||||
|
ref_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Target {
|
||||||
|
pub fn new(expr: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
datasource: Datasource::prometheus(),
|
||||||
|
expr: expr.into(),
|
||||||
|
legend: String::new(),
|
||||||
|
ref_id: ref_letter(0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn legend(mut self, legend: impl Into<String>) -> Self {
|
||||||
|
self.legend = legend.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A per-series style override, matched by series name.
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct FieldOverride {
|
||||||
|
matcher: Matcher,
|
||||||
|
properties: Vec<OverrideProperty>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FieldOverride {
|
||||||
|
pub fn by_name(name: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
matcher: Matcher {
|
||||||
|
id: MatcherKind::ByName,
|
||||||
|
options: name.into(),
|
||||||
|
},
|
||||||
|
properties: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fixed_color(mut self, color: impl Into<String>) -> Self {
|
||||||
|
self.properties.push(OverrideProperty {
|
||||||
|
id: PropertyId::Color,
|
||||||
|
value: PropertyValue::Color(Color::fixed(color.into())),
|
||||||
|
});
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dashed_line(mut self) -> Self {
|
||||||
|
self.properties.push(OverrideProperty {
|
||||||
|
id: PropertyId::LineStyle,
|
||||||
|
value: PropertyValue::LineStyle(LineStyle {
|
||||||
|
dash: [8, 4],
|
||||||
|
fill: Fill::Dash,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
enum Kind {
|
||||||
|
Stat,
|
||||||
|
TimeSeries,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A dashboard panel builder. Grid position and panel id are assigned by
|
||||||
|
/// [`Dashboard::row`]; everything else is set here.
|
||||||
|
pub struct Panel {
|
||||||
|
title: String,
|
||||||
|
kind: Kind,
|
||||||
|
targets: Vec<Target>,
|
||||||
|
width: u32,
|
||||||
|
unit: Option<String>,
|
||||||
|
decimals: Option<u32>,
|
||||||
|
fixed_color: Option<String>,
|
||||||
|
span_nulls: bool,
|
||||||
|
overrides: Vec<FieldOverride>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Panel {
|
||||||
|
fn new(title: impl Into<String>, kind: Kind) -> Self {
|
||||||
|
Self {
|
||||||
|
title: title.into(),
|
||||||
|
kind,
|
||||||
|
targets: Vec::new(),
|
||||||
|
width: 0,
|
||||||
|
unit: None,
|
||||||
|
decimals: None,
|
||||||
|
fixed_color: None,
|
||||||
|
span_nulls: false,
|
||||||
|
overrides: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A single big-number panel.
|
||||||
|
pub fn stat(title: impl Into<String>) -> Self {
|
||||||
|
Self::new(title, Kind::Stat)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A time-series line panel.
|
||||||
|
pub fn timeseries(title: impl Into<String>) -> Self {
|
||||||
|
Self::new(title, Kind::TimeSeries)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Grid width in Grafana's 24-column units. Unset panels split the row's
|
||||||
|
/// remaining width evenly.
|
||||||
|
pub fn width(mut self, width: u32) -> Self {
|
||||||
|
self.width = width;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unit(mut self, unit: impl Into<String>) -> Self {
|
||||||
|
self.unit = Some(unit.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decimals(mut self, decimals: u32) -> Self {
|
||||||
|
self.decimals = Some(decimals);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fixed_color(mut self, color: impl Into<String>) -> Self {
|
||||||
|
self.fixed_color = Some(color.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn span_nulls(mut self) -> Self {
|
||||||
|
self.span_nulls = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn target(mut self, target: Target) -> Self {
|
||||||
|
self.targets.push(target);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn targets(mut self, targets: impl IntoIterator<Item = Target>) -> Self {
|
||||||
|
self.targets.extend(targets);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_override(mut self, over: FieldOverride) -> Self {
|
||||||
|
self.overrides.push(over);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finalize(self, id: u32, grid_pos: GridPos) -> PanelModel {
|
||||||
|
let targets: Vec<Target> = self
|
||||||
|
.targets
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, mut t)| {
|
||||||
|
t.ref_id = ref_letter(i);
|
||||||
|
t
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let unit = self.unit.unwrap_or_else(default_unit);
|
||||||
|
let (defaults, options, panel_type) = match self.kind {
|
||||||
|
Kind::Stat => {
|
||||||
|
let defaults = Defaults {
|
||||||
|
color: self.fixed_color.map(Color::fixed),
|
||||||
|
custom: None,
|
||||||
|
unit,
|
||||||
|
decimals: self.decimals,
|
||||||
|
};
|
||||||
|
let options = Options::Stat(StatOptions {
|
||||||
|
color_mode: StatColorMode::Value,
|
||||||
|
graph_mode: GraphMode::Area,
|
||||||
|
reduce_options: ReduceOptions {
|
||||||
|
calcs: vec![Calc::LastNotNull],
|
||||||
|
fields: "",
|
||||||
|
values: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
(defaults, options, PanelType::Stat)
|
||||||
|
}
|
||||||
|
Kind::TimeSeries => {
|
||||||
|
let defaults = Defaults {
|
||||||
|
color: None,
|
||||||
|
custom: Some(Custom {
|
||||||
|
draw_style: DrawStyle::Line,
|
||||||
|
line_width: 1,
|
||||||
|
fill_opacity: 10,
|
||||||
|
span_nulls: self.span_nulls.then_some(true),
|
||||||
|
}),
|
||||||
|
unit,
|
||||||
|
decimals: None,
|
||||||
|
};
|
||||||
|
// Panels with several series read better as a sortable table with
|
||||||
|
// a multi-series tooltip; single-series panels stay compact.
|
||||||
|
let multi = targets.len() > 1;
|
||||||
|
let options = Options::TimeSeries(TimeSeriesOptions {
|
||||||
|
legend: Legend {
|
||||||
|
display_mode: if multi {
|
||||||
|
LegendDisplay::Table
|
||||||
|
} else {
|
||||||
|
LegendDisplay::List
|
||||||
|
},
|
||||||
|
placement: Placement::Bottom,
|
||||||
|
calcs: vec![Calc::Last, Calc::Max],
|
||||||
|
},
|
||||||
|
tooltip: if multi {
|
||||||
|
Tooltip {
|
||||||
|
mode: TooltipMode::Multi,
|
||||||
|
sort: Some(SortOrder::Desc),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Tooltip {
|
||||||
|
mode: TooltipMode::Single,
|
||||||
|
sort: None,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
(defaults, options, PanelType::Timeseries)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
PanelModel {
|
||||||
|
datasource: Datasource::prometheus(),
|
||||||
|
field_config: FieldConfig {
|
||||||
|
defaults,
|
||||||
|
overrides: self.overrides,
|
||||||
|
},
|
||||||
|
grid_pos,
|
||||||
|
id,
|
||||||
|
options,
|
||||||
|
targets,
|
||||||
|
title: self.title,
|
||||||
|
panel_type,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A dashboard, built row by row. Serialize it directly to get the JSON.
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Dashboard {
|
||||||
|
annotations: EmptyList,
|
||||||
|
editable: bool,
|
||||||
|
graph_tooltip: u32,
|
||||||
|
panels: Vec<PanelModel>,
|
||||||
|
refresh: String,
|
||||||
|
schema_version: u32,
|
||||||
|
tags: Vec<String>,
|
||||||
|
templating: EmptyList,
|
||||||
|
time: TimeRange,
|
||||||
|
timezone: String,
|
||||||
|
title: String,
|
||||||
|
uid: String,
|
||||||
|
|
||||||
|
// Layout cursor — not part of the dashboard schema.
|
||||||
|
#[serde(skip)]
|
||||||
|
next_id: u32,
|
||||||
|
#[serde(skip)]
|
||||||
|
cursor_y: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Dashboard {
|
||||||
|
pub fn new(title: impl Into<String>, uid: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
annotations: EmptyList::default(),
|
||||||
|
editable: true,
|
||||||
|
graph_tooltip: 1,
|
||||||
|
panels: Vec::new(),
|
||||||
|
refresh: "5s".to_owned(),
|
||||||
|
schema_version: 39,
|
||||||
|
tags: Vec::new(),
|
||||||
|
templating: EmptyList::default(),
|
||||||
|
time: TimeRange {
|
||||||
|
from: "now-15m",
|
||||||
|
to: "now",
|
||||||
|
},
|
||||||
|
timezone: String::new(),
|
||||||
|
title: title.into(),
|
||||||
|
uid: uid.into(),
|
||||||
|
next_id: 1,
|
||||||
|
cursor_y: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tag(mut self, tag: impl Into<String>) -> Self {
|
||||||
|
self.tags.push(tag.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn refresh(mut self, refresh: impl Into<String>) -> Self {
|
||||||
|
self.refresh = refresh.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Place a horizontal row of panels at the current vertical cursor. Panel
|
||||||
|
/// ids, x offsets and y are assigned here; unset widths split the remaining
|
||||||
|
/// 24 columns evenly.
|
||||||
|
pub fn row(mut self, height: u32, panels: impl IntoIterator<Item = Panel>) -> Self {
|
||||||
|
let panels: Vec<Panel> = panels.into_iter().collect();
|
||||||
|
let specified: u32 = panels.iter().map(|p| p.width).sum();
|
||||||
|
let auto_count = panels.iter().filter(|p| p.width == 0).count() as u32;
|
||||||
|
let auto_width = if auto_count > 0 {
|
||||||
|
24u32.saturating_sub(specified) / auto_count
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut x = 0;
|
||||||
|
for panel in panels {
|
||||||
|
let w = if panel.width == 0 {
|
||||||
|
auto_width
|
||||||
|
} else {
|
||||||
|
panel.width
|
||||||
|
};
|
||||||
|
let grid_pos = GridPos {
|
||||||
|
h: height,
|
||||||
|
w,
|
||||||
|
x,
|
||||||
|
y: self.cursor_y,
|
||||||
|
};
|
||||||
|
let id = self.next_id;
|
||||||
|
self.next_id += 1;
|
||||||
|
x += w;
|
||||||
|
self.panels.push(panel.finalize(id, grid_pos));
|
||||||
|
}
|
||||||
|
self.cursor_y += height;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Percentile line targets for a summary metric: `p50`, `p90`, … each querying
|
||||||
|
/// the matching `quantile="0.x"` series.
|
||||||
|
pub fn percentiles(metric: &str, percentiles: &[u32]) -> Vec<Target> {
|
||||||
|
percentiles_labeled(metric, percentiles, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Like [`percentiles`], but appends `legend_suffix` to every legend — handy
|
||||||
|
/// when the metric carries labels (e.g. ` · {{kind}} · {{origin}}`).
|
||||||
|
pub fn percentiles_labeled(metric: &str, percentiles: &[u32], legend_suffix: &str) -> Vec<Target> {
|
||||||
|
percentiles
|
||||||
|
.iter()
|
||||||
|
.map(|&p| {
|
||||||
|
let quantile = f64::from(p) / 100.0;
|
||||||
|
Target::new(format!("{metric}{{quantile=\"{quantile}\"}}"))
|
||||||
|
.legend(format!("p{p}{legend_suffix}"))
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An `avg` target for a summary metric: `rate(sum) / rate(count)` over 1m.
|
||||||
|
pub fn avg(metric: &str) -> Target {
|
||||||
|
Target::new(format!("rate({metric}_sum[1m]) / rate({metric}_count[1m])")).legend("avg")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A per-minute rate target for a counter metric.
|
||||||
|
pub fn rate_per_min(metric: &str, legend: &str) -> Target {
|
||||||
|
Target::new(format!("rate({metric}[1m]) * 60")).legend(legend)
|
||||||
|
}
|
||||||
111
tools/dashboard_gen/src/main.rs
Normal file
111
tools/dashboard_gen/src/main.rs
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
//! Generates the sequencer dashboard and prints it to stdout.
|
||||||
|
//!
|
||||||
|
//! Compare against the committed file:
|
||||||
|
//! cargo run -p dashboard_gen | diff - monitoring/grafana/dashboards/sequencer.json
|
||||||
|
//! Regenerate it:
|
||||||
|
//! cargo run -p dashboard_gen > monitoring/grafana/dashboards/sequencer.json
|
||||||
|
|
||||||
|
use dashboard_gen::{
|
||||||
|
Dashboard, FieldOverride, Panel, Target, avg, percentiles, percentiles_labeled, rate_per_min,
|
||||||
|
};
|
||||||
|
use json_pretty_compact::PrettyCompactFormatter;
|
||||||
|
use serde::Serialize as _;
|
||||||
|
|
||||||
|
// Rendered Prometheus metric names. In a real setup these would be re-exported
|
||||||
|
// from `sequencer_core::metrics` (base name + unit/counter suffix rule) so that
|
||||||
|
// a rename in the recorder is a compile error here — that is the whole point of
|
||||||
|
// generating dashboards from the same codebase that emits the metrics.
|
||||||
|
const BLOCK_COUNT: &str = "block_count_total";
|
||||||
|
const BLOCK_CREATION_TIME: &str = "block_creation_time_seconds";
|
||||||
|
const TX_APPLY_TIME: &str = "mempool_transaction_application_time_seconds";
|
||||||
|
const MEMPOOL_SIZE: &str = "mempool_size";
|
||||||
|
const TX_PER_BLOCK: &str = "transactions_per_block";
|
||||||
|
const SUBMITTED_TX: &str = "submitted_transaction_count_total";
|
||||||
|
const FAILED_TX: &str = "failed_transaction_count_total";
|
||||||
|
|
||||||
|
const PERCENTILES: &[u32] = &[50, 90, 95, 99];
|
||||||
|
|
||||||
|
fn sequencer_dashboard() -> Dashboard {
|
||||||
|
Dashboard::new("Sequencer", "sequencer")
|
||||||
|
.tag("sequencer")
|
||||||
|
.row(
|
||||||
|
7,
|
||||||
|
[
|
||||||
|
Panel::stat("Chain height")
|
||||||
|
.width(6)
|
||||||
|
.unit("short")
|
||||||
|
.decimals(0)
|
||||||
|
.fixed_color("blue")
|
||||||
|
.target(Target::new(BLOCK_COUNT).legend("height")),
|
||||||
|
Panel::timeseries("Block production rate")
|
||||||
|
.width(18)
|
||||||
|
.unit("short")
|
||||||
|
.target(rate_per_min(BLOCK_COUNT, "blocks/min")),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.row(
|
||||||
|
9,
|
||||||
|
[Panel::timeseries("Block creation time")
|
||||||
|
.width(24)
|
||||||
|
.unit("s")
|
||||||
|
.targets(percentiles(BLOCK_CREATION_TIME, PERCENTILES))
|
||||||
|
.target(avg(BLOCK_CREATION_TIME))
|
||||||
|
.with_override(
|
||||||
|
FieldOverride::by_name("avg")
|
||||||
|
.dashed_line()
|
||||||
|
.fixed_color("text"),
|
||||||
|
)],
|
||||||
|
)
|
||||||
|
.row(
|
||||||
|
9,
|
||||||
|
[
|
||||||
|
Panel::timeseries("Transaction application time")
|
||||||
|
.width(12)
|
||||||
|
.unit("s")
|
||||||
|
.targets(percentiles_labeled(
|
||||||
|
TX_APPLY_TIME,
|
||||||
|
PERCENTILES,
|
||||||
|
" · {{kind}} · {{origin}}",
|
||||||
|
)),
|
||||||
|
Panel::timeseries("Mempool size")
|
||||||
|
.width(12)
|
||||||
|
.unit("short")
|
||||||
|
.span_nulls()
|
||||||
|
.target(Target::new(MEMPOOL_SIZE).legend("mempool size")),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.row(
|
||||||
|
9,
|
||||||
|
[
|
||||||
|
Panel::timeseries("Transactions per block")
|
||||||
|
.width(12)
|
||||||
|
.unit("short")
|
||||||
|
.targets(percentiles(TX_PER_BLOCK, PERCENTILES))
|
||||||
|
.target(avg(TX_PER_BLOCK))
|
||||||
|
.with_override(
|
||||||
|
FieldOverride::by_name("avg")
|
||||||
|
.dashed_line()
|
||||||
|
.fixed_color("text"),
|
||||||
|
),
|
||||||
|
Panel::timeseries("Transaction throughput (per minute)")
|
||||||
|
.width(12)
|
||||||
|
.unit("short")
|
||||||
|
.target(rate_per_min(SUBMITTED_TX, "submitted"))
|
||||||
|
.target(rate_per_min(FAILED_TX, "failed"))
|
||||||
|
.with_override(FieldOverride::by_name("failed").fixed_color("red"))
|
||||||
|
.with_override(FieldOverride::by_name("submitted").fixed_color("green")),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let dashboard = sequencer_dashboard();
|
||||||
|
|
||||||
|
let formatter = PrettyCompactFormatter::new();
|
||||||
|
let mut output = Vec::new();
|
||||||
|
let mut ser = serde_json::Serializer::with_formatter(&mut output, formatter);
|
||||||
|
dashboard.serialize(&mut ser).unwrap();
|
||||||
|
|
||||||
|
let json = String::from_utf8(output).unwrap();
|
||||||
|
println!("{json}");
|
||||||
|
}
|
||||||
260
tools/dashboard_gen/src/schema.rs
Normal file
260
tools/dashboard_gen/src/schema.rs
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
//! The serializable Grafana dashboard schema — the internal data model the
|
||||||
|
//! public builders assemble into.
|
||||||
|
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use crate::{DATASOURCE_UID, FieldOverride, Target};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Serialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum DatasourceKind {
|
||||||
|
Prometheus,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Serialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum ColorMode {
|
||||||
|
Fixed,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Serialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum Fill {
|
||||||
|
Dash,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Serialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum DrawStyle {
|
||||||
|
Line,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub enum MatcherKind {
|
||||||
|
ByName,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Serialize)]
|
||||||
|
pub enum PropertyId {
|
||||||
|
#[serde(rename = "color")]
|
||||||
|
Color,
|
||||||
|
#[serde(rename = "custom.lineStyle")]
|
||||||
|
LineStyle,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub enum Calc {
|
||||||
|
LastNotNull,
|
||||||
|
Last,
|
||||||
|
Max,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Serialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum StatColorMode {
|
||||||
|
Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Serialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum GraphMode {
|
||||||
|
Area,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Serialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum LegendDisplay {
|
||||||
|
Table,
|
||||||
|
List,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Serialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum Placement {
|
||||||
|
Bottom,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Serialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum TooltipMode {
|
||||||
|
Single,
|
||||||
|
Multi,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Serialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum SortOrder {
|
||||||
|
Desc,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Serialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum PanelType {
|
||||||
|
Stat,
|
||||||
|
Timeseries,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Serialize)]
|
||||||
|
pub struct Datasource {
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
pub kind: DatasourceKind,
|
||||||
|
pub uid: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Datasource {
|
||||||
|
pub fn prometheus() -> Self {
|
||||||
|
Self {
|
||||||
|
kind: DatasourceKind::Prometheus,
|
||||||
|
uid: DATASOURCE_UID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Color {
|
||||||
|
pub mode: ColorMode,
|
||||||
|
pub fixed_color: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Color {
|
||||||
|
pub fn fixed(color: String) -> Self {
|
||||||
|
Self {
|
||||||
|
mode: ColorMode::Fixed,
|
||||||
|
fixed_color: color,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct LineStyle {
|
||||||
|
pub dash: [u32; 2],
|
||||||
|
pub fill: Fill,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum PropertyValue {
|
||||||
|
Color(Color),
|
||||||
|
LineStyle(LineStyle),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct OverrideProperty {
|
||||||
|
pub id: PropertyId,
|
||||||
|
pub value: PropertyValue,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct Matcher {
|
||||||
|
pub id: MatcherKind,
|
||||||
|
pub options: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Custom {
|
||||||
|
pub draw_style: DrawStyle,
|
||||||
|
pub line_width: u32,
|
||||||
|
pub fill_opacity: u32,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub span_nulls: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct Defaults {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub color: Option<Color>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub custom: Option<Custom>,
|
||||||
|
pub unit: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub decimals: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct FieldConfig {
|
||||||
|
pub defaults: Defaults,
|
||||||
|
pub overrides: Vec<FieldOverride>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct ReduceOptions {
|
||||||
|
pub calcs: Vec<Calc>,
|
||||||
|
// Empty string means "all fields"; genuinely free-form, not a vocabulary.
|
||||||
|
pub fields: &'static str,
|
||||||
|
pub values: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct StatOptions {
|
||||||
|
pub color_mode: StatColorMode,
|
||||||
|
pub graph_mode: GraphMode,
|
||||||
|
pub reduce_options: ReduceOptions,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Legend {
|
||||||
|
pub display_mode: LegendDisplay,
|
||||||
|
pub placement: Placement,
|
||||||
|
pub calcs: Vec<Calc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct Tooltip {
|
||||||
|
pub mode: TooltipMode,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub sort: Option<SortOrder>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct TimeSeriesOptions {
|
||||||
|
pub legend: Legend,
|
||||||
|
pub tooltip: Tooltip,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum Options {
|
||||||
|
Stat(StatOptions),
|
||||||
|
TimeSeries(TimeSeriesOptions),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Serialize)]
|
||||||
|
pub struct GridPos {
|
||||||
|
pub h: u32,
|
||||||
|
pub w: u32,
|
||||||
|
pub x: u32,
|
||||||
|
pub y: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A fully positioned panel, ready to serialize.
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct PanelModel {
|
||||||
|
pub datasource: Datasource,
|
||||||
|
pub field_config: FieldConfig,
|
||||||
|
pub grid_pos: GridPos,
|
||||||
|
pub id: u32,
|
||||||
|
pub options: Options,
|
||||||
|
pub targets: Vec<Target>,
|
||||||
|
pub title: String,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
pub panel_type: PanelType,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Default)]
|
||||||
|
pub struct EmptyList {
|
||||||
|
pub list: [u8; 0],
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct TimeRange {
|
||||||
|
// Free-form Grafana time expressions, not a closed vocabulary.
|
||||||
|
pub from: &'static str,
|
||||||
|
pub to: &'static str,
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user