mirror of
https://github.com/logos-blockchain/logos-execution-zone.git
synced 2026-08-05 04:23:28 +00:00
feat(dashboard_gen): remove panel json to rust generator
This commit is contained in:
parent
aa6969386e
commit
343ba0b97c
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -2296,6 +2296,7 @@ dependencies = [
|
||||
name = "dashboard_gen"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"json-pretty-compact",
|
||||
"sequencer_core_metrics",
|
||||
"sequencer_service_metrics",
|
||||
|
||||
10
Justfile
10
Justfile
@ -57,14 +57,8 @@ regenerate-test-fixture:
|
||||
# (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 --bin gen_sequencer_dashboard
|
||||
@cargo run -q -p dashboard_gen --bin gen_sequencer_dashboard > monitoring/grafana/dashboards/sequencer.json
|
||||
|
||||
# Transpile a single Grafana panel JSON (stdin) — Inspect → Panel JSON — into a
|
||||
# Rust builder expression (stdout), omitting Grafana defaults. Paste into a row.
|
||||
# Usage: `just panel-to-rust < panel.json`.
|
||||
panel-to-rust:
|
||||
@cargo run -q -p dashboard_gen --bin panel_json_to_rust
|
||||
@cargo build -q -p dashboard_gen
|
||||
@cargo run -q -p dashboard_gen -- sequencer > monitoring/grafana/dashboards/sequencer.json
|
||||
|
||||
# Run criterion benches: fast crypto primitives, then the slow PPE verify (real proving setup).
|
||||
bench:
|
||||
|
||||
@ -11,6 +11,7 @@ workspace = true
|
||||
sequencer_core_metrics.workspace = true
|
||||
sequencer_service_metrics.workspace = true
|
||||
|
||||
clap = { workspace = true, features = ["derive"] }
|
||||
serde = { workspace = true, features = ["derive", "alloc"] }
|
||||
serde_json.workspace = true
|
||||
json-pretty-compact = "0.1.2"
|
||||
|
||||
@ -1,50 +0,0 @@
|
||||
//! Reads a single Grafana panel JSON on stdin (Grafana → panel menu → Inspect →
|
||||
//! Panel JSON) and prints the Rust `Panel::…` builder expression that rebuilds
|
||||
//! it through `dashboard_gen`, omitting values Grafana supplies by default.
|
||||
//!
|
||||
//! Paste the result into a dashboard's `.row(…)` and run `cargo fmt`.
|
||||
|
||||
#![expect(
|
||||
clippy::print_stderr,
|
||||
reason = "CLI tool: diagnostics on stderr are the deliverable"
|
||||
)]
|
||||
#![expect(
|
||||
clippy::non_ascii_literal,
|
||||
reason = "help text mirrors Grafana's `Inspect → Panel JSON` menu path"
|
||||
)]
|
||||
|
||||
use std::{
|
||||
io::{self, Read as _, Write as _},
|
||||
process::ExitCode,
|
||||
};
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let mut input = String::new();
|
||||
if let Err(err) = io::stdin().read_to_string(&mut input) {
|
||||
eprintln!("error: failed to read panel JSON from stdin: {err}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
match dashboard_gen::panel_to_rust_source(&input) {
|
||||
Ok(source) => {
|
||||
if let Err(err) = io::stdout().write_all(source.as_bytes()) {
|
||||
eprintln!("error: failed to write generated source to stdout: {err}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("error: could not parse panel JSON: {err}");
|
||||
eprintln!(
|
||||
"hint: paste one panel (Inspect → Panel JSON);\
|
||||
only stat and timeseries are supported."
|
||||
);
|
||||
eprintln!(
|
||||
"hint: this tool supports only the subset of Grafana's panel JSON, you might need \
|
||||
to manually implement support for new fields."
|
||||
);
|
||||
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,242 +0,0 @@
|
||||
//! Reverse of the builder: turn a single parsed panel back into the Rust
|
||||
//! `Panel::…` builder expression, omitting values Grafana supplies by default.
|
||||
//! Backs the `panel_json_to_rust` binary.
|
||||
//!
|
||||
//! Input is parsed leniently (see [`crate::input`]) so a raw Grafana panel
|
||||
//! export — full of fields and vocabularies we don't model — still works;
|
||||
//! anything unrecognized is dropped. The emitted expression is valid but only
|
||||
//! lightly formatted, so `cargo fmt` re-indents it once it lands in a file.
|
||||
|
||||
use std::fmt::Write as _;
|
||||
|
||||
use crate::{
|
||||
DEFAULT_FILL_OPACITY, Unit,
|
||||
input::{Defaults, PanelInput},
|
||||
schema::{
|
||||
AxisPlacement, Color, GradientMode, LineInterpolation, PanelType, ShowPoints, StackingMode,
|
||||
ThresholdMode, Thresholds,
|
||||
},
|
||||
};
|
||||
|
||||
/// Parse one Grafana panel JSON (Inspect → Panel JSON) and emit the Rust
|
||||
/// builder expression that reproduces it (minus Grafana defaults).
|
||||
pub fn panel_to_rust_source(json: &str) -> serde_json::Result<String> {
|
||||
let panel: PanelInput = serde_json::from_str(json)?;
|
||||
Ok(format!("{}\n", panel_expr(&panel)))
|
||||
}
|
||||
|
||||
/// A `Panel::…()` expression with one method call per line.
|
||||
fn panel_expr(panel: &PanelInput) -> String {
|
||||
panel_expr_inner(panel).expect("writing to a String never fails")
|
||||
}
|
||||
|
||||
fn panel_expr_inner(panel: &PanelInput) -> Result<String, std::fmt::Error> {
|
||||
let mut expr = match panel.panel_type {
|
||||
PanelType::Stat => format!("Panel::stat({:?})", panel.title),
|
||||
PanelType::Timeseries => format!("Panel::timeseries({:?})", panel.title),
|
||||
PanelType::Gauge => format!("Panel::gauge({:?})", panel.title),
|
||||
};
|
||||
write!(expr, "\n .width({})", panel.grid_pos.w)?;
|
||||
|
||||
let defaults = &panel.field_config.defaults;
|
||||
write_defaults(&mut expr, defaults)?;
|
||||
|
||||
if let Some(custom) = &defaults.custom {
|
||||
// Emitted against the builder's default, not Grafana's (which is 0), so
|
||||
// a genuinely unfilled panel still round-trips.
|
||||
if let Some(opacity) = custom.fill_opacity.filter(|&o| o != DEFAULT_FILL_OPACITY) {
|
||||
write!(expr, "\n .fill_opacity({opacity})")?;
|
||||
}
|
||||
if custom.span_nulls == Some(true) {
|
||||
expr.push_str("\n .span_nulls()");
|
||||
}
|
||||
// Each optional styling field is emitted only when it differs from the
|
||||
// Grafana default (matching the setters' panic-on-default contract).
|
||||
if let Some(value) = custom
|
||||
.line_interpolation
|
||||
.filter(|&v| v != LineInterpolation::Linear)
|
||||
{
|
||||
write!(
|
||||
expr,
|
||||
"\n .line_interpolation(LineInterpolation::{})",
|
||||
line_interp(value)
|
||||
)?;
|
||||
}
|
||||
if let Some(value) = custom.show_points.filter(|&v| v != ShowPoints::Auto) {
|
||||
write!(
|
||||
expr,
|
||||
"\n .show_points(ShowPoints::{})",
|
||||
show_points(value)
|
||||
)?;
|
||||
}
|
||||
if let Some(value) = custom.gradient_mode.filter(|&v| v != GradientMode::None) {
|
||||
write!(
|
||||
expr,
|
||||
"\n .gradient_mode(GradientMode::{})",
|
||||
gradient_mode(value)
|
||||
)?;
|
||||
}
|
||||
let stacking = custom.stacking.as_ref().and_then(|s| s.mode);
|
||||
if let Some(mode) = stacking.filter(|&m| m != StackingMode::None) {
|
||||
write!(
|
||||
expr,
|
||||
"\n .stacking(StackingMode::{})",
|
||||
stacking_mode(mode)
|
||||
)?;
|
||||
}
|
||||
if let Some(value) = custom.axis_placement.filter(|&v| v != AxisPlacement::Auto) {
|
||||
write!(
|
||||
expr,
|
||||
"\n .axis_placement(AxisPlacement::{})",
|
||||
axis_placement(value)
|
||||
)?;
|
||||
}
|
||||
if let Some(label) = custom.axis_label.as_deref().filter(|l| !l.is_empty()) {
|
||||
write!(expr, "\n .axis_label({label:?})")?;
|
||||
}
|
||||
}
|
||||
|
||||
for over in &panel.field_config.overrides {
|
||||
// Only `byName` matchers map to the builder; skip anything else.
|
||||
let Some(name) = over.matcher.by_name() else {
|
||||
continue;
|
||||
};
|
||||
let mut calls = String::new();
|
||||
for property in &over.properties {
|
||||
match property.id.as_str() {
|
||||
"color" => {
|
||||
if let Ok(Color::Fixed { fixed_color }) =
|
||||
serde_json::from_value::<Color>(property.value.clone())
|
||||
{
|
||||
write!(calls, ".color(Color::fixed({fixed_color:?}))")?;
|
||||
}
|
||||
}
|
||||
"custom.lineStyle" => calls.push_str(".dashed_line()"),
|
||||
_ => {} // property kind the builder can't express — drop it
|
||||
}
|
||||
}
|
||||
// An override with nothing representable adds no information.
|
||||
if !calls.is_empty() {
|
||||
write!(
|
||||
expr,
|
||||
"\n .with_override(FieldOverride::by_name({name:?}){calls})"
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
for target in &panel.targets {
|
||||
if target.expr.is_empty() {
|
||||
continue;
|
||||
}
|
||||
write!(expr, "\n .target(Target::new({:?})", target.expr)?;
|
||||
// `__auto` is Grafana's "no explicit legend" sentinel, i.e. the default.
|
||||
if !target.legend.is_empty() && target.legend != "__auto" {
|
||||
write!(expr, ".legend({:?})", target.legend)?;
|
||||
}
|
||||
expr.push(')');
|
||||
}
|
||||
|
||||
Ok(expr)
|
||||
}
|
||||
|
||||
/// The field-level setters — everything outside the `custom` styling block.
|
||||
fn write_defaults(expr: &mut String, defaults: &Defaults) -> Result<(), std::fmt::Error> {
|
||||
// `short` is the builder's own default unit, so it round-trips without a call.
|
||||
if let Some(unit) = defaults.unit.as_deref().filter(|u| *u != "short") {
|
||||
write!(
|
||||
expr,
|
||||
"\n .unit({})",
|
||||
Unit::from_id(unit).to_rust_source()
|
||||
)?;
|
||||
}
|
||||
if let Some(decimals) = defaults.decimals {
|
||||
write!(expr, "\n .decimals({decimals})")?;
|
||||
}
|
||||
// Only a fixed color is worth emitting; `palette-classic` is Grafana's default.
|
||||
if let Some(Color::Fixed { fixed_color }) = &defaults.color {
|
||||
write!(expr, "\n .color(Color::fixed({fixed_color:?}))")?;
|
||||
}
|
||||
if let Some(min) = defaults.min {
|
||||
write!(expr, "\n .min({min:?})")?;
|
||||
}
|
||||
if let Some(max) = defaults.max {
|
||||
write!(expr, "\n .max({max:?})")?;
|
||||
}
|
||||
|
||||
let ladder = defaults
|
||||
.thresholds
|
||||
.as_ref()
|
||||
.filter(|thresholds| is_expressible_ladder(thresholds))
|
||||
.and_then(|thresholds| thresholds.steps.split_first());
|
||||
if let Some((base, steps)) = ladder {
|
||||
write!(expr, "\n .thresholds(Thresholds::base({:?})", base.color)?;
|
||||
for step in steps {
|
||||
// A non-base step without a value is nonsense Grafana wouldn't render.
|
||||
if let Some(value) = step.value {
|
||||
write!(expr, ".step({value:?}, {:?})", step.color)?;
|
||||
}
|
||||
}
|
||||
expr.push(')');
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Whether a ladder is worth emitting: `percentage` mode is beyond the builder,
|
||||
/// and green/red-at-80 is the ladder Grafana attaches to every panel by default.
|
||||
fn is_expressible_ladder(thresholds: &Thresholds) -> bool {
|
||||
if thresholds.mode != ThresholdMode::Absolute {
|
||||
return false;
|
||||
}
|
||||
!matches!(
|
||||
thresholds.steps.as_slice(),
|
||||
[base, red]
|
||||
if base.color == "green"
|
||||
&& base.value.is_none()
|
||||
&& red.color == "red"
|
||||
&& red.value == Some(80.0)
|
||||
)
|
||||
}
|
||||
|
||||
const fn line_interp(value: LineInterpolation) -> &'static str {
|
||||
match value {
|
||||
LineInterpolation::Linear => "Linear",
|
||||
LineInterpolation::Smooth => "Smooth",
|
||||
LineInterpolation::StepBefore => "StepBefore",
|
||||
LineInterpolation::StepAfter => "StepAfter",
|
||||
}
|
||||
}
|
||||
|
||||
const fn show_points(value: ShowPoints) -> &'static str {
|
||||
match value {
|
||||
ShowPoints::Auto => "Auto",
|
||||
ShowPoints::Never => "Never",
|
||||
ShowPoints::Always => "Always",
|
||||
}
|
||||
}
|
||||
|
||||
const fn gradient_mode(value: GradientMode) -> &'static str {
|
||||
match value {
|
||||
GradientMode::None => "None",
|
||||
GradientMode::Opacity => "Opacity",
|
||||
GradientMode::Hue => "Hue",
|
||||
GradientMode::Scheme => "Scheme",
|
||||
}
|
||||
}
|
||||
|
||||
const fn stacking_mode(value: StackingMode) -> &'static str {
|
||||
match value {
|
||||
StackingMode::None => "None",
|
||||
StackingMode::Normal => "Normal",
|
||||
StackingMode::Percent => "Percent",
|
||||
}
|
||||
}
|
||||
|
||||
const fn axis_placement(value: AxisPlacement) -> &'static str {
|
||||
match value {
|
||||
AxisPlacement::Auto => "Auto",
|
||||
AxisPlacement::Left => "Left",
|
||||
AxisPlacement::Right => "Right",
|
||||
AxisPlacement::Hidden => "Hidden",
|
||||
}
|
||||
}
|
||||
3
tools/dashboard_gen/src/dashboards.rs
Normal file
3
tools/dashboard_gen/src/dashboards.rs
Normal file
@ -0,0 +1,3 @@
|
||||
//! One module per dashboard, each exposing a `dashboard()` builder.
|
||||
|
||||
pub mod sequencer;
|
||||
@ -1,9 +1,6 @@
|
||||
//! Generates the sequencer dashboard and prints it to stdout.
|
||||
//! The sequencer dashboard: chain progress, block timings, mempool and
|
||||
//! transaction outcomes.
|
||||
|
||||
#![expect(
|
||||
clippy::print_stdout,
|
||||
reason = "CLI tool: emitting the dashboard JSON on stdout is the deliverable"
|
||||
)]
|
||||
#![expect(
|
||||
clippy::non_ascii_literal,
|
||||
reason = "legend separators use `·` intentionally, matching the rendered Grafana labels"
|
||||
@ -13,13 +10,11 @@ use dashboard_gen::{
|
||||
Color, Dashboard, FieldOverride, GradientMode, Panel, Target, Thresholds, Unit, avg,
|
||||
percentile_legend, percentile_variable, rate_per_min, selected_percentile,
|
||||
};
|
||||
use json_pretty_compact::PrettyCompactFormatter;
|
||||
use serde::Serialize as _;
|
||||
|
||||
const PERCENTILES: &[u32] = &[50, 90, 95, 99];
|
||||
const DEFAULT_PERCENTILE: u32 = 95;
|
||||
|
||||
fn sequencer_dashboard() -> Dashboard {
|
||||
pub fn dashboard() -> Dashboard {
|
||||
Dashboard::new("Sequencer", "sequencer")
|
||||
.tag("sequencer")
|
||||
.variable(percentile_variable(PERCENTILES, DEFAULT_PERCENTILE))
|
||||
@ -186,15 +181,3 @@ fn sequencer_dashboard() -> Dashboard {
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
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}");
|
||||
}
|
||||
@ -1,137 +0,0 @@
|
||||
//! Lenient, deserialize-only model of a Grafana panel, for the panel→Rust
|
||||
//! transpiler (`codegen`).
|
||||
//!
|
||||
//! A real Grafana export is far wider than what we emit: `unit` may be absent,
|
||||
//! `options.tooltip.sort` may be `"none"`, overrides carry property types we
|
||||
//! don't model, and there are dozens of fields we ignore. So this model is
|
||||
//! deliberately separate from the strict `schema` (sized for *output*): it
|
||||
//! captures only what codegen reads, makes every field optional, and lets serde
|
||||
//! drop everything else — including the whole `options` block, which codegen
|
||||
//! reconstructs from the panel type rather than reading.
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::schema::{
|
||||
AxisPlacement, Color, GradientMode, LineInterpolation, PanelType, ShowPoints, StackingMode,
|
||||
Thresholds,
|
||||
};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct PanelInput {
|
||||
#[serde(rename = "type")]
|
||||
pub panel_type: PanelType,
|
||||
#[serde(default)]
|
||||
pub title: String,
|
||||
#[serde(rename = "gridPos", default)]
|
||||
pub grid_pos: GridPos,
|
||||
#[serde(rename = "fieldConfig", default)]
|
||||
pub field_config: FieldConfig,
|
||||
#[serde(default)]
|
||||
pub targets: Vec<Target>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
pub struct GridPos {
|
||||
#[serde(default)]
|
||||
pub w: u32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
pub struct FieldConfig {
|
||||
#[serde(default)]
|
||||
pub defaults: Defaults,
|
||||
#[serde(default)]
|
||||
pub overrides: Vec<Override>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
pub struct Defaults {
|
||||
#[serde(default)]
|
||||
pub unit: Option<String>,
|
||||
#[serde(default)]
|
||||
pub decimals: Option<u32>,
|
||||
#[serde(default)]
|
||||
pub color: Option<Color>,
|
||||
#[serde(default)]
|
||||
pub custom: Option<Custom>,
|
||||
#[serde(default)]
|
||||
pub min: Option<f64>,
|
||||
#[serde(default)]
|
||||
pub max: Option<f64>,
|
||||
#[serde(default)]
|
||||
pub thresholds: Option<Thresholds>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Custom {
|
||||
#[serde(default)]
|
||||
pub fill_opacity: Option<u32>,
|
||||
#[serde(default)]
|
||||
pub span_nulls: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub line_interpolation: Option<LineInterpolation>,
|
||||
#[serde(default)]
|
||||
pub show_points: Option<ShowPoints>,
|
||||
#[serde(default)]
|
||||
pub gradient_mode: Option<GradientMode>,
|
||||
#[serde(default)]
|
||||
pub stacking: Option<Stacking>,
|
||||
#[serde(default)]
|
||||
pub axis_placement: Option<AxisPlacement>,
|
||||
#[serde(default)]
|
||||
pub axis_label: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
pub struct Stacking {
|
||||
#[serde(default)]
|
||||
pub mode: Option<StackingMode>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Override {
|
||||
#[serde(default)]
|
||||
pub matcher: Matcher,
|
||||
#[serde(default)]
|
||||
pub properties: Vec<Property>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
pub struct Matcher {
|
||||
#[serde(default)]
|
||||
pub id: String,
|
||||
#[serde(default)]
|
||||
pub options: Value,
|
||||
}
|
||||
|
||||
impl Matcher {
|
||||
/// The series name for a `byName` matcher; `None` for matcher kinds the
|
||||
/// builder can't express (which the caller skips).
|
||||
pub fn by_name(&self) -> Option<&str> {
|
||||
if self.id == "byName" {
|
||||
self.options.as_str()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One override property. `id`/`value` are kept raw so unknown kinds are simply
|
||||
/// ignored by codegen rather than failing the whole parse.
|
||||
#[derive(Deserialize)]
|
||||
pub struct Property {
|
||||
#[serde(default)]
|
||||
pub id: String,
|
||||
#[serde(default)]
|
||||
pub value: Value,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
pub struct Target {
|
||||
#[serde(default)]
|
||||
pub expr: String,
|
||||
#[serde(rename = "legendFormat", default)]
|
||||
pub legend: String,
|
||||
}
|
||||
@ -13,7 +13,6 @@
|
||||
|
||||
// Styling vocabularies passed to the optional `styling` setters are part of the
|
||||
// public API (and are used by this module's `Panel` fields and `finalize`).
|
||||
pub use codegen::panel_to_rust_source;
|
||||
pub use schema::{
|
||||
AxisPlacement, Color, GradientMode, LineInterpolation, ShowPoints, StackingMode, Thresholds,
|
||||
Variable,
|
||||
@ -28,8 +27,6 @@ use schema::{
|
||||
use serde::Serialize;
|
||||
pub use unit::Unit;
|
||||
|
||||
mod codegen;
|
||||
mod input;
|
||||
mod schema;
|
||||
mod styling;
|
||||
mod unit;
|
||||
|
||||
45
tools/dashboard_gen/src/main.rs
Normal file
45
tools/dashboard_gen/src/main.rs
Normal file
@ -0,0 +1,45 @@
|
||||
//! Builds one of the Grafana dashboards and prints its JSON to stdout.
|
||||
|
||||
#![expect(
|
||||
clippy::print_stdout,
|
||||
reason = "CLI tool: emitting the dashboard JSON on stdout is the deliverable"
|
||||
)]
|
||||
|
||||
use clap::{Parser, ValueEnum};
|
||||
use dashboard_gen::Dashboard;
|
||||
use json_pretty_compact::PrettyCompactFormatter;
|
||||
use serde::Serialize as _;
|
||||
|
||||
mod dashboards;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[clap(version)]
|
||||
struct Args {
|
||||
/// Which dashboard to build.
|
||||
dashboard: DashboardKind,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, ValueEnum)]
|
||||
enum DashboardKind {
|
||||
Sequencer,
|
||||
}
|
||||
|
||||
impl DashboardKind {
|
||||
fn build(self) -> Dashboard {
|
||||
match self {
|
||||
Self::Sequencer => dashboards::sequencer::dashboard(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let Args { dashboard } = Args::parse();
|
||||
|
||||
let formatter = PrettyCompactFormatter::new();
|
||||
let mut output = Vec::new();
|
||||
let mut ser = serde_json::Serializer::with_formatter(&mut output, formatter);
|
||||
dashboard.build().serialize(&mut ser).unwrap();
|
||||
|
||||
let json = String::from_utf8(output).unwrap();
|
||||
println!("{json}");
|
||||
}
|
||||
@ -1,12 +1,9 @@
|
||||
//! The serializable Grafana dashboard schema — the internal data model the
|
||||
//! public builders assemble into.
|
||||
//!
|
||||
//! Most types are `Serialize`-only, sized for what we *emit*. A handful of
|
||||
//! vocabularies (`PanelType`, `Color`, and the styling enums) additionally
|
||||
//! derive `Deserialize` because the lenient `input` model — which backs the
|
||||
//! panel→Rust transpiler — reuses them.
|
||||
//! Every type here is `Serialize`-only: the model is sized for what we emit.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::{DATASOURCE_UID, FieldOverride, Target, unit::Unit};
|
||||
|
||||
@ -88,8 +85,7 @@ pub enum SortOrder {
|
||||
Desc,
|
||||
}
|
||||
|
||||
// Reused by the `input` model, hence `Deserialize`.
|
||||
#[derive(Clone, Copy, Serialize, Deserialize)]
|
||||
#[derive(Clone, Copy, Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum PanelType {
|
||||
Stat,
|
||||
@ -97,7 +93,7 @@ pub enum PanelType {
|
||||
Gauge,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ThresholdMode {
|
||||
Absolute,
|
||||
@ -108,14 +104,14 @@ pub enum ThresholdMode {
|
||||
|
||||
/// One threshold step: the color values at or above `value` take. The base step
|
||||
/// carries `value: null` — Grafana's "everything below the first threshold".
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[derive(Clone, Serialize)]
|
||||
pub struct ThresholdStep {
|
||||
pub color: String,
|
||||
pub value: Option<f64>,
|
||||
}
|
||||
|
||||
/// A threshold ladder, driving gauge/stat coloring.
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[derive(Clone, Serialize)]
|
||||
pub struct Thresholds {
|
||||
pub mode: ThresholdMode,
|
||||
pub steps: Vec<ThresholdStep>,
|
||||
@ -145,10 +141,9 @@ impl Thresholds {
|
||||
}
|
||||
|
||||
// Optional timeseries styling vocabularies. Each derives `PartialEq` so the
|
||||
// public setters can panic when handed the Grafana default (see `styling`), and
|
||||
// `Deserialize` because the `input` model reuses them.
|
||||
// public setters can panic when handed the Grafana default (see `styling`).
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum LineInterpolation {
|
||||
Linear,
|
||||
@ -157,7 +152,7 @@ pub enum LineInterpolation {
|
||||
StepAfter,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ShowPoints {
|
||||
Auto,
|
||||
@ -165,7 +160,7 @@ pub enum ShowPoints {
|
||||
Always,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum GradientMode {
|
||||
None,
|
||||
@ -174,7 +169,7 @@ pub enum GradientMode {
|
||||
Scheme,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum StackingMode {
|
||||
None,
|
||||
@ -182,7 +177,7 @@ pub enum StackingMode {
|
||||
Percent,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum AxisPlacement {
|
||||
Auto,
|
||||
@ -237,8 +232,7 @@ impl Datasource {
|
||||
}
|
||||
}
|
||||
|
||||
// Reused by the `input` model, hence `Deserialize`.
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[derive(Clone, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[serde(tag = "mode")]
|
||||
pub enum Color {
|
||||
|
||||
@ -60,44 +60,6 @@ impl Unit {
|
||||
Self::Custom(id) => id,
|
||||
}
|
||||
}
|
||||
|
||||
/// Reverse of [`Self::as_id`]: map a Grafana unit id back to a `Unit`,
|
||||
/// falling back to [`Self::Custom`] for ids we don't name. Used by the
|
||||
/// panel→Rust transpiler.
|
||||
#[must_use]
|
||||
pub(crate) fn from_id(id: &str) -> Self {
|
||||
match id {
|
||||
"short" => Self::Short,
|
||||
"percent" => Self::Percent,
|
||||
"percentunit" => Self::PercentUnit,
|
||||
"s" => Self::Seconds,
|
||||
"ms" => Self::Milliseconds,
|
||||
"ns" => Self::Nanoseconds,
|
||||
"bytes" => Self::Bytes,
|
||||
"Bps" => Self::BytesPerSec,
|
||||
"reqps" => Self::RequestsPerSec,
|
||||
"ops" => Self::OpsPerSec,
|
||||
other => Self::custom(other),
|
||||
}
|
||||
}
|
||||
|
||||
/// The Rust builder expression that reconstructs this unit, for codegen.
|
||||
#[must_use]
|
||||
pub(crate) fn to_rust_source(&self) -> String {
|
||||
match self {
|
||||
Self::Custom(id) => format!("Unit::custom({id:?})"),
|
||||
named @ (Self::Short
|
||||
| Self::Percent
|
||||
| Self::PercentUnit
|
||||
| Self::Seconds
|
||||
| Self::Milliseconds
|
||||
| Self::Nanoseconds
|
||||
| Self::Bytes
|
||||
| Self::BytesPerSec
|
||||
| Self::RequestsPerSec
|
||||
| Self::OpsPerSec) => format!("Unit::{named:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for Unit {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user